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/__init__.py +1 -1
- prefab/__main__.py +29 -24
- prefab/compare.py +49 -61
- prefab/device.py +163 -394
- prefab/geometry.py +102 -137
- prefab/models.py +19 -48
- prefab/predict.py +68 -55
- prefab/py.typed +0 -0
- prefab/read.py +57 -303
- prefab/shapes.py +357 -187
- {prefab-1.3.0.dist-info → prefab-1.4.0.dist-info}/METADATA +21 -35
- prefab-1.4.0.dist-info/RECORD +15 -0
- prefab-1.3.0.dist-info/RECORD +0 -14
- {prefab-1.3.0.dist-info → prefab-1.4.0.dist-info}/WHEEL +0 -0
- {prefab-1.3.0.dist-info → prefab-1.4.0.dist-info}/entry_points.txt +0 -0
- {prefab-1.3.0.dist-info → prefab-1.4.0.dist-info}/licenses/LICENSE +0 -0
prefab/device.py
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Core device representation and manipulation for photonic geometries.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
This module provides the Device class for representing planar photonic device
|
|
5
|
+
geometries and performing operations on them. It includes buffer zone management,
|
|
6
|
+
prediction and correction of nanofabrication outcomes, visualization capabilities,
|
|
7
|
+
and export functions to various formats. The module also supports geometric
|
|
8
|
+
transformations (rotation, dilation, erosion), image processing operations (blur,
|
|
9
|
+
binarization), and comparison tools for analyzing fabrication deviations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, Literal
|
|
4
13
|
|
|
5
14
|
import cv2
|
|
6
15
|
import gdstk
|
|
7
16
|
import matplotlib.pyplot as plt
|
|
8
17
|
import numpy as np
|
|
18
|
+
import numpy.typing as npt
|
|
9
19
|
from matplotlib.axes import Axes
|
|
10
20
|
from matplotlib.patches import Rectangle
|
|
11
21
|
from PIL import Image
|
|
12
|
-
from pydantic import BaseModel, Field,
|
|
22
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
13
23
|
from scipy.ndimage import distance_transform_edt
|
|
14
|
-
from skimage import measure
|
|
15
24
|
|
|
16
|
-
from . import
|
|
25
|
+
from . import geometry
|
|
17
26
|
from .models import Model
|
|
18
27
|
from .predict import predict_array
|
|
19
28
|
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
import gdsfactory as gf
|
|
22
|
-
import tidy3d as td
|
|
23
|
-
|
|
24
29
|
Image.MAX_IMAGE_PIXELS = None
|
|
25
30
|
|
|
26
31
|
|
|
@@ -33,7 +38,7 @@ class BufferSpec(BaseModel):
|
|
|
33
38
|
providing extra space for device fabrication processes or for ensuring that the
|
|
34
39
|
device is isolated from surrounding structures.
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
Attributes
|
|
37
42
|
----------
|
|
38
43
|
mode : dict[str, str]
|
|
39
44
|
A dictionary that defines the buffer mode for each side of the device
|
|
@@ -53,8 +58,8 @@ class BufferSpec(BaseModel):
|
|
|
53
58
|
allowed values ('constant', 'edge', 'none'). Or if any of the thickness values
|
|
54
59
|
are negative.
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
Examples
|
|
62
|
+
--------
|
|
58
63
|
import prefab as pf
|
|
59
64
|
|
|
60
65
|
buffer_spec = pf.BufferSpec(
|
|
@@ -90,23 +95,25 @@ class BufferSpec(BaseModel):
|
|
|
90
95
|
}
|
|
91
96
|
)
|
|
92
97
|
|
|
93
|
-
@
|
|
94
|
-
|
|
98
|
+
@field_validator("mode", mode="before")
|
|
99
|
+
@classmethod
|
|
100
|
+
def check_mode(cls, v: dict[str, str]) -> dict[str, str]:
|
|
95
101
|
allowed_modes = ["constant", "edge", "none"]
|
|
96
102
|
if not all(mode in allowed_modes for mode in v.values()):
|
|
97
103
|
raise ValueError(f"Buffer mode must be one of {allowed_modes}, got '{v}'.")
|
|
98
104
|
return v
|
|
99
105
|
|
|
100
|
-
@
|
|
101
|
-
|
|
106
|
+
@field_validator("thickness")
|
|
107
|
+
@classmethod
|
|
108
|
+
def check_thickness(cls, v: dict[str, int]) -> dict[str, int]:
|
|
102
109
|
if not all(t >= 0 for t in v.values()):
|
|
103
110
|
raise ValueError("All thickness values must be greater than or equal to 0.")
|
|
104
111
|
return v
|
|
105
112
|
|
|
106
113
|
@model_validator(mode="after")
|
|
107
|
-
def check_none_thickness(
|
|
108
|
-
mode =
|
|
109
|
-
thickness =
|
|
114
|
+
def check_none_thickness(self) -> "BufferSpec":
|
|
115
|
+
mode = self.mode
|
|
116
|
+
thickness = self.thickness
|
|
110
117
|
for side in mode:
|
|
111
118
|
if mode[side] == "none" and thickness[side] != 0:
|
|
112
119
|
raise ValueError(
|
|
@@ -116,22 +123,22 @@ class BufferSpec(BaseModel):
|
|
|
116
123
|
raise ValueError(
|
|
117
124
|
f"Mode must be 'none' when thickness is 0 for {side} side"
|
|
118
125
|
)
|
|
119
|
-
return
|
|
126
|
+
return self
|
|
120
127
|
|
|
121
128
|
|
|
122
129
|
class Device(BaseModel):
|
|
123
|
-
device_array:
|
|
130
|
+
device_array: npt.NDArray[Any] = Field(...)
|
|
124
131
|
buffer_spec: BufferSpec = Field(default_factory=BufferSpec)
|
|
125
132
|
|
|
126
133
|
class Config:
|
|
127
|
-
arbitrary_types_allowed = True
|
|
134
|
+
arbitrary_types_allowed: bool = True
|
|
128
135
|
|
|
129
136
|
@property
|
|
130
137
|
def shape(self) -> tuple[int, ...]:
|
|
131
138
|
return tuple(self.device_array.shape)
|
|
132
139
|
|
|
133
140
|
def __init__(
|
|
134
|
-
self, device_array:
|
|
141
|
+
self, device_array: npt.NDArray[Any], buffer_spec: BufferSpec | None = None
|
|
135
142
|
):
|
|
136
143
|
"""
|
|
137
144
|
Represents the planar geometry of a photonic device design that will have its
|
|
@@ -174,10 +181,10 @@ class Device(BaseModel):
|
|
|
174
181
|
)
|
|
175
182
|
self._initial_processing()
|
|
176
183
|
|
|
177
|
-
def __call__(self, *args, **kwargs):
|
|
184
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Axes:
|
|
178
185
|
return self.plot(*args, **kwargs)
|
|
179
186
|
|
|
180
|
-
def _initial_processing(self):
|
|
187
|
+
def _initial_processing(self) -> None:
|
|
181
188
|
buffer_thickness = self.buffer_spec.thickness
|
|
182
189
|
buffer_mode = self.buffer_spec.mode
|
|
183
190
|
|
|
@@ -212,7 +219,8 @@ class Device(BaseModel):
|
|
|
212
219
|
self.device_array = np.expand_dims(self.device_array, axis=-1)
|
|
213
220
|
|
|
214
221
|
@model_validator(mode="before")
|
|
215
|
-
|
|
222
|
+
@classmethod
|
|
223
|
+
def check_device_array(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
216
224
|
device_array = values.get("device_array")
|
|
217
225
|
if not isinstance(device_array, np.ndarray):
|
|
218
226
|
raise ValueError("device_array must be a numpy ndarray.")
|
|
@@ -254,11 +262,10 @@ class Device(BaseModel):
|
|
|
254
262
|
----------
|
|
255
263
|
model : Model
|
|
256
264
|
The model to use for prediction, representing a specific fabrication process
|
|
257
|
-
and dataset. This model encapsulates details about the fabrication foundry
|
|
258
|
-
process,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
is tailored to specific fabrication parameters.
|
|
265
|
+
and dataset. This model encapsulates details about the fabrication foundry
|
|
266
|
+
and process, as defined in `models.py`. Each model is associated with a
|
|
267
|
+
version and dataset that detail its creation and the data it was trained on,
|
|
268
|
+
ensuring the prediction is tailored to specific fabrication parameters.
|
|
262
269
|
binarize : bool
|
|
263
270
|
If True, the predicted device geometry will be binarized using a threshold
|
|
264
271
|
method. This is useful for converting probabilistic predictions into binary
|
|
@@ -301,11 +308,10 @@ class Device(BaseModel):
|
|
|
301
308
|
----------
|
|
302
309
|
model : Model
|
|
303
310
|
The model to use for correction, representing a specific fabrication process
|
|
304
|
-
and dataset. This model encapsulates details about the fabrication foundry
|
|
305
|
-
process,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
is tailored to specific fabrication parameters.
|
|
311
|
+
and dataset. This model encapsulates details about the fabrication foundry
|
|
312
|
+
and process, as defined in `models.py`. Each model is associated with a
|
|
313
|
+
version and dataset that detail its creation and the data it was trained on,
|
|
314
|
+
ensuring the correction is tailored to specific fabrication parameters.
|
|
309
315
|
binarize : bool
|
|
310
316
|
If True, the corrected device geometry will be binarized using a threshold
|
|
311
317
|
method. This is useful for converting probabilistic corrections into binary
|
|
@@ -330,98 +336,7 @@ class Device(BaseModel):
|
|
|
330
336
|
)
|
|
331
337
|
return self.model_copy(update={"device_array": correction_array})
|
|
332
338
|
|
|
333
|
-
def
|
|
334
|
-
self,
|
|
335
|
-
model: Model,
|
|
336
|
-
) -> "Device":
|
|
337
|
-
"""
|
|
338
|
-
Simulate the appearance of the device as if viewed under a scanning electron
|
|
339
|
-
microscope (SEM).
|
|
340
|
-
|
|
341
|
-
This method applies a specified machine learning model to transform the device
|
|
342
|
-
geometry into a style that resembles an SEM image. This can be useful for
|
|
343
|
-
visualizing how the device might appear under an SEM, which is often used for
|
|
344
|
-
inspecting the surface and composition of materials at high magnification.
|
|
345
|
-
|
|
346
|
-
Parameters
|
|
347
|
-
----------
|
|
348
|
-
model : Model
|
|
349
|
-
The model to use for SEMulation, representing a specific fabrication process
|
|
350
|
-
and dataset. This model encapsulates details about the fabrication foundry,
|
|
351
|
-
process, material, technology, thickness, and sidewall presence, as defined
|
|
352
|
-
in `models.py`. Each model is associated with a version and dataset that
|
|
353
|
-
detail its creation and the data it was trained on, ensuring the SEMulation
|
|
354
|
-
is tailored to specific fabrication parameters.
|
|
355
|
-
|
|
356
|
-
Notes
|
|
357
|
-
-----
|
|
358
|
-
The salt-and-pepper noise is added manually until the model is trained to
|
|
359
|
-
generate this noise (not a big priority).
|
|
360
|
-
|
|
361
|
-
Returns
|
|
362
|
-
-------
|
|
363
|
-
Device
|
|
364
|
-
A new instance of the Device class with its geometry transformed to simulate
|
|
365
|
-
an SEM image style.
|
|
366
|
-
|
|
367
|
-
Raises
|
|
368
|
-
------
|
|
369
|
-
RuntimeError
|
|
370
|
-
If the prediction service returns an error or if the response from the
|
|
371
|
-
service cannot be processed correctly.
|
|
372
|
-
"""
|
|
373
|
-
semulated_array = predict_array(
|
|
374
|
-
device_array=self.device_array,
|
|
375
|
-
model=model,
|
|
376
|
-
model_type="s",
|
|
377
|
-
binarize=False,
|
|
378
|
-
)
|
|
379
|
-
semulated_array += np.random.normal(0, 0.03, semulated_array.shape)
|
|
380
|
-
return self.model_copy(update={"device_array": semulated_array})
|
|
381
|
-
|
|
382
|
-
def segment(
|
|
383
|
-
self,
|
|
384
|
-
model: Model,
|
|
385
|
-
) -> "Device":
|
|
386
|
-
"""
|
|
387
|
-
Segment a scanning electron microscope (SEM) image into a binary mask.
|
|
388
|
-
|
|
389
|
-
This method applies a specified machine learning model to transform a grayscale
|
|
390
|
-
SEM image into a binary mask, where 1 represents the device structure and 0
|
|
391
|
-
represents the background. This is useful for extracting the device geometry
|
|
392
|
-
from experimental SEM images for analysis or comparison with design intent.
|
|
393
|
-
|
|
394
|
-
Parameters
|
|
395
|
-
----------
|
|
396
|
-
model : Model
|
|
397
|
-
The model to use for segmentation, representing a specific fabrication
|
|
398
|
-
process and dataset. This model encapsulates details about the fabrication
|
|
399
|
-
foundry, process, material, technology, thickness, and sidewall presence, as
|
|
400
|
-
defined in `models.py`. Each model is associated with a version and dataset
|
|
401
|
-
that detail its creation and the data it was trained on, ensuring the
|
|
402
|
-
segmentation is tailored to specific fabrication parameters.
|
|
403
|
-
|
|
404
|
-
Returns
|
|
405
|
-
-------
|
|
406
|
-
Device
|
|
407
|
-
A new instance of the Device class with its geometry transformed into a
|
|
408
|
-
binary mask.
|
|
409
|
-
|
|
410
|
-
Raises
|
|
411
|
-
------
|
|
412
|
-
RuntimeError
|
|
413
|
-
If the prediction service returns an error or if the response from the
|
|
414
|
-
service cannot be processed correctly.
|
|
415
|
-
"""
|
|
416
|
-
segmented_array = predict_array(
|
|
417
|
-
device_array=self.normalize().device_array,
|
|
418
|
-
model=model,
|
|
419
|
-
model_type="b",
|
|
420
|
-
binarize=False,
|
|
421
|
-
)
|
|
422
|
-
return self.model_copy(update={"device_array": segmented_array})
|
|
423
|
-
|
|
424
|
-
def to_ndarray(self) -> np.ndarray:
|
|
339
|
+
def to_ndarray(self) -> npt.NDArray[Any]:
|
|
425
340
|
"""
|
|
426
341
|
Converts the device geometry to an ndarray.
|
|
427
342
|
|
|
@@ -449,7 +364,11 @@ class Device(BaseModel):
|
|
|
449
364
|
]
|
|
450
365
|
return ndarray
|
|
451
366
|
|
|
452
|
-
def to_img(
|
|
367
|
+
def to_img(
|
|
368
|
+
self,
|
|
369
|
+
img_path: str = "prefab_device.png",
|
|
370
|
+
bounds: tuple[tuple[int, int], tuple[int, int]] | None = None,
|
|
371
|
+
) -> None:
|
|
453
372
|
"""
|
|
454
373
|
Exports the device geometry as an image file.
|
|
455
374
|
|
|
@@ -462,8 +381,49 @@ class Device(BaseModel):
|
|
|
462
381
|
img_path : str
|
|
463
382
|
The path where the image file will be saved. If not specified, the image is
|
|
464
383
|
saved as "prefab_device.png" in the current directory.
|
|
465
|
-
|
|
466
|
-
|
|
384
|
+
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
385
|
+
Specifies the bounds for cropping the device geometry, formatted as
|
|
386
|
+
((min_x, min_y), (max_x, max_y)). Negative values count from the end
|
|
387
|
+
(e.g., -50 means 50 pixels from the edge). If 'max_x' or 'max_y' is set
|
|
388
|
+
to "end", it will be replaced with the corresponding dimension size of the
|
|
389
|
+
device array. If None, the entire device geometry is exported.
|
|
390
|
+
"""
|
|
391
|
+
device_array = self.flatten().to_ndarray()
|
|
392
|
+
|
|
393
|
+
if bounds is not None:
|
|
394
|
+
min_x, min_y = bounds[0]
|
|
395
|
+
max_x, max_y = bounds[1]
|
|
396
|
+
|
|
397
|
+
# Handle negative indices (count from end)
|
|
398
|
+
if min_x < 0:
|
|
399
|
+
min_x = device_array.shape[1] + min_x
|
|
400
|
+
if min_y < 0:
|
|
401
|
+
min_y = device_array.shape[0] + min_y
|
|
402
|
+
if max_x != "end" and max_x < 0:
|
|
403
|
+
max_x = device_array.shape[1] + max_x
|
|
404
|
+
if max_y != "end" and max_y < 0:
|
|
405
|
+
max_y = device_array.shape[0] + max_y
|
|
406
|
+
|
|
407
|
+
# Clamp to valid range
|
|
408
|
+
min_x = max(min_x, 0)
|
|
409
|
+
min_y = max(min_y, 0)
|
|
410
|
+
max_x = "end" if max_x == "end" else min(max_x, device_array.shape[1])
|
|
411
|
+
max_y = "end" if max_y == "end" else min(max_y, device_array.shape[0])
|
|
412
|
+
max_x = device_array.shape[1] if max_x == "end" else max_x
|
|
413
|
+
max_y = device_array.shape[0] if max_y == "end" else max_y
|
|
414
|
+
|
|
415
|
+
if min_x >= max_x or min_y >= max_y:
|
|
416
|
+
raise ValueError(
|
|
417
|
+
"Invalid bounds: min values must be less than max values. "
|
|
418
|
+
+ f"Got min_x={min_x}, max_x={max_x}, min_y={min_y}, max_y={max_y}"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
device_array = device_array[
|
|
422
|
+
device_array.shape[0] - max_y : device_array.shape[0] - min_y,
|
|
423
|
+
min_x:max_x,
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
_ = cv2.imwrite(img_path, (255 * device_array).astype(np.uint8))
|
|
467
427
|
print(f"Saved Device image to '{img_path}'")
|
|
468
428
|
|
|
469
429
|
def to_gds(
|
|
@@ -473,7 +433,7 @@ class Device(BaseModel):
|
|
|
473
433
|
gds_layer: tuple[int, int] = (1, 0),
|
|
474
434
|
contour_approx_mode: int = 2,
|
|
475
435
|
origin: tuple[float, float] = (0.0, 0.0),
|
|
476
|
-
):
|
|
436
|
+
) -> None:
|
|
477
437
|
"""
|
|
478
438
|
Exports the device geometry as a GDSII file.
|
|
479
439
|
|
|
@@ -507,7 +467,7 @@ class Device(BaseModel):
|
|
|
507
467
|
)
|
|
508
468
|
print(f"Saving GDS to '{gds_path}'...")
|
|
509
469
|
gdstk_library = gdstk.Library()
|
|
510
|
-
gdstk_library.add(gdstk_cell)
|
|
470
|
+
_ = gdstk_library.add(gdstk_cell)
|
|
511
471
|
gdstk_library.write_gds(outfile=gds_path, max_points=8190)
|
|
512
472
|
|
|
513
473
|
def to_gdstk(
|
|
@@ -516,7 +476,7 @@ class Device(BaseModel):
|
|
|
516
476
|
gds_layer: tuple[int, int] = (1, 0),
|
|
517
477
|
contour_approx_mode: int = 2,
|
|
518
478
|
origin: tuple[float, float] = (0.0, 0.0),
|
|
519
|
-
):
|
|
479
|
+
) -> gdstk.Cell:
|
|
520
480
|
"""
|
|
521
481
|
Converts the device geometry to a GDSTK cell object.
|
|
522
482
|
|
|
@@ -628,85 +588,11 @@ class Device(BaseModel):
|
|
|
628
588
|
datatype=gds_layer[1],
|
|
629
589
|
)
|
|
630
590
|
for polygon in processed_polygons:
|
|
631
|
-
cell.add(polygon)
|
|
591
|
+
_ = cell.add(polygon)
|
|
632
592
|
|
|
633
593
|
return cell
|
|
634
594
|
|
|
635
|
-
def
|
|
636
|
-
"""
|
|
637
|
-
Convert the device geometry to a gdsfactory Component.
|
|
638
|
-
|
|
639
|
-
Returns
|
|
640
|
-
-------
|
|
641
|
-
gf.Component
|
|
642
|
-
A gdsfactory Component object representing the device geometry.
|
|
643
|
-
|
|
644
|
-
Raises
|
|
645
|
-
------
|
|
646
|
-
ImportError
|
|
647
|
-
If the gdsfactory package is not installed.
|
|
648
|
-
"""
|
|
649
|
-
try:
|
|
650
|
-
import gdsfactory as gf
|
|
651
|
-
except ImportError:
|
|
652
|
-
raise ImportError(
|
|
653
|
-
"The gdsfactory package is required to use this function; "
|
|
654
|
-
"try `pip install gdsfactory`."
|
|
655
|
-
) from None
|
|
656
|
-
|
|
657
|
-
device_array = np.rot90(self.to_ndarray(), k=-1)
|
|
658
|
-
return gf.read.from_np(device_array, nm_per_pixel=1)
|
|
659
|
-
|
|
660
|
-
def to_tidy3d(
|
|
661
|
-
self,
|
|
662
|
-
eps0: float,
|
|
663
|
-
thickness: float,
|
|
664
|
-
) -> "td.Structure":
|
|
665
|
-
"""
|
|
666
|
-
Convert the device geometry to a Tidy3D Structure.
|
|
667
|
-
|
|
668
|
-
Parameters
|
|
669
|
-
----------
|
|
670
|
-
eps0 : float
|
|
671
|
-
The permittivity value to assign to the device array.
|
|
672
|
-
thickness : float
|
|
673
|
-
The thickness of the device in the z-direction.
|
|
674
|
-
|
|
675
|
-
Returns
|
|
676
|
-
-------
|
|
677
|
-
td.Structure
|
|
678
|
-
A Tidy3D Structure object representing the device geometry.
|
|
679
|
-
|
|
680
|
-
Raises
|
|
681
|
-
------
|
|
682
|
-
ImportError
|
|
683
|
-
If the tidy3d package is not installed.
|
|
684
|
-
"""
|
|
685
|
-
try:
|
|
686
|
-
from tidy3d import Box, CustomMedium, SpatialDataArray, Structure, inf
|
|
687
|
-
except ImportError:
|
|
688
|
-
raise ImportError(
|
|
689
|
-
"The tidy3d package is required to use this function; "
|
|
690
|
-
"try `pip install tidy3d`."
|
|
691
|
-
) from None
|
|
692
|
-
|
|
693
|
-
X = np.linspace(-self.shape[1] / 2000, self.shape[1] / 2000, self.shape[1])
|
|
694
|
-
Y = np.linspace(-self.shape[0] / 2000, self.shape[0] / 2000, self.shape[0])
|
|
695
|
-
Z = np.array([0])
|
|
696
|
-
|
|
697
|
-
device_array = np.rot90(np.fliplr(self.device_array), k=1)
|
|
698
|
-
eps_array = np.where(device_array >= 1.0, eps0, device_array)
|
|
699
|
-
eps_array = np.where(eps_array < 1.0, 1.0, eps_array)
|
|
700
|
-
eps_dataset = SpatialDataArray(eps_array, coords=dict(x=X, y=Y, z=Z))
|
|
701
|
-
medium = CustomMedium.from_eps_raw(eps_dataset)
|
|
702
|
-
return Structure(
|
|
703
|
-
geometry=Box(center=(0, 0, 0), size=(inf, inf, thickness), attrs={}),
|
|
704
|
-
medium=medium,
|
|
705
|
-
name="device",
|
|
706
|
-
attrs={},
|
|
707
|
-
)
|
|
708
|
-
|
|
709
|
-
def to_3d(self, thickness_nm: int) -> np.ndarray:
|
|
595
|
+
def to_3d(self, thickness_nm: int) -> npt.NDArray[Any]:
|
|
710
596
|
"""
|
|
711
597
|
Convert the 2D device geometry into a 3D representation.
|
|
712
598
|
|
|
@@ -740,62 +626,33 @@ class Device(BaseModel):
|
|
|
740
626
|
layered_array[:, :, i] = dt_interp >= 0
|
|
741
627
|
return layered_array
|
|
742
628
|
|
|
743
|
-
def to_stl(self, thickness_nm: int, filename: str = "prefab_device.stl"):
|
|
744
|
-
"""
|
|
745
|
-
Export the device geometry as an STL file.
|
|
746
|
-
|
|
747
|
-
Parameters
|
|
748
|
-
----------
|
|
749
|
-
thickness_nm : int
|
|
750
|
-
The thickness of the 3D representation in nanometers.
|
|
751
|
-
filename : str
|
|
752
|
-
The name of the STL file to save. Defaults to "prefab_device.stl".
|
|
753
|
-
|
|
754
|
-
Raises
|
|
755
|
-
------
|
|
756
|
-
ValueError
|
|
757
|
-
If the thickness is not a positive integer.
|
|
758
|
-
ImportError
|
|
759
|
-
If the numpy-stl package is not installed.
|
|
760
|
-
"""
|
|
761
|
-
try:
|
|
762
|
-
from stl import mesh # type: ignore
|
|
763
|
-
except ImportError:
|
|
764
|
-
raise ImportError(
|
|
765
|
-
"The stl package is required to use this function; "
|
|
766
|
-
"try `pip install numpy-stl`."
|
|
767
|
-
) from None
|
|
768
|
-
|
|
769
|
-
if thickness_nm <= 0:
|
|
770
|
-
raise ValueError("Thickness must be a positive integer.")
|
|
771
|
-
|
|
772
|
-
layered_array = self.to_3d(thickness_nm)
|
|
773
|
-
layered_array = np.pad(
|
|
774
|
-
layered_array, ((0, 0), (0, 0), (10, 10)), mode="constant"
|
|
775
|
-
)
|
|
776
|
-
verts, faces, _, _ = measure.marching_cubes(layered_array, level=0.5)
|
|
777
|
-
cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
|
|
778
|
-
for i, f in enumerate(faces):
|
|
779
|
-
for j in range(3):
|
|
780
|
-
cube.vectors[i][j] = verts[f[j], :]
|
|
781
|
-
cube.save(filename)
|
|
782
|
-
print(f"Saved Device to '{filename}'")
|
|
783
|
-
|
|
784
629
|
def _plot_base(
|
|
785
630
|
self,
|
|
786
|
-
plot_array:
|
|
631
|
+
plot_array: npt.NDArray[Any],
|
|
787
632
|
show_buffer: bool,
|
|
788
|
-
bounds:
|
|
789
|
-
ax:
|
|
790
|
-
**kwargs,
|
|
633
|
+
bounds: tuple[tuple[int, int], tuple[int, int]] | None,
|
|
634
|
+
ax: Axes | None,
|
|
635
|
+
**kwargs: Any,
|
|
791
636
|
) -> tuple[plt.cm.ScalarMappable, Axes]:
|
|
792
637
|
if ax is None:
|
|
793
638
|
_, ax = plt.subplots()
|
|
794
|
-
ax.set_ylabel("y (nm)")
|
|
795
|
-
ax.set_xlabel("x (nm)")
|
|
639
|
+
_ = ax.set_ylabel("y (nm)")
|
|
640
|
+
_ = ax.set_xlabel("x (nm)")
|
|
796
641
|
|
|
797
642
|
min_x, min_y = (0, 0) if bounds is None else bounds[0]
|
|
798
643
|
max_x, max_y = plot_array.shape[::-1] if bounds is None else bounds[1]
|
|
644
|
+
|
|
645
|
+
# Handle negative indices (count from end)
|
|
646
|
+
if min_x < 0:
|
|
647
|
+
min_x = plot_array.shape[1] + min_x
|
|
648
|
+
if min_y < 0:
|
|
649
|
+
min_y = plot_array.shape[0] + min_y
|
|
650
|
+
if max_x != "end" and max_x < 0:
|
|
651
|
+
max_x = plot_array.shape[1] + max_x
|
|
652
|
+
if max_y != "end" and max_y < 0:
|
|
653
|
+
max_y = plot_array.shape[0] + max_y
|
|
654
|
+
|
|
655
|
+
# Clamp to valid range
|
|
799
656
|
min_x = max(min_x, 0)
|
|
800
657
|
min_y = max(min_y, 0)
|
|
801
658
|
max_x = "end" if max_x == "end" else min(max_x, plot_array.shape[1])
|
|
@@ -848,10 +705,10 @@ class Device(BaseModel):
|
|
|
848
705
|
def plot(
|
|
849
706
|
self,
|
|
850
707
|
show_buffer: bool = True,
|
|
851
|
-
bounds:
|
|
852
|
-
level:
|
|
853
|
-
ax:
|
|
854
|
-
**kwargs,
|
|
708
|
+
bounds: tuple[tuple[int, int], tuple[int, int]] | None = None,
|
|
709
|
+
level: int | None = None,
|
|
710
|
+
ax: Axes | None = None,
|
|
711
|
+
**kwargs: Any,
|
|
855
712
|
) -> Axes:
|
|
856
713
|
"""
|
|
857
714
|
Visualizes the device geometry.
|
|
@@ -867,9 +724,10 @@ class Device(BaseModel):
|
|
|
867
724
|
If True, visualizes the buffer zones around the device. Defaults to True.
|
|
868
725
|
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]], optional
|
|
869
726
|
Specifies the bounds for zooming into the device geometry, formatted as
|
|
870
|
-
((min_x, min_y), (max_x, max_y)).
|
|
871
|
-
|
|
872
|
-
|
|
727
|
+
((min_x, min_y), (max_x, max_y)). Negative values count from the end
|
|
728
|
+
(e.g., -50 means 50 pixels from the edge). If 'max_x' or 'max_y' is set
|
|
729
|
+
to "end", it will be replaced with the corresponding dimension size of the
|
|
730
|
+
device array. If None, the entire device geometry is visualized.
|
|
873
731
|
level : int
|
|
874
732
|
The vertical layer to plot. If None, the device geometry is flattened.
|
|
875
733
|
Defaults to None.
|
|
@@ -900,14 +758,14 @@ class Device(BaseModel):
|
|
|
900
758
|
|
|
901
759
|
def plot_contour(
|
|
902
760
|
self,
|
|
903
|
-
linewidth:
|
|
904
|
-
# label:
|
|
761
|
+
linewidth: int | None = None,
|
|
762
|
+
# label: str | None = "Device contour",
|
|
905
763
|
show_buffer: bool = True,
|
|
906
|
-
bounds:
|
|
907
|
-
level:
|
|
908
|
-
ax:
|
|
909
|
-
**kwargs,
|
|
910
|
-
):
|
|
764
|
+
bounds: tuple[tuple[int, int], tuple[int, int]] | None = None,
|
|
765
|
+
level: int | None = None,
|
|
766
|
+
ax: Axes | None = None,
|
|
767
|
+
**kwargs: Any,
|
|
768
|
+
) -> Axes:
|
|
911
769
|
"""
|
|
912
770
|
Visualizes the contour of the device geometry.
|
|
913
771
|
|
|
@@ -926,9 +784,10 @@ class Device(BaseModel):
|
|
|
926
784
|
it is set to True.
|
|
927
785
|
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
928
786
|
Specifies the bounds for zooming into the device geometry, formatted as
|
|
929
|
-
((min_x, min_y), (max_x, max_y)).
|
|
930
|
-
|
|
931
|
-
|
|
787
|
+
((min_x, min_y), (max_x, max_y)). Negative values count from the end
|
|
788
|
+
(e.g., -50 means 50 pixels from the edge). If 'max_x' or 'max_y' is set
|
|
789
|
+
to "end", it will be replaced with the corresponding dimension size of the
|
|
790
|
+
device array. If None, the entire device geometry is visualized.
|
|
932
791
|
level : int
|
|
933
792
|
The vertical layer to plot. If None, the device geometry is flattened.
|
|
934
793
|
Defaults to None.
|
|
@@ -950,8 +809,9 @@ class Device(BaseModel):
|
|
|
950
809
|
device_array = self.device_array[:, :, level]
|
|
951
810
|
|
|
952
811
|
kwargs.setdefault("cmap", "spring")
|
|
953
|
-
|
|
954
|
-
linewidth
|
|
812
|
+
linewidth_value = (
|
|
813
|
+
linewidth if linewidth is not None else device_array.shape[0] // 100
|
|
814
|
+
)
|
|
955
815
|
|
|
956
816
|
contours, _ = cv2.findContours(
|
|
957
817
|
geometry.binarize_hard(device_array).astype(np.uint8),
|
|
@@ -959,7 +819,7 @@ class Device(BaseModel):
|
|
|
959
819
|
cv2.CHAIN_APPROX_SIMPLE,
|
|
960
820
|
)
|
|
961
821
|
contour_array = np.zeros_like(device_array, dtype=np.uint8)
|
|
962
|
-
cv2.drawContours(contour_array, contours, -1, (255,),
|
|
822
|
+
_ = cv2.drawContours(contour_array, contours, -1, (255,), linewidth_value)
|
|
963
823
|
contour_array = np.ma.masked_equal(contour_array, 0)
|
|
964
824
|
|
|
965
825
|
_, ax = self._plot_base(
|
|
@@ -977,11 +837,11 @@ class Device(BaseModel):
|
|
|
977
837
|
def plot_uncertainty(
|
|
978
838
|
self,
|
|
979
839
|
show_buffer: bool = True,
|
|
980
|
-
bounds:
|
|
981
|
-
level:
|
|
982
|
-
ax:
|
|
983
|
-
**kwargs,
|
|
984
|
-
):
|
|
840
|
+
bounds: tuple[tuple[int, int], tuple[int, int]] | None = None,
|
|
841
|
+
level: int | None = None,
|
|
842
|
+
ax: Axes | None = None,
|
|
843
|
+
**kwargs: Any,
|
|
844
|
+
) -> Axes:
|
|
985
845
|
"""
|
|
986
846
|
Visualizes the uncertainty in the edge positions of the predicted device.
|
|
987
847
|
|
|
@@ -999,9 +859,10 @@ class Device(BaseModel):
|
|
|
999
859
|
default, it is set to True.
|
|
1000
860
|
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
1001
861
|
Specifies the bounds for zooming into the device geometry, formatted as
|
|
1002
|
-
((min_x, min_y), (max_x, max_y)).
|
|
1003
|
-
|
|
1004
|
-
|
|
862
|
+
((min_x, min_y), (max_x, max_y)). Negative values count from the end
|
|
863
|
+
(e.g., -50 means 50 pixels from the edge). If 'max_x' or 'max_y' is set
|
|
864
|
+
to "end", it will be replaced with the corresponding dimension size of the
|
|
865
|
+
device array. If None, the entire device geometry is visualized.
|
|
1005
866
|
level : int
|
|
1006
867
|
The vertical layer to plot. If None, the device geometry is flattened.
|
|
1007
868
|
Defaults to None.
|
|
@@ -1040,10 +901,10 @@ class Device(BaseModel):
|
|
|
1040
901
|
self,
|
|
1041
902
|
ref_device: "Device",
|
|
1042
903
|
show_buffer: bool = True,
|
|
1043
|
-
bounds:
|
|
1044
|
-
level:
|
|
1045
|
-
ax:
|
|
1046
|
-
**kwargs,
|
|
904
|
+
bounds: tuple[tuple[int, int], tuple[int, int]] | None = None,
|
|
905
|
+
level: int | None = None,
|
|
906
|
+
ax: Axes | None = None,
|
|
907
|
+
**kwargs: Any,
|
|
1047
908
|
) -> Axes:
|
|
1048
909
|
"""
|
|
1049
910
|
Visualizes the comparison between the current device geometry and a reference
|
|
@@ -1061,9 +922,10 @@ class Device(BaseModel):
|
|
|
1061
922
|
If True, visualizes the buffer zones around the device. Defaults to True.
|
|
1062
923
|
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
1063
924
|
Specifies the bounds for zooming into the device geometry, formatted as
|
|
1064
|
-
((min_x, min_y), (max_x, max_y)).
|
|
1065
|
-
|
|
1066
|
-
|
|
925
|
+
((min_x, min_y), (max_x, max_y)). Negative values count from the end
|
|
926
|
+
(e.g., -50 means 50 pixels from the edge). If 'max_x' or 'max_y' is set
|
|
927
|
+
to "end", it will be replaced with the corresponding dimension size of the
|
|
928
|
+
device array. If None, the entire device geometry is visualized.
|
|
1067
929
|
level : int
|
|
1068
930
|
The vertical layer to plot. If None, the device geometry is flattened.
|
|
1069
931
|
Defaults to None.
|
|
@@ -1099,7 +961,7 @@ class Device(BaseModel):
|
|
|
1099
961
|
cbar.set_label("Added (a.u.) Removed (a.u.)")
|
|
1100
962
|
return ax
|
|
1101
963
|
|
|
1102
|
-
def _add_buffer_visualization(self, ax: Axes):
|
|
964
|
+
def _add_buffer_visualization(self, ax: Axes) -> None:
|
|
1103
965
|
plot_array = self.device_array
|
|
1104
966
|
|
|
1105
967
|
buffer_thickness = self.buffer_spec.thickness
|
|
@@ -1114,7 +976,7 @@ class Device(BaseModel):
|
|
|
1114
976
|
edgecolor="black",
|
|
1115
977
|
linewidth=1,
|
|
1116
978
|
)
|
|
1117
|
-
ax.add_patch(mid_rect)
|
|
979
|
+
_ = ax.add_patch(mid_rect)
|
|
1118
980
|
|
|
1119
981
|
top_rect = Rectangle(
|
|
1120
982
|
(0, 0),
|
|
@@ -1123,7 +985,7 @@ class Device(BaseModel):
|
|
|
1123
985
|
facecolor=buffer_fill,
|
|
1124
986
|
hatch=buffer_hatch,
|
|
1125
987
|
)
|
|
1126
|
-
ax.add_patch(top_rect)
|
|
988
|
+
_ = ax.add_patch(top_rect)
|
|
1127
989
|
|
|
1128
990
|
bottom_rect = Rectangle(
|
|
1129
991
|
(0, plot_array.shape[0] - buffer_thickness["bottom"]),
|
|
@@ -1132,7 +994,7 @@ class Device(BaseModel):
|
|
|
1132
994
|
facecolor=buffer_fill,
|
|
1133
995
|
hatch=buffer_hatch,
|
|
1134
996
|
)
|
|
1135
|
-
ax.add_patch(bottom_rect)
|
|
997
|
+
_ = ax.add_patch(bottom_rect)
|
|
1136
998
|
|
|
1137
999
|
left_rect = Rectangle(
|
|
1138
1000
|
(0, buffer_thickness["top"]),
|
|
@@ -1141,7 +1003,7 @@ class Device(BaseModel):
|
|
|
1141
1003
|
facecolor=buffer_fill,
|
|
1142
1004
|
hatch=buffer_hatch,
|
|
1143
1005
|
)
|
|
1144
|
-
ax.add_patch(left_rect)
|
|
1006
|
+
_ = ax.add_patch(left_rect)
|
|
1145
1007
|
|
|
1146
1008
|
right_rect = Rectangle(
|
|
1147
1009
|
(
|
|
@@ -1153,7 +1015,7 @@ class Device(BaseModel):
|
|
|
1153
1015
|
facecolor=buffer_fill,
|
|
1154
1016
|
hatch=buffer_hatch,
|
|
1155
1017
|
)
|
|
1156
|
-
ax.add_patch(right_rect)
|
|
1018
|
+
_ = ax.add_patch(right_rect)
|
|
1157
1019
|
|
|
1158
1020
|
def normalize(self) -> "Device":
|
|
1159
1021
|
"""
|
|
@@ -1214,7 +1076,7 @@ class Device(BaseModel):
|
|
|
1214
1076
|
update={"device_array": binarized_device_array.astype(np.uint8)}
|
|
1215
1077
|
)
|
|
1216
1078
|
|
|
1217
|
-
def
|
|
1079
|
+
def binarize_with_roughness(
|
|
1218
1080
|
self,
|
|
1219
1081
|
noise_magnitude: float = 2.0,
|
|
1220
1082
|
blur_radius: float = 8.0,
|
|
@@ -1250,7 +1112,7 @@ class Device(BaseModel):
|
|
|
1250
1112
|
Device
|
|
1251
1113
|
A new instance of the Device with the binarized geometry.
|
|
1252
1114
|
"""
|
|
1253
|
-
binarized_device_array = geometry.
|
|
1115
|
+
binarized_device_array = geometry.binarize_with_roughness(
|
|
1254
1116
|
device_array=self.device_array,
|
|
1255
1117
|
noise_magnitude=noise_magnitude,
|
|
1256
1118
|
blur_radius=blur_radius,
|
|
@@ -1394,7 +1256,7 @@ class Device(BaseModel):
|
|
|
1394
1256
|
flattened_device_array = geometry.flatten(device_array=self.device_array)
|
|
1395
1257
|
return self.model_copy(update={"device_array": flattened_device_array})
|
|
1396
1258
|
|
|
1397
|
-
def get_uncertainty(self) ->
|
|
1259
|
+
def get_uncertainty(self) -> npt.NDArray[Any]:
|
|
1398
1260
|
"""
|
|
1399
1261
|
Calculate the uncertainty in the edge positions of the predicted device.
|
|
1400
1262
|
|
|
@@ -1410,96 +1272,3 @@ class Device(BaseModel):
|
|
|
1410
1272
|
with higher values indicating greater uncertainty.
|
|
1411
1273
|
"""
|
|
1412
1274
|
return 1 - 2 * np.abs(0.5 - self.device_array)
|
|
1413
|
-
|
|
1414
|
-
def enforce_feature_size(
|
|
1415
|
-
self, min_feature_size: int, strel: str = "disk"
|
|
1416
|
-
) -> "Device":
|
|
1417
|
-
"""
|
|
1418
|
-
Enforce a minimum feature size on the device geometry.
|
|
1419
|
-
|
|
1420
|
-
This method applies morphological operations to ensure that all features in the
|
|
1421
|
-
device geometry are at least the specified minimum size. It uses either a disk
|
|
1422
|
-
or square structuring element for the operations.
|
|
1423
|
-
|
|
1424
|
-
Notes
|
|
1425
|
-
-----
|
|
1426
|
-
This function does not guarantee that the minimum feature size is enforced in
|
|
1427
|
-
all cases. A better process is needed.
|
|
1428
|
-
|
|
1429
|
-
Parameters
|
|
1430
|
-
----------
|
|
1431
|
-
min_feature_size : int
|
|
1432
|
-
The minimum feature size to enforce, in nanometers.
|
|
1433
|
-
strel : str
|
|
1434
|
-
The type of structuring element to use. Can be either "disk" or "square".
|
|
1435
|
-
Defaults to "disk".
|
|
1436
|
-
|
|
1437
|
-
Returns
|
|
1438
|
-
-------
|
|
1439
|
-
Device
|
|
1440
|
-
A new instance of the Device with the modified geometry.
|
|
1441
|
-
|
|
1442
|
-
Raises
|
|
1443
|
-
------
|
|
1444
|
-
ValueError
|
|
1445
|
-
If an invalid structuring element type is specified.
|
|
1446
|
-
"""
|
|
1447
|
-
modified_geometry = geometry.enforce_feature_size(
|
|
1448
|
-
device_array=self.device_array,
|
|
1449
|
-
min_feature_size=min_feature_size,
|
|
1450
|
-
strel=strel,
|
|
1451
|
-
)
|
|
1452
|
-
return self.model_copy(update={"device_array": modified_geometry})
|
|
1453
|
-
|
|
1454
|
-
def check_feature_size(self, min_feature_size: int, strel: str = "disk"):
|
|
1455
|
-
"""
|
|
1456
|
-
Check and visualize the effect of enforcing a minimum feature size on the device
|
|
1457
|
-
geometry.
|
|
1458
|
-
|
|
1459
|
-
This method enforces a minimum feature size on the device geometry using the
|
|
1460
|
-
specified structuring element, compares the modified geometry with the original,
|
|
1461
|
-
and plots the differences. It also calculates and prints the Hamming distance
|
|
1462
|
-
between the original and modified geometries, providing a measure of the changes
|
|
1463
|
-
introduced by the feature size enforcement.
|
|
1464
|
-
|
|
1465
|
-
Notes
|
|
1466
|
-
-----
|
|
1467
|
-
This is not a design-rule-checking function, but it can be useful for quick
|
|
1468
|
-
checks.
|
|
1469
|
-
|
|
1470
|
-
Parameters
|
|
1471
|
-
----------
|
|
1472
|
-
min_feature_size : int
|
|
1473
|
-
The minimum feature size to enforce, in nanometers.
|
|
1474
|
-
strel : str
|
|
1475
|
-
The type of structuring element to use. Can be either "disk" or "square".
|
|
1476
|
-
Defaults to "disk".
|
|
1477
|
-
|
|
1478
|
-
Raises
|
|
1479
|
-
------
|
|
1480
|
-
ValueError
|
|
1481
|
-
If an invalid structuring element type is specified or if min_feature_size
|
|
1482
|
-
is not a positive integer.
|
|
1483
|
-
"""
|
|
1484
|
-
if min_feature_size <= 0:
|
|
1485
|
-
raise ValueError("min_feature_size must be a positive integer.")
|
|
1486
|
-
|
|
1487
|
-
enforced_device = self.enforce_feature_size(min_feature_size, strel)
|
|
1488
|
-
|
|
1489
|
-
difference = np.abs(
|
|
1490
|
-
enforced_device.device_array[:, :, 0] - self.device_array[:, :, 0]
|
|
1491
|
-
)
|
|
1492
|
-
_, ax = self._plot_base(
|
|
1493
|
-
plot_array=difference,
|
|
1494
|
-
show_buffer=False,
|
|
1495
|
-
ax=None,
|
|
1496
|
-
bounds=None,
|
|
1497
|
-
cmap="jet",
|
|
1498
|
-
)
|
|
1499
|
-
|
|
1500
|
-
hamming_distance = compare.hamming_distance(self, enforced_device)
|
|
1501
|
-
print(
|
|
1502
|
-
f"Feature size check with minimum size {min_feature_size} "
|
|
1503
|
-
f"using '{strel}' structuring element resulted in a Hamming "
|
|
1504
|
-
f"distance of: {hamming_distance}"
|
|
1505
|
-
)
|