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/read.py CHANGED
@@ -1,22 +1,52 @@
1
- """Functions to create a Device from various data sources."""
1
+ """
2
+ Functions to create Device objects from various data sources.
2
3
 
3
- import re
4
- from typing import TYPE_CHECKING, Optional
4
+ This module provides utilities for loading device geometries from multiple formats,
5
+ including image files, numpy arrays, and GDS layout files. All functions return
6
+ Device objects with optional preprocessing capabilities.
7
+ """
8
+
9
+ from typing import Any, cast
5
10
 
6
11
  import cv2
7
12
  import gdstk
8
13
  import numpy as np
9
14
 
10
15
  from . import geometry
11
- from .device import BufferSpec, Device
16
+ from .device import Device
17
+
18
+ # Conversion factor from GDS units (micrometers) to nanometers
19
+ _GDS_UM_TO_NM = 1000
20
+
21
+
22
+ def _binarize_if_needed(
23
+ device_array: np.ndarray[Any, Any], binarize: bool
24
+ ) -> np.ndarray[Any, Any]:
25
+ """
26
+ Conditionally binarize a device array.
12
27
 
13
- if TYPE_CHECKING:
14
- import gdsfactory as gf
15
- import tidy3d as td
28
+ Parameters
29
+ ----------
30
+ device_array : np.ndarray
31
+ The array to potentially binarize.
32
+ binarize : bool
33
+ If True, binarize the array using hard thresholding.
34
+
35
+ Returns
36
+ -------
37
+ np.ndarray
38
+ The binarized array if binarize is True, otherwise the original array.
39
+ """
40
+ if binarize:
41
+ return geometry.binarize_hard(np.asarray(device_array, dtype=np.float64))
42
+ return device_array
16
43
 
17
44
 
18
45
  def from_ndarray(
19
- ndarray: np.ndarray, resolution: float = 1.0, binarize: bool = True, **kwargs
46
+ ndarray: np.ndarray[Any, Any],
47
+ resolution: float = 1.0,
48
+ binarize: bool = True,
49
+ **kwargs: Any,
20
50
  ) -> Device:
21
51
  """
22
52
  Create a Device from an ndarray.
@@ -46,13 +76,12 @@ def from_ndarray(
46
76
  device_array = cv2.resize(
47
77
  device_array, dsize=(0, 0), fx=resolution, fy=resolution
48
78
  )
49
- if binarize:
50
- device_array = geometry.binarize_hard(device_array)
79
+ device_array = _binarize_if_needed(device_array, binarize)
51
80
  return Device(device_array=device_array, **kwargs)
52
81
 
53
82
 
54
83
  def from_img(
55
- img_path: str, img_width_nm: Optional[int] = None, binarize: bool = True, **kwargs
84
+ img_path: str, img_width_nm: int | None = None, binarize: bool = True, **kwargs: Any
56
85
  ) -> Device:
57
86
  """
58
87
  Create a Device from an image file.
@@ -83,8 +112,7 @@ def from_img(
83
112
  device_array = cv2.resize(
84
113
  device_array, dsize=(0, 0), fx=resolution, fy=resolution
85
114
  )
86
- if binarize:
87
- device_array = geometry.binarize_hard(device_array)
115
+ device_array = _binarize_if_needed(device_array, binarize)
88
116
  return Device(device_array=device_array, **kwargs)
89
117
 
90
118
 
@@ -92,8 +120,8 @@ def from_gds(
92
120
  gds_path: str,
93
121
  cell_name: str,
94
122
  gds_layer: tuple[int, int] = (1, 0),
95
- bounds: Optional[tuple[tuple[float, float], tuple[float, float]]] = None,
96
- **kwargs,
123
+ bounds: tuple[tuple[float, float], tuple[float, float]] | None = None,
124
+ **kwargs: Any,
97
125
  ) -> Device:
98
126
  """
99
127
  Create a Device from a GDS cell.
@@ -121,7 +149,7 @@ def from_gds(
121
149
  processing based on the specified layer.
122
150
  """
123
151
  gdstk_library = gdstk.read_gds(gds_path)
124
- gdstk_cell = gdstk_library[cell_name] # type: ignore
152
+ gdstk_cell = cast(gdstk.Cell, gdstk_library[cell_name]) # pyright: ignore[reportIndexIssue]
125
153
  device_array = _gdstk_to_device_array(
126
154
  gdstk_cell=gdstk_cell, gds_layer=gds_layer, bounds=bounds
127
155
  )
@@ -131,9 +159,9 @@ def from_gds(
131
159
  def from_gdstk(
132
160
  gdstk_cell: gdstk.Cell,
133
161
  gds_layer: tuple[int, int] = (1, 0),
134
- bounds: Optional[tuple[tuple[float, float], tuple[float, float]]] = None,
135
- **kwargs,
136
- ):
162
+ bounds: tuple[tuple[float, float], tuple[float, float]] | None = None,
163
+ **kwargs: Any,
164
+ ) -> Device:
137
165
  """
138
166
  Create a Device from a gdstk cell.
139
167
 
@@ -144,7 +172,7 @@ def from_gdstk(
144
172
  gds_layer : tuple[int, int]
145
173
  A tuple specifying the layer and datatype to be used from the cell. Defaults to
146
174
  (1, 0).
147
- bounds : tuple[tuple[float, float], tuple[float, float]]
175
+ bounds : Optional[tuple[tuple[float, float], tuple[float, float]]]
148
176
  A tuple specifying the bounds for cropping the cell before conversion, formatted
149
177
  as ((min_x, min_y), (max_x, max_y)), in units of the GDS cell. If None, the
150
178
  entire cell is used.
@@ -166,8 +194,8 @@ def from_gdstk(
166
194
  def _gdstk_to_device_array(
167
195
  gdstk_cell: gdstk.Cell,
168
196
  gds_layer: tuple[int, int] = (1, 0),
169
- bounds: Optional[tuple[tuple[float, float], tuple[float, float]]] = None,
170
- ) -> np.ndarray:
197
+ bounds: tuple[tuple[float, float], tuple[float, float]] | None = None,
198
+ ) -> np.ndarray[Any, Any]:
171
199
  """
172
200
  Convert a gdstk.Cell to a device array.
173
201
 
@@ -195,24 +223,24 @@ def _gdstk_to_device_array(
195
223
  polygons, position=(bounds[0][1], bounds[1][1]), axis="y"
196
224
  )[1]
197
225
  bounds = (
198
- (int(1000 * bounds[0][0]), int(1000 * bounds[0][1])),
199
- (int(1000 * bounds[1][0]), int(1000 * bounds[1][1])),
226
+ (int(_GDS_UM_TO_NM * bounds[0][0]), int(_GDS_UM_TO_NM * bounds[0][1])),
227
+ (int(_GDS_UM_TO_NM * bounds[1][0]), int(_GDS_UM_TO_NM * bounds[1][1])),
200
228
  )
201
229
  else:
202
230
  bbox = gdstk_cell.bounding_box()
203
231
  if bbox is None:
204
232
  raise ValueError("Cell has no geometry, cannot determine bounds.")
205
233
  bounds = (
206
- (float(1000 * bbox[0][0]), float(1000 * bbox[0][1])),
207
- (float(1000 * bbox[1][0]), float(1000 * bbox[1][1])),
234
+ (float(_GDS_UM_TO_NM * bbox[0][0]), float(_GDS_UM_TO_NM * bbox[0][1])),
235
+ (float(_GDS_UM_TO_NM * bbox[1][0]), float(_GDS_UM_TO_NM * bbox[1][1])),
208
236
  )
209
237
  contours = [
210
238
  np.array(
211
239
  [
212
240
  [
213
241
  [
214
- int(1000 * vertex[0] - bounds[0][0]),
215
- int(1000 * vertex[1] - bounds[0][1]),
242
+ int(_GDS_UM_TO_NM * vertex[0] - bounds[0][0]),
243
+ int(_GDS_UM_TO_NM * vertex[1] - bounds[0][1]),
216
244
  ]
217
245
  ]
218
246
  for vertex in polygon.points
@@ -224,280 +252,6 @@ def _gdstk_to_device_array(
224
252
  (int(bounds[1][1] - bounds[0][1]), int(bounds[1][0] - bounds[0][0])),
225
253
  dtype=np.uint8,
226
254
  )
227
- cv2.fillPoly(img=device_array, pts=contours, color=(1, 1, 1))
255
+ _ = cv2.fillPoly(device_array, contours, (1,))
228
256
  device_array = np.flipud(device_array)
229
257
  return device_array
230
-
231
-
232
- def from_gdsfactory(
233
- component: "gf.Component",
234
- **kwargs,
235
- ) -> Device:
236
- """
237
- Create a Device from a gdsfactory component.
238
-
239
- Parameters
240
- ----------
241
- component : gf.Component
242
- The gdsfactory component to be converted into a Device object.
243
- **kwargs
244
- Additional keyword arguments to be passed to the Device constructor.
245
-
246
- Returns
247
- -------
248
- Device
249
- A Device object representing the gdsfactory component.
250
-
251
- Raises
252
- ------
253
- ImportError
254
- If the gdsfactory package is not installed.
255
- """
256
- try:
257
- import gdsfactory as gf # noqa: F401
258
- except ImportError:
259
- raise ImportError(
260
- "The gdsfactory package is required to use this function; "
261
- "try `pip install gdsfactory`."
262
- ) from None
263
-
264
- bounds = (
265
- (component.xmin * 1000, component.ymin * 1000),
266
- (component.xmax * 1000, component.ymax * 1000),
267
- )
268
-
269
- polygons = [
270
- polygon
271
- for polygons_list in component.get_polygons_points().values()
272
- for polygon in polygons_list
273
- ]
274
-
275
- contours = [
276
- np.array(
277
- [
278
- [[int(1000 * x - bounds[0][0]), int(1000 * y - bounds[0][1])]]
279
- for x, y in polygon # type: ignore
280
- ]
281
- )
282
- for polygon in polygons
283
- ]
284
-
285
- device_array = np.zeros(
286
- (int(bounds[1][1] - bounds[0][1]), int(bounds[1][0] - bounds[0][0])),
287
- dtype=np.uint8,
288
- )
289
- cv2.fillPoly(img=device_array, pts=contours, color=(1, 1, 1))
290
- device_array = np.flipud(device_array)
291
- return Device(device_array=device_array, **kwargs)
292
-
293
-
294
- def from_sem(
295
- sem_path: str,
296
- sem_resolution: Optional[float] = None,
297
- sem_resolution_key: Optional[str] = None,
298
- binarize: bool = False,
299
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
300
- **kwargs,
301
- ) -> Device:
302
- """
303
- Create a Device from a scanning electron microscope (SEM) image file.
304
-
305
- Parameters
306
- ----------
307
- sem_path : str
308
- The file path to the SEM image.
309
- sem_resolution : Optional[float]
310
- The resolution of the SEM image in nanometers per pixel. If not provided, it
311
- will be extracted from the image metadata using the `sem_resolution_key`.
312
- sem_resolution_key : Optional[str]
313
- The key to look for in the SEM image metadata to extract the resolution.
314
- Required if `sem_resolution` is not provided.
315
- binarize : bool
316
- If True, the SEM image will be binarized (converted to binary values) before
317
- conversion to a Device object. This is needed for processing grayscale images
318
- into binary masks. Defaults to False.
319
- bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
320
- A tuple specifying the bounds in nm for cropping the image before conversion,
321
- formatted as ((min_x, min_y), (max_x, max_y)). If None, the entire image is
322
- used.
323
- **kwargs
324
- Additional keyword arguments to be passed to the Device constructor.
325
-
326
- Returns
327
- -------
328
- Device
329
- A Device object representing the processed SEM image.
330
-
331
- Raises
332
- ------
333
- ValueError
334
- If neither `sem_resolution` nor `sem_resolution_key` is provided.
335
- """
336
- if sem_resolution is None and sem_resolution_key is not None:
337
- sem_resolution = get_sem_resolution(sem_path, sem_resolution_key)
338
- elif sem_resolution is None:
339
- raise ValueError("Either sem_resolution or resolution_key must be provided.")
340
-
341
- device_array = cv2.imread(sem_path, flags=cv2.IMREAD_GRAYSCALE)
342
- device_array = cv2.resize(
343
- device_array, dsize=(0, 0), fx=sem_resolution, fy=sem_resolution
344
- )
345
-
346
- if bounds is not None:
347
- pad_left = max(0, -bounds[0][0])
348
- pad_right = max(0, bounds[1][0] - device_array.shape[1])
349
- pad_bottom = max(0, -bounds[0][1])
350
- pad_top = max(0, bounds[1][1] - device_array.shape[0])
351
-
352
- if pad_left or pad_right or pad_top or pad_bottom:
353
- device_array = np.pad(
354
- device_array,
355
- ((pad_top, pad_bottom), (pad_left, pad_right)),
356
- mode="constant",
357
- constant_values=0,
358
- )
359
-
360
- start_x = max(0, bounds[0][0] + pad_left)
361
- end_x = min(device_array.shape[1], bounds[1][0] + pad_left)
362
- start_y = max(0, device_array.shape[0] - (bounds[1][1] + pad_top))
363
- end_y = min(
364
- device_array.shape[0], device_array.shape[0] - (bounds[0][1] + pad_top)
365
- )
366
-
367
- if start_x >= end_x or start_y >= end_y:
368
- raise ValueError(
369
- "Invalid bounds resulted in zero-size array: "
370
- f"x=[{start_x}, {end_x}], "
371
- f"y=[{start_y}, {end_y}]"
372
- )
373
-
374
- device_array = device_array[start_y:end_y, start_x:end_x]
375
-
376
- if binarize:
377
- device_array = geometry.binarize_sem(device_array)
378
-
379
- buffer_spec = BufferSpec(
380
- mode={
381
- "top": "none",
382
- "bottom": "none",
383
- "left": "none",
384
- "right": "none",
385
- },
386
- thickness={
387
- "top": 0,
388
- "bottom": 0,
389
- "left": 0,
390
- "right": 0,
391
- },
392
- )
393
- return Device(device_array=device_array, buffer_spec=buffer_spec, **kwargs)
394
-
395
-
396
- def get_sem_resolution(sem_path: str, sem_resolution_key: str) -> float:
397
- """
398
- Extracts the resolution of a scanning electron microscope (SEM) image from its
399
- metadata.
400
-
401
- Notes
402
- -----
403
- This function is used internally and may not be useful for most users.
404
-
405
- Parameters
406
- ----------
407
- sem_path : str
408
- The file path to the SEM image.
409
- sem_resolution_key : str
410
- The key to look for in the SEM image metadata to extract the resolution.
411
-
412
- Returns
413
- -------
414
- float
415
- The resolution of the SEM image in nanometers per pixel.
416
-
417
- Raises
418
- ------
419
- ValueError
420
- If the resolution key is not found in the SEM image metadata.
421
- """
422
- with open(sem_path, "rb") as file:
423
- resolution_key_bytes = sem_resolution_key.encode("utf-8")
424
- for line in file:
425
- if resolution_key_bytes in line:
426
- line_str = line.decode("utf-8")
427
- match = re.search(r"-?\d+(\.\d+)?", line_str)
428
- if match:
429
- value = float(match.group())
430
- if value > 100:
431
- value /= 1000
432
- return value
433
- raise ValueError(f"Resolution key '{sem_resolution_key}' not found in {sem_path}.")
434
-
435
-
436
- def from_tidy3d(
437
- tidy3d_sim: "td.Simulation",
438
- eps: float,
439
- z: float,
440
- freq: float,
441
- buffer_width: float = 0.1,
442
- **kwargs,
443
- ) -> Device:
444
- """
445
- Create a Device from a Tidy3D simulation.
446
-
447
- Parameters
448
- ----------
449
- tidy3d_sim : tidy3d.Simulation
450
- The Tidy3D simulation object.
451
- eps : float
452
- The permittivity of the layer to extract from the simulation.
453
- z : float
454
- The z-coordinate of the layer to extract from the simulation.
455
- freq : float
456
- The frequency at which to extract the permittivity.
457
- buffer_width : float
458
- The width of the buffer region around the layer to extract from the
459
- simulation. Defaults to 0.1 µm. This is useful for ensuring the inputs/outputs
460
- of the simulation are not affected by prediction.
461
- **kwargs
462
- Additional keyword arguments to be passed to the Device constructor.
463
-
464
- Returns
465
- -------
466
- Device
467
- A Device object representing the permittivity cross-section at the specified
468
- z-coordinate for the Tidy3D simulation.
469
-
470
- Raises
471
- ------
472
- ImportError
473
- If the tidy3d package is not installed.
474
- """
475
- try:
476
- from tidy3d import Coords, Grid
477
- except ImportError:
478
- raise ImportError(
479
- "The tidy3d package is required to use this function; "
480
- "try `pip install tidy3d`."
481
- ) from None
482
-
483
- X = np.arange(
484
- tidy3d_sim.bounds[0][0] - buffer_width,
485
- tidy3d_sim.bounds[1][0] + buffer_width,
486
- 0.001,
487
- )
488
- Y = np.arange(
489
- tidy3d_sim.bounds[0][1] - buffer_width,
490
- tidy3d_sim.bounds[1][1] + buffer_width,
491
- 0.001,
492
- )
493
- Z = np.array([z])
494
-
495
- grid = Grid(attrs={}, boundaries=Coords(attrs={}, x=X, y=Y, z=Z))
496
- eps_array = np.real(
497
- tidy3d_sim.epsilon_on_grid(grid=grid, coord_key="boundaries", freq=freq).values
498
- )
499
- device_array = geometry.binarize_hard(device_array=eps_array, eta=eps - 0.1)[
500
- :, :, 0
501
- ]
502
- device_array = np.rot90(device_array, k=1)
503
- return Device(device_array=device_array, **kwargs)