1"""
2write_blazed.py
3Andrew Lorimer, January 2025
4Monash University
5
6Writes a reflective brazed grating pattern (sawtooth) using
7the confocal direct laser writing setup on Sb2S3 thin-film PCM.
8"""
9
10import configparser as cp
11import cv2
12import numpy as np
13import sys
14import matplotlib.pyplot as plt
15#import write_path
16#from pipython import datarectools, pitools
17#import nidaqmx
18#import pipython
19import time
20
21def calculate_offset_vertices(outer_vertices, offset_distance):
22 """
23 Calculate the vertices of an inner triangle offset inward from an outer triangle.
24
25 Parameters:
26 - outer_vertices: A list of tuples [(x1, y1), (x2, y2), (x3, y3)] defining the outer triangle.
27 - offset_distance: The distance by which the inner triangle is offset inward.
28
29 Returns:
30 - A list of tuples [(x1', y1'), (x2', y2'), (x3', y3')] defining the inner triangle.
31 """
32 def unit_vector(vector):
33 """Return the unit vector from two points"""
34 return vector / np.linalg.norm(vector)
35
36 def normal(vector):
37 """Return the normal vector from two points"""
38 return np.array([-vector[1], vector[0]])
39
40 def intersection(p1, p2, p3, p4):
41 """Find the intersection point of two lines defined by points (p1, p2) and (p3, p4)"""
42 A1, B1 = p2[1] - p1[1], p1[0] - p2[0]
43 C1 = A1 * p1[0] + B1 * p1[1]
44
45 A2, B2 = p4[1] - p3[1], p3[0] - p4[0]
46 C2 = A2 * p3[0] + B2 * p3[1]
47
48 determinant = A1 * B2 - A2 * B1
49 if determinant == 0:
50 raise ValueError("Lines do not intersect")
51
52 x = (B2 * C1 - B1 * C2) / determinant
53 y = (A1 * C2 - A2 * C1) / determinant
54 return (x, y)
55
56 def is_parallel(vec1, vec2):
57 uv1 = unit_vector(vec1)
58 uv2 = unit_vector(vec2)
59 return np.all(abs(uv1 - uv2) < 0.001)
60
61 inner_vertices = []
62 num_vertices = len(outer_vertices)
63
64 for i in range(num_vertices):
65 # Current vertex and next vertex
66 p1 = np.array(outer_vertices[i])
67 p2 = np.array(outer_vertices[(i + 1) % num_vertices])
68
69 # Calculate edge direction and inward normal
70 edge_vector = p2 - p1
71 inward_normal = unit_vector(normal(edge_vector)) * offset_distance
72
73 # Offset the two points on the edge
74 offset_p1 = p1 + inward_normal
75 offset_p2 = p2 + inward_normal
76
77 # Store the offset line
78 inner_vertices.append((offset_p1, offset_p2))
79
80 # Find intersections of the offset lines
81 result_vertices = []
82 for i in range(num_vertices):
83 # Current offset line and next offset line
84 line1 = inner_vertices[i]
85 line2 = inner_vertices[(i + 1) % num_vertices]
86
87 # Calculate the intersection of the two lines
88 int = intersection(line1[0], line1[1], line2[0], line2[1])
89 result_vertices.append(int)
90
91 result_vertices = np.array([result_vertices[2], result_vertices[0], result_vertices[1]])
92
93 result_edges = np.array([
94 [result_vertices[0], result_vertices[1]],
95 [result_vertices[1], result_vertices[2]],
96 [result_vertices[2], result_vertices[0]]
97 ])
98 outer_edges = np.array([
99 [outer_vertices[0], outer_vertices[1]],
100 [outer_vertices[1], outer_vertices[2]],
101 [outer_vertices[2], outer_vertices[0]]
102 ])
103
104 if not is_parallel(outer_vertices[1] - outer_vertices[0], result_vertices[1] - result_vertices[0]):
105 raise ValueError("Offset is too large for the given outer trianlge")
106
107 return result_vertices
108
109if __name__ == "__main__":
110
111 #task, pidevice = write_path.setup()
112
113 # Input parameters
114 grating_height = None # h, nm
115 grating_pitch = 2000 # Lambda, nm
116 #blaze_angle = None # theta_b, degrees
117 n_blazes = 10 # number of steps ("teeth")
118 beam_dia = 500 # diameter of laser beam, nm
119 base_height = 0 # nm
120 wavelength = 450 # nm
121 order = 1
122 n_cr = 1.3
123
124 blaze_angle = np.arcsin(order*wavelength/(2*grating_pitch)) / np.pi * 180
125 print("Calculated blaze angle: {:.1f} deg".format(blaze_angle))
126
127 grating_height = np.tan(blaze_angle*np.pi/180)*grating_pitch
128 print("Calculated grating height: {:.1f} nm".format(grating_height))
129
130 diff_angle = np.arcsin(wavelength / grating_pitch) / np.pi * 180
131 print("First order diffraction angle: {:.1f} deg".format(diff_angle))
132
133 # Calculate vertices of a single step ("tooth")
134 if blaze_angle is None:
135 d1 = grating_pitch # axial length of upwards section of sawtooth
136 d2 = 0 # axial length of downwards section of sawtooth
137 back_angle = 90 # angle between grating normal and downwards section of sawtooth, degrees
138 else:
139 d1 = grating_height / np.tan(blaze_angle * np.pi / 180) # axial length of upwards section of sawtooth
140 d2 = grating_pitch - d1 # axial length of downwards section of sawtooth
141 back_angle = np.arctan(d2 / grating_height) / np.pi * 180 # angle between grating normal and downwards section of sawtooth, degrees
142
143 # Put these vertices into an array as coordinates [ [x1, y1], [x2, y2], [x3, y3] ]
144 pts = [
145 [0, 0 ],
146 [d1, grating_height ],
147 [grating_pitch, 0 ]
148 ]
149
150 # Calculate vertices of the inner triangles to fill in the middle
151 n_passes = 1
152 while True:
153 try:
154 pts_inner = calculate_offset_vertices(np.array(pts[-3:]), -beam_dia)
155 except ValueError:
156 # Not possible to fit any smaller triangles in the middle with given beam diameter
157 break
158 # Add inner triangles to the list of vertices
159 for pt_inner in pts_inner:
160 pts.append(pt_inner)
161 n_passes += 1
162
163 lines = []
164
165 # Generate lines between each set of three consecutive vertices
166 for i in range(len(pts) // 3):
167 lines.append((pts[i*3+0], pts[i*3+1]))
168 lines.append((pts[i*3+1], pts[i*3+2]))
169 lines.append((pts[i*3+2], pts[i*3+0]))
170 lines = np.array(lines)
171
172 # Remove a bit of the line in the corner to prevent overwriting
173 lines[2,1,0] += beam_dia
174
175
176 # Duplicate this set of triangles a number of times along the axis of the grating (defined by n_blazes)
177 blazes_addition_x = np.tile(np.repeat(np.arange(n_blazes) * grating_pitch, 3*n_passes), (2, 1)).T
178 blazes_addition_y = np.zeros((n_blazes * 3 * n_passes, 2))
179 blazes_addition = np.stack((blazes_addition_x, blazes_addition_y), axis=-1)
180 lines = np.tile(lines, (n_blazes, 1, 1)) + blazes_addition
181
182 # Calculate a set of y values for lines to make up the solid rectangular base
183 base_y_vals = np.arange(-base_height, 0, beam_dia)
184
185 # Calculate alternating x values for a zig-zag raster scan
186 base_x_vals_single = np.array([0, grating_pitch*n_blazes])
187 base_x_vals_duplicated = np.array((base_x_vals_single, np.flip(base_x_vals_single)))
188 base_x_vals = np.tile(base_x_vals_duplicated, (int(len(base_y_vals)//2), 1))
189 # Deal with odd number of lines
190 if len(base_y_vals) % 2 != 0:
191 base_x_vals = np.vstack((base_x_vals, base_x_vals_single))
192
193 # Put x and y values into a list of pairs of points
194 base_y_vals = np.tile(base_y_vals, (2, 1)).T
195 base_lines = np.stack((base_x_vals, base_y_vals), axis=2)
196
197 # Add these lines to the overall array
198 lines = np.concatenate((lines, base_lines))
199
200 # Plot
201 fig, ax = plt.subplots()
202 ax.set_aspect("equal")
203 for pair in lines:
204 ax.plot(pair[:,0], pair[:,1], '-', color='black')
205 plt.show()