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/__init__.py +1 -1
- prefab/__main__.py +14 -11
- prefab/compare.py +18 -3
- prefab/device.py +223 -133
- prefab/geometry.py +66 -22
- prefab/predict.py +51 -21
- prefab/read.py +122 -76
- prefab/shapes.py +82 -81
- {prefab-1.1.4.dist-info → prefab-1.1.6.dist-info}/METADATA +4 -2
- prefab-1.1.6.dist-info/RECORD +13 -0
- {prefab-1.1.4.dist-info → prefab-1.1.6.dist-info}/WHEEL +1 -1
- prefab-1.1.4.dist-info/RECORD +0 -13
- {prefab-1.1.4.dist-info → prefab-1.1.6.dist-info}/licenses/LICENSE +0 -0
prefab/device.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Provides the Device class for representing photonic devices."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import TYPE_CHECKING, Literal, Optional
|
|
4
4
|
|
|
5
5
|
import cv2
|
|
6
6
|
import gdstk
|
|
@@ -9,7 +9,7 @@ import numpy as np
|
|
|
9
9
|
from matplotlib.axes import Axes
|
|
10
10
|
from matplotlib.patches import Rectangle
|
|
11
11
|
from PIL import Image
|
|
12
|
-
from pydantic import BaseModel, Field,
|
|
12
|
+
from pydantic import BaseModel, Field, model_validator, validator
|
|
13
13
|
from scipy.ndimage import distance_transform_edt
|
|
14
14
|
from skimage import measure
|
|
15
15
|
|
|
@@ -17,6 +17,10 @@ from . import compare, geometry
|
|
|
17
17
|
from .models import Model
|
|
18
18
|
from .predict import predict_array
|
|
19
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import gdsfactory as gf
|
|
22
|
+
import tidy3d as td
|
|
23
|
+
|
|
20
24
|
Image.MAX_IMAGE_PIXELS = None
|
|
21
25
|
|
|
22
26
|
|
|
@@ -33,19 +37,21 @@ class BufferSpec(BaseModel):
|
|
|
33
37
|
----------
|
|
34
38
|
mode : dict[str, str]
|
|
35
39
|
A dictionary that defines the buffer mode for each side of the device
|
|
36
|
-
('top', 'bottom', 'left', 'right'), where
|
|
37
|
-
|
|
38
|
-
connections
|
|
39
|
-
|
|
40
|
+
('top', 'bottom', 'left', 'right'), where:
|
|
41
|
+
- 'constant' is used for isolated structures
|
|
42
|
+
- 'edge' is utilized for preserving the edge, such as for waveguide connections
|
|
43
|
+
- 'none' for no buffer on that side
|
|
44
|
+
thickness : dict[str, int]
|
|
40
45
|
A dictionary that defines the thickness of the buffer zone for each side of the
|
|
41
|
-
device ('top', 'bottom', 'left', 'right'). Each value must be greater than
|
|
46
|
+
device ('top', 'bottom', 'left', 'right'). Each value must be greater than or
|
|
47
|
+
equal to 0.
|
|
42
48
|
|
|
43
49
|
Raises
|
|
44
50
|
------
|
|
45
51
|
ValueError
|
|
46
52
|
If any of the modes specified in the 'mode' dictionary are not one of the
|
|
47
|
-
allowed values ('constant', 'edge'). Or if any of the thickness values
|
|
48
|
-
|
|
53
|
+
allowed values ('constant', 'edge', 'none'). Or if any of the thickness values
|
|
54
|
+
are negative.
|
|
49
55
|
|
|
50
56
|
Example
|
|
51
57
|
-------
|
|
@@ -54,20 +60,20 @@ class BufferSpec(BaseModel):
|
|
|
54
60
|
buffer_spec = pf.BufferSpec(
|
|
55
61
|
mode={
|
|
56
62
|
"top": "constant",
|
|
57
|
-
"bottom": "
|
|
63
|
+
"bottom": "none",
|
|
58
64
|
"left": "constant",
|
|
59
65
|
"right": "edge",
|
|
60
66
|
},
|
|
61
67
|
thickness={
|
|
62
68
|
"top": 150,
|
|
63
|
-
"bottom":
|
|
69
|
+
"bottom": 0,
|
|
64
70
|
"left": 200,
|
|
65
71
|
"right": 250,
|
|
66
72
|
},
|
|
67
73
|
)
|
|
68
74
|
"""
|
|
69
75
|
|
|
70
|
-
mode: dict[str,
|
|
76
|
+
mode: dict[str, Literal["constant", "edge", "none"]] = Field(
|
|
71
77
|
default_factory=lambda: {
|
|
72
78
|
"top": "constant",
|
|
73
79
|
"bottom": "constant",
|
|
@@ -75,7 +81,7 @@ class BufferSpec(BaseModel):
|
|
|
75
81
|
"right": "constant",
|
|
76
82
|
}
|
|
77
83
|
)
|
|
78
|
-
thickness: dict[str,
|
|
84
|
+
thickness: dict[str, int] = Field(
|
|
79
85
|
default_factory=lambda: {
|
|
80
86
|
"top": 128,
|
|
81
87
|
"bottom": 128,
|
|
@@ -86,11 +92,32 @@ class BufferSpec(BaseModel):
|
|
|
86
92
|
|
|
87
93
|
@validator("mode", pre=True)
|
|
88
94
|
def check_mode(cls, v):
|
|
89
|
-
allowed_modes = ["constant", "edge"]
|
|
95
|
+
allowed_modes = ["constant", "edge", "none"]
|
|
90
96
|
if not all(mode in allowed_modes for mode in v.values()):
|
|
91
|
-
raise ValueError(f"Buffer mode must be one of {allowed_modes}, got '{v}'")
|
|
97
|
+
raise ValueError(f"Buffer mode must be one of {allowed_modes}, got '{v}'.")
|
|
98
|
+
return v
|
|
99
|
+
|
|
100
|
+
@validator("thickness")
|
|
101
|
+
def check_thickness(cls, v):
|
|
102
|
+
if not all(t >= 0 for t in v.values()):
|
|
103
|
+
raise ValueError("All thickness values must be greater than or equal to 0.")
|
|
92
104
|
return v
|
|
93
105
|
|
|
106
|
+
@model_validator(mode="after")
|
|
107
|
+
def check_none_thickness(cls, values):
|
|
108
|
+
mode = values.mode
|
|
109
|
+
thickness = values.thickness
|
|
110
|
+
for side in mode:
|
|
111
|
+
if mode[side] == "none" and thickness[side] != 0:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"Thickness must be 0 when mode is 'none' for {side} side"
|
|
114
|
+
)
|
|
115
|
+
if mode[side] != "none" and thickness[side] == 0:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"Mode must be 'none' when thickness is 0 for {side} side"
|
|
118
|
+
)
|
|
119
|
+
return values
|
|
120
|
+
|
|
94
121
|
|
|
95
122
|
class Device(BaseModel):
|
|
96
123
|
device_array: np.ndarray = Field(...)
|
|
@@ -100,8 +127,8 @@ class Device(BaseModel):
|
|
|
100
127
|
arbitrary_types_allowed = True
|
|
101
128
|
|
|
102
129
|
@property
|
|
103
|
-
def shape(self) -> tuple[int,
|
|
104
|
-
return self.device_array.shape
|
|
130
|
+
def shape(self) -> tuple[int, ...]:
|
|
131
|
+
return tuple(self.device_array.shape)
|
|
105
132
|
|
|
106
133
|
def __init__(
|
|
107
134
|
self, device_array: np.ndarray, buffer_spec: Optional[BufferSpec] = None
|
|
@@ -112,11 +139,11 @@ class Device(BaseModel):
|
|
|
112
139
|
|
|
113
140
|
This class is designed to encapsulate the geometric representation of a photonic
|
|
114
141
|
device, facilitating operations such as padding, normalization, binarization,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
fabrication workflows.
|
|
142
|
+
erosion/dilation, trimming, and blurring. These operations are useful for
|
|
143
|
+
preparingthe device design for prediction or correction. Additionally, the class
|
|
144
|
+
providesmethods for exporting the device representation to various formats,
|
|
145
|
+
includingndarray, image files, and GDSII files, supporting a range of analysis
|
|
146
|
+
and fabrication workflows.
|
|
120
147
|
|
|
121
148
|
Parameters
|
|
122
149
|
----------
|
|
@@ -124,7 +151,7 @@ class Device(BaseModel):
|
|
|
124
151
|
A 2D array representing the planar geometry of the device. This array
|
|
125
152
|
undergoes various transformations to predict or correct the nanofabrication
|
|
126
153
|
process.
|
|
127
|
-
buffer_spec : BufferSpec
|
|
154
|
+
buffer_spec : Optional[BufferSpec]
|
|
128
155
|
Defines the parameters for adding a buffer zone around the device geometry.
|
|
129
156
|
This buffer zone is needed for providing surrounding context for prediction
|
|
130
157
|
or correction and for ensuring seamless integration with the surrounding
|
|
@@ -154,30 +181,37 @@ class Device(BaseModel):
|
|
|
154
181
|
buffer_thickness = self.buffer_spec.thickness
|
|
155
182
|
buffer_mode = self.buffer_spec.mode
|
|
156
183
|
|
|
157
|
-
|
|
158
|
-
self.device_array
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
if buffer_mode["top"] != "none":
|
|
185
|
+
self.device_array = np.pad(
|
|
186
|
+
self.device_array,
|
|
187
|
+
pad_width=((buffer_thickness["top"], 0), (0, 0)),
|
|
188
|
+
mode=buffer_mode["top"],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if buffer_mode["bottom"] != "none":
|
|
192
|
+
self.device_array = np.pad(
|
|
193
|
+
self.device_array,
|
|
194
|
+
pad_width=((0, buffer_thickness["bottom"]), (0, 0)),
|
|
195
|
+
mode=buffer_mode["bottom"],
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if buffer_mode["left"] != "none":
|
|
199
|
+
self.device_array = np.pad(
|
|
200
|
+
self.device_array,
|
|
201
|
+
pad_width=((0, 0), (buffer_thickness["left"], 0)),
|
|
202
|
+
mode=buffer_mode["left"],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if buffer_mode["right"] != "none":
|
|
206
|
+
self.device_array = np.pad(
|
|
207
|
+
self.device_array,
|
|
208
|
+
pad_width=((0, 0), (0, buffer_thickness["right"])),
|
|
209
|
+
mode=buffer_mode["right"],
|
|
210
|
+
)
|
|
177
211
|
|
|
178
212
|
self.device_array = np.expand_dims(self.device_array, axis=-1)
|
|
179
213
|
|
|
180
|
-
@
|
|
214
|
+
@model_validator(mode="before")
|
|
181
215
|
def check_device_array(cls, values):
|
|
182
216
|
device_array = values.get("device_array")
|
|
183
217
|
if not isinstance(device_array, np.ndarray):
|
|
@@ -226,11 +260,11 @@ class Device(BaseModel):
|
|
|
226
260
|
in `models.py`. Each model is associated with a version and dataset that
|
|
227
261
|
detail its creation and the data it was trained on, ensuring the prediction
|
|
228
262
|
is tailored to specific fabrication parameters.
|
|
229
|
-
binarize : bool
|
|
263
|
+
binarize : bool
|
|
230
264
|
If True, the predicted device geometry will be binarized using a threshold
|
|
231
265
|
method. This is useful for converting probabilistic predictions into binary
|
|
232
266
|
geometries. Defaults to False.
|
|
233
|
-
gpu : bool
|
|
267
|
+
gpu : bool
|
|
234
268
|
If True, the prediction will be performed on a GPU. Defaults to False.
|
|
235
269
|
Note: The GPU option has more overhead and will take longer for small
|
|
236
270
|
devices, but will be faster for larger devices.
|
|
@@ -242,7 +276,7 @@ class Device(BaseModel):
|
|
|
242
276
|
|
|
243
277
|
Raises
|
|
244
278
|
------
|
|
245
|
-
|
|
279
|
+
RuntimeError
|
|
246
280
|
If the prediction service returns an error or if the response from the
|
|
247
281
|
service cannot be processed correctly.
|
|
248
282
|
"""
|
|
@@ -279,11 +313,11 @@ class Device(BaseModel):
|
|
|
279
313
|
in `models.py`. Each model is associated with a version and dataset that
|
|
280
314
|
detail its creation and the data it was trained on, ensuring the correction
|
|
281
315
|
is tailored to specific fabrication parameters.
|
|
282
|
-
binarize : bool
|
|
316
|
+
binarize : bool
|
|
283
317
|
If True, the corrected device geometry will be binarized using a threshold
|
|
284
318
|
method. This is useful for converting probabilistic corrections into binary
|
|
285
319
|
geometries. Defaults to True.
|
|
286
|
-
gpu : bool
|
|
320
|
+
gpu : bool
|
|
287
321
|
If True, the prediction will be performed on a GPU. Defaults to False.
|
|
288
322
|
Note: The GPU option has more overhead and will take longer for small
|
|
289
323
|
devices, but will be faster for larger devices.
|
|
@@ -295,7 +329,7 @@ class Device(BaseModel):
|
|
|
295
329
|
|
|
296
330
|
Raises
|
|
297
331
|
------
|
|
298
|
-
|
|
332
|
+
RuntimeError
|
|
299
333
|
If the correction service returns an error or if the response from the
|
|
300
334
|
service cannot be processed correctly.
|
|
301
335
|
"""
|
|
@@ -331,16 +365,27 @@ class Device(BaseModel):
|
|
|
331
365
|
in `models.py`. Each model is associated with a version and dataset that
|
|
332
366
|
detail its creation and the data it was trained on, ensuring the SEMulation
|
|
333
367
|
is tailored to specific fabrication parameters.
|
|
334
|
-
gpu : bool
|
|
368
|
+
gpu : bool
|
|
335
369
|
If True, the prediction will be performed on a GPU. Defaults to False.
|
|
336
370
|
Note: The GPU option has more overhead and will take longer for small
|
|
337
371
|
devices, but will be faster for larger devices.
|
|
338
372
|
|
|
373
|
+
Notes
|
|
374
|
+
-----
|
|
375
|
+
The salt-and-pepper noise is added manually until the model is trained to
|
|
376
|
+
generate this noise (not a big priority).
|
|
377
|
+
|
|
339
378
|
Returns
|
|
340
379
|
-------
|
|
341
380
|
Device
|
|
342
381
|
A new instance of the Device class with its geometry transformed to simulate
|
|
343
382
|
an SEM image style.
|
|
383
|
+
|
|
384
|
+
Raises
|
|
385
|
+
------
|
|
386
|
+
RuntimeError
|
|
387
|
+
If the prediction service returns an error or if the response from the
|
|
388
|
+
service cannot be processed correctly.
|
|
344
389
|
"""
|
|
345
390
|
semulated_array = predict_array(
|
|
346
391
|
device_array=self.device_array,
|
|
@@ -370,12 +415,14 @@ class Device(BaseModel):
|
|
|
370
415
|
buffer_thickness = self.buffer_spec.thickness
|
|
371
416
|
buffer_mode = self.buffer_spec.mode
|
|
372
417
|
|
|
373
|
-
crop_top = buffer_thickness["top"] if buffer_mode["top"] == "
|
|
418
|
+
crop_top = buffer_thickness["top"] if buffer_mode["top"] == "constant" else 0
|
|
374
419
|
crop_bottom = (
|
|
375
|
-
buffer_thickness["bottom"] if buffer_mode["bottom"] == "
|
|
420
|
+
buffer_thickness["bottom"] if buffer_mode["bottom"] == "constant" else 0
|
|
421
|
+
)
|
|
422
|
+
crop_left = buffer_thickness["left"] if buffer_mode["left"] == "constant" else 0
|
|
423
|
+
crop_right = (
|
|
424
|
+
buffer_thickness["right"] if buffer_mode["right"] == "constant" else 0
|
|
376
425
|
)
|
|
377
|
-
crop_left = buffer_thickness["left"] if buffer_mode["left"] == "edge" else 0
|
|
378
|
-
crop_right = buffer_thickness["right"] if buffer_mode["right"] == "edge" else 0
|
|
379
426
|
|
|
380
427
|
ndarray = device_array[
|
|
381
428
|
crop_top : device_array.shape[0] - crop_bottom,
|
|
@@ -393,12 +440,12 @@ class Device(BaseModel):
|
|
|
393
440
|
|
|
394
441
|
Parameters
|
|
395
442
|
----------
|
|
396
|
-
img_path : str
|
|
443
|
+
img_path : str
|
|
397
444
|
The path where the image file will be saved. If not specified, the image is
|
|
398
445
|
saved as "prefab_device.png" in the current directory.
|
|
399
446
|
"""
|
|
400
447
|
cv2.imwrite(img_path, 255 * self.flatten().to_ndarray())
|
|
401
|
-
print(f"Saved Device to '{img_path}'")
|
|
448
|
+
print(f"Saved Device image to '{img_path}'")
|
|
402
449
|
|
|
403
450
|
def to_gds(
|
|
404
451
|
self,
|
|
@@ -417,21 +464,21 @@ class Device(BaseModel):
|
|
|
417
464
|
|
|
418
465
|
Parameters
|
|
419
466
|
----------
|
|
420
|
-
gds_path : str
|
|
467
|
+
gds_path : str
|
|
421
468
|
The path where the GDSII file will be saved. If not specified, the file is
|
|
422
469
|
saved as "prefab_device.gds" in the current directory.
|
|
423
|
-
cell_name : str
|
|
470
|
+
cell_name : str
|
|
424
471
|
The name of the cell within the GDSII file. If not specified, defaults to
|
|
425
472
|
"prefab_device".
|
|
426
|
-
gds_layer : tuple[int, int]
|
|
473
|
+
gds_layer : tuple[int, int]
|
|
427
474
|
The layer and datatype to use within the GDSII file. Defaults to (1, 0).
|
|
428
|
-
contour_approx_mode : int
|
|
475
|
+
contour_approx_mode : int
|
|
429
476
|
The mode of contour approximation used during the conversion. Defaults to 2,
|
|
430
477
|
which corresponds to `cv2.CHAIN_APPROX_SIMPLE`, a method that compresses
|
|
431
478
|
horizontal, vertical, and diagonal segments and leaves only their endpoints.
|
|
432
|
-
origin : tuple[float, float]
|
|
433
|
-
The x and y coordinates of the origin for the GDSII export. Defaults
|
|
434
|
-
(0.0, 0.0).
|
|
479
|
+
origin : tuple[float, float]
|
|
480
|
+
The x and y coordinates of the origin in µm for the GDSII export. Defaults
|
|
481
|
+
to (0.0, 0.0).
|
|
435
482
|
"""
|
|
436
483
|
gdstk_cell = self.flatten()._device_to_gdstk(
|
|
437
484
|
cell_name=cell_name,
|
|
@@ -461,17 +508,17 @@ class Device(BaseModel):
|
|
|
461
508
|
|
|
462
509
|
Parameters
|
|
463
510
|
----------
|
|
464
|
-
cell_name : str
|
|
511
|
+
cell_name : str
|
|
465
512
|
The name of the cell to be created. Defaults to "prefab_device".
|
|
466
|
-
gds_layer : tuple[int, int]
|
|
513
|
+
gds_layer : tuple[int, int]
|
|
467
514
|
The layer and datatype to use within the GDSTK cell. Defaults to (1, 0).
|
|
468
|
-
contour_approx_mode : int
|
|
515
|
+
contour_approx_mode : int
|
|
469
516
|
The mode of contour approximation used during the conversion. Defaults to 2,
|
|
470
517
|
which corresponds to `cv2.CHAIN_APPROX_SIMPLE`, a method that compresses
|
|
471
518
|
horizontal, vertical, and diagonal segments and leaves only their endpoints.
|
|
472
|
-
origin : tuple[float, float]
|
|
473
|
-
The x and y coordinates of the origin for the GDSTK cell. Defaults
|
|
474
|
-
(0.0, 0.0).
|
|
519
|
+
origin : tuple[float, float]
|
|
520
|
+
The x and y coordinates of the origin in µm for the GDSTK cell. Defaults
|
|
521
|
+
to (0.0, 0.0).
|
|
475
522
|
|
|
476
523
|
Returns
|
|
477
524
|
-------
|
|
@@ -529,17 +576,29 @@ class Device(BaseModel):
|
|
|
529
576
|
polygons_to_process = hierarchy_polygons[level]
|
|
530
577
|
|
|
531
578
|
if polygons_to_process:
|
|
532
|
-
|
|
533
|
-
|
|
579
|
+
buffer_thickness = self.buffer_spec.thickness
|
|
580
|
+
|
|
581
|
+
center_x_nm = (
|
|
582
|
+
self.device_array.shape[1]
|
|
583
|
+
- buffer_thickness["left"]
|
|
584
|
+
- buffer_thickness["right"]
|
|
585
|
+
) / 2
|
|
586
|
+
center_y_nm = (
|
|
587
|
+
self.device_array.shape[0]
|
|
588
|
+
- buffer_thickness["top"]
|
|
589
|
+
- buffer_thickness["bottom"]
|
|
590
|
+
) / 2
|
|
534
591
|
|
|
535
592
|
center_x_um = center_x_nm / 1000
|
|
536
593
|
center_y_um = center_y_nm / 1000
|
|
537
594
|
|
|
538
595
|
adjusted_polygons = [
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
596
|
+
gdstk.Polygon(
|
|
597
|
+
[
|
|
598
|
+
(x - center_x_um + origin[0], y - center_y_um + origin[1])
|
|
599
|
+
for x, y in polygon
|
|
600
|
+
]
|
|
601
|
+
)
|
|
543
602
|
for polygon in polygons_to_process
|
|
544
603
|
]
|
|
545
604
|
processed_polygons = gdstk.boolean(
|
|
@@ -554,7 +613,7 @@ class Device(BaseModel):
|
|
|
554
613
|
|
|
555
614
|
return cell
|
|
556
615
|
|
|
557
|
-
def to_gdsfactory(self) -> "gf.Component":
|
|
616
|
+
def to_gdsfactory(self) -> "gf.Component":
|
|
558
617
|
"""
|
|
559
618
|
Convert the device geometry to a gdsfactory Component.
|
|
560
619
|
|
|
@@ -583,7 +642,7 @@ class Device(BaseModel):
|
|
|
583
642
|
self,
|
|
584
643
|
eps0: float,
|
|
585
644
|
thickness: float,
|
|
586
|
-
) -> "td.Structure":
|
|
645
|
+
) -> "td.Structure":
|
|
587
646
|
"""
|
|
588
647
|
Convert the device geometry to a Tidy3D Structure.
|
|
589
648
|
|
|
@@ -622,7 +681,10 @@ class Device(BaseModel):
|
|
|
622
681
|
eps_dataset = SpatialDataArray(eps_array, coords=dict(x=X, y=Y, z=Z))
|
|
623
682
|
medium = CustomMedium.from_eps_raw(eps_dataset)
|
|
624
683
|
return Structure(
|
|
625
|
-
geometry=Box(center=(0, 0, 0), size=(inf, inf, thickness)
|
|
684
|
+
geometry=Box(center=(0, 0, 0), size=(inf, inf, thickness), attrs={}),
|
|
685
|
+
medium=medium,
|
|
686
|
+
name="device",
|
|
687
|
+
attrs={},
|
|
626
688
|
)
|
|
627
689
|
|
|
628
690
|
def to_3d(self, thickness_nm: int) -> np.ndarray:
|
|
@@ -644,11 +706,11 @@ class Device(BaseModel):
|
|
|
644
706
|
"""
|
|
645
707
|
bottom_layer = self.device_array[:, :, 0]
|
|
646
708
|
top_layer = self.device_array[:, :, -1]
|
|
647
|
-
dt_bottom = distance_transform_edt(bottom_layer) -
|
|
648
|
-
1 - bottom_layer
|
|
709
|
+
dt_bottom = np.array(distance_transform_edt(bottom_layer)) - np.array(
|
|
710
|
+
distance_transform_edt(1 - bottom_layer)
|
|
649
711
|
)
|
|
650
|
-
dt_top = distance_transform_edt(top_layer) -
|
|
651
|
-
1 - top_layer
|
|
712
|
+
dt_top = np.array(distance_transform_edt(top_layer)) - np.array(
|
|
713
|
+
distance_transform_edt(1 - top_layer)
|
|
652
714
|
)
|
|
653
715
|
weights = np.linspace(0, 1, thickness_nm)
|
|
654
716
|
layered_array = np.zeros(
|
|
@@ -667,7 +729,7 @@ class Device(BaseModel):
|
|
|
667
729
|
----------
|
|
668
730
|
thickness_nm : int
|
|
669
731
|
The thickness of the 3D representation in nanometers.
|
|
670
|
-
filename : str
|
|
732
|
+
filename : str
|
|
671
733
|
The name of the STL file to save. Defaults to "prefab_device.stl".
|
|
672
734
|
|
|
673
735
|
Raises
|
|
@@ -678,7 +740,7 @@ class Device(BaseModel):
|
|
|
678
740
|
If the numpy-stl package is not installed.
|
|
679
741
|
"""
|
|
680
742
|
try:
|
|
681
|
-
from stl import mesh
|
|
743
|
+
from stl import mesh # type: ignore
|
|
682
744
|
except ImportError:
|
|
683
745
|
raise ImportError(
|
|
684
746
|
"The stl package is required to use this function; "
|
|
@@ -725,7 +787,6 @@ class Device(BaseModel):
|
|
|
725
787
|
plot_array.shape[0] - max_y : plot_array.shape[0] - min_y,
|
|
726
788
|
min_x:max_x,
|
|
727
789
|
]
|
|
728
|
-
extent = [min_x, max_x, min_y, max_y]
|
|
729
790
|
|
|
730
791
|
if not np.ma.is_masked(plot_array):
|
|
731
792
|
max_size = (1000, 1000)
|
|
@@ -744,7 +805,12 @@ class Device(BaseModel):
|
|
|
744
805
|
|
|
745
806
|
mappable = ax.imshow(
|
|
746
807
|
plot_array,
|
|
747
|
-
extent=
|
|
808
|
+
extent=(
|
|
809
|
+
float(min_x),
|
|
810
|
+
float(max_x),
|
|
811
|
+
float(min_y),
|
|
812
|
+
float(max_y),
|
|
813
|
+
),
|
|
748
814
|
**kwargs,
|
|
749
815
|
)
|
|
750
816
|
|
|
@@ -764,7 +830,7 @@ class Device(BaseModel):
|
|
|
764
830
|
self,
|
|
765
831
|
show_buffer: bool = True,
|
|
766
832
|
bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
|
|
767
|
-
level: int = None,
|
|
833
|
+
level: Optional[int] = None,
|
|
768
834
|
ax: Optional[Axes] = None,
|
|
769
835
|
**kwargs,
|
|
770
836
|
) -> Axes:
|
|
@@ -778,17 +844,17 @@ class Device(BaseModel):
|
|
|
778
844
|
|
|
779
845
|
Parameters
|
|
780
846
|
----------
|
|
781
|
-
show_buffer : bool
|
|
847
|
+
show_buffer : bool
|
|
782
848
|
If True, visualizes the buffer zones around the device. Defaults to True.
|
|
783
849
|
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]], optional
|
|
784
850
|
Specifies the bounds for zooming into the device geometry, formatted as
|
|
785
851
|
((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
|
|
786
852
|
will be replaced with the corresponding dimension size of the device array.
|
|
787
853
|
If None, the entire device geometry is visualized.
|
|
788
|
-
level : int
|
|
854
|
+
level : int
|
|
789
855
|
The vertical layer to plot. If None, the device geometry is flattened.
|
|
790
856
|
Defaults to None.
|
|
791
|
-
ax : Optional[Axes]
|
|
857
|
+
ax : Optional[Axes]
|
|
792
858
|
An existing matplotlib Axes object to draw the device geometry on. If
|
|
793
859
|
None, a new figure and axes will be created. Defaults to None.
|
|
794
860
|
**kwargs
|
|
@@ -819,7 +885,7 @@ class Device(BaseModel):
|
|
|
819
885
|
# label: Optional[str] = "Device contour",
|
|
820
886
|
show_buffer: bool = True,
|
|
821
887
|
bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
|
|
822
|
-
level: int = None,
|
|
888
|
+
level: Optional[int] = None,
|
|
823
889
|
ax: Optional[Axes] = None,
|
|
824
890
|
**kwargs,
|
|
825
891
|
):
|
|
@@ -833,21 +899,21 @@ class Device(BaseModel):
|
|
|
833
899
|
|
|
834
900
|
Parameters
|
|
835
901
|
----------
|
|
836
|
-
linewidth : Optional[int]
|
|
902
|
+
linewidth : Optional[int]
|
|
837
903
|
The width of the contour lines. If None, the linewidth is automatically
|
|
838
904
|
determined based on the size of the device array. Defaults to None.
|
|
839
|
-
show_buffer : bool
|
|
905
|
+
show_buffer : bool
|
|
840
906
|
If True, the buffer zones around the device will be visualized. By default,
|
|
841
907
|
it is set to True.
|
|
842
|
-
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
908
|
+
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
843
909
|
Specifies the bounds for zooming into the device geometry, formatted as
|
|
844
910
|
((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
|
|
845
911
|
will be replaced with the corresponding dimension size of the device array.
|
|
846
912
|
If None, the entire device geometry is visualized.
|
|
847
|
-
level : int
|
|
913
|
+
level : int
|
|
848
914
|
The vertical layer to plot. If None, the device geometry is flattened.
|
|
849
915
|
Defaults to None.
|
|
850
|
-
ax : Optional[Axes]
|
|
916
|
+
ax : Optional[Axes]
|
|
851
917
|
An existing matplotlib Axes object to draw the device contour on. If None, a
|
|
852
918
|
new figure and axes will be created. Defaults to None.
|
|
853
919
|
**kwargs
|
|
@@ -893,7 +959,7 @@ class Device(BaseModel):
|
|
|
893
959
|
self,
|
|
894
960
|
show_buffer: bool = True,
|
|
895
961
|
bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
|
|
896
|
-
level: int = None,
|
|
962
|
+
level: Optional[int] = None,
|
|
897
963
|
ax: Optional[Axes] = None,
|
|
898
964
|
**kwargs,
|
|
899
965
|
):
|
|
@@ -909,18 +975,18 @@ class Device(BaseModel):
|
|
|
909
975
|
|
|
910
976
|
Parameters
|
|
911
977
|
----------
|
|
912
|
-
show_buffer : bool
|
|
978
|
+
show_buffer : bool
|
|
913
979
|
If True, the buffer zones around the device will also be visualized. By
|
|
914
980
|
default, it is set to True.
|
|
915
|
-
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
981
|
+
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
916
982
|
Specifies the bounds for zooming into the device geometry, formatted as
|
|
917
983
|
((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
|
|
918
984
|
will be replaced with the corresponding dimension size of the device array.
|
|
919
985
|
If None, the entire device geometry is visualized.
|
|
920
|
-
level : int
|
|
986
|
+
level : int
|
|
921
987
|
The vertical layer to plot. If None, the device geometry is flattened.
|
|
922
988
|
Defaults to None.
|
|
923
|
-
ax : Optional[Axes]
|
|
989
|
+
ax : Optional[Axes]
|
|
924
990
|
An existing matplotlib Axes object to draw the uncertainty visualization on.
|
|
925
991
|
If None, a new figure and axes will be created. Defaults to None.
|
|
926
992
|
**kwargs
|
|
@@ -956,7 +1022,7 @@ class Device(BaseModel):
|
|
|
956
1022
|
ref_device: "Device",
|
|
957
1023
|
show_buffer: bool = True,
|
|
958
1024
|
bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
|
|
959
|
-
level: int = None,
|
|
1025
|
+
level: Optional[int] = None,
|
|
960
1026
|
ax: Optional[Axes] = None,
|
|
961
1027
|
**kwargs,
|
|
962
1028
|
) -> Axes:
|
|
@@ -972,17 +1038,17 @@ class Device(BaseModel):
|
|
|
972
1038
|
----------
|
|
973
1039
|
ref_device : Device
|
|
974
1040
|
The reference device to compare against.
|
|
975
|
-
show_buffer : bool
|
|
1041
|
+
show_buffer : bool
|
|
976
1042
|
If True, visualizes the buffer zones around the device. Defaults to True.
|
|
977
|
-
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
1043
|
+
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
978
1044
|
Specifies the bounds for zooming into the device geometry, formatted as
|
|
979
1045
|
((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
|
|
980
1046
|
will be replaced with the corresponding dimension size of the device array.
|
|
981
1047
|
If None, the entire device geometry is visualized.
|
|
982
|
-
level : int
|
|
1048
|
+
level : int
|
|
983
1049
|
The vertical layer to plot. If None, the device geometry is flattened.
|
|
984
1050
|
Defaults to None.
|
|
985
|
-
ax : Optional[Axes]
|
|
1051
|
+
ax : Optional[Axes]
|
|
986
1052
|
An existing matplotlib Axes object to draw the comparison on. If None, a new
|
|
987
1053
|
figure and axes will be created. Defaults to None.
|
|
988
1054
|
**kwargs
|
|
@@ -1088,9 +1154,9 @@ class Device(BaseModel):
|
|
|
1088
1154
|
|
|
1089
1155
|
Parameters
|
|
1090
1156
|
----------
|
|
1091
|
-
eta : float
|
|
1157
|
+
eta : float
|
|
1092
1158
|
The threshold value for binarization. Defaults to 0.5.
|
|
1093
|
-
beta : float
|
|
1159
|
+
beta : float
|
|
1094
1160
|
The scaling factor for the binarization process. A higher value makes the
|
|
1095
1161
|
transition sharper. Defaults to np.inf, which results in a hard threshold.
|
|
1096
1162
|
|
|
@@ -1112,15 +1178,15 @@ class Device(BaseModel):
|
|
|
1112
1178
|
is generally preferred for most use cases, but it can create numerical artifacts
|
|
1113
1179
|
for large beta values.
|
|
1114
1180
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1181
|
+
Parameters
|
|
1182
|
+
----------
|
|
1183
|
+
eta : float
|
|
1184
|
+
The threshold value for binarization. Defaults to 0.5.
|
|
1119
1185
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1186
|
+
Returns
|
|
1187
|
+
-------
|
|
1188
|
+
Device
|
|
1189
|
+
A new instance of the Device with the threshold-binarized geometry.
|
|
1124
1190
|
"""
|
|
1125
1191
|
binarized_device_array = geometry.binarize_hard(
|
|
1126
1192
|
device_array=self.device_array, eta=eta
|
|
@@ -1131,26 +1197,31 @@ class Device(BaseModel):
|
|
|
1131
1197
|
|
|
1132
1198
|
def binarize_monte_carlo(
|
|
1133
1199
|
self,
|
|
1134
|
-
|
|
1135
|
-
|
|
1200
|
+
noise_magnitude: float = 2.0,
|
|
1201
|
+
blur_radius: float = 8.0,
|
|
1136
1202
|
) -> "Device":
|
|
1137
1203
|
"""
|
|
1138
|
-
Binarize the
|
|
1139
|
-
|
|
1204
|
+
Binarize the input ndarray using a dynamic thresholding approach to simulate
|
|
1205
|
+
surfaceroughness.
|
|
1140
1206
|
|
|
1141
|
-
This
|
|
1207
|
+
This function applies a dynamic thresholding technique where the threshold value
|
|
1142
1208
|
is determined by a base value perturbed by Gaussian-distributed random noise.
|
|
1143
|
-
|
|
1144
|
-
|
|
1209
|
+
Thethreshold is then spatially varied across the array using Gaussian blurring,
|
|
1210
|
+
simulating a potentially more realistic scenario where the threshold is not
|
|
1145
1211
|
uniform across the device.
|
|
1146
1212
|
|
|
1213
|
+
Notes
|
|
1214
|
+
-----
|
|
1215
|
+
This is a temporary solution, where the defaults are chosen based on what looks
|
|
1216
|
+
good. A better, data-driven approach is needed.
|
|
1217
|
+
|
|
1147
1218
|
Parameters
|
|
1148
1219
|
----------
|
|
1149
|
-
|
|
1220
|
+
noise_magnitude : float
|
|
1150
1221
|
The standard deviation of the Gaussian distribution used to generate noise
|
|
1151
1222
|
for the threshold values. This controls the amount of randomness in the
|
|
1152
1223
|
threshold. Defaults to 2.0.
|
|
1153
|
-
|
|
1224
|
+
blur_radius : float
|
|
1154
1225
|
The standard deviation for the Gaussian kernel used in blurring the
|
|
1155
1226
|
threshold map. This controls the spatial variation of the threshold across
|
|
1156
1227
|
the array. Defaults to 9.0.
|
|
@@ -1162,8 +1233,8 @@ class Device(BaseModel):
|
|
|
1162
1233
|
"""
|
|
1163
1234
|
binarized_device_array = geometry.binarize_monte_carlo(
|
|
1164
1235
|
device_array=self.device_array,
|
|
1165
|
-
|
|
1166
|
-
|
|
1236
|
+
noise_magnitude=noise_magnitude,
|
|
1237
|
+
blur_radius=blur_radius,
|
|
1167
1238
|
)
|
|
1168
1239
|
return self.model_copy(update={"device_array": binarized_device_array})
|
|
1169
1240
|
|
|
@@ -1174,9 +1245,9 @@ class Device(BaseModel):
|
|
|
1174
1245
|
|
|
1175
1246
|
Parameters
|
|
1176
1247
|
----------
|
|
1177
|
-
eta1 : float
|
|
1248
|
+
eta1 : float
|
|
1178
1249
|
The first threshold value for ternarization. Defaults to 1/3.
|
|
1179
|
-
eta2 : float
|
|
1250
|
+
eta2 : float
|
|
1180
1251
|
The second threshold value for ternarization. Defaults to 2/3.
|
|
1181
1252
|
|
|
1182
1253
|
Returns
|
|
@@ -1204,13 +1275,22 @@ class Device(BaseModel):
|
|
|
1204
1275
|
)
|
|
1205
1276
|
return self.model_copy(update={"device_array": trimmed_device_array})
|
|
1206
1277
|
|
|
1278
|
+
def pad(self, pad_width: int) -> "Device":
|
|
1279
|
+
"""
|
|
1280
|
+
Pad the device geometry with a specified width on all sides.
|
|
1281
|
+
"""
|
|
1282
|
+
padded_device_array = geometry.pad(
|
|
1283
|
+
device_array=self.device_array, pad_width=pad_width
|
|
1284
|
+
)
|
|
1285
|
+
return self.model_copy(update={"device_array": padded_device_array})
|
|
1286
|
+
|
|
1207
1287
|
def blur(self, sigma: float = 1.0) -> "Device":
|
|
1208
1288
|
"""
|
|
1209
1289
|
Apply Gaussian blur to the device geometry and normalize the result.
|
|
1210
1290
|
|
|
1211
1291
|
Parameters
|
|
1212
1292
|
----------
|
|
1213
|
-
sigma : float
|
|
1293
|
+
sigma : float
|
|
1214
1294
|
The standard deviation for the Gaussian kernel. This controls the amount of
|
|
1215
1295
|
blurring. Defaults to 1.0.
|
|
1216
1296
|
|
|
@@ -1322,11 +1402,16 @@ class Device(BaseModel):
|
|
|
1322
1402
|
device geometry are at least the specified minimum size. It uses either a disk
|
|
1323
1403
|
or square structuring element for the operations.
|
|
1324
1404
|
|
|
1405
|
+
Notes
|
|
1406
|
+
-----
|
|
1407
|
+
This function does not guarantee that the minimum feature size is enforced in
|
|
1408
|
+
all cases. A better process is needed.
|
|
1409
|
+
|
|
1325
1410
|
Parameters
|
|
1326
1411
|
----------
|
|
1327
1412
|
min_feature_size : int
|
|
1328
1413
|
The minimum feature size to enforce, in nanometers.
|
|
1329
|
-
strel : str
|
|
1414
|
+
strel : str
|
|
1330
1415
|
The type of structuring element to use. Can be either "disk" or "square".
|
|
1331
1416
|
Defaults to "disk".
|
|
1332
1417
|
|
|
@@ -1358,11 +1443,16 @@ class Device(BaseModel):
|
|
|
1358
1443
|
between the original and modified geometries, providing a measure of the changes
|
|
1359
1444
|
introduced by the feature size enforcement.
|
|
1360
1445
|
|
|
1446
|
+
Notes
|
|
1447
|
+
-----
|
|
1448
|
+
This is not a design-rule-checking function, but it can be useful for quick
|
|
1449
|
+
checks.
|
|
1450
|
+
|
|
1361
1451
|
Parameters
|
|
1362
1452
|
----------
|
|
1363
1453
|
min_feature_size : int
|
|
1364
1454
|
The minimum feature size to enforce, in nanometers.
|
|
1365
|
-
strel : str
|
|
1455
|
+
strel : str
|
|
1366
1456
|
The type of structuring element to use. Can be either "disk" or "square".
|
|
1367
1457
|
Defaults to "disk".
|
|
1368
1458
|
|