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/__init__.py +15 -38
- prefab/__main__.py +95 -0
- prefab/compare.py +126 -0
- prefab/device.py +1486 -0
- prefab/geometry.py +394 -0
- prefab/models.py +114 -0
- prefab/predict.py +337 -0
- prefab/read.py +503 -0
- prefab/shapes.py +773 -0
- {prefab-0.4.7.dist-info → prefab-1.1.8.dist-info}/METADATA +47 -34
- prefab-1.1.8.dist-info/RECORD +14 -0
- {prefab-0.4.7.dist-info → prefab-1.1.8.dist-info}/WHEEL +1 -1
- prefab-1.1.8.dist-info/entry_points.txt +2 -0
- prefab/io.py +0 -200
- prefab/predictor.py +0 -161
- prefab/processor.py +0 -248
- prefab-0.4.7.dist-info/RECORD +0 -8
- {prefab-0.4.7.dist-info → prefab-1.1.8.dist-info}/licenses/LICENSE +0 -0
prefab/device.py
ADDED
|
@@ -0,0 +1,1486 @@
|
|
|
1
|
+
"""Provides the Device class for representing photonic devices."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Literal, Optional
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import gdstk
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import numpy as np
|
|
9
|
+
from matplotlib.axes import Axes
|
|
10
|
+
from matplotlib.patches import Rectangle
|
|
11
|
+
from PIL import Image
|
|
12
|
+
from pydantic import BaseModel, Field, model_validator, validator
|
|
13
|
+
from scipy.ndimage import distance_transform_edt
|
|
14
|
+
from skimage import measure
|
|
15
|
+
|
|
16
|
+
from . import compare, geometry
|
|
17
|
+
from .models import Model
|
|
18
|
+
from .predict import predict_array
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import gdsfactory as gf
|
|
22
|
+
import tidy3d as td
|
|
23
|
+
|
|
24
|
+
Image.MAX_IMAGE_PIXELS = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BufferSpec(BaseModel):
|
|
28
|
+
"""
|
|
29
|
+
Defines the specifications for a buffer zone around a device.
|
|
30
|
+
|
|
31
|
+
This class is used to specify the mode and thickness of a buffer zone that is added
|
|
32
|
+
around the device geometry. The buffer zone can be used for various purposes such as
|
|
33
|
+
providing extra space for device fabrication processes or for ensuring that the
|
|
34
|
+
device is isolated from surrounding structures.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
mode : dict[str, str]
|
|
39
|
+
A dictionary that defines the buffer mode for each side of the device
|
|
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]
|
|
45
|
+
A dictionary that defines the thickness of the buffer zone for each side of the
|
|
46
|
+
device ('top', 'bottom', 'left', 'right'). Each value must be greater than or
|
|
47
|
+
equal to 0.
|
|
48
|
+
|
|
49
|
+
Raises
|
|
50
|
+
------
|
|
51
|
+
ValueError
|
|
52
|
+
If any of the modes specified in the 'mode' dictionary are not one of the
|
|
53
|
+
allowed values ('constant', 'edge', 'none'). Or if any of the thickness values
|
|
54
|
+
are negative.
|
|
55
|
+
|
|
56
|
+
Example
|
|
57
|
+
-------
|
|
58
|
+
import prefab as pf
|
|
59
|
+
|
|
60
|
+
buffer_spec = pf.BufferSpec(
|
|
61
|
+
mode={
|
|
62
|
+
"top": "constant",
|
|
63
|
+
"bottom": "none",
|
|
64
|
+
"left": "constant",
|
|
65
|
+
"right": "edge",
|
|
66
|
+
},
|
|
67
|
+
thickness={
|
|
68
|
+
"top": 150,
|
|
69
|
+
"bottom": 0,
|
|
70
|
+
"left": 200,
|
|
71
|
+
"right": 250,
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
mode: dict[str, Literal["constant", "edge", "none"]] = Field(
|
|
77
|
+
default_factory=lambda: {
|
|
78
|
+
"top": "constant",
|
|
79
|
+
"bottom": "constant",
|
|
80
|
+
"left": "constant",
|
|
81
|
+
"right": "constant",
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
thickness: dict[str, int] = Field(
|
|
85
|
+
default_factory=lambda: {
|
|
86
|
+
"top": 128,
|
|
87
|
+
"bottom": 128,
|
|
88
|
+
"left": 128,
|
|
89
|
+
"right": 128,
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@validator("mode", pre=True)
|
|
94
|
+
def check_mode(cls, v):
|
|
95
|
+
allowed_modes = ["constant", "edge", "none"]
|
|
96
|
+
if not all(mode in allowed_modes for mode in v.values()):
|
|
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.")
|
|
104
|
+
return v
|
|
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
|
+
|
|
121
|
+
|
|
122
|
+
class Device(BaseModel):
|
|
123
|
+
device_array: np.ndarray = Field(...)
|
|
124
|
+
buffer_spec: BufferSpec = Field(default_factory=BufferSpec)
|
|
125
|
+
|
|
126
|
+
class Config:
|
|
127
|
+
arbitrary_types_allowed = True
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def shape(self) -> tuple[int, ...]:
|
|
131
|
+
return tuple(self.device_array.shape)
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self, device_array: np.ndarray, buffer_spec: Optional[BufferSpec] = None
|
|
135
|
+
):
|
|
136
|
+
"""
|
|
137
|
+
Represents the planar geometry of a photonic device design that will have its
|
|
138
|
+
nanofabrication outcome predicted and/or corrected.
|
|
139
|
+
|
|
140
|
+
This class is designed to encapsulate the geometric representation of a photonic
|
|
141
|
+
device, facilitating operations such as padding, normalization, binarization,
|
|
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.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
device_array : np.ndarray
|
|
151
|
+
A 2D array representing the planar geometry of the device. This array
|
|
152
|
+
undergoes various transformations to predict or correct the nanofabrication
|
|
153
|
+
process.
|
|
154
|
+
buffer_spec : Optional[BufferSpec]
|
|
155
|
+
Defines the parameters for adding a buffer zone around the device geometry.
|
|
156
|
+
This buffer zone is needed for providing surrounding context for prediction
|
|
157
|
+
or correction and for ensuring seamless integration with the surrounding
|
|
158
|
+
circuitry. By default, a generous padding is applied to accommodate isolated
|
|
159
|
+
structures.
|
|
160
|
+
|
|
161
|
+
Attributes
|
|
162
|
+
----------
|
|
163
|
+
shape : tuple[int, int]
|
|
164
|
+
The shape of the device array.
|
|
165
|
+
|
|
166
|
+
Raises
|
|
167
|
+
------
|
|
168
|
+
ValueError
|
|
169
|
+
If the provided `device_array` is not a numpy ndarray or is not a 2D array,
|
|
170
|
+
indicating an invalid device geometry.
|
|
171
|
+
"""
|
|
172
|
+
super().__init__(
|
|
173
|
+
device_array=device_array, buffer_spec=buffer_spec or BufferSpec()
|
|
174
|
+
)
|
|
175
|
+
self._initial_processing()
|
|
176
|
+
|
|
177
|
+
def __call__(self, *args, **kwargs):
|
|
178
|
+
return self.plot(*args, **kwargs)
|
|
179
|
+
|
|
180
|
+
def _initial_processing(self):
|
|
181
|
+
buffer_thickness = self.buffer_spec.thickness
|
|
182
|
+
buffer_mode = self.buffer_spec.mode
|
|
183
|
+
|
|
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
|
+
)
|
|
211
|
+
|
|
212
|
+
self.device_array = np.expand_dims(self.device_array, axis=-1)
|
|
213
|
+
|
|
214
|
+
@model_validator(mode="before")
|
|
215
|
+
def check_device_array(cls, values):
|
|
216
|
+
device_array = values.get("device_array")
|
|
217
|
+
if not isinstance(device_array, np.ndarray):
|
|
218
|
+
raise ValueError("device_array must be a numpy ndarray.")
|
|
219
|
+
if device_array.ndim != 2:
|
|
220
|
+
raise ValueError("device_array must be a 2D array.")
|
|
221
|
+
return values
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def is_binary(self) -> bool:
|
|
225
|
+
"""
|
|
226
|
+
Check if the device geometry is binary.
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
bool
|
|
231
|
+
True if the device geometry is binary, False otherwise.
|
|
232
|
+
"""
|
|
233
|
+
unique_values = np.unique(self.device_array)
|
|
234
|
+
return (
|
|
235
|
+
np.array_equal(unique_values, [0, 1])
|
|
236
|
+
or np.array_equal(unique_values, [1, 0])
|
|
237
|
+
or np.array_equal(unique_values, [0])
|
|
238
|
+
or np.array_equal(unique_values, [1])
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def predict(
|
|
242
|
+
self,
|
|
243
|
+
model: Model,
|
|
244
|
+
binarize: bool = False,
|
|
245
|
+
gpu: bool = False,
|
|
246
|
+
) -> "Device":
|
|
247
|
+
"""
|
|
248
|
+
Predict the nanofabrication outcome of the device using a specified model.
|
|
249
|
+
|
|
250
|
+
This method sends the device geometry to a serverless prediction service, which
|
|
251
|
+
uses a specified machine learning model to predict the outcome of the
|
|
252
|
+
nanofabrication process.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
model : Model
|
|
257
|
+
The model to use for prediction, representing a specific fabrication process
|
|
258
|
+
and dataset. This model encapsulates details about the fabrication foundry,
|
|
259
|
+
process, material, technology, thickness, and sidewall presence, as defined
|
|
260
|
+
in `models.py`. Each model is associated with a version and dataset that
|
|
261
|
+
detail its creation and the data it was trained on, ensuring the prediction
|
|
262
|
+
is tailored to specific fabrication parameters.
|
|
263
|
+
binarize : bool
|
|
264
|
+
If True, the predicted device geometry will be binarized using a threshold
|
|
265
|
+
method. This is useful for converting probabilistic predictions into binary
|
|
266
|
+
geometries. Defaults to False.
|
|
267
|
+
gpu : bool
|
|
268
|
+
If True, the prediction will be performed on a GPU. Defaults to False.
|
|
269
|
+
Note: The GPU option has more overhead and will take longer for small
|
|
270
|
+
devices, but will be faster for larger devices.
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
Device
|
|
275
|
+
A new instance of the Device class with the predicted geometry.
|
|
276
|
+
|
|
277
|
+
Raises
|
|
278
|
+
------
|
|
279
|
+
RuntimeError
|
|
280
|
+
If the prediction service returns an error or if the response from the
|
|
281
|
+
service cannot be processed correctly.
|
|
282
|
+
"""
|
|
283
|
+
prediction_array = predict_array(
|
|
284
|
+
device_array=self.device_array,
|
|
285
|
+
model=model,
|
|
286
|
+
model_type="p",
|
|
287
|
+
binarize=binarize,
|
|
288
|
+
gpu=gpu,
|
|
289
|
+
)
|
|
290
|
+
return self.model_copy(update={"device_array": prediction_array})
|
|
291
|
+
|
|
292
|
+
def correct(
|
|
293
|
+
self,
|
|
294
|
+
model: Model,
|
|
295
|
+
binarize: bool = True,
|
|
296
|
+
gpu: bool = False,
|
|
297
|
+
) -> "Device":
|
|
298
|
+
"""
|
|
299
|
+
Correct the nanofabrication outcome of the device using a specified model.
|
|
300
|
+
|
|
301
|
+
This method sends the device geometry to a serverless correction service, which
|
|
302
|
+
uses a specified machine learning model to correct the outcome of the
|
|
303
|
+
nanofabrication process. The correction aims to adjust the device geometry to
|
|
304
|
+
compensate for known fabrication errors and improve the accuracy of the final
|
|
305
|
+
device structure.
|
|
306
|
+
|
|
307
|
+
Parameters
|
|
308
|
+
----------
|
|
309
|
+
model : Model
|
|
310
|
+
The model to use for correction, representing a specific fabrication process
|
|
311
|
+
and dataset. This model encapsulates details about the fabrication foundry,
|
|
312
|
+
process, material, technology, thickness, and sidewall presence, as defined
|
|
313
|
+
in `models.py`. Each model is associated with a version and dataset that
|
|
314
|
+
detail its creation and the data it was trained on, ensuring the correction
|
|
315
|
+
is tailored to specific fabrication parameters.
|
|
316
|
+
binarize : bool
|
|
317
|
+
If True, the corrected device geometry will be binarized using a threshold
|
|
318
|
+
method. This is useful for converting probabilistic corrections into binary
|
|
319
|
+
geometries. Defaults to True.
|
|
320
|
+
gpu : bool
|
|
321
|
+
If True, the prediction will be performed on a GPU. Defaults to False.
|
|
322
|
+
Note: The GPU option has more overhead and will take longer for small
|
|
323
|
+
devices, but will be faster for larger devices.
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
Device
|
|
328
|
+
A new instance of the Device class with the corrected geometry.
|
|
329
|
+
|
|
330
|
+
Raises
|
|
331
|
+
------
|
|
332
|
+
RuntimeError
|
|
333
|
+
If the correction service returns an error or if the response from the
|
|
334
|
+
service cannot be processed correctly.
|
|
335
|
+
"""
|
|
336
|
+
correction_array = predict_array(
|
|
337
|
+
device_array=self.device_array,
|
|
338
|
+
model=model,
|
|
339
|
+
model_type="c",
|
|
340
|
+
binarize=binarize,
|
|
341
|
+
gpu=gpu,
|
|
342
|
+
)
|
|
343
|
+
return self.model_copy(update={"device_array": correction_array})
|
|
344
|
+
|
|
345
|
+
def semulate(
|
|
346
|
+
self,
|
|
347
|
+
model: Model,
|
|
348
|
+
gpu: bool = False,
|
|
349
|
+
) -> "Device":
|
|
350
|
+
"""
|
|
351
|
+
Simulate the appearance of the device as if viewed under a scanning electron
|
|
352
|
+
microscope (SEM).
|
|
353
|
+
|
|
354
|
+
This method applies a specified machine learning model to transform the device
|
|
355
|
+
geometry into a style that resembles an SEM image. This can be useful for
|
|
356
|
+
visualizing how the device might appear under an SEM, which is often used for
|
|
357
|
+
inspecting the surface and composition of materials at high magnification.
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
model : Model
|
|
362
|
+
The model to use for SEMulation, representing a specific fabrication process
|
|
363
|
+
and dataset. This model encapsulates details about the fabrication foundry,
|
|
364
|
+
process, material, technology, thickness, and sidewall presence, as defined
|
|
365
|
+
in `models.py`. Each model is associated with a version and dataset that
|
|
366
|
+
detail its creation and the data it was trained on, ensuring the SEMulation
|
|
367
|
+
is tailored to specific fabrication parameters.
|
|
368
|
+
gpu : bool
|
|
369
|
+
If True, the prediction will be performed on a GPU. Defaults to False.
|
|
370
|
+
Note: The GPU option has more overhead and will take longer for small
|
|
371
|
+
devices, but will be faster for larger devices.
|
|
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
|
+
|
|
378
|
+
Returns
|
|
379
|
+
-------
|
|
380
|
+
Device
|
|
381
|
+
A new instance of the Device class with its geometry transformed to simulate
|
|
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.
|
|
389
|
+
"""
|
|
390
|
+
semulated_array = predict_array(
|
|
391
|
+
device_array=self.device_array,
|
|
392
|
+
model=model,
|
|
393
|
+
model_type="s",
|
|
394
|
+
binarize=False,
|
|
395
|
+
gpu=gpu,
|
|
396
|
+
)
|
|
397
|
+
semulated_array += np.random.normal(0, 0.03, semulated_array.shape)
|
|
398
|
+
return self.model_copy(update={"device_array": semulated_array})
|
|
399
|
+
|
|
400
|
+
def to_ndarray(self) -> np.ndarray:
|
|
401
|
+
"""
|
|
402
|
+
Converts the device geometry to an ndarray.
|
|
403
|
+
|
|
404
|
+
This method applies the buffer specifications to crop the device array if
|
|
405
|
+
necessary, based on the buffer mode ('edge' or 'constant'). It then returns the
|
|
406
|
+
resulting ndarray representing the device geometry.
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
np.ndarray
|
|
411
|
+
The ndarray representation of the device geometry, with any applied buffer
|
|
412
|
+
cropping.
|
|
413
|
+
"""
|
|
414
|
+
device_array = np.copy(self.device_array)
|
|
415
|
+
buffer_thickness = self.buffer_spec.thickness
|
|
416
|
+
buffer_mode = self.buffer_spec.mode
|
|
417
|
+
|
|
418
|
+
crop_top = buffer_thickness["top"] if buffer_mode["top"] == "constant" else 0
|
|
419
|
+
crop_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
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
ndarray = device_array[
|
|
428
|
+
crop_top : device_array.shape[0] - crop_bottom,
|
|
429
|
+
crop_left : device_array.shape[1] - crop_right,
|
|
430
|
+
]
|
|
431
|
+
return np.squeeze(ndarray)
|
|
432
|
+
|
|
433
|
+
def to_img(self, img_path: str = "prefab_device.png"):
|
|
434
|
+
"""
|
|
435
|
+
Exports the device geometry as an image file.
|
|
436
|
+
|
|
437
|
+
This method converts the device geometry to an ndarray using `to_ndarray`,
|
|
438
|
+
scales the values to the range [0, 255] for image representation, and saves the
|
|
439
|
+
result as an image file.
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
img_path : str
|
|
444
|
+
The path where the image file will be saved. If not specified, the image is
|
|
445
|
+
saved as "prefab_device.png" in the current directory.
|
|
446
|
+
"""
|
|
447
|
+
cv2.imwrite(img_path, 255 * self.flatten().to_ndarray())
|
|
448
|
+
print(f"Saved Device image to '{img_path}'")
|
|
449
|
+
|
|
450
|
+
def to_gds(
|
|
451
|
+
self,
|
|
452
|
+
gds_path: str = "prefab_device.gds",
|
|
453
|
+
cell_name: str = "prefab_device",
|
|
454
|
+
gds_layer: tuple[int, int] = (1, 0),
|
|
455
|
+
contour_approx_mode: int = 2,
|
|
456
|
+
origin: tuple[float, float] = (0.0, 0.0),
|
|
457
|
+
):
|
|
458
|
+
"""
|
|
459
|
+
Exports the device geometry as a GDSII file.
|
|
460
|
+
|
|
461
|
+
This method converts the device geometry into a format suitable for GDSII files.
|
|
462
|
+
The conversion involves contour approximation to simplify the geometry while
|
|
463
|
+
preserving essential features.
|
|
464
|
+
|
|
465
|
+
Parameters
|
|
466
|
+
----------
|
|
467
|
+
gds_path : str
|
|
468
|
+
The path where the GDSII file will be saved. If not specified, the file is
|
|
469
|
+
saved as "prefab_device.gds" in the current directory.
|
|
470
|
+
cell_name : str
|
|
471
|
+
The name of the cell within the GDSII file. If not specified, defaults to
|
|
472
|
+
"prefab_device".
|
|
473
|
+
gds_layer : tuple[int, int]
|
|
474
|
+
The layer and datatype to use within the GDSII file. Defaults to (1, 0).
|
|
475
|
+
contour_approx_mode : int
|
|
476
|
+
The mode of contour approximation used during the conversion. Defaults to 2,
|
|
477
|
+
which corresponds to `cv2.CHAIN_APPROX_SIMPLE`, a method that compresses
|
|
478
|
+
horizontal, vertical, and diagonal segments and leaves only their endpoints.
|
|
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).
|
|
482
|
+
"""
|
|
483
|
+
gdstk_cell = self.flatten()._device_to_gdstk(
|
|
484
|
+
cell_name=cell_name,
|
|
485
|
+
gds_layer=gds_layer,
|
|
486
|
+
contour_approx_mode=contour_approx_mode,
|
|
487
|
+
origin=origin,
|
|
488
|
+
)
|
|
489
|
+
print(f"Saving GDS to '{gds_path}'...")
|
|
490
|
+
gdstk_library = gdstk.Library()
|
|
491
|
+
gdstk_library.add(gdstk_cell)
|
|
492
|
+
gdstk_library.write_gds(outfile=gds_path, max_points=8190)
|
|
493
|
+
|
|
494
|
+
def to_gdstk(
|
|
495
|
+
self,
|
|
496
|
+
cell_name: str = "prefab_device",
|
|
497
|
+
gds_layer: tuple[int, int] = (1, 0),
|
|
498
|
+
contour_approx_mode: int = 2,
|
|
499
|
+
origin: tuple[float, float] = (0.0, 0.0),
|
|
500
|
+
):
|
|
501
|
+
"""
|
|
502
|
+
Converts the device geometry to a GDSTK cell object.
|
|
503
|
+
|
|
504
|
+
This method prepares the device geometry for GDSII file export by converting it
|
|
505
|
+
into a GDSTK cell object. GDSTK is a Python module for creating and manipulating
|
|
506
|
+
GDSII layout files. The conversion involves contour approximation to simplify
|
|
507
|
+
the geometry while preserving essential features.
|
|
508
|
+
|
|
509
|
+
Parameters
|
|
510
|
+
----------
|
|
511
|
+
cell_name : str
|
|
512
|
+
The name of the cell to be created. Defaults to "prefab_device".
|
|
513
|
+
gds_layer : tuple[int, int]
|
|
514
|
+
The layer and datatype to use within the GDSTK cell. Defaults to (1, 0).
|
|
515
|
+
contour_approx_mode : int
|
|
516
|
+
The mode of contour approximation used during the conversion. Defaults to 2,
|
|
517
|
+
which corresponds to `cv2.CHAIN_APPROX_SIMPLE`, a method that compresses
|
|
518
|
+
horizontal, vertical, and diagonal segments and leaves only their endpoints.
|
|
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).
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
gdstk.Cell
|
|
526
|
+
The GDSTK cell object representing the device geometry.
|
|
527
|
+
"""
|
|
528
|
+
print(f"Creating cell '{cell_name}'...")
|
|
529
|
+
gdstk_cell = self.flatten()._device_to_gdstk(
|
|
530
|
+
cell_name=cell_name,
|
|
531
|
+
gds_layer=gds_layer,
|
|
532
|
+
contour_approx_mode=contour_approx_mode,
|
|
533
|
+
origin=origin,
|
|
534
|
+
)
|
|
535
|
+
return gdstk_cell
|
|
536
|
+
|
|
537
|
+
def _device_to_gdstk(
|
|
538
|
+
self,
|
|
539
|
+
cell_name: str,
|
|
540
|
+
gds_layer: tuple[int, int],
|
|
541
|
+
contour_approx_mode: int,
|
|
542
|
+
origin: tuple[float, float],
|
|
543
|
+
) -> gdstk.Cell:
|
|
544
|
+
approx_mode_mapping = {
|
|
545
|
+
1: cv2.CHAIN_APPROX_NONE,
|
|
546
|
+
2: cv2.CHAIN_APPROX_SIMPLE,
|
|
547
|
+
3: cv2.CHAIN_APPROX_TC89_L1,
|
|
548
|
+
4: cv2.CHAIN_APPROX_TC89_KCOS,
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
contours, hierarchy = cv2.findContours(
|
|
552
|
+
np.flipud(self.to_ndarray()).astype(np.uint8),
|
|
553
|
+
cv2.RETR_TREE,
|
|
554
|
+
approx_mode_mapping[contour_approx_mode],
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
hierarchy_polygons = {}
|
|
558
|
+
for idx, contour in enumerate(contours):
|
|
559
|
+
level = 0
|
|
560
|
+
current_idx = idx
|
|
561
|
+
while hierarchy[0][current_idx][3] != -1:
|
|
562
|
+
level += 1
|
|
563
|
+
current_idx = hierarchy[0][current_idx][3]
|
|
564
|
+
|
|
565
|
+
if len(contour) > 2:
|
|
566
|
+
contour = contour / 1000
|
|
567
|
+
points = [tuple(point) for point in contour.squeeze().tolist()]
|
|
568
|
+
if level not in hierarchy_polygons:
|
|
569
|
+
hierarchy_polygons[level] = []
|
|
570
|
+
hierarchy_polygons[level].append(points)
|
|
571
|
+
|
|
572
|
+
cell = gdstk.Cell(cell_name)
|
|
573
|
+
processed_polygons = []
|
|
574
|
+
for level in sorted(hierarchy_polygons.keys()):
|
|
575
|
+
operation = "or" if level % 2 == 0 else "xor"
|
|
576
|
+
polygons_to_process = hierarchy_polygons[level]
|
|
577
|
+
|
|
578
|
+
if polygons_to_process:
|
|
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
|
|
591
|
+
|
|
592
|
+
center_x_um = center_x_nm / 1000
|
|
593
|
+
center_y_um = center_y_nm / 1000
|
|
594
|
+
|
|
595
|
+
adjusted_polygons = [
|
|
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
|
+
)
|
|
602
|
+
for polygon in polygons_to_process
|
|
603
|
+
]
|
|
604
|
+
processed_polygons = gdstk.boolean(
|
|
605
|
+
adjusted_polygons,
|
|
606
|
+
processed_polygons,
|
|
607
|
+
operation,
|
|
608
|
+
layer=gds_layer[0],
|
|
609
|
+
datatype=gds_layer[1],
|
|
610
|
+
)
|
|
611
|
+
for polygon in processed_polygons:
|
|
612
|
+
cell.add(polygon)
|
|
613
|
+
|
|
614
|
+
return cell
|
|
615
|
+
|
|
616
|
+
def to_gdsfactory(self) -> "gf.Component":
|
|
617
|
+
"""
|
|
618
|
+
Convert the device geometry to a gdsfactory Component.
|
|
619
|
+
|
|
620
|
+
Returns
|
|
621
|
+
-------
|
|
622
|
+
gf.Component
|
|
623
|
+
A gdsfactory Component object representing the device geometry.
|
|
624
|
+
|
|
625
|
+
Raises
|
|
626
|
+
------
|
|
627
|
+
ImportError
|
|
628
|
+
If the gdsfactory package is not installed.
|
|
629
|
+
"""
|
|
630
|
+
try:
|
|
631
|
+
import gdsfactory as gf
|
|
632
|
+
except ImportError:
|
|
633
|
+
raise ImportError(
|
|
634
|
+
"The gdsfactory package is required to use this function; "
|
|
635
|
+
"try `pip install gdsfactory`."
|
|
636
|
+
) from None
|
|
637
|
+
|
|
638
|
+
device_array = np.rot90(self.to_ndarray(), k=-1)
|
|
639
|
+
return gf.read.from_np(device_array, nm_per_pixel=1)
|
|
640
|
+
|
|
641
|
+
def to_tidy3d(
|
|
642
|
+
self,
|
|
643
|
+
eps0: float,
|
|
644
|
+
thickness: float,
|
|
645
|
+
) -> "td.Structure":
|
|
646
|
+
"""
|
|
647
|
+
Convert the device geometry to a Tidy3D Structure.
|
|
648
|
+
|
|
649
|
+
Parameters
|
|
650
|
+
----------
|
|
651
|
+
eps0 : float
|
|
652
|
+
The permittivity value to assign to the device array.
|
|
653
|
+
thickness : float
|
|
654
|
+
The thickness of the device in the z-direction.
|
|
655
|
+
|
|
656
|
+
Returns
|
|
657
|
+
-------
|
|
658
|
+
td.Structure
|
|
659
|
+
A Tidy3D Structure object representing the device geometry.
|
|
660
|
+
|
|
661
|
+
Raises
|
|
662
|
+
------
|
|
663
|
+
ImportError
|
|
664
|
+
If the tidy3d package is not installed.
|
|
665
|
+
"""
|
|
666
|
+
try:
|
|
667
|
+
from tidy3d import Box, CustomMedium, SpatialDataArray, Structure, inf
|
|
668
|
+
except ImportError:
|
|
669
|
+
raise ImportError(
|
|
670
|
+
"The tidy3d package is required to use this function; "
|
|
671
|
+
"try `pip install tidy3d`."
|
|
672
|
+
) from None
|
|
673
|
+
|
|
674
|
+
X = np.linspace(-self.shape[1] / 2000, self.shape[1] / 2000, self.shape[1])
|
|
675
|
+
Y = np.linspace(-self.shape[0] / 2000, self.shape[0] / 2000, self.shape[0])
|
|
676
|
+
Z = np.array([0])
|
|
677
|
+
|
|
678
|
+
device_array = np.rot90(np.fliplr(self.device_array), k=1)
|
|
679
|
+
eps_array = np.where(device_array >= 1.0, eps0, device_array)
|
|
680
|
+
eps_array = np.where(eps_array < 1.0, 1.0, eps_array)
|
|
681
|
+
eps_dataset = SpatialDataArray(eps_array, coords=dict(x=X, y=Y, z=Z))
|
|
682
|
+
medium = CustomMedium.from_eps_raw(eps_dataset)
|
|
683
|
+
return Structure(
|
|
684
|
+
geometry=Box(center=(0, 0, 0), size=(inf, inf, thickness), attrs={}),
|
|
685
|
+
medium=medium,
|
|
686
|
+
name="device",
|
|
687
|
+
attrs={},
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
def to_3d(self, thickness_nm: int) -> np.ndarray:
|
|
691
|
+
"""
|
|
692
|
+
Convert the 2D device geometry into a 3D representation.
|
|
693
|
+
|
|
694
|
+
This method creates a 3D array by interpolating between the bottom and top
|
|
695
|
+
layers of the device geometry. The interpolation is linear.
|
|
696
|
+
|
|
697
|
+
Parameters
|
|
698
|
+
----------
|
|
699
|
+
thickness_nm : int
|
|
700
|
+
The thickness of the 3D representation in nanometers.
|
|
701
|
+
|
|
702
|
+
Returns
|
|
703
|
+
-------
|
|
704
|
+
np.ndarray
|
|
705
|
+
A 3D narray representing the device geometry with the specified thickness.
|
|
706
|
+
"""
|
|
707
|
+
bottom_layer = self.device_array[:, :, 0]
|
|
708
|
+
top_layer = self.device_array[:, :, -1]
|
|
709
|
+
dt_bottom = np.array(distance_transform_edt(bottom_layer)) - np.array(
|
|
710
|
+
distance_transform_edt(1 - bottom_layer)
|
|
711
|
+
)
|
|
712
|
+
dt_top = np.array(distance_transform_edt(top_layer)) - np.array(
|
|
713
|
+
distance_transform_edt(1 - top_layer)
|
|
714
|
+
)
|
|
715
|
+
weights = np.linspace(0, 1, thickness_nm)
|
|
716
|
+
layered_array = np.zeros(
|
|
717
|
+
(bottom_layer.shape[0], bottom_layer.shape[1], thickness_nm)
|
|
718
|
+
)
|
|
719
|
+
for i, w in enumerate(weights):
|
|
720
|
+
dt_interp = (1 - w) * dt_bottom + w * dt_top
|
|
721
|
+
layered_array[:, :, i] = dt_interp >= 0
|
|
722
|
+
return layered_array
|
|
723
|
+
|
|
724
|
+
def to_stl(self, thickness_nm: int, filename: str = "prefab_device.stl"):
|
|
725
|
+
"""
|
|
726
|
+
Export the device geometry as an STL file.
|
|
727
|
+
|
|
728
|
+
Parameters
|
|
729
|
+
----------
|
|
730
|
+
thickness_nm : int
|
|
731
|
+
The thickness of the 3D representation in nanometers.
|
|
732
|
+
filename : str
|
|
733
|
+
The name of the STL file to save. Defaults to "prefab_device.stl".
|
|
734
|
+
|
|
735
|
+
Raises
|
|
736
|
+
------
|
|
737
|
+
ValueError
|
|
738
|
+
If the thickness is not a positive integer.
|
|
739
|
+
ImportError
|
|
740
|
+
If the numpy-stl package is not installed.
|
|
741
|
+
"""
|
|
742
|
+
try:
|
|
743
|
+
from stl import mesh # type: ignore
|
|
744
|
+
except ImportError:
|
|
745
|
+
raise ImportError(
|
|
746
|
+
"The stl package is required to use this function; "
|
|
747
|
+
"try `pip install numpy-stl`."
|
|
748
|
+
) from None
|
|
749
|
+
|
|
750
|
+
if thickness_nm <= 0:
|
|
751
|
+
raise ValueError("Thickness must be a positive integer.")
|
|
752
|
+
|
|
753
|
+
layered_array = self.to_3d(thickness_nm)
|
|
754
|
+
layered_array = np.pad(
|
|
755
|
+
layered_array, ((0, 0), (0, 0), (10, 10)), mode="constant"
|
|
756
|
+
)
|
|
757
|
+
verts, faces, _, _ = measure.marching_cubes(layered_array, level=0.5)
|
|
758
|
+
cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
|
|
759
|
+
for i, f in enumerate(faces):
|
|
760
|
+
for j in range(3):
|
|
761
|
+
cube.vectors[i][j] = verts[f[j], :]
|
|
762
|
+
cube.save(filename)
|
|
763
|
+
print(f"Saved Device to '{filename}'")
|
|
764
|
+
|
|
765
|
+
def _plot_base(
|
|
766
|
+
self,
|
|
767
|
+
plot_array: np.ndarray,
|
|
768
|
+
show_buffer: bool,
|
|
769
|
+
bounds: Optional[tuple[tuple[int, int], tuple[int, int]]],
|
|
770
|
+
ax: Optional[Axes],
|
|
771
|
+
**kwargs,
|
|
772
|
+
) -> tuple[plt.cm.ScalarMappable, Axes]:
|
|
773
|
+
if ax is None:
|
|
774
|
+
_, ax = plt.subplots()
|
|
775
|
+
ax.set_ylabel("y (nm)")
|
|
776
|
+
ax.set_xlabel("x (nm)")
|
|
777
|
+
|
|
778
|
+
min_x, min_y = (0, 0) if bounds is None else bounds[0]
|
|
779
|
+
max_x, max_y = plot_array.shape[::-1] if bounds is None else bounds[1]
|
|
780
|
+
min_x = max(min_x, 0)
|
|
781
|
+
min_y = max(min_y, 0)
|
|
782
|
+
max_x = "end" if max_x == "end" else min(max_x, plot_array.shape[1])
|
|
783
|
+
max_y = "end" if max_y == "end" else min(max_y, plot_array.shape[0])
|
|
784
|
+
max_x = plot_array.shape[1] if max_x == "end" else max_x
|
|
785
|
+
max_y = plot_array.shape[0] if max_y == "end" else max_y
|
|
786
|
+
plot_array = plot_array[
|
|
787
|
+
plot_array.shape[0] - max_y : plot_array.shape[0] - min_y,
|
|
788
|
+
min_x:max_x,
|
|
789
|
+
]
|
|
790
|
+
|
|
791
|
+
if not np.ma.is_masked(plot_array):
|
|
792
|
+
max_size = (1000, 1000)
|
|
793
|
+
scale_x = min(1, max_size[0] / plot_array.shape[1])
|
|
794
|
+
scale_y = min(1, max_size[1] / plot_array.shape[0])
|
|
795
|
+
fx = min(scale_x, scale_y)
|
|
796
|
+
fy = fx
|
|
797
|
+
|
|
798
|
+
plot_array = cv2.resize(
|
|
799
|
+
plot_array,
|
|
800
|
+
dsize=(0, 0),
|
|
801
|
+
fx=fx,
|
|
802
|
+
fy=fy,
|
|
803
|
+
interpolation=cv2.INTER_NEAREST,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
mappable = ax.imshow(
|
|
807
|
+
plot_array,
|
|
808
|
+
extent=(
|
|
809
|
+
float(min_x),
|
|
810
|
+
float(max_x),
|
|
811
|
+
float(min_y),
|
|
812
|
+
float(max_y),
|
|
813
|
+
),
|
|
814
|
+
**kwargs,
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
if show_buffer:
|
|
818
|
+
self._add_buffer_visualization(ax)
|
|
819
|
+
|
|
820
|
+
# # Adjust colorbar font size if a colorbar is added
|
|
821
|
+
# if "cmap" in kwargs:
|
|
822
|
+
# cbar = plt.colorbar(mappable, ax=ax)
|
|
823
|
+
# cbar.ax.tick_params(labelsize=14)
|
|
824
|
+
# if "label" in kwargs:
|
|
825
|
+
# cbar.set_label(kwargs["label"], fontsize=16)
|
|
826
|
+
|
|
827
|
+
return mappable, ax
|
|
828
|
+
|
|
829
|
+
def plot(
|
|
830
|
+
self,
|
|
831
|
+
show_buffer: bool = True,
|
|
832
|
+
bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
|
|
833
|
+
level: Optional[int] = None,
|
|
834
|
+
ax: Optional[Axes] = None,
|
|
835
|
+
**kwargs,
|
|
836
|
+
) -> Axes:
|
|
837
|
+
"""
|
|
838
|
+
Visualizes the device geometry.
|
|
839
|
+
|
|
840
|
+
This method allows for the visualization of the device geometry. The
|
|
841
|
+
visualization can be customized with various matplotlib parameters and can be
|
|
842
|
+
drawn on an existing matplotlib Axes object or create a new one if none is
|
|
843
|
+
provided.
|
|
844
|
+
|
|
845
|
+
Parameters
|
|
846
|
+
----------
|
|
847
|
+
show_buffer : bool
|
|
848
|
+
If True, visualizes the buffer zones around the device. Defaults to True.
|
|
849
|
+
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]], optional
|
|
850
|
+
Specifies the bounds for zooming into the device geometry, formatted as
|
|
851
|
+
((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
|
|
852
|
+
will be replaced with the corresponding dimension size of the device array.
|
|
853
|
+
If None, the entire device geometry is visualized.
|
|
854
|
+
level : int
|
|
855
|
+
The vertical layer to plot. If None, the device geometry is flattened.
|
|
856
|
+
Defaults to None.
|
|
857
|
+
ax : Optional[Axes]
|
|
858
|
+
An existing matplotlib Axes object to draw the device geometry on. If
|
|
859
|
+
None, a new figure and axes will be created. Defaults to None.
|
|
860
|
+
**kwargs
|
|
861
|
+
Additional matplotlib parameters for plot customization.
|
|
862
|
+
|
|
863
|
+
Returns
|
|
864
|
+
-------
|
|
865
|
+
Axes
|
|
866
|
+
The matplotlib Axes object containing the plot. This object can be used for
|
|
867
|
+
further plot customization or saving the plot after the method returns.
|
|
868
|
+
"""
|
|
869
|
+
if level is None:
|
|
870
|
+
plot_array = geometry.flatten(self.device_array)[:, :, 0]
|
|
871
|
+
else:
|
|
872
|
+
plot_array = self.device_array[:, :, level]
|
|
873
|
+
_, ax = self._plot_base(
|
|
874
|
+
plot_array=plot_array,
|
|
875
|
+
show_buffer=show_buffer,
|
|
876
|
+
bounds=bounds,
|
|
877
|
+
ax=ax,
|
|
878
|
+
**kwargs,
|
|
879
|
+
)
|
|
880
|
+
return ax
|
|
881
|
+
|
|
882
|
+
def plot_contour(
|
|
883
|
+
self,
|
|
884
|
+
linewidth: Optional[int] = None,
|
|
885
|
+
# label: Optional[str] = "Device contour",
|
|
886
|
+
show_buffer: bool = True,
|
|
887
|
+
bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
|
|
888
|
+
level: Optional[int] = None,
|
|
889
|
+
ax: Optional[Axes] = None,
|
|
890
|
+
**kwargs,
|
|
891
|
+
):
|
|
892
|
+
"""
|
|
893
|
+
Visualizes the contour of the device geometry.
|
|
894
|
+
|
|
895
|
+
This method plots the contour of the device geometry, emphasizing the edges and
|
|
896
|
+
boundaries of the device. The contour plot can be customized with various
|
|
897
|
+
matplotlib parameters, including line width and color. The plot can be drawn on
|
|
898
|
+
an existing matplotlib Axes object or create a new one if none is provided.
|
|
899
|
+
|
|
900
|
+
Parameters
|
|
901
|
+
----------
|
|
902
|
+
linewidth : Optional[int]
|
|
903
|
+
The width of the contour lines. If None, the linewidth is automatically
|
|
904
|
+
determined based on the size of the device array. Defaults to None.
|
|
905
|
+
show_buffer : bool
|
|
906
|
+
If True, the buffer zones around the device will be visualized. By default,
|
|
907
|
+
it is set to True.
|
|
908
|
+
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
909
|
+
Specifies the bounds for zooming into the device geometry, formatted as
|
|
910
|
+
((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
|
|
911
|
+
will be replaced with the corresponding dimension size of the device array.
|
|
912
|
+
If None, the entire device geometry is visualized.
|
|
913
|
+
level : int
|
|
914
|
+
The vertical layer to plot. If None, the device geometry is flattened.
|
|
915
|
+
Defaults to None.
|
|
916
|
+
ax : Optional[Axes]
|
|
917
|
+
An existing matplotlib Axes object to draw the device contour on. If None, a
|
|
918
|
+
new figure and axes will be created. Defaults to None.
|
|
919
|
+
**kwargs
|
|
920
|
+
Additional matplotlib parameters for plot customization.
|
|
921
|
+
|
|
922
|
+
Returns
|
|
923
|
+
-------
|
|
924
|
+
Axes
|
|
925
|
+
The matplotlib Axes object containing the contour plot. This can be used for
|
|
926
|
+
further customization or saving the plot after the method returns.
|
|
927
|
+
"""
|
|
928
|
+
if level is None:
|
|
929
|
+
device_array = geometry.flatten(self.device_array)[:, :, 0]
|
|
930
|
+
else:
|
|
931
|
+
device_array = self.device_array[:, :, level]
|
|
932
|
+
|
|
933
|
+
kwargs.setdefault("cmap", "spring")
|
|
934
|
+
if linewidth is None:
|
|
935
|
+
linewidth = device_array.shape[0] // 100
|
|
936
|
+
|
|
937
|
+
contours, _ = cv2.findContours(
|
|
938
|
+
geometry.binarize_hard(device_array).astype(np.uint8),
|
|
939
|
+
cv2.RETR_CCOMP,
|
|
940
|
+
cv2.CHAIN_APPROX_SIMPLE,
|
|
941
|
+
)
|
|
942
|
+
contour_array = np.zeros_like(device_array, dtype=np.uint8)
|
|
943
|
+
cv2.drawContours(contour_array, contours, -1, (255,), linewidth)
|
|
944
|
+
contour_array = np.ma.masked_equal(contour_array, 0)
|
|
945
|
+
|
|
946
|
+
_, ax = self._plot_base(
|
|
947
|
+
plot_array=contour_array,
|
|
948
|
+
show_buffer=show_buffer,
|
|
949
|
+
bounds=bounds,
|
|
950
|
+
ax=ax,
|
|
951
|
+
**kwargs,
|
|
952
|
+
)
|
|
953
|
+
# cmap = cm.get_cmap(kwargs.get("cmap", "spring"))
|
|
954
|
+
# legend_proxy = Line2D([0], [0], linestyle="-", color=cmap(1))
|
|
955
|
+
# ax.legend([legend_proxy], [label], loc="upper right")
|
|
956
|
+
return ax
|
|
957
|
+
|
|
958
|
+
def plot_uncertainty(
|
|
959
|
+
self,
|
|
960
|
+
show_buffer: bool = True,
|
|
961
|
+
bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
|
|
962
|
+
level: Optional[int] = None,
|
|
963
|
+
ax: Optional[Axes] = None,
|
|
964
|
+
**kwargs,
|
|
965
|
+
):
|
|
966
|
+
"""
|
|
967
|
+
Visualizes the uncertainty in the edge positions of the predicted device.
|
|
968
|
+
|
|
969
|
+
This method plots the uncertainty associated with the positions of the edges of
|
|
970
|
+
the device. The uncertainty is represented as a gradient, with areas of higher
|
|
971
|
+
uncertainty indicating a greater likelihood of the edge position from run to run
|
|
972
|
+
(due to inconsistencies in the fabrication process). This visualization can help
|
|
973
|
+
in identifying areas within the device geometry that may require design
|
|
974
|
+
adjustments to improve fabrication consistency.
|
|
975
|
+
|
|
976
|
+
Parameters
|
|
977
|
+
----------
|
|
978
|
+
show_buffer : bool
|
|
979
|
+
If True, the buffer zones around the device will also be visualized. By
|
|
980
|
+
default, it is set to True.
|
|
981
|
+
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
982
|
+
Specifies the bounds for zooming into the device geometry, formatted as
|
|
983
|
+
((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
|
|
984
|
+
will be replaced with the corresponding dimension size of the device array.
|
|
985
|
+
If None, the entire device geometry is visualized.
|
|
986
|
+
level : int
|
|
987
|
+
The vertical layer to plot. If None, the device geometry is flattened.
|
|
988
|
+
Defaults to None.
|
|
989
|
+
ax : Optional[Axes]
|
|
990
|
+
An existing matplotlib Axes object to draw the uncertainty visualization on.
|
|
991
|
+
If None, a new figure and axes will be created. Defaults to None.
|
|
992
|
+
**kwargs
|
|
993
|
+
Additional matplotlib parameters for plot customization.
|
|
994
|
+
|
|
995
|
+
Returns
|
|
996
|
+
-------
|
|
997
|
+
Axes
|
|
998
|
+
The matplotlib Axes object containing the uncertainty visualization. This
|
|
999
|
+
can be used for further customization or saving the plot after the method
|
|
1000
|
+
returns.
|
|
1001
|
+
"""
|
|
1002
|
+
uncertainty_array = self.get_uncertainty()
|
|
1003
|
+
|
|
1004
|
+
if level is None:
|
|
1005
|
+
uncertainty_array = geometry.flatten(uncertainty_array)[:, :, 0]
|
|
1006
|
+
else:
|
|
1007
|
+
uncertainty_array = uncertainty_array[:, :, level]
|
|
1008
|
+
|
|
1009
|
+
mappable, ax = self._plot_base(
|
|
1010
|
+
plot_array=uncertainty_array,
|
|
1011
|
+
show_buffer=show_buffer,
|
|
1012
|
+
bounds=bounds,
|
|
1013
|
+
ax=ax,
|
|
1014
|
+
**kwargs,
|
|
1015
|
+
)
|
|
1016
|
+
cbar = plt.colorbar(mappable, ax=ax)
|
|
1017
|
+
cbar.set_label("Uncertainty (a.u.)")
|
|
1018
|
+
return ax
|
|
1019
|
+
|
|
1020
|
+
def plot_compare(
|
|
1021
|
+
self,
|
|
1022
|
+
ref_device: "Device",
|
|
1023
|
+
show_buffer: bool = True,
|
|
1024
|
+
bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
|
|
1025
|
+
level: Optional[int] = None,
|
|
1026
|
+
ax: Optional[Axes] = None,
|
|
1027
|
+
**kwargs,
|
|
1028
|
+
) -> Axes:
|
|
1029
|
+
"""
|
|
1030
|
+
Visualizes the comparison between the current device geometry and a reference
|
|
1031
|
+
device geometry.
|
|
1032
|
+
|
|
1033
|
+
Positive values (dilation) and negative values (erosion) are visualized with a
|
|
1034
|
+
color map to indicate areas where the current device has expanded or contracted
|
|
1035
|
+
relative to the reference.
|
|
1036
|
+
|
|
1037
|
+
Parameters
|
|
1038
|
+
----------
|
|
1039
|
+
ref_device : Device
|
|
1040
|
+
The reference device to compare against.
|
|
1041
|
+
show_buffer : bool
|
|
1042
|
+
If True, visualizes the buffer zones around the device. Defaults to True.
|
|
1043
|
+
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
1044
|
+
Specifies the bounds for zooming into the device geometry, formatted as
|
|
1045
|
+
((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
|
|
1046
|
+
will be replaced with the corresponding dimension size of the device array.
|
|
1047
|
+
If None, the entire device geometry is visualized.
|
|
1048
|
+
level : int
|
|
1049
|
+
The vertical layer to plot. If None, the device geometry is flattened.
|
|
1050
|
+
Defaults to None.
|
|
1051
|
+
ax : Optional[Axes]
|
|
1052
|
+
An existing matplotlib Axes object to draw the comparison on. If None, a new
|
|
1053
|
+
figure and axes will be created. Defaults to None.
|
|
1054
|
+
**kwargs
|
|
1055
|
+
Additional matplotlib parameters for plot customization.
|
|
1056
|
+
|
|
1057
|
+
Returns
|
|
1058
|
+
-------
|
|
1059
|
+
Axes
|
|
1060
|
+
The matplotlib Axes object containing the comparison plot. This object can
|
|
1061
|
+
be used for further plot customization or saving the plot after the method
|
|
1062
|
+
returns.
|
|
1063
|
+
"""
|
|
1064
|
+
plot_array = ref_device.device_array - self.device_array
|
|
1065
|
+
|
|
1066
|
+
if level is None:
|
|
1067
|
+
plot_array = geometry.flatten(plot_array)[:, :, 0]
|
|
1068
|
+
else:
|
|
1069
|
+
plot_array = plot_array[:, :, level]
|
|
1070
|
+
|
|
1071
|
+
mappable, ax = self._plot_base(
|
|
1072
|
+
plot_array=plot_array,
|
|
1073
|
+
show_buffer=show_buffer,
|
|
1074
|
+
bounds=bounds,
|
|
1075
|
+
ax=ax,
|
|
1076
|
+
cmap="jet",
|
|
1077
|
+
**kwargs,
|
|
1078
|
+
)
|
|
1079
|
+
cbar = plt.colorbar(mappable, ax=ax)
|
|
1080
|
+
cbar.set_label("Added (a.u.) Removed (a.u.)")
|
|
1081
|
+
return ax
|
|
1082
|
+
|
|
1083
|
+
def _add_buffer_visualization(self, ax: Axes):
|
|
1084
|
+
plot_array = self.device_array
|
|
1085
|
+
|
|
1086
|
+
buffer_thickness = self.buffer_spec.thickness
|
|
1087
|
+
buffer_fill = (0, 1, 0, 0.2)
|
|
1088
|
+
buffer_hatch = "/"
|
|
1089
|
+
|
|
1090
|
+
mid_rect = Rectangle(
|
|
1091
|
+
(buffer_thickness["left"], buffer_thickness["top"]),
|
|
1092
|
+
plot_array.shape[1] - buffer_thickness["left"] - buffer_thickness["right"],
|
|
1093
|
+
plot_array.shape[0] - buffer_thickness["top"] - buffer_thickness["bottom"],
|
|
1094
|
+
facecolor="none",
|
|
1095
|
+
edgecolor="black",
|
|
1096
|
+
linewidth=1,
|
|
1097
|
+
)
|
|
1098
|
+
ax.add_patch(mid_rect)
|
|
1099
|
+
|
|
1100
|
+
top_rect = Rectangle(
|
|
1101
|
+
(0, 0),
|
|
1102
|
+
plot_array.shape[1],
|
|
1103
|
+
buffer_thickness["top"],
|
|
1104
|
+
facecolor=buffer_fill,
|
|
1105
|
+
hatch=buffer_hatch,
|
|
1106
|
+
)
|
|
1107
|
+
ax.add_patch(top_rect)
|
|
1108
|
+
|
|
1109
|
+
bottom_rect = Rectangle(
|
|
1110
|
+
(0, plot_array.shape[0] - buffer_thickness["bottom"]),
|
|
1111
|
+
plot_array.shape[1],
|
|
1112
|
+
buffer_thickness["bottom"],
|
|
1113
|
+
facecolor=buffer_fill,
|
|
1114
|
+
hatch=buffer_hatch,
|
|
1115
|
+
)
|
|
1116
|
+
ax.add_patch(bottom_rect)
|
|
1117
|
+
|
|
1118
|
+
left_rect = Rectangle(
|
|
1119
|
+
(0, buffer_thickness["top"]),
|
|
1120
|
+
buffer_thickness["left"],
|
|
1121
|
+
plot_array.shape[0] - buffer_thickness["top"] - buffer_thickness["bottom"],
|
|
1122
|
+
facecolor=buffer_fill,
|
|
1123
|
+
hatch=buffer_hatch,
|
|
1124
|
+
)
|
|
1125
|
+
ax.add_patch(left_rect)
|
|
1126
|
+
|
|
1127
|
+
right_rect = Rectangle(
|
|
1128
|
+
(
|
|
1129
|
+
plot_array.shape[1] - buffer_thickness["right"],
|
|
1130
|
+
buffer_thickness["top"],
|
|
1131
|
+
),
|
|
1132
|
+
buffer_thickness["right"],
|
|
1133
|
+
plot_array.shape[0] - buffer_thickness["top"] - buffer_thickness["bottom"],
|
|
1134
|
+
facecolor=buffer_fill,
|
|
1135
|
+
hatch=buffer_hatch,
|
|
1136
|
+
)
|
|
1137
|
+
ax.add_patch(right_rect)
|
|
1138
|
+
|
|
1139
|
+
def normalize(self) -> "Device":
|
|
1140
|
+
"""
|
|
1141
|
+
Normalize the device geometry.
|
|
1142
|
+
|
|
1143
|
+
Returns
|
|
1144
|
+
-------
|
|
1145
|
+
Device
|
|
1146
|
+
A new instance of the Device with the normalized geometry.
|
|
1147
|
+
"""
|
|
1148
|
+
normalized_device_array = geometry.normalize(device_array=self.device_array)
|
|
1149
|
+
return self.model_copy(update={"device_array": normalized_device_array})
|
|
1150
|
+
|
|
1151
|
+
def binarize(self, eta: float = 0.5, beta: float = np.inf) -> "Device":
|
|
1152
|
+
"""
|
|
1153
|
+
Binarize the device geometry based on a threshold and a scaling factor.
|
|
1154
|
+
|
|
1155
|
+
Parameters
|
|
1156
|
+
----------
|
|
1157
|
+
eta : float
|
|
1158
|
+
The threshold value for binarization. Defaults to 0.5.
|
|
1159
|
+
beta : float
|
|
1160
|
+
The scaling factor for the binarization process. A higher value makes the
|
|
1161
|
+
transition sharper. Defaults to np.inf, which results in a hard threshold.
|
|
1162
|
+
|
|
1163
|
+
Returns
|
|
1164
|
+
-------
|
|
1165
|
+
Device
|
|
1166
|
+
A new instance of the Device with the binarized geometry.
|
|
1167
|
+
"""
|
|
1168
|
+
binarized_device_array = geometry.binarize(
|
|
1169
|
+
device_array=self.device_array, eta=eta, beta=beta
|
|
1170
|
+
)
|
|
1171
|
+
return self.model_copy(
|
|
1172
|
+
update={"device_array": binarized_device_array.astype(np.uint8)}
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
def binarize_hard(self, eta: float = 0.5) -> "Device":
|
|
1176
|
+
"""
|
|
1177
|
+
Apply a hard threshold to binarize the device geometry. The `binarize` function
|
|
1178
|
+
is generally preferred for most use cases, but it can create numerical artifacts
|
|
1179
|
+
for large beta values.
|
|
1180
|
+
|
|
1181
|
+
Parameters
|
|
1182
|
+
----------
|
|
1183
|
+
eta : float
|
|
1184
|
+
The threshold value for binarization. Defaults to 0.5.
|
|
1185
|
+
|
|
1186
|
+
Returns
|
|
1187
|
+
-------
|
|
1188
|
+
Device
|
|
1189
|
+
A new instance of the Device with the threshold-binarized geometry.
|
|
1190
|
+
"""
|
|
1191
|
+
binarized_device_array = geometry.binarize_hard(
|
|
1192
|
+
device_array=self.device_array, eta=eta
|
|
1193
|
+
)
|
|
1194
|
+
return self.model_copy(
|
|
1195
|
+
update={"device_array": binarized_device_array.astype(np.uint8)}
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
def binarize_monte_carlo(
|
|
1199
|
+
self,
|
|
1200
|
+
noise_magnitude: float = 2.0,
|
|
1201
|
+
blur_radius: float = 8.0,
|
|
1202
|
+
) -> "Device":
|
|
1203
|
+
"""
|
|
1204
|
+
Binarize the input ndarray using a dynamic thresholding approach to simulate
|
|
1205
|
+
surfaceroughness.
|
|
1206
|
+
|
|
1207
|
+
This function applies a dynamic thresholding technique where the threshold value
|
|
1208
|
+
is determined by a base value perturbed by Gaussian-distributed random noise.
|
|
1209
|
+
Thethreshold is then spatially varied across the array using Gaussian blurring,
|
|
1210
|
+
simulating a potentially more realistic scenario where the threshold is not
|
|
1211
|
+
uniform across the device.
|
|
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
|
+
|
|
1218
|
+
Parameters
|
|
1219
|
+
----------
|
|
1220
|
+
noise_magnitude : float
|
|
1221
|
+
The standard deviation of the Gaussian distribution used to generate noise
|
|
1222
|
+
for the threshold values. This controls the amount of randomness in the
|
|
1223
|
+
threshold. Defaults to 2.0.
|
|
1224
|
+
blur_radius : float
|
|
1225
|
+
The standard deviation for the Gaussian kernel used in blurring the
|
|
1226
|
+
threshold map. This controls the spatial variation of the threshold across
|
|
1227
|
+
the array. Defaults to 9.0.
|
|
1228
|
+
|
|
1229
|
+
Returns
|
|
1230
|
+
-------
|
|
1231
|
+
Device
|
|
1232
|
+
A new instance of the Device with the binarized geometry.
|
|
1233
|
+
"""
|
|
1234
|
+
binarized_device_array = geometry.binarize_monte_carlo(
|
|
1235
|
+
device_array=self.device_array,
|
|
1236
|
+
noise_magnitude=noise_magnitude,
|
|
1237
|
+
blur_radius=blur_radius,
|
|
1238
|
+
)
|
|
1239
|
+
return self.model_copy(update={"device_array": binarized_device_array})
|
|
1240
|
+
|
|
1241
|
+
def ternarize(self, eta1: float = 1 / 3, eta2: float = 2 / 3) -> "Device":
|
|
1242
|
+
"""
|
|
1243
|
+
Ternarize the device geometry based on two thresholds. This function is useful
|
|
1244
|
+
for flattened devices with angled sidewalls (i.e., three segments).
|
|
1245
|
+
|
|
1246
|
+
Parameters
|
|
1247
|
+
----------
|
|
1248
|
+
eta1 : float
|
|
1249
|
+
The first threshold value for ternarization. Defaults to 1/3.
|
|
1250
|
+
eta2 : float
|
|
1251
|
+
The second threshold value for ternarization. Defaults to 2/3.
|
|
1252
|
+
|
|
1253
|
+
Returns
|
|
1254
|
+
-------
|
|
1255
|
+
Device
|
|
1256
|
+
A new instance of the Device with the ternarized geometry.
|
|
1257
|
+
"""
|
|
1258
|
+
ternarized_device_array = geometry.ternarize(
|
|
1259
|
+
device_array=self.flatten().device_array, eta1=eta1, eta2=eta2
|
|
1260
|
+
)
|
|
1261
|
+
return self.model_copy(update={"device_array": ternarized_device_array})
|
|
1262
|
+
|
|
1263
|
+
def trim(self) -> "Device":
|
|
1264
|
+
"""
|
|
1265
|
+
Trim the device geometry by removing empty space around it.
|
|
1266
|
+
|
|
1267
|
+
Returns
|
|
1268
|
+
-------
|
|
1269
|
+
Device
|
|
1270
|
+
A new instance of the Device with the trimmed geometry.
|
|
1271
|
+
"""
|
|
1272
|
+
trimmed_device_array = geometry.trim(
|
|
1273
|
+
device_array=self.device_array,
|
|
1274
|
+
buffer_thickness=self.buffer_spec.thickness,
|
|
1275
|
+
)
|
|
1276
|
+
return self.model_copy(update={"device_array": trimmed_device_array})
|
|
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
|
+
|
|
1287
|
+
def blur(self, sigma: float = 1.0) -> "Device":
|
|
1288
|
+
"""
|
|
1289
|
+
Apply Gaussian blur to the device geometry and normalize the result.
|
|
1290
|
+
|
|
1291
|
+
Parameters
|
|
1292
|
+
----------
|
|
1293
|
+
sigma : float
|
|
1294
|
+
The standard deviation for the Gaussian kernel. This controls the amount of
|
|
1295
|
+
blurring. Defaults to 1.0.
|
|
1296
|
+
|
|
1297
|
+
Returns
|
|
1298
|
+
-------
|
|
1299
|
+
Device
|
|
1300
|
+
A new instance of the Device with the blurred and normalized geometry.
|
|
1301
|
+
"""
|
|
1302
|
+
blurred_device_array = geometry.blur(
|
|
1303
|
+
device_array=self.device_array, sigma=sigma
|
|
1304
|
+
)
|
|
1305
|
+
return self.model_copy(update={"device_array": blurred_device_array})
|
|
1306
|
+
|
|
1307
|
+
def rotate(self, angle: float) -> "Device":
|
|
1308
|
+
"""
|
|
1309
|
+
Rotate the device geometry by a given angle.
|
|
1310
|
+
|
|
1311
|
+
Parameters
|
|
1312
|
+
----------
|
|
1313
|
+
angle : float
|
|
1314
|
+
The angle of rotation in degrees. Positive values mean counter-clockwise
|
|
1315
|
+
rotation.
|
|
1316
|
+
|
|
1317
|
+
Returns
|
|
1318
|
+
-------
|
|
1319
|
+
Device
|
|
1320
|
+
A new instance of the Device with the rotated geometry.
|
|
1321
|
+
"""
|
|
1322
|
+
rotated_device_array = geometry.rotate(
|
|
1323
|
+
device_array=self.device_array, angle=angle
|
|
1324
|
+
)
|
|
1325
|
+
return self.model_copy(update={"device_array": rotated_device_array})
|
|
1326
|
+
|
|
1327
|
+
def erode(self, kernel_size: int = 3) -> "Device":
|
|
1328
|
+
"""
|
|
1329
|
+
Erode the device geometry by removing small areas of overlap.
|
|
1330
|
+
|
|
1331
|
+
Parameters
|
|
1332
|
+
----------
|
|
1333
|
+
kernel_size : int
|
|
1334
|
+
The size of the kernel used for erosion.
|
|
1335
|
+
|
|
1336
|
+
Returns
|
|
1337
|
+
-------
|
|
1338
|
+
Device
|
|
1339
|
+
A new instance of the Device with the eroded geometry.
|
|
1340
|
+
"""
|
|
1341
|
+
eroded_device_array = geometry.erode(
|
|
1342
|
+
device_array=self.device_array, kernel_size=kernel_size
|
|
1343
|
+
)
|
|
1344
|
+
return self.model_copy(update={"device_array": eroded_device_array})
|
|
1345
|
+
|
|
1346
|
+
def dilate(self, kernel_size: int = 3) -> "Device":
|
|
1347
|
+
"""
|
|
1348
|
+
Dilate the device geometry by expanding areas of overlap.
|
|
1349
|
+
|
|
1350
|
+
Parameters
|
|
1351
|
+
----------
|
|
1352
|
+
kernel_size : int
|
|
1353
|
+
The size of the kernel used for dilation.
|
|
1354
|
+
|
|
1355
|
+
Returns
|
|
1356
|
+
-------
|
|
1357
|
+
Device
|
|
1358
|
+
A new instance of the Device with the dilated geometry.
|
|
1359
|
+
"""
|
|
1360
|
+
dilated_device_array = geometry.dilate(
|
|
1361
|
+
device_array=self.device_array, kernel_size=kernel_size
|
|
1362
|
+
)
|
|
1363
|
+
return self.model_copy(update={"device_array": dilated_device_array})
|
|
1364
|
+
|
|
1365
|
+
def flatten(self) -> "Device":
|
|
1366
|
+
"""
|
|
1367
|
+
Flatten the device geometry by summing the vertical layers and normalizing the
|
|
1368
|
+
result.
|
|
1369
|
+
|
|
1370
|
+
Returns
|
|
1371
|
+
-------
|
|
1372
|
+
Device
|
|
1373
|
+
A new instance of the Device with the flattened geometry.
|
|
1374
|
+
"""
|
|
1375
|
+
flattened_device_array = geometry.flatten(device_array=self.device_array)
|
|
1376
|
+
return self.model_copy(update={"device_array": flattened_device_array})
|
|
1377
|
+
|
|
1378
|
+
def get_uncertainty(self) -> np.ndarray:
|
|
1379
|
+
"""
|
|
1380
|
+
Calculate the uncertainty in the edge positions of the predicted device.
|
|
1381
|
+
|
|
1382
|
+
This method computes the uncertainty based on the deviation of the device's
|
|
1383
|
+
geometry values from the midpoint (0.5). The uncertainty is defined as the
|
|
1384
|
+
absolute difference from 0.5, scaled and inverted to provide a measure where
|
|
1385
|
+
higher values indicate greater uncertainty.
|
|
1386
|
+
|
|
1387
|
+
Returns
|
|
1388
|
+
-------
|
|
1389
|
+
np.ndarray
|
|
1390
|
+
An array representing the uncertainty in the edge positions of the device,
|
|
1391
|
+
with higher values indicating greater uncertainty.
|
|
1392
|
+
"""
|
|
1393
|
+
return 1 - 2 * np.abs(0.5 - self.device_array)
|
|
1394
|
+
|
|
1395
|
+
def enforce_feature_size(
|
|
1396
|
+
self, min_feature_size: int, strel: str = "disk"
|
|
1397
|
+
) -> "Device":
|
|
1398
|
+
"""
|
|
1399
|
+
Enforce a minimum feature size on the device geometry.
|
|
1400
|
+
|
|
1401
|
+
This method applies morphological operations to ensure that all features in the
|
|
1402
|
+
device geometry are at least the specified minimum size. It uses either a disk
|
|
1403
|
+
or square structuring element for the operations.
|
|
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
|
+
|
|
1410
|
+
Parameters
|
|
1411
|
+
----------
|
|
1412
|
+
min_feature_size : int
|
|
1413
|
+
The minimum feature size to enforce, in nanometers.
|
|
1414
|
+
strel : str
|
|
1415
|
+
The type of structuring element to use. Can be either "disk" or "square".
|
|
1416
|
+
Defaults to "disk".
|
|
1417
|
+
|
|
1418
|
+
Returns
|
|
1419
|
+
-------
|
|
1420
|
+
Device
|
|
1421
|
+
A new instance of the Device with the modified geometry.
|
|
1422
|
+
|
|
1423
|
+
Raises
|
|
1424
|
+
------
|
|
1425
|
+
ValueError
|
|
1426
|
+
If an invalid structuring element type is specified.
|
|
1427
|
+
"""
|
|
1428
|
+
modified_geometry = geometry.enforce_feature_size(
|
|
1429
|
+
device_array=self.device_array,
|
|
1430
|
+
min_feature_size=min_feature_size,
|
|
1431
|
+
strel=strel,
|
|
1432
|
+
)
|
|
1433
|
+
return self.model_copy(update={"device_array": modified_geometry})
|
|
1434
|
+
|
|
1435
|
+
def check_feature_size(self, min_feature_size: int, strel: str = "disk"):
|
|
1436
|
+
"""
|
|
1437
|
+
Check and visualize the effect of enforcing a minimum feature size on the device
|
|
1438
|
+
geometry.
|
|
1439
|
+
|
|
1440
|
+
This method enforces a minimum feature size on the device geometry using the
|
|
1441
|
+
specified structuring element, compares the modified geometry with the original,
|
|
1442
|
+
and plots the differences. It also calculates and prints the Hamming distance
|
|
1443
|
+
between the original and modified geometries, providing a measure of the changes
|
|
1444
|
+
introduced by the feature size enforcement.
|
|
1445
|
+
|
|
1446
|
+
Notes
|
|
1447
|
+
-----
|
|
1448
|
+
This is not a design-rule-checking function, but it can be useful for quick
|
|
1449
|
+
checks.
|
|
1450
|
+
|
|
1451
|
+
Parameters
|
|
1452
|
+
----------
|
|
1453
|
+
min_feature_size : int
|
|
1454
|
+
The minimum feature size to enforce, in nanometers.
|
|
1455
|
+
strel : str
|
|
1456
|
+
The type of structuring element to use. Can be either "disk" or "square".
|
|
1457
|
+
Defaults to "disk".
|
|
1458
|
+
|
|
1459
|
+
Raises
|
|
1460
|
+
------
|
|
1461
|
+
ValueError
|
|
1462
|
+
If an invalid structuring element type is specified or if min_feature_size
|
|
1463
|
+
is not a positive integer.
|
|
1464
|
+
"""
|
|
1465
|
+
if min_feature_size <= 0:
|
|
1466
|
+
raise ValueError("min_feature_size must be a positive integer.")
|
|
1467
|
+
|
|
1468
|
+
enforced_device = self.enforce_feature_size(min_feature_size, strel)
|
|
1469
|
+
|
|
1470
|
+
difference = np.abs(
|
|
1471
|
+
enforced_device.device_array[:, :, 0] - self.device_array[:, :, 0]
|
|
1472
|
+
)
|
|
1473
|
+
_, ax = self._plot_base(
|
|
1474
|
+
plot_array=difference,
|
|
1475
|
+
show_buffer=False,
|
|
1476
|
+
ax=None,
|
|
1477
|
+
bounds=None,
|
|
1478
|
+
cmap="jet",
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
hamming_distance = compare.hamming_distance(self, enforced_device)
|
|
1482
|
+
print(
|
|
1483
|
+
f"Feature size check with minimum size {min_feature_size} "
|
|
1484
|
+
f"using '{strel}' structuring element resulted in a Hamming "
|
|
1485
|
+
f"distance of: {hamming_distance}"
|
|
1486
|
+
)
|