prefab 1.1.4__py3-none-any.whl → 1.1.6__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,13 +1,18 @@
1
- """Provides functions to create a Device from various data sources."""
1
+ """Functions to create a Device from various data sources."""
2
2
 
3
3
  import re
4
+ from typing import TYPE_CHECKING, Optional
4
5
 
5
6
  import cv2
6
7
  import gdstk
7
8
  import numpy as np
8
9
 
9
10
  from . import geometry
10
- from .device import Device
11
+ from .device import BufferSpec, Device
12
+
13
+ if TYPE_CHECKING:
14
+ import gdsfactory as gf
15
+ import tidy3d as td
11
16
 
12
17
 
13
18
  def from_ndarray(
@@ -19,12 +24,12 @@ def from_ndarray(
19
24
  Parameters
20
25
  ----------
21
26
  ndarray : np.ndarray
22
- The input array representing the device layout.
23
- resolution : float, optional
27
+ The input array representing the device geometry.
28
+ resolution : float
24
29
  The resolution of the ndarray in nanometers per pixel, defaulting to 1.0 nm per
25
30
  pixel. If specified, the input array will be resized based on this resolution to
26
31
  match the desired physical size.
27
- binarize : bool, optional
32
+ binarize : bool
28
33
  If True, the input array will be binarized (converted to binary values) before
29
34
  conversion to a Device object. This is useful for processing grayscale arrays
30
35
  into binary masks. Defaults to True.
@@ -34,8 +39,7 @@ def from_ndarray(
34
39
  Returns
35
40
  -------
36
41
  Device
37
- A Device object representing the input array, after optional resizing and
38
- binarization.
42
+ A Device object representing the input array, after resizing and binarization.
39
43
  """
40
44
  device_array = ndarray
41
45
  if resolution != 1.0:
@@ -48,7 +52,7 @@ def from_ndarray(
48
52
 
49
53
 
50
54
  def from_img(
51
- img_path: str, img_width_nm: int = None, binarize: bool = True, **kwargs
55
+ img_path: str, img_width_nm: Optional[int] = None, binarize: bool = True, **kwargs
52
56
  ) -> Device:
53
57
  """
54
58
  Create a Device from an image file.
@@ -57,10 +61,10 @@ def from_img(
57
61
  ----------
58
62
  img_path : str
59
63
  The path to the image file to be converted into a Device object.
60
- img_width_nm : int, optional
64
+ img_width_nm : Optional[int]
61
65
  The width of the image in nanometers. If specified, the Device will be resized
62
66
  to this width while maintaining aspect ratio. If None, no resizing is performed.
63
- binarize : bool, optional
67
+ binarize : bool
64
68
  If True, the image will be binarized (converted to binary values) before
65
69
  conversion to a Device object. This is useful for processing grayscale images
66
70
  into binary masks. Defaults to True.
@@ -88,9 +92,9 @@ def from_gds(
88
92
  gds_path: str,
89
93
  cell_name: str,
90
94
  gds_layer: tuple[int, int] = (1, 0),
91
- bounds: tuple[tuple[int, int], tuple[int, int]] = None,
95
+ bounds: Optional[tuple[tuple[float, float], tuple[float, float]]] = None,
92
96
  **kwargs,
93
- ):
97
+ ) -> Device:
94
98
  """
95
99
  Create a Device from a GDS cell.
96
100
 
@@ -100,10 +104,10 @@ def from_gds(
100
104
  The file path to the GDS file.
101
105
  cell_name : str
102
106
  The name of the cell within the GDS file to be converted into a Device object.
103
- gds_layer : tuple[int, int], optional
107
+ gds_layer : tuple[int, int]
104
108
  A tuple specifying the layer and datatype to be used from the GDS file. Defaults
105
109
  to (1, 0).
106
- bounds : tuple[tuple[int, int], tuple[int, int]], optional
110
+ bounds : Optional[tuple[tuple[float, float], tuple[float, float]]]
107
111
  A tuple specifying the bounds for cropping the cell before conversion, formatted
108
112
  as ((min_x, min_y), (max_x, max_y)), in units of the GDS file. If None, the
109
113
  entire cell is used.
@@ -117,7 +121,7 @@ def from_gds(
117
121
  processing based on the specified layer.
118
122
  """
119
123
  gdstk_library = gdstk.read_gds(gds_path)
120
- gdstk_cell = gdstk_library[cell_name]
124
+ gdstk_cell = gdstk_library[cell_name] # type: ignore
121
125
  device_array = _gdstk_to_device_array(
122
126
  gdstk_cell=gdstk_cell, gds_layer=gds_layer, bounds=bounds
123
127
  )
@@ -127,7 +131,7 @@ def from_gds(
127
131
  def from_gdstk(
128
132
  gdstk_cell: gdstk.Cell,
129
133
  gds_layer: tuple[int, int] = (1, 0),
130
- bounds: tuple[tuple[int, int], tuple[int, int]] = None,
134
+ bounds: Optional[tuple[tuple[float, float], tuple[float, float]]] = None,
131
135
  **kwargs,
132
136
  ):
133
137
  """
@@ -137,12 +141,12 @@ def from_gdstk(
137
141
  ----------
138
142
  gdstk_cell : gdstk.Cell
139
143
  The gdstk.Cell object to be converted into a Device object.
140
- gds_layer : tuple[int, int], optional
144
+ gds_layer : tuple[int, int]
141
145
  A tuple specifying the layer and datatype to be used from the cell. Defaults to
142
146
  (1, 0).
143
- bounds : tuple[tuple[int, int], tuple[int, int]], optional
147
+ bounds : tuple[tuple[float, float], tuple[float, float]]
144
148
  A tuple specifying the bounds for cropping the cell before conversion, formatted
145
- as ((min_x, min_y), (max_x, max_y)), in units of the GDS file. If None, the
149
+ as ((min_x, min_y), (max_x, max_y)), in units of the GDS cell. If None, the
146
150
  entire cell is used.
147
151
  **kwargs
148
152
  Additional keyword arguments to be passed to the Device constructor.
@@ -162,7 +166,7 @@ def from_gdstk(
162
166
  def _gdstk_to_device_array(
163
167
  gdstk_cell: gdstk.Cell,
164
168
  gds_layer: tuple[int, int] = (1, 0),
165
- bounds: tuple[tuple[int, int], tuple[int, int]] = None,
169
+ bounds: Optional[tuple[tuple[float, float], tuple[float, float]]] = None,
166
170
  ) -> np.ndarray:
167
171
  """
168
172
  Convert a gdstk.Cell to a device array.
@@ -171,9 +175,9 @@ def _gdstk_to_device_array(
171
175
  ----------
172
176
  gdstk_cell : gdstk.Cell
173
177
  The gdstk.Cell object to be converted.
174
- gds_layer : tuple[int, int], optional
178
+ gds_layer : tuple[int, int]
175
179
  The layer and datatype to be used from the cell. Defaults to (1, 0).
176
- bounds : tuple[tuple[int, int], tuple[int, int]], optional
180
+ bounds : Optional[tuple[tuple[float, float], tuple[float, float]]]
177
181
  Bounds for cropping the cell, formatted as ((min_x, min_y), (max_x, max_y)).
178
182
  If None, the entire cell is used.
179
183
 
@@ -190,11 +194,17 @@ def _gdstk_to_device_array(
190
194
  polygons = gdstk.slice(
191
195
  polygons, position=(bounds[0][1], bounds[1][1]), axis="y"
192
196
  )[1]
193
- bounds = tuple(tuple(x * 1000 for x in sub_tuple) for sub_tuple in bounds)
197
+ bounds = (
198
+ (int(1000 * bounds[0][0]), int(1000 * bounds[0][1])),
199
+ (int(1000 * bounds[1][0]), int(1000 * bounds[1][1])),
200
+ )
194
201
  else:
195
- bounds = tuple(
196
- tuple(1000 * x for x in sub_tuple)
197
- for sub_tuple in gdstk_cell.bounding_box()
202
+ bbox = gdstk_cell.bounding_box()
203
+ if bbox is None:
204
+ raise ValueError("Cell has no geometry, cannot determine bounds.")
205
+ bounds = (
206
+ (float(1000 * bbox[0][0]), float(1000 * bbox[0][1])),
207
+ (float(1000 * bbox[1][0]), float(1000 * bbox[1][1])),
198
208
  )
199
209
  contours = [
200
210
  np.array(
@@ -220,7 +230,7 @@ def _gdstk_to_device_array(
220
230
 
221
231
 
222
232
  def from_gdsfactory(
223
- component: "gf.Component", # noqa: F821
233
+ component: "gf.Component",
224
234
  **kwargs,
225
235
  ) -> Device:
226
236
  """
@@ -265,13 +275,8 @@ def from_gdsfactory(
265
275
  contours = [
266
276
  np.array(
267
277
  [
268
- [
269
- [
270
- int(1000 * vertex[0] - bounds[0][0]),
271
- int(1000 * vertex[1] - bounds[0][1]),
272
- ]
273
- ]
274
- for vertex in polygon
278
+ [[int(1000 * x - bounds[0][0]), int(1000 * y - bounds[0][1])]]
279
+ for x, y in polygon # type: ignore
275
280
  ]
276
281
  )
277
282
  for polygon in polygons
@@ -288,10 +293,10 @@ def from_gdsfactory(
288
293
 
289
294
  def from_sem(
290
295
  sem_path: str,
291
- sem_resolution: float = None,
292
- sem_resolution_key: str = None,
296
+ sem_resolution: Optional[float] = None,
297
+ sem_resolution_key: Optional[str] = None,
293
298
  binarize: bool = False,
294
- bounds: tuple[tuple[int, int], tuple[int, int]] = None,
299
+ bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
295
300
  **kwargs,
296
301
  ) -> Device:
297
302
  """
@@ -301,18 +306,18 @@ def from_sem(
301
306
  ----------
302
307
  sem_path : str
303
308
  The file path to the SEM image.
304
- sem_resolution : float, optional
309
+ sem_resolution : Optional[float]
305
310
  The resolution of the SEM image in nanometers per pixel. If not provided, it
306
311
  will be extracted from the image metadata using the `sem_resolution_key`.
307
- sem_resolution_key : str, optional
312
+ sem_resolution_key : Optional[str]
308
313
  The key to look for in the SEM image metadata to extract the resolution.
309
314
  Required if `sem_resolution` is not provided.
310
- binarize : bool, optional
315
+ binarize : bool
311
316
  If True, the SEM image will be binarized (converted to binary values) before
312
317
  conversion to a Device object. This is needed for processing grayscale images
313
318
  into binary masks. Defaults to False.
314
- bounds : tuple[tuple[int, int], tuple[int, int]], optional
315
- A tuple specifying the bounds for cropping the image before conversion,
319
+ bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
320
+ A tuple specifying the bounds in nm for cropping the image before conversion,
316
321
  formatted as ((min_x, min_y), (max_x, max_y)). If None, the entire image is
317
322
  used.
318
323
  **kwargs
@@ -337,14 +342,55 @@ def from_sem(
337
342
  device_array = cv2.resize(
338
343
  device_array, dsize=(0, 0), fx=sem_resolution, fy=sem_resolution
339
344
  )
345
+
340
346
  if bounds is not None:
341
- device_array = device_array[
342
- device_array.shape[0] - bounds[1][1] : device_array.shape[0] - bounds[0][1],
343
- bounds[0][0] : bounds[1][0],
344
- ]
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
+
345
376
  if binarize:
346
377
  device_array = geometry.binarize_sem(device_array)
347
- return Device(device_array=device_array, **kwargs)
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)
348
394
 
349
395
 
350
396
  def get_sem_resolution(sem_path: str, sem_resolution_key: str) -> float:
@@ -352,7 +398,7 @@ def get_sem_resolution(sem_path: str, sem_resolution_key: str) -> float:
352
398
  Extracts the resolution of a scanning electron microscope (SEM) image from its
353
399
  metadata.
354
400
 
355
- Note:
401
+ Notes
356
402
  -----
357
403
  This function is used internally and may not be useful for most users.
358
404
 
@@ -388,9 +434,11 @@ def get_sem_resolution(sem_path: str, sem_resolution_key: str) -> float:
388
434
 
389
435
 
390
436
  def from_tidy3d(
391
- tidy3d_sim: "tidy3d.Simulation", # noqa: F821
392
- eps_threshold: float,
437
+ tidy3d_sim: "td.Simulation",
438
+ eps: float,
393
439
  z: float,
440
+ freq: float,
441
+ buffer_width: float = 0.1,
394
442
  **kwargs,
395
443
  ) -> Device:
396
444
  """
@@ -400,10 +448,16 @@ def from_tidy3d(
400
448
  ----------
401
449
  tidy3d_sim : tidy3d.Simulation
402
450
  The Tidy3D simulation object.
403
- eps_threshold : float
404
- The threshold value for the permittivity to binarize the device array.
451
+ eps : float
452
+ The permittivity of the layer to extract from the simulation.
405
453
  z : float
406
- The z-coordinate at which to extract the permittivity.
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.
407
461
  **kwargs
408
462
  Additional keyword arguments to be passed to the Device constructor.
409
463
 
@@ -415,9 +469,6 @@ def from_tidy3d(
415
469
 
416
470
  Raises
417
471
  ------
418
- ValueError
419
- If the z-coordinate is outside the bounds of the simulation size in the
420
- z-direction.
421
472
  ImportError
422
473
  If the tidy3d package is not installed.
423
474
  """
@@ -429,29 +480,24 @@ def from_tidy3d(
429
480
  "try `pip install tidy3d`."
430
481
  ) from None
431
482
 
432
- if not (
433
- tidy3d_sim.center[2] - tidy3d_sim.size[2] / 2
434
- <= z
435
- <= tidy3d_sim.center[2] + tidy3d_sim.size[2] / 2
436
- ):
437
- raise ValueError(
438
- f"z={z} is outside the bounds of the simulation size in the z-direction."
439
- )
440
-
441
- x = np.arange(
442
- tidy3d_sim.center[0] - tidy3d_sim.size[0] / 2,
443
- tidy3d_sim.center[0] + tidy3d_sim.size[0] / 2,
483
+ X = np.arange(
484
+ tidy3d_sim.bounds[0][0] - buffer_width,
485
+ tidy3d_sim.bounds[1][0] + buffer_width,
444
486
  0.001,
445
487
  )
446
- y = np.arange(
447
- tidy3d_sim.center[1] - tidy3d_sim.size[1] / 2,
448
- tidy3d_sim.center[1] + tidy3d_sim.size[1] / 2,
488
+ Y = np.arange(
489
+ tidy3d_sim.bounds[0][1] - buffer_width,
490
+ tidy3d_sim.bounds[1][1] + buffer_width,
449
491
  0.001,
450
492
  )
451
- z = np.array([z])
493
+ Z = np.array([z])
452
494
 
453
- grid = Grid(boundaries=Coords(x=x, y=y, z=z))
454
- eps = np.real(tidy3d_sim.epsilon_on_grid(grid=grid, coord_key="boundaries").values)
455
- device_array = geometry.binarize_hard(device_array=eps, eta=eps_threshold)[:, :, 0]
456
- device_array = np.fliplr(np.rot90(device_array, k=-1))
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)
457
503
  return Device(device_array=device_array, **kwargs)