prefab 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prefab/shapes.py CHANGED
@@ -1,14 +1,98 @@
1
- """Contains functions for creating various shapes as Device objects."""
1
+ """
2
+ Shape generation functions for creating test device geometries.
2
3
 
3
- from typing import Optional
4
+ Provides functions for creating common shapes including rectangles, circles,
5
+ gratings, polygons, and grid patterns. All functions return Device objects.
6
+ """
7
+
8
+ from typing import Any
4
9
 
5
10
  import numpy as np
11
+ import numpy.typing as npt
6
12
  from skimage.draw import polygon
7
13
 
8
14
  from .device import Device
9
15
 
10
16
 
11
- def rectangle(width: int = 200, height: Optional[int] = None, **kwargs) -> Device:
17
+ def _default_height(height: int | None, width: int) -> int:
18
+ """Return height if provided, otherwise default to width for square shapes."""
19
+ return height if height is not None else width
20
+
21
+
22
+ def _create_ellipse_mask(
23
+ width: int, height: int
24
+ ) -> tuple[npt.NDArray[np.bool_], int, int]:
25
+ """
26
+ Create an ellipse mask for the given width and height.
27
+
28
+ Parameters
29
+ ----------
30
+ width : int
31
+ Width of the ellipse.
32
+ height : int
33
+ Height of the ellipse.
34
+
35
+ Returns
36
+ -------
37
+ tuple[NDArray[np.bool_], int, int]
38
+ Boolean mask array, radius_x, and radius_y.
39
+ """
40
+ radius_x = width // 2
41
+ radius_y = height // 2
42
+ y, x = np.ogrid[-radius_y:radius_y, -radius_x:radius_x]
43
+ mask = (x**2 / radius_x**2) + (y**2 / radius_y**2) <= 1
44
+ return mask, radius_x, radius_y
45
+
46
+
47
+ def _create_circular_mask(radius: int) -> npt.NDArray[np.bool_]:
48
+ """
49
+ Create a circular mask for the given radius.
50
+
51
+ Parameters
52
+ ----------
53
+ radius : int
54
+ Radius of the circle.
55
+
56
+ Returns
57
+ -------
58
+ NDArray[np.bool_]
59
+ Boolean mask array with circular region.
60
+ """
61
+ y, x = np.ogrid[-radius:radius, -radius:radius]
62
+ return x**2 + y**2 <= radius**2
63
+
64
+
65
+ def _place_disk_in_grid(
66
+ grid: npt.NDArray[np.floating[Any]],
67
+ center_y: int,
68
+ center_x: int,
69
+ radius: int,
70
+ value: float = 1.0,
71
+ ) -> None:
72
+ """
73
+ Place a disk in a grid at the specified center position.
74
+
75
+ Parameters
76
+ ----------
77
+ grid : NDArray
78
+ The grid array to modify in-place.
79
+ center_y : int
80
+ Y-coordinate of disk center.
81
+ center_x : int
82
+ X-coordinate of disk center.
83
+ radius : int
84
+ Radius of the disk.
85
+ value : float
86
+ Value to set for the disk pixels (1.0 for disks, 0.0 for holes).
87
+ """
88
+ mask = _create_circular_mask(radius)
89
+ grid[
90
+ center_y - radius : center_y + radius,
91
+ center_x - radius : center_x + radius,
92
+ ][mask] = value
93
+
94
+
95
+ def rectangle(width: int = 200, height: int | None = None, **kwargs: Any) -> Device:
12
96
  """
13
97
  Create a Device object with a rectangular shape.
14
98
 
@@ -16,7 +100,7 @@ def rectangle(width: int = 200, height: Optional[int] = None, **kwargs) -> Devic
16
100
  ----------
17
101
  width : int
18
102
  The width of the rectangle. Defaults to 200.
19
- height : Optional[int]
103
+ height : int | None
20
104
  The height of the rectangle. Defaults to the value of width if None.
21
105
  **kwargs : dict
22
106
  Additional keyword arguments to be passed to the Device constructor.
@@ -26,14 +110,13 @@ def rectangle(width: int = 200, height: Optional[int] = None, **kwargs) -> Devic
26
110
  Device
27
111
  A Device object containing the rectangular shape.
28
112
  """
29
- if height is None:
30
- height = width
31
- rectangle = np.ones((height, width))
32
- return Device(device_array=rectangle, **kwargs)
113
+ height = _default_height(height, width)
114
+ shape_array = np.ones((height, width))
115
+ return Device(device_array=shape_array, **kwargs)
33
116
 
34
117
 
35
118
  def window(
36
- width: int = 200, height: Optional[int] = None, border_width: int = 60, **kwargs
119
+ width: int = 200, height: int | None = None, border_width: int = 60, **kwargs: Any
37
120
  ) -> Device:
38
121
  """
39
122
  Create a Device object with a window shape (hollow rectangle).
@@ -42,7 +125,7 @@ def window(
42
125
  ----------
43
126
  width : int
44
127
  The overall width of the window. Defaults to 200.
45
- height : Optional[int]
128
+ height : int | None
46
129
  The overall height of the window. Defaults to the value of width.
47
130
  border_width : int
48
131
  The width of the window border. Defaults to 60.
@@ -54,18 +137,17 @@ def window(
54
137
  Device
55
138
  A Device object containing the window shape.
56
139
  """
57
- if height is None:
58
- height = width
59
- window = np.zeros((height, width))
60
- window[:border_width, :] = 1
61
- window[-border_width:, :] = 1
62
- window[:, :border_width] = 1
63
- window[:, -border_width:] = 1
64
- return Device(device_array=window, **kwargs)
140
+ height = _default_height(height, width)
141
+ shape_array = np.zeros((height, width))
142
+ shape_array[:border_width, :] = 1
143
+ shape_array[-border_width:, :] = 1
144
+ shape_array[:, :border_width] = 1
145
+ shape_array[:, -border_width:] = 1
146
+ return Device(device_array=shape_array, **kwargs)
65
147
 
66
148
 
67
149
  def cross(
68
- width: int = 200, height: Optional[int] = None, arm_width: int = 60, **kwargs
150
+ width: int = 200, height: int | None = None, arm_width: int = 60, **kwargs: Any
69
151
  ) -> Device:
70
152
  """
71
153
  Create a Device object with a cross shape.
@@ -74,7 +156,7 @@ def cross(
74
156
  ----------
75
157
  width : int
76
158
  The overall width of the cross. Defaults to 200.
77
- height : Optional[int]
159
+ height : int | None
78
160
  The overall height of the cross. Defaults to the value of width.
79
161
  arm_width : int
80
162
  The width of the cross arms. Defaults to 60.
@@ -86,19 +168,18 @@ def cross(
86
168
  Device
87
169
  A Device object containing the cross shape.
88
170
  """
89
- if height is None:
90
- height = width
91
- cross = np.zeros((height, width))
171
+ height = _default_height(height, width)
172
+ shape_array = np.zeros((height, width))
92
173
  center_x = width // 2
93
174
  center_y = height // 2
94
175
  half_arm_width = arm_width // 2
95
- cross[center_y - half_arm_width : center_y + half_arm_width + 1, :] = 1
96
- cross[:, center_x - half_arm_width : center_x + half_arm_width + 1] = 1
97
- return Device(device_array=cross, **kwargs)
176
+ shape_array[center_y - half_arm_width : center_y + half_arm_width + 1, :] = 1
177
+ shape_array[:, center_x - half_arm_width : center_x + half_arm_width + 1] = 1
178
+ return Device(device_array=shape_array, **kwargs)
98
179
 
99
180
 
100
181
  def target(
101
- width: int = 200, height: Optional[int] = None, arm_width: int = 60, **kwargs
182
+ width: int = 200, height: int | None = None, arm_width: int = 60, **kwargs: Any
102
183
  ) -> Device:
103
184
  """
104
185
  Create a Device object with a target shape (cross with center removed).
@@ -107,7 +188,7 @@ def target(
107
188
  ----------
108
189
  width : int
109
190
  The overall width of the target. Defaults to 200.
110
- height : Optional[int]
191
+ height : int | None
111
192
  The overall height of the target. Defaults to the value of width.
112
193
  arm_width : int
113
194
  The width of the target arms. Defaults to 60.
@@ -119,22 +200,21 @@ def target(
119
200
  Device
120
201
  A Device object containing the target shape.
121
202
  """
122
- if height is None:
123
- height = width
124
- target = np.zeros((height, width))
203
+ height = _default_height(height, width)
204
+ shape_array = np.zeros((height, width))
125
205
  center_x = width // 2
126
206
  center_y = height // 2
127
207
  half_arm_width = arm_width // 2
128
- target[center_y - half_arm_width : center_y + half_arm_width + 1, :] = 1
129
- target[:, center_x - half_arm_width : center_x + half_arm_width + 1] = 1
130
- target[
208
+ shape_array[center_y - half_arm_width : center_y + half_arm_width + 1, :] = 1
209
+ shape_array[:, center_x - half_arm_width : center_x + half_arm_width + 1] = 1
210
+ shape_array[
131
211
  center_y - half_arm_width : center_y + half_arm_width + 1,
132
212
  center_x - half_arm_width : center_x + half_arm_width + 1,
133
213
  ] = 0
134
- return Device(device_array=target, **kwargs)
214
+ return Device(device_array=shape_array, **kwargs)
135
215
 
136
216
 
137
- def disk(width: int = 200, height: Optional[int] = None, **kwargs) -> Device:
217
+ def disk(width: int = 200, height: int | None = None, **kwargs: Any) -> Device:
138
218
  """
139
219
  Create a Device object with an elliptical shape.
140
220
 
@@ -142,7 +222,7 @@ def disk(width: int = 200, height: Optional[int] = None, **kwargs) -> Device:
142
222
  ----------
143
223
  width : int
144
224
  The width of the ellipse. Defaults to 200.
145
- height : Optional[int]
225
+ height : int | None
146
226
  The height of the ellipse. Defaults to the value of width.
147
227
  **kwargs : dict
148
228
  Additional keyword arguments to be passed to the Device constructor.
@@ -152,19 +232,15 @@ def disk(width: int = 200, height: Optional[int] = None, **kwargs) -> Device:
152
232
  Device
153
233
  A Device object containing the elliptical shape.
154
234
  """
155
- if height is None:
156
- height = width
157
- radius_x = width // 2
158
- radius_y = height // 2
159
- y, x = np.ogrid[-radius_y:radius_y, -radius_x:radius_x]
160
- mask = (x**2 / radius_x**2) + (y**2 / radius_y**2) <= 1
161
- ellipse = np.zeros((height, width))
162
- ellipse[mask] = 1
163
- return Device(device_array=ellipse, **kwargs)
235
+ height = _default_height(height, width)
236
+ mask, _, _ = _create_ellipse_mask(width, height)
237
+ shape_array = np.zeros((height, width))
238
+ shape_array[mask] = 1
239
+ return Device(device_array=shape_array, **kwargs)
164
240
 
165
241
 
166
242
  def ring(
167
- width: int = 200, height: Optional[int] = None, border_width: int = 60, **kwargs
243
+ width: int = 200, height: int | None = None, border_width: int = 60, **kwargs: Any
168
244
  ) -> Device:
169
245
  """
170
246
  Create a Device object with a ring shape (hollow ellipse).
@@ -173,7 +249,7 @@ def ring(
173
249
  ----------
174
250
  width : int
175
251
  The overall width of the ring. Defaults to 200.
176
- height : Optional[int]
252
+ height : int | None
177
253
  The overall height of the ring. Defaults to the value of width.
178
254
  border_width : int
179
255
  The width of the ring border. Defaults to 60.
@@ -185,22 +261,26 @@ def ring(
185
261
  Device
186
262
  A Device object containing the ring shape.
187
263
  """
188
- if height is None:
189
- height = width
190
- radius_x = width // 2
191
- radius_y = height // 2
264
+ height = _default_height(height, width)
265
+ outer_mask, radius_x, radius_y = _create_ellipse_mask(width, height)
266
+
267
+ # Create inner ellipse mask
192
268
  inner_radius_x = radius_x - border_width
193
269
  inner_radius_y = radius_y - border_width
194
270
  y, x = np.ogrid[-radius_y:radius_y, -radius_x:radius_x]
195
- outer_mask = x**2 / radius_x**2 + y**2 / radius_y**2 <= 1
196
271
  inner_mask = x**2 / inner_radius_x**2 + y**2 / inner_radius_y**2 <= 1
197
- ring = np.zeros((height, width))
198
- ring[outer_mask & ~inner_mask] = 1
199
- return Device(device_array=ring, **kwargs)
272
+
273
+ shape_array = np.zeros((height, width))
274
+ shape_array[outer_mask & ~inner_mask] = 1
275
+ return Device(device_array=shape_array, **kwargs)
200
276
 
201
277
 
202
278
  def disk_wavy(
203
- width: int = 200, wave_amplitude: float = 10, wave_frequency: float = 10, **kwargs
279
+ width: int = 200,
280
+ height: int | None = None,
281
+ wave_amplitude: float = 10,
282
+ wave_frequency: float = 10,
283
+ **kwargs: Any,
204
284
  ) -> Device:
205
285
  """
206
286
  Create a Device object with a circular shape with wavy edges.
@@ -208,7 +288,9 @@ def disk_wavy(
208
288
  Parameters
209
289
  ----------
210
290
  width : int
211
- The overall width and height of the wavy circle. Defaults to 200.
291
+ The overall width of the wavy circle. Defaults to 200.
292
+ height : int | None
293
+ The overall height of the wavy circle. Defaults to the value of width.
212
294
  wave_amplitude : float
213
295
  The amplitude of the waves. Defaults to 10.
214
296
  wave_frequency : float
@@ -220,21 +302,28 @@ def disk_wavy(
220
302
  -------
221
303
  Device
222
304
  A Device object containing the wavy circular shape.
305
+
306
+ Notes
307
+ -----
308
+ The effective radius is reduced by wave_amplitude to ensure the wavy
309
+ edges stay within the specified dimensions.
223
310
  """
224
- effective_radius = (width // 2) - wave_amplitude
225
- y, x = np.ogrid[-width // 2 : width // 2, -width // 2 : width // 2]
311
+ height = _default_height(height, width)
312
+ size = min(width, height)
313
+ effective_radius = (size // 2) - wave_amplitude
314
+ y, x = np.ogrid[-size // 2 : size // 2, -size // 2 : size // 2]
226
315
  distance_from_center = np.sqrt(x**2 + y**2)
227
316
  sinusoidal_boundary = effective_radius + wave_amplitude * np.sin(
228
317
  wave_frequency * np.arctan2(y, x)
229
318
  )
230
319
  mask = distance_from_center <= sinusoidal_boundary
231
- circle_wavy = np.zeros((width, width))
232
- circle_wavy[mask] = 1
233
- return Device(device_array=circle_wavy, **kwargs)
320
+ shape_array = np.zeros((size, size))
321
+ shape_array[mask] = 1
322
+ return Device(device_array=shape_array, **kwargs)
234
323
 
235
324
 
236
325
  def pie(
237
- width: int = 200, height: Optional[int] = None, arc_angle: float = 270, **kwargs
326
+ width: int = 200, height: int | None = None, arc_angle: float = 270, **kwargs: Any
238
327
  ) -> Device:
239
328
  """
240
329
  Create a Device object with a pie shape.
@@ -243,7 +332,7 @@ def pie(
243
332
  ----------
244
333
  width : int
245
334
  The width of the pie. Defaults to 200.
246
- height : Optional[int]
335
+ height : int | None
247
336
  The height of the pie. Defaults to the value of width.
248
337
  arc_angle : float
249
338
  The angle of the pie slice in degrees. Defaults to 270.
@@ -254,18 +343,24 @@ def pie(
254
343
  -------
255
344
  Device
256
345
  A Device object containing the pie shape.
346
+
347
+ Notes
348
+ -----
349
+ The arc angle starts from the positive x-axis (right) and sweeps
350
+ counter-clockwise. Angle is measured in degrees.
257
351
  """
258
- if height is None:
259
- height = width
260
- radius_x = width // 2
261
- radius_y = height // 2
352
+ height = _default_height(height, width)
353
+ ellipse_mask, radius_x, radius_y = _create_ellipse_mask(width, height)
354
+
355
+ # Calculate angle mask
262
356
  y, x = np.ogrid[-radius_y:radius_y, -radius_x:radius_x]
263
357
  angle = np.arctan2(y, x) * 180 / np.pi
264
358
  angle = (angle + 360) % 360
265
- mask = (x**2 / radius_x**2 + y**2 / radius_y**2 <= 1) & (angle <= arc_angle)
266
- pie = np.zeros((height, width))
267
- pie[mask] = 1
268
- return Device(device_array=pie, **kwargs)
359
+ angle_mask = angle <= arc_angle
360
+
361
+ shape_array = np.zeros((height, width))
362
+ shape_array[ellipse_mask & angle_mask] = 1
363
+ return Device(device_array=shape_array, **kwargs)
269
364
 
270
365
 
271
366
  def grating(
@@ -273,7 +368,7 @@ def grating(
273
368
  pitch: int = 120,
274
369
  duty_cycle: float = 0.5,
275
370
  num_gratings: int = 3,
276
- **kwargs,
371
+ **kwargs: Any,
277
372
  ) -> Device:
278
373
  """
279
374
  Create a Device object with a grating pattern.
@@ -285,7 +380,8 @@ def grating(
285
380
  pitch : int
286
381
  The pitch (period) of the grating. Defaults to 120.
287
382
  duty_cycle : float
288
- The duty cycle of the grating. Defaults to 0.5.
383
+ The duty cycle of the grating (fraction of pitch that is filled). Defaults to
384
+ 0.5.
289
385
  num_gratings : int
290
386
  The number of grating periods. Defaults to 3.
291
387
  **kwargs : dict
@@ -295,24 +391,33 @@ def grating(
295
391
  -------
296
392
  Device
297
393
  A Device object containing the grating pattern.
394
+
395
+ Notes
396
+ -----
397
+ The total width is calculated as pitch * num_gratings.
398
+ Each grating line has width = pitch * duty_cycle.
298
399
  """
299
- width = pitch * num_gratings - pitch // 2
300
- grating = np.zeros((height, width))
400
+ width = pitch * num_gratings
401
+ shape_array = np.zeros((height, width))
301
402
  grating_width = int(pitch * duty_cycle)
302
403
  for i in range(num_gratings):
303
404
  start = i * pitch
304
- grating[:, start : start + grating_width] = 1
305
- return Device(device_array=grating, **kwargs)
405
+ shape_array[:, start : start + grating_width] = 1
406
+ return Device(device_array=shape_array, **kwargs)
306
407
 
307
408
 
308
- def star(width: int = 200, num_points: int = 5, **kwargs) -> Device:
409
+ def star(
410
+ width: int = 200, height: int | None = None, num_points: int = 5, **kwargs: Any
411
+ ) -> Device:
309
412
  """
310
413
  Create a Device object with a star shape.
311
414
 
312
415
  Parameters
313
416
  ----------
314
417
  width : int
315
- The overall width and height of the star. Defaults to 200.
418
+ The overall width of the star. Defaults to 200.
419
+ height : int | None
420
+ The overall height of the star. Defaults to the value of width.
316
421
  num_points : int
317
422
  The number of points on the star. Defaults to 5.
318
423
  **kwargs : dict
@@ -322,37 +427,51 @@ def star(width: int = 200, num_points: int = 5, **kwargs) -> Device:
322
427
  -------
323
428
  Device
324
429
  A Device object containing the star shape.
430
+
431
+ Notes
432
+ -----
433
+ The inner radius is set to 50% of the outer radius by default.
325
434
  """
326
- radius_outer = width // 2
327
- radius_inner = radius_outer // 2
435
+ height = _default_height(height, width)
436
+ size = min(width, height)
437
+ radius_outer = size // 2
438
+ radius_inner = radius_outer // 2 # Inner radius is 50% of outer radius
439
+
328
440
  angles_outer = np.linspace(0, 2 * np.pi, num_points, endpoint=False) - np.pi / 2
329
441
  angles_inner = angles_outer + np.pi / num_points
442
+
330
443
  x_outer = (radius_outer * np.cos(angles_outer) + radius_outer).astype(int)
331
444
  y_outer = (radius_outer * np.sin(angles_outer) + radius_outer).astype(int)
332
445
  x_inner = (radius_inner * np.cos(angles_inner) + radius_outer).astype(int)
333
446
  y_inner = (radius_inner * np.sin(angles_inner) + radius_outer).astype(int)
447
+
334
448
  x = np.empty(2 * num_points, dtype=int)
335
449
  y = np.empty(2 * num_points, dtype=int)
336
450
  x[0::2] = x_outer
337
451
  x[1::2] = x_inner
338
452
  y[0::2] = y_outer
339
453
  y[1::2] = y_inner
340
- star = np.zeros((width, width))
454
+
455
+ shape_array = np.zeros((size, size))
341
456
  rr, cc = polygon(y, x)
342
- rr = np.clip(rr, 0, width - 1)
343
- cc = np.clip(cc, 0, width - 1)
344
- star[rr, cc] = 1
345
- return Device(device_array=star, **kwargs)
457
+ rr = np.clip(rr, 0, size - 1)
458
+ cc = np.clip(cc, 0, size - 1)
459
+ shape_array[rr, cc] = 1
460
+ return Device(device_array=shape_array, **kwargs)
346
461
 
347
462
 
348
- def poly(width: int = 200, num_points: int = 5, **kwargs) -> Device:
463
+ def poly(
464
+ width: int = 200, height: int | None = None, num_points: int = 5, **kwargs: Any
465
+ ) -> Device:
349
466
  """
350
467
  Create a Device object with a regular polygon shape.
351
468
 
352
469
  Parameters
353
470
  ----------
354
471
  width : int
355
- The overall width and height of the polygon. Defaults to 200.
472
+ The overall width of the polygon. Defaults to 200.
473
+ height : int | None
474
+ The overall height of the polygon. Defaults to the value of width.
356
475
  num_points : int
357
476
  The number of sides of the polygon. Defaults to 5.
358
477
  **kwargs : dict
@@ -363,20 +482,28 @@ def poly(width: int = 200, num_points: int = 5, **kwargs) -> Device:
363
482
  Device
364
483
  A Device object containing the regular polygon shape.
365
484
  """
366
- radius = width // 2
485
+ height = _default_height(height, width)
486
+ size = min(width, height)
487
+ radius = size // 2
488
+
367
489
  angles = np.linspace(0, 2 * np.pi, num_points, endpoint=False) - np.pi / 2
368
490
  x = (radius * np.cos(angles) + radius).astype(int)
369
491
  y = (radius * np.sin(angles) + radius).astype(int)
370
- poly = np.zeros((width, width))
492
+
493
+ shape_array = np.zeros((size, size))
371
494
  rr, cc = polygon(y, x)
372
- rr = np.clip(rr, 0, width - 1)
373
- cc = np.clip(cc, 0, width - 1)
374
- poly[rr, cc] = 1
375
- return Device(device_array=poly, **kwargs)
495
+ rr = np.clip(rr, 0, size - 1)
496
+ cc = np.clip(cc, 0, size - 1)
497
+ shape_array[rr, cc] = 1
498
+ return Device(device_array=shape_array, **kwargs)
376
499
 
377
500
 
378
501
  def radial_grating(
379
- width: int = 200, grating_skew: int = 0, num_gratings: int = 6, **kwargs
502
+ width: int = 200,
503
+ height: int | None = None,
504
+ grating_skew: int = 0,
505
+ num_gratings: int = 6,
506
+ **kwargs: Any,
380
507
  ) -> Device:
381
508
  """
382
509
  Create a Device object with a radial grating pattern.
@@ -384,7 +511,9 @@ def radial_grating(
384
511
  Parameters
385
512
  ----------
386
513
  width : int
387
- The overall width and height of the radial grating. Defaults to 200.
514
+ The overall width of the radial grating. Defaults to 200.
515
+ height : int | None
516
+ The overall height of the radial grating. Defaults to the value of width.
388
517
  grating_skew : int
389
518
  The skew angle of the grating arms. Defaults to 0.
390
519
  num_gratings : int
@@ -396,11 +525,18 @@ def radial_grating(
396
525
  -------
397
526
  Device
398
527
  A Device object containing the radial grating pattern.
528
+
529
+ Notes
530
+ -----
531
+ The grating_skew parameter controls the angular width of each arm.
399
532
  """
400
- radial_grating = np.zeros((width, width))
401
- center = width // 2
533
+ height = _default_height(height, width)
534
+ size = min(width, height)
535
+ shape_array = np.zeros((size, size))
536
+ center = size // 2
402
537
  radius = center
403
538
  theta = np.linspace(0, 2 * np.pi, num_gratings, endpoint=False)
539
+
404
540
  for angle in theta:
405
541
  x0, y0 = center, center
406
542
  x1 = int(center + radius * np.cos(angle))
@@ -412,10 +548,11 @@ def radial_grating(
412
548
  center + (radius - grating_skew) * np.sin(angle + np.pi / num_gratings)
413
549
  )
414
550
  rr, cc = polygon([y0, y1, y2], [x0, x1, x2])
415
- rr = np.clip(rr, 0, width - 1)
416
- cc = np.clip(cc, 0, width - 1)
417
- radial_grating[rr, cc] = 1
418
- return Device(device_array=radial_grating, **kwargs)
551
+ rr = np.clip(rr, 0, size - 1)
552
+ cc = np.clip(cc, 0, size - 1)
553
+ shape_array[rr, cc] = 1
554
+
555
+ return Device(device_array=shape_array, **kwargs)
419
556
 
420
557
 
421
558
  def offset_grating(
@@ -423,7 +560,7 @@ def offset_grating(
423
560
  pitch: int = 120,
424
561
  duty_cycle: float = 0.5,
425
562
  num_gratings: int = 3,
426
- **kwargs,
563
+ **kwargs: Any,
427
564
  ) -> Device:
428
565
  """
429
566
  Create a Device object with an offset grating pattern (alternating rows).
@@ -435,7 +572,8 @@ def offset_grating(
435
572
  pitch : int
436
573
  The pitch (period) of the grating. Defaults to 120.
437
574
  duty_cycle : float
438
- The duty cycle of the grating. Defaults to 0.5.
575
+ The duty cycle of the grating (fraction of pitch that is filled). Defaults to
576
+ 0.5.
439
577
  num_gratings : int
440
578
  The number of grating periods. Defaults to 3.
441
579
  **kwargs : dict
@@ -445,26 +583,36 @@ def offset_grating(
445
583
  -------
446
584
  Device
447
585
  A Device object containing the offset grating pattern.
586
+
587
+ Notes
588
+ -----
589
+ The top half of the grating is offset by pitch // 2 relative to the bottom half,
590
+ creating an alternating pattern useful for certain optical applications.
448
591
  """
449
592
  width = pitch * num_gratings
450
- grating = np.zeros((height, width))
593
+ shape_array = np.zeros((height, width))
451
594
  grating_width = int(pitch * duty_cycle)
452
595
  half_height = height // 2
596
+
597
+ # Bottom half - standard alignment
453
598
  for i in range(num_gratings):
454
599
  start = i * pitch
455
- grating[half_height:, start : start + grating_width] = 1
600
+ shape_array[half_height:, start : start + grating_width] = 1
601
+
602
+ # Top half - offset by half pitch
456
603
  for i in range(num_gratings):
457
604
  start = i * pitch + pitch // 2
458
- grating[:half_height, start : start + grating_width] = 1
459
- return Device(device_array=grating, **kwargs)
605
+ shape_array[:half_height, start : start + grating_width] = 1
606
+
607
+ return Device(device_array=shape_array, **kwargs)
460
608
 
461
609
 
462
610
  def l_grating(
463
611
  width: int = 200,
464
- height: Optional[int] = None,
612
+ height: int | None = None,
465
613
  pitch: int = 100,
466
614
  duty_cycle: float = 0.5,
467
- **kwargs,
615
+ **kwargs: Any,
468
616
  ) -> Device:
469
617
  """
470
618
  Create a Device object with an L-shaped grating pattern.
@@ -473,12 +621,12 @@ def l_grating(
473
621
  ----------
474
622
  width : int
475
623
  The width of the L-grating. Defaults to 200.
476
- height : Optional[int]
624
+ height : int | None
477
625
  The height of the L-grating. Defaults to the value of width.
478
626
  pitch : int
479
627
  The pitch (period) of the L-shapes. Defaults to 100.
480
628
  duty_cycle : float
481
- The duty cycle of the L-shapes. Defaults to 0.5.
629
+ The duty cycle of the L-shapes (fraction of pitch). Defaults to 0.5.
482
630
  **kwargs : dict
483
631
  Additional keyword arguments to be passed to the Device constructor.
484
632
 
@@ -486,21 +634,33 @@ def l_grating(
486
634
  -------
487
635
  Device
488
636
  A Device object containing the L-shaped grating pattern.
637
+
638
+ Notes
639
+ -----
640
+ Each L-shape consists of a horizontal and vertical line extending from
641
+ the diagonal, creating a stepped pattern across the device.
489
642
  """
490
- if height is None:
491
- height = width
492
- L_grating = np.zeros((height, width))
493
- num_L_shapes = min(height, width) // pitch
494
- L_width = int(pitch * duty_cycle)
495
- for i in range(num_L_shapes):
643
+ height = _default_height(height, width)
644
+ shape_array = np.zeros((height, width))
645
+ num_l_shapes = min(height, width) // pitch
646
+ l_width = int(pitch * duty_cycle)
647
+
648
+ for i in range(num_l_shapes):
496
649
  start = i * pitch
497
- L_grating[start : start + L_width, start:] = 1
498
- L_grating[start:, start : start + L_width] = 1
499
- return Device(device_array=L_grating, **kwargs)
650
+ # Horizontal bar of L extending right from diagonal
651
+ shape_array[start : start + l_width, start:] = 1
652
+ # Vertical bar of L extending down from diagonal
653
+ shape_array[start:, start : start + l_width] = 1
654
+
655
+ return Device(device_array=shape_array, **kwargs)
500
656
 
501
657
 
502
658
  def disks(
503
- rows: int = 5, cols: int = 5, disk_radius: int = 30, spacing: int = 60, **kwargs
659
+ rows: int = 5,
660
+ cols: int = 5,
661
+ disk_radius: int = 30,
662
+ spacing: int = 60,
663
+ **kwargs: Any,
504
664
  ) -> Device:
505
665
  """
506
666
  Create a Device object with a grid of uniform disks.
@@ -525,22 +685,23 @@ def disks(
525
685
  """
526
686
  grid_height = rows * (2 * disk_radius + spacing) - spacing
527
687
  grid_width = cols * (2 * disk_radius + spacing) - spacing
528
- disks = np.zeros((grid_height, grid_width))
529
- y, x = np.ogrid[-disk_radius:disk_radius, -disk_radius:disk_radius]
530
- mask = x**2 + y**2 <= disk_radius**2
688
+ shape_array = np.zeros((grid_height, grid_width))
689
+
531
690
  for row in range(rows):
532
691
  for col in range(cols):
533
692
  center_y = row * (2 * disk_radius + spacing) + disk_radius
534
693
  center_x = col * (2 * disk_radius + spacing) + disk_radius
535
- disks[
536
- center_y - disk_radius : center_y + disk_radius,
537
- center_x - disk_radius : center_x + disk_radius,
538
- ][mask] = 1
539
- return Device(device_array=disks, **kwargs)
694
+ _place_disk_in_grid(shape_array, center_y, center_x, disk_radius, value=1.0)
695
+
696
+ return Device(device_array=shape_array, **kwargs)
540
697
 
541
698
 
542
699
  def disks_offset(
543
- rows: int = 5, cols: int = 5, disk_radius: int = 30, spacing: int = 30, **kwargs
700
+ rows: int = 5,
701
+ cols: int = 5,
702
+ disk_radius: int = 30,
703
+ spacing: int = 30,
704
+ **kwargs: Any,
544
705
  ) -> Device:
545
706
  """
546
707
  Create a Device object with an offset grid of disks.
@@ -562,27 +723,26 @@ def disks_offset(
562
723
  -------
563
724
  Device
564
725
  A Device object containing an offset grid of disks.
726
+
727
+ Notes
728
+ -----
729
+ Odd-numbered rows are shifted by (disk_radius + spacing // 2) to create
730
+ an offset hexagonal packing pattern.
565
731
  """
566
732
  grid_height = rows * (2 * disk_radius + spacing) - spacing
567
733
  grid_width = (
568
734
  cols * (2 * disk_radius + spacing) - spacing + (disk_radius + spacing // 2)
569
735
  )
570
- disks_offset = np.zeros((grid_height, grid_width))
571
- y, x = np.ogrid[-disk_radius:disk_radius, -disk_radius:disk_radius]
572
- mask = x**2 + y**2 <= disk_radius**2
736
+ shape_array = np.zeros((grid_height, grid_width))
737
+
573
738
  for row in range(rows):
574
739
  for col in range(cols):
575
740
  center_y = row * (2 * disk_radius + spacing) + disk_radius
576
- center_x = (
577
- col * (2 * disk_radius + spacing)
578
- + disk_radius
579
- + (disk_radius + spacing // 2 if row % 2 == 1 else 0)
580
- )
581
- disks_offset[
582
- center_y - disk_radius : center_y + disk_radius,
583
- center_x - disk_radius : center_x + disk_radius,
584
- ][mask] = 1
585
- return Device(device_array=disks_offset, **kwargs)
741
+ offset_x = disk_radius + spacing // 2 if row % 2 == 1 else 0
742
+ center_x = col * (2 * disk_radius + spacing) + disk_radius + offset_x
743
+ _place_disk_in_grid(shape_array, center_y, center_x, disk_radius, value=1.0)
744
+
745
+ return Device(device_array=shape_array, **kwargs)
586
746
 
587
747
 
588
748
  def disks_varying(
@@ -591,7 +751,7 @@ def disks_varying(
591
751
  min_disk_radius: int = 10,
592
752
  max_disk_radius: int = 30,
593
753
  spacing: int = 30,
594
- **kwargs,
754
+ **kwargs: Any,
595
755
  ) -> Device:
596
756
  """
597
757
  Create a Device object with a grid of disks with varying radii.
@@ -615,29 +775,36 @@ def disks_varying(
615
775
  -------
616
776
  Device
617
777
  A Device object containing a grid of disks with varying radii.
778
+
779
+ Notes
780
+ -----
781
+ Disk radii vary linearly from min_disk_radius to max_disk_radius across
782
+ the grid, progressing row by row, left to right.
618
783
  """
619
784
  grid_height = rows * (2 * max_disk_radius + spacing) - spacing
620
785
  grid_width = cols * (2 * max_disk_radius + spacing) - spacing
621
- disks_varying = np.zeros((grid_height, grid_width))
786
+ shape_array = np.zeros((grid_height, grid_width))
787
+
622
788
  radius_range = np.linspace(min_disk_radius, max_disk_radius, rows * cols).reshape(
623
789
  rows, cols
624
790
  )
791
+
625
792
  for row in range(rows):
626
793
  for col in range(cols):
627
794
  disk_radius = int(radius_range[row, col])
628
- y, x = np.ogrid[-disk_radius:disk_radius, -disk_radius:disk_radius]
629
- mask = x**2 + y**2 <= disk_radius**2
630
795
  center_y = row * (2 * max_disk_radius + spacing) + max_disk_radius
631
796
  center_x = col * (2 * max_disk_radius + spacing) + max_disk_radius
632
- disks_varying[
633
- center_y - disk_radius : center_y + disk_radius,
634
- center_x - disk_radius : center_x + disk_radius,
635
- ][mask] = 1
636
- return Device(device_array=disks_varying, **kwargs)
797
+ _place_disk_in_grid(shape_array, center_y, center_x, disk_radius, value=1.0)
798
+
799
+ return Device(device_array=shape_array, **kwargs)
637
800
 
638
801
 
639
802
  def holes(
640
- rows: int = 5, cols: int = 5, hole_radius: int = 30, spacing: int = 30, **kwargs
803
+ rows: int = 5,
804
+ cols: int = 5,
805
+ hole_radius: int = 30,
806
+ spacing: int = 30,
807
+ **kwargs: Any,
641
808
  ) -> Device:
642
809
  """
643
810
  Create a Device object with a grid of uniform circular holes.
@@ -662,22 +829,23 @@ def holes(
662
829
  """
663
830
  grid_height = rows * (2 * hole_radius + spacing) - spacing
664
831
  grid_width = cols * (2 * hole_radius + spacing) - spacing
665
- holes = np.ones((grid_height, grid_width))
666
- y, x = np.ogrid[-hole_radius:hole_radius, -hole_radius:hole_radius]
667
- mask = x**2 + y**2 <= hole_radius**2
832
+ shape_array = np.ones((grid_height, grid_width))
833
+
668
834
  for row in range(rows):
669
835
  for col in range(cols):
670
836
  center_y = row * (2 * hole_radius + spacing) + hole_radius
671
837
  center_x = col * (2 * hole_radius + spacing) + hole_radius
672
- holes[
673
- center_y - hole_radius : center_y + hole_radius,
674
- center_x - hole_radius : center_x + hole_radius,
675
- ][mask] = 0
676
- return Device(device_array=holes, **kwargs)
838
+ _place_disk_in_grid(shape_array, center_y, center_x, hole_radius, value=0.0)
839
+
840
+ return Device(device_array=shape_array, **kwargs)
677
841
 
678
842
 
679
843
  def holes_offset(
680
- rows: int = 5, cols: int = 5, hole_radius: int = 30, spacing: int = 30, **kwargs
844
+ rows: int = 5,
845
+ cols: int = 5,
846
+ hole_radius: int = 30,
847
+ spacing: int = 30,
848
+ **kwargs: Any,
681
849
  ) -> Device:
682
850
  """
683
851
  Create a Device object with an offset grid of circular holes.
@@ -699,27 +867,26 @@ def holes_offset(
699
867
  -------
700
868
  Device
701
869
  A Device object containing an offset grid of circular holes.
870
+
871
+ Notes
872
+ -----
873
+ Odd-numbered rows are shifted by (hole_radius + spacing // 2) to create
874
+ an offset hexagonal packing pattern.
702
875
  """
703
876
  grid_height = rows * (2 * hole_radius + spacing) - spacing
704
877
  grid_width = (
705
878
  cols * (2 * hole_radius + spacing) - spacing + (hole_radius + spacing // 2)
706
879
  )
707
- holes_offset = np.ones((grid_height, grid_width))
708
- y, x = np.ogrid[-hole_radius:hole_radius, -hole_radius:hole_radius]
709
- mask = x**2 + y**2 <= hole_radius**2
880
+ shape_array = np.ones((grid_height, grid_width))
881
+
710
882
  for row in range(rows):
711
883
  for col in range(cols):
712
884
  center_y = row * (2 * hole_radius + spacing) + hole_radius
713
- center_x = (
714
- col * (2 * hole_radius + spacing)
715
- + hole_radius
716
- + (hole_radius + spacing // 2 if row % 2 == 1 else 0)
717
- )
718
- holes_offset[
719
- center_y - hole_radius : center_y + hole_radius,
720
- center_x - hole_radius : center_x + hole_radius,
721
- ][mask] = 0
722
- return Device(device_array=holes_offset, **kwargs)
885
+ offset_x = hole_radius + spacing // 2 if row % 2 == 1 else 0
886
+ center_x = col * (2 * hole_radius + spacing) + hole_radius + offset_x
887
+ _place_disk_in_grid(shape_array, center_y, center_x, hole_radius, value=0.0)
888
+
889
+ return Device(device_array=shape_array, **kwargs)
723
890
 
724
891
 
725
892
  def holes_varying(
@@ -728,7 +895,7 @@ def holes_varying(
728
895
  min_hole_radius: int = 10,
729
896
  max_hole_radius: int = 30,
730
897
  spacing: int = 30,
731
- **kwargs,
898
+ **kwargs: Any,
732
899
  ) -> Device:
733
900
  """
734
901
  Create a Device object with a grid of circular holes with varying radii.
@@ -752,22 +919,25 @@ def holes_varying(
752
919
  -------
753
920
  Device
754
921
  A Device object containing a grid of circular holes with varying radii.
922
+
923
+ Notes
924
+ -----
925
+ Hole radii vary linearly from min_hole_radius to max_hole_radius across
926
+ the grid, progressing row by row, left to right.
755
927
  """
756
928
  grid_height = rows * (2 * max_hole_radius + spacing) - spacing
757
929
  grid_width = cols * (2 * max_hole_radius + spacing) - spacing
758
- holes_varying = np.ones((grid_height, grid_width))
930
+ shape_array = np.ones((grid_height, grid_width))
931
+
759
932
  radius_range = np.linspace(min_hole_radius, max_hole_radius, rows * cols).reshape(
760
933
  rows, cols
761
934
  )
935
+
762
936
  for row in range(rows):
763
937
  for col in range(cols):
764
938
  hole_radius = int(radius_range[row, col])
765
- y, x = np.ogrid[-hole_radius:hole_radius, -hole_radius:hole_radius]
766
- mask = x**2 + y**2 <= hole_radius**2
767
939
  center_y = row * (2 * max_hole_radius + spacing) + max_hole_radius
768
940
  center_x = col * (2 * max_hole_radius + spacing) + max_hole_radius
769
- holes_varying[
770
- center_y - hole_radius : center_y + hole_radius,
771
- center_x - hole_radius : center_x + hole_radius,
772
- ][mask] = 0
773
- return Device(device_array=holes_varying, **kwargs)
941
+ _place_disk_in_grid(shape_array, center_y, center_x, hole_radius, value=0.0)
942
+
943
+ return Device(device_array=shape_array, **kwargs)