prefab 0.4.7__py3-none-any.whl → 1.1.8__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 ADDED
@@ -0,0 +1,503 @@
1
+ """Functions to create a Device from various data sources."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ import cv2
7
+ import gdstk
8
+ import numpy as np
9
+
10
+ from . import geometry
11
+ from .device import BufferSpec, Device
12
+
13
+ if TYPE_CHECKING:
14
+ import gdsfactory as gf
15
+ import tidy3d as td
16
+
17
+
18
+ def from_ndarray(
19
+ ndarray: np.ndarray, resolution: float = 1.0, binarize: bool = True, **kwargs
20
+ ) -> Device:
21
+ """
22
+ Create a Device from an ndarray.
23
+
24
+ Parameters
25
+ ----------
26
+ ndarray : np.ndarray
27
+ The input array representing the device geometry.
28
+ resolution : float
29
+ The resolution of the ndarray in nanometers per pixel, defaulting to 1.0 nm per
30
+ pixel. If specified, the input array will be resized based on this resolution to
31
+ match the desired physical size.
32
+ binarize : bool
33
+ If True, the input array will be binarized (converted to binary values) before
34
+ conversion to a Device object. This is useful for processing grayscale arrays
35
+ into binary masks. Defaults to True.
36
+ **kwargs
37
+ Additional keyword arguments to be passed to the Device constructor.
38
+
39
+ Returns
40
+ -------
41
+ Device
42
+ A Device object representing the input array, after resizing and binarization.
43
+ """
44
+ device_array = ndarray
45
+ if resolution != 1.0:
46
+ device_array = cv2.resize(
47
+ device_array, dsize=(0, 0), fx=resolution, fy=resolution
48
+ )
49
+ if binarize:
50
+ device_array = geometry.binarize_hard(device_array)
51
+ return Device(device_array=device_array, **kwargs)
52
+
53
+
54
+ def from_img(
55
+ img_path: str, img_width_nm: Optional[int] = None, binarize: bool = True, **kwargs
56
+ ) -> Device:
57
+ """
58
+ Create a Device from an image file.
59
+
60
+ Parameters
61
+ ----------
62
+ img_path : str
63
+ The path to the image file to be converted into a Device object.
64
+ img_width_nm : Optional[int]
65
+ The width of the image in nanometers. If specified, the Device will be resized
66
+ to this width while maintaining aspect ratio. If None, no resizing is performed.
67
+ binarize : bool
68
+ If True, the image will be binarized (converted to binary values) before
69
+ conversion to a Device object. This is useful for processing grayscale images
70
+ into binary masks. Defaults to True.
71
+ **kwargs
72
+ Additional keyword arguments to be passed to the Device constructor.
73
+
74
+ Returns
75
+ -------
76
+ Device
77
+ A Device object representing the processed image, after optional resizing and
78
+ binarization.
79
+ """
80
+ device_array = cv2.imread(img_path, flags=cv2.IMREAD_GRAYSCALE) / 255
81
+ if img_width_nm is not None:
82
+ resolution = img_width_nm / device_array.shape[1]
83
+ device_array = cv2.resize(
84
+ device_array, dsize=(0, 0), fx=resolution, fy=resolution
85
+ )
86
+ if binarize:
87
+ device_array = geometry.binarize_hard(device_array)
88
+ return Device(device_array=device_array, **kwargs)
89
+
90
+
91
+ def from_gds(
92
+ gds_path: str,
93
+ cell_name: str,
94
+ gds_layer: tuple[int, int] = (1, 0),
95
+ bounds: Optional[tuple[tuple[float, float], tuple[float, float]]] = None,
96
+ **kwargs,
97
+ ) -> Device:
98
+ """
99
+ Create a Device from a GDS cell.
100
+
101
+ Parameters
102
+ ----------
103
+ gds_path : str
104
+ The file path to the GDS file.
105
+ cell_name : str
106
+ The name of the cell within the GDS file to be converted into a Device object.
107
+ gds_layer : tuple[int, int]
108
+ A tuple specifying the layer and datatype to be used from the GDS file. Defaults
109
+ to (1, 0).
110
+ bounds : Optional[tuple[tuple[float, float], tuple[float, float]]]
111
+ A tuple specifying the bounds for cropping the cell before conversion, formatted
112
+ as ((min_x, min_y), (max_x, max_y)), in units of the GDS file. If None, the
113
+ entire cell is used.
114
+ **kwargs
115
+ Additional keyword arguments to be passed to the Device constructor.
116
+
117
+ Returns
118
+ -------
119
+ Device
120
+ A Device object representing the specified cell from the GDS file, after
121
+ processing based on the specified layer.
122
+ """
123
+ gdstk_library = gdstk.read_gds(gds_path)
124
+ gdstk_cell = gdstk_library[cell_name] # type: ignore
125
+ device_array = _gdstk_to_device_array(
126
+ gdstk_cell=gdstk_cell, gds_layer=gds_layer, bounds=bounds
127
+ )
128
+ return Device(device_array=device_array, **kwargs)
129
+
130
+
131
+ def from_gdstk(
132
+ gdstk_cell: gdstk.Cell,
133
+ gds_layer: tuple[int, int] = (1, 0),
134
+ bounds: Optional[tuple[tuple[float, float], tuple[float, float]]] = None,
135
+ **kwargs,
136
+ ):
137
+ """
138
+ Create a Device from a gdstk cell.
139
+
140
+ Parameters
141
+ ----------
142
+ gdstk_cell : gdstk.Cell
143
+ The gdstk.Cell object to be converted into a Device object.
144
+ gds_layer : tuple[int, int]
145
+ A tuple specifying the layer and datatype to be used from the cell. Defaults to
146
+ (1, 0).
147
+ bounds : tuple[tuple[float, float], tuple[float, float]]
148
+ A tuple specifying the bounds for cropping the cell before conversion, formatted
149
+ as ((min_x, min_y), (max_x, max_y)), in units of the GDS cell. If None, the
150
+ entire cell is used.
151
+ **kwargs
152
+ Additional keyword arguments to be passed to the Device constructor.
153
+
154
+ Returns
155
+ -------
156
+ Device
157
+ A Device object representing the gdstk.Cell, after processing based on the
158
+ specified layer.
159
+ """
160
+ device_array = _gdstk_to_device_array(
161
+ gdstk_cell=gdstk_cell, gds_layer=gds_layer, bounds=bounds
162
+ )
163
+ return Device(device_array=device_array, **kwargs)
164
+
165
+
166
+ def _gdstk_to_device_array(
167
+ gdstk_cell: gdstk.Cell,
168
+ gds_layer: tuple[int, int] = (1, 0),
169
+ bounds: Optional[tuple[tuple[float, float], tuple[float, float]]] = None,
170
+ ) -> np.ndarray:
171
+ """
172
+ Convert a gdstk.Cell to a device array.
173
+
174
+ Parameters
175
+ ----------
176
+ gdstk_cell : gdstk.Cell
177
+ The gdstk.Cell object to be converted.
178
+ gds_layer : tuple[int, int]
179
+ The layer and datatype to be used from the cell. Defaults to (1, 0).
180
+ bounds : Optional[tuple[tuple[float, float], tuple[float, float]]]
181
+ Bounds for cropping the cell, formatted as ((min_x, min_y), (max_x, max_y)).
182
+ If None, the entire cell is used.
183
+
184
+ Returns
185
+ -------
186
+ np.ndarray
187
+ The resulting device array.
188
+ """
189
+ polygons = gdstk_cell.get_polygons(layer=gds_layer[0], datatype=gds_layer[1])
190
+ if bounds:
191
+ polygons = gdstk.slice(
192
+ polygons, position=(bounds[0][0], bounds[1][0]), axis="x"
193
+ )[1]
194
+ polygons = gdstk.slice(
195
+ polygons, position=(bounds[0][1], bounds[1][1]), axis="y"
196
+ )[1]
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
+ )
201
+ else:
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])),
208
+ )
209
+ contours = [
210
+ np.array(
211
+ [
212
+ [
213
+ [
214
+ int(1000 * vertex[0] - bounds[0][0]),
215
+ int(1000 * vertex[1] - bounds[0][1]),
216
+ ]
217
+ ]
218
+ for vertex in polygon.points
219
+ ],
220
+ )
221
+ for polygon in polygons
222
+ ]
223
+ device_array = np.zeros(
224
+ (int(bounds[1][1] - bounds[0][1]), int(bounds[1][0] - bounds[0][0])),
225
+ dtype=np.uint8,
226
+ )
227
+ cv2.fillPoly(img=device_array, pts=contours, color=(1, 1, 1))
228
+ device_array = np.flipud(device_array)
229
+ 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)