prefab 0.5.1__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefab/__init__.py +12 -40
- prefab/__main__.py +8 -10
- prefab/compare.py +92 -0
- prefab/device.py +1233 -0
- prefab/geometry.py +302 -0
- prefab/models.py +114 -0
- prefab/read.py +293 -0
- {prefab-0.5.1.dist-info → prefab-1.0.0.dist-info}/METADATA +25 -31
- prefab-1.0.0.dist-info/RECORD +11 -0
- {prefab-0.5.1.dist-info → prefab-1.0.0.dist-info}/WHEEL +1 -1
- prefab/io.py +0 -214
- prefab/predictor.py +0 -231
- prefab/processor.py +0 -248
- prefab-0.5.1.dist-info/RECORD +0 -9
- {prefab-0.5.1.dist-info → prefab-1.0.0.dist-info}/licenses/LICENSE +0 -0
prefab/geometry.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Provides functions for manipulating ndarrays of device geometries."""
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def normalize(device_array: np.ndarray) -> np.ndarray:
|
|
8
|
+
"""
|
|
9
|
+
Normalize the input ndarray to have values between 0 and 1.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
device_array : np.ndarray
|
|
14
|
+
The input array to be normalized.
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
np.ndarray
|
|
19
|
+
The normalized array with values scaled between 0 and 1.
|
|
20
|
+
"""
|
|
21
|
+
return (device_array - np.min(device_array)) / (
|
|
22
|
+
np.max(device_array) - np.min(device_array)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def binarize(
|
|
27
|
+
device_array: np.ndarray, eta: float = 0.5, beta: float = np.inf
|
|
28
|
+
) -> np.ndarray:
|
|
29
|
+
"""
|
|
30
|
+
Binarize the input ndarray based on a threshold and a scaling factor.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
device_array : np.ndarray
|
|
35
|
+
The input array to be binarized.
|
|
36
|
+
eta : float, optional
|
|
37
|
+
The threshold value for binarization. Defaults to 0.5.
|
|
38
|
+
beta : float, optional
|
|
39
|
+
The scaling factor for the binarization process. A higher value makes the
|
|
40
|
+
transition sharper. Defaults to np.inf, which results in a hard threshold.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
np.ndarray
|
|
45
|
+
The binarized array with elements scaled to 0 or 1.
|
|
46
|
+
"""
|
|
47
|
+
return (np.tanh(beta * eta) + np.tanh(beta * (device_array - eta))) / (
|
|
48
|
+
np.tanh(beta * eta) + np.tanh(beta * (1 - eta))
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def binarize_hard(device_array: np.ndarray, eta: float = 0.5) -> np.ndarray:
|
|
53
|
+
"""
|
|
54
|
+
Apply a hard threshold to binarize the input ndarray. The `binarize` function is
|
|
55
|
+
generally preferred for most use cases, but it can create numerical artifacts for
|
|
56
|
+
large beta values.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
device_array : np.ndarray
|
|
61
|
+
The input array to be binarized.
|
|
62
|
+
eta : float, optional
|
|
63
|
+
The threshold value for binarization. Defaults to 0.5.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
np.ndarray
|
|
68
|
+
The binarized array with elements set to 0 or 1 based on the threshold.
|
|
69
|
+
"""
|
|
70
|
+
return np.where(device_array < eta, 0.0, 1.0)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def binarize_sem(sem_array: np.ndarray) -> np.ndarray:
|
|
74
|
+
"""
|
|
75
|
+
Binarize a grayscale scanning electron microscope (SEM) image.
|
|
76
|
+
|
|
77
|
+
This function applies Otsu's method to automatically determine the optimal threshold
|
|
78
|
+
value for binarization of a grayscale SEM image.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
sem_array : np.ndarray
|
|
83
|
+
The input SEM image array to be binarized.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
np.ndarray
|
|
88
|
+
The binarized SEM image array with elements scaled to 0 or 1.
|
|
89
|
+
"""
|
|
90
|
+
return cv2.threshold(
|
|
91
|
+
sem_array.astype("uint8"), 0, 1, cv2.THRESH_BINARY + cv2.THRESH_OTSU
|
|
92
|
+
)[1]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def binarize_monte_carlo(
|
|
96
|
+
device_array: np.ndarray,
|
|
97
|
+
threshold_noise_std: float,
|
|
98
|
+
threshold_blur_std: float,
|
|
99
|
+
) -> np.ndarray:
|
|
100
|
+
"""
|
|
101
|
+
Binarize the input ndarray using a Monte Carlo approach with Gaussian blurring.
|
|
102
|
+
|
|
103
|
+
This function applies a dynamic thresholding technique where the threshold value is
|
|
104
|
+
determined by a base value perturbed by Gaussian-distributed random noise. The
|
|
105
|
+
threshold is then spatially varied across the array using Gaussian blurring,
|
|
106
|
+
simulating a potentially more realistic scenario where the threshold is not uniform
|
|
107
|
+
across the device.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
device_array : np.ndarray
|
|
112
|
+
The input array to be binarized.
|
|
113
|
+
threshold_noise_std : float
|
|
114
|
+
The standard deviation of the Gaussian distribution used to generate noise for
|
|
115
|
+
the threshold values. This controls the amount of randomness in the threshold.
|
|
116
|
+
threshold_blur_std : float
|
|
117
|
+
The standard deviation for the Gaussian kernel used in blurring the threshold
|
|
118
|
+
map. This controls the spatial variation of the threshold across the array.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
np.ndarray
|
|
123
|
+
The binarized array with elements set to 0 or 1 based on the dynamically
|
|
124
|
+
generated threshold.
|
|
125
|
+
"""
|
|
126
|
+
device_array = np.squeeze(device_array)
|
|
127
|
+
base_threshold = np.clip(np.random.normal(loc=0.5, scale=0.5 / 2), 0.4, 0.6)
|
|
128
|
+
threshold_noise = np.random.normal(
|
|
129
|
+
loc=0, scale=threshold_noise_std, size=device_array.shape
|
|
130
|
+
)
|
|
131
|
+
spatial_threshold = cv2.GaussianBlur(
|
|
132
|
+
threshold_noise, ksize=(0, 0), sigmaX=threshold_blur_std
|
|
133
|
+
)
|
|
134
|
+
dynamic_threshold = base_threshold + spatial_threshold
|
|
135
|
+
binarized_array = np.where(device_array < dynamic_threshold, 0.0, 1.0)
|
|
136
|
+
binarized_array = np.expand_dims(binarized_array, axis=-1)
|
|
137
|
+
return binarized_array
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def ternarize(
|
|
141
|
+
device_array: np.ndarray, eta1: float = 1 / 3, eta2: float = 2 / 3
|
|
142
|
+
) -> np.ndarray:
|
|
143
|
+
"""
|
|
144
|
+
Ternarize the input ndarray based on two thresholds. This function is useful for
|
|
145
|
+
flattened devices with angled sidewalls (i.e., three segments).
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
device_array : np.ndarray
|
|
150
|
+
The input array to be ternarized.
|
|
151
|
+
eta1 : float, optional
|
|
152
|
+
The first threshold value for ternarization. Defaults to 1/3.
|
|
153
|
+
eta2 : float, optional
|
|
154
|
+
The second threshold value for ternarization. Defaults to 2/3.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
np.ndarray
|
|
159
|
+
The ternarized array with elements set to 0, 0.5, or 1 based on the thresholds.
|
|
160
|
+
"""
|
|
161
|
+
return np.where(device_array < eta1, 0.0, np.where(device_array >= eta2, 1.0, 0.5))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def trim(device_array: np.ndarray, buffer_thickness: int = 0) -> np.ndarray:
|
|
165
|
+
"""
|
|
166
|
+
Trim the input ndarray by removing rows and columns that are completely zero.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
device_array : np.ndarray
|
|
171
|
+
The input array to be trimmed.
|
|
172
|
+
buffer_thickness : int, optional
|
|
173
|
+
The thickness of the buffer to leave around the non-zero elements of the array.
|
|
174
|
+
Defaults to 0, which means no buffer is added.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
np.ndarray
|
|
179
|
+
The trimmed array, potentially with a buffer around the non-zero elements.
|
|
180
|
+
"""
|
|
181
|
+
flattened_device_array = np.squeeze(flatten(device_array))
|
|
182
|
+
nonzero_rows, nonzero_cols = np.nonzero(flattened_device_array)
|
|
183
|
+
row_min = max(nonzero_rows.min() - buffer_thickness, 0)
|
|
184
|
+
row_max = min(
|
|
185
|
+
nonzero_rows.max() + buffer_thickness + 1,
|
|
186
|
+
device_array.shape[0],
|
|
187
|
+
)
|
|
188
|
+
col_min = max(nonzero_cols.min() - buffer_thickness, 0)
|
|
189
|
+
col_max = min(
|
|
190
|
+
nonzero_cols.max() + buffer_thickness + 1,
|
|
191
|
+
device_array.shape[1],
|
|
192
|
+
)
|
|
193
|
+
return device_array[
|
|
194
|
+
row_min:row_max,
|
|
195
|
+
col_min:col_max,
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def blur(device_array: np.ndarray, sigma: float = 1.0) -> np.ndarray:
|
|
200
|
+
"""
|
|
201
|
+
Apply Gaussian blur to the input ndarray and normalize the result.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
device_array : np.ndarray
|
|
206
|
+
The input array to be blurred.
|
|
207
|
+
sigma : float, optional
|
|
208
|
+
The standard deviation for the Gaussian kernel. This controls the amount of
|
|
209
|
+
blurring. Defaults to 1.0.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
np.ndarray
|
|
214
|
+
The blurred and normalized array with values scaled between 0 and 1.
|
|
215
|
+
"""
|
|
216
|
+
return np.expand_dims(
|
|
217
|
+
normalize(cv2.GaussianBlur(device_array, ksize=(0, 0), sigmaX=sigma)), axis=-1
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def rotate(device_array: np.ndarray, angle: float) -> np.ndarray:
|
|
222
|
+
"""
|
|
223
|
+
Rotate the input ndarray by a given angle.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
device_array : np.ndarray
|
|
228
|
+
The input array to be rotated.
|
|
229
|
+
angle : float
|
|
230
|
+
The angle of rotation in degrees. Positive values mean counter-clockwise
|
|
231
|
+
rotation.
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
np.ndarray
|
|
236
|
+
The rotated array.
|
|
237
|
+
"""
|
|
238
|
+
center = (device_array.shape[1] / 2, device_array.shape[0] / 2)
|
|
239
|
+
rotation_matrix = cv2.getRotationMatrix2D(center=center, angle=angle, scale=1)
|
|
240
|
+
rotated_device_array = cv2.warpAffine(
|
|
241
|
+
flatten(device_array),
|
|
242
|
+
M=rotation_matrix,
|
|
243
|
+
dsize=(device_array.shape[1], device_array.shape[0]),
|
|
244
|
+
)
|
|
245
|
+
return np.expand_dims(rotated_device_array, axis=-1)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def erode(device_array: np.ndarray, kernel_size: int) -> np.ndarray:
|
|
249
|
+
"""
|
|
250
|
+
Erode the input ndarray using a specified kernel size and number of iterations.
|
|
251
|
+
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
device_array : np.ndarray
|
|
255
|
+
The input array representing the device geometry to be eroded.
|
|
256
|
+
kernel_size : int
|
|
257
|
+
The size of the kernel used for erosion.
|
|
258
|
+
|
|
259
|
+
Returns
|
|
260
|
+
-------
|
|
261
|
+
np.ndarray
|
|
262
|
+
The eroded array.
|
|
263
|
+
"""
|
|
264
|
+
kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
|
|
265
|
+
return np.expand_dims(cv2.erode(device_array, kernel=kernel), axis=-1)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def dilate(device_array: np.ndarray, kernel_size: int) -> np.ndarray:
|
|
269
|
+
"""
|
|
270
|
+
Dilate the input ndarray using a specified kernel size.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
device_array : np.ndarray
|
|
275
|
+
The input array representing the device geometry to be dilated.
|
|
276
|
+
kernel_size : int
|
|
277
|
+
The size of the kernel used for dilation.
|
|
278
|
+
|
|
279
|
+
Returns
|
|
280
|
+
-------
|
|
281
|
+
np.ndarray
|
|
282
|
+
The dilated array.
|
|
283
|
+
"""
|
|
284
|
+
kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
|
|
285
|
+
return np.expand_dims(cv2.dilate(device_array, kernel=kernel), axis=-1)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def flatten(device_array: np.ndarray) -> np.ndarray:
|
|
289
|
+
"""
|
|
290
|
+
Flatten the input ndarray by summing the vertical layers and normalizing the result.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
device_array : np.ndarray
|
|
295
|
+
The input array to be flattened.
|
|
296
|
+
|
|
297
|
+
Returns
|
|
298
|
+
-------
|
|
299
|
+
np.ndarray
|
|
300
|
+
The flattened array with values scaled between 0 and 1.
|
|
301
|
+
"""
|
|
302
|
+
return normalize(np.sum(device_array, axis=-1, keepdims=True))
|
prefab/models.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Models for the PreFab library."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import date
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Fab(BaseModel):
|
|
10
|
+
"""
|
|
11
|
+
Represents a fabrication process in the PreFab model library.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
foundry : str
|
|
16
|
+
The name of the foundry where the fabrication process takes place.
|
|
17
|
+
process : str
|
|
18
|
+
The specific process used in the fabrication.
|
|
19
|
+
material : str
|
|
20
|
+
The material used in the fabrication process.
|
|
21
|
+
technology : str
|
|
22
|
+
The technology used in the fabrication process.
|
|
23
|
+
thickness : int
|
|
24
|
+
The thickness of the material used, measured in nanometers.
|
|
25
|
+
has_sidewall : bool
|
|
26
|
+
Indicates whether the fabrication has angled sidewalls.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
foundry: str
|
|
30
|
+
process: str
|
|
31
|
+
material: str
|
|
32
|
+
technology: str
|
|
33
|
+
thickness: int
|
|
34
|
+
has_sidewall: bool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Model(BaseModel):
|
|
38
|
+
"""
|
|
39
|
+
Represents a model of a fabrication process including versioning and dataset detail.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
fab : Fab
|
|
44
|
+
An instance of the Fab class representing the fabrication details.
|
|
45
|
+
version : str
|
|
46
|
+
The version identifier of the model.
|
|
47
|
+
version_date : date
|
|
48
|
+
The release date of this version of the model.
|
|
49
|
+
dataset : str
|
|
50
|
+
The identifier for the dataset used in this model.
|
|
51
|
+
dataset_date : date
|
|
52
|
+
The date when the dataset was last updated or released.
|
|
53
|
+
tag : str
|
|
54
|
+
An optional tag for additional categorization or notes.
|
|
55
|
+
|
|
56
|
+
Methods
|
|
57
|
+
-------
|
|
58
|
+
to_json()
|
|
59
|
+
Serializes the model instance to a JSON formatted string.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
fab: Fab
|
|
63
|
+
version: str
|
|
64
|
+
version_date: date
|
|
65
|
+
dataset: str
|
|
66
|
+
dataset_date: date
|
|
67
|
+
tag: str
|
|
68
|
+
|
|
69
|
+
def to_json(self):
|
|
70
|
+
return json.dumps(self.dict(), default=str)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
ANT_NanoSOI = Fab(
|
|
74
|
+
foundry="ANT",
|
|
75
|
+
process="NanoSOI",
|
|
76
|
+
material="SOI",
|
|
77
|
+
technology="E-Beam",
|
|
78
|
+
thickness=220,
|
|
79
|
+
has_sidewall=False,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
ANT_SiN = Fab(
|
|
83
|
+
foundry="ANT",
|
|
84
|
+
process="SiN",
|
|
85
|
+
material="SiN",
|
|
86
|
+
technology="E-Beam",
|
|
87
|
+
thickness=400,
|
|
88
|
+
has_sidewall=True,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
ANT_NanoSOI_ANF1_d9 = Model(
|
|
92
|
+
fab=ANT_NanoSOI,
|
|
93
|
+
version="ANF1",
|
|
94
|
+
version_date=date(2024, 5, 6),
|
|
95
|
+
dataset="d9",
|
|
96
|
+
dataset_date=date(2024, 2, 6),
|
|
97
|
+
tag="",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
ANT_SiN_ANF1_d1 = Model(
|
|
101
|
+
fab=ANT_SiN,
|
|
102
|
+
version="ANF1",
|
|
103
|
+
version_date=date(2024, 5, 6),
|
|
104
|
+
dataset="d1",
|
|
105
|
+
dataset_date=date(2024, 1, 31),
|
|
106
|
+
tag="",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
models = dict(
|
|
110
|
+
ANT_NanoSOI=ANT_NanoSOI_ANF1_d9,
|
|
111
|
+
ANT_NanoSOI_ANF1_d9=ANT_NanoSOI_ANF1_d9,
|
|
112
|
+
ANT_SiN=ANT_SiN_ANF1_d1,
|
|
113
|
+
ANT_SiN_ANF1_d1=ANT_SiN_ANF1_d1,
|
|
114
|
+
)
|
prefab/read.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Provides functions to create Devices from various data sources."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import gdstk
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from . import geometry
|
|
10
|
+
from .device import Device
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def from_ndarray(
|
|
14
|
+
ndarray: np.ndarray, resolution: int = 1, binarize: bool = True, **kwargs
|
|
15
|
+
) -> Device:
|
|
16
|
+
"""
|
|
17
|
+
Create a Device from an ndarray.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
ndarray : np.ndarray
|
|
22
|
+
The input array representing the device layout.
|
|
23
|
+
resolution : int, optional
|
|
24
|
+
The resolution of the ndarray in nanometers per pixel, defaulting to 1 nm per
|
|
25
|
+
pixel. If specified, the input array will be resized based on this resolution to
|
|
26
|
+
match the desired physical size.
|
|
27
|
+
binarize : bool, optional
|
|
28
|
+
If True, the input array will be binarized (converted to binary values) before
|
|
29
|
+
conversion to a Device object. This is useful for processing grayscale images
|
|
30
|
+
into binary masks. Defaults to True.
|
|
31
|
+
**kwargs
|
|
32
|
+
Additional keyword arguments to be passed to the Device constructor.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
Device
|
|
37
|
+
A Device object representing the input array, after optional resizing and
|
|
38
|
+
binarization.
|
|
39
|
+
"""
|
|
40
|
+
device_array = ndarray
|
|
41
|
+
device_array = cv2.resize(device_array, dsize=(0, 0), fx=resolution, fy=resolution)
|
|
42
|
+
if binarize:
|
|
43
|
+
device_array = geometry.binarize_hard(device_array)
|
|
44
|
+
return Device(device_array=device_array, **kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def from_img(
|
|
48
|
+
img_path: str, img_width_nm: int = None, binarize: bool = True, **kwargs
|
|
49
|
+
) -> Device:
|
|
50
|
+
"""
|
|
51
|
+
Create a Device from an image file.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
img_path : str
|
|
56
|
+
The path to the image file to be converted into a Device object.
|
|
57
|
+
img_width_nm : int, optional
|
|
58
|
+
The desired width of the device in nanometers. If specified, the image will be
|
|
59
|
+
resized to this width while maintaining aspect ratio. If None, no resizing is
|
|
60
|
+
performed.
|
|
61
|
+
binarize : bool, optional
|
|
62
|
+
If True, the image will be binarized (converted to binary values) before
|
|
63
|
+
conversion to a Device object. This is useful for converting grayscale images
|
|
64
|
+
into binary masks. Defaults to True.
|
|
65
|
+
**kwargs
|
|
66
|
+
Additional keyword arguments to be passed to the Device constructor.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
Device
|
|
71
|
+
A Device object representing the processed image, after optional resizing and
|
|
72
|
+
binarization.
|
|
73
|
+
"""
|
|
74
|
+
device_array = cv2.imread(img_path, flags=cv2.IMREAD_GRAYSCALE) / 255
|
|
75
|
+
if img_width_nm is not None:
|
|
76
|
+
scale = img_width_nm / device_array.shape[1]
|
|
77
|
+
device_array = cv2.resize(device_array, dsize=(0, 0), fx=scale, fy=scale)
|
|
78
|
+
if binarize:
|
|
79
|
+
device_array = geometry.binarize_hard(device_array)
|
|
80
|
+
return Device(device_array=device_array, **kwargs)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def from_gds(
|
|
84
|
+
gds_path: str,
|
|
85
|
+
cell_name: str,
|
|
86
|
+
gds_layer: tuple[int, int] = (1, 0),
|
|
87
|
+
bounds: tuple[tuple[int, int], tuple[int, int]] = None,
|
|
88
|
+
**kwargs,
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Create a Device from a GDS cell.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
gds_path : str
|
|
96
|
+
The file path to the GDS file.
|
|
97
|
+
cell_name : str
|
|
98
|
+
The name of the cell within the GDS file to be converted into a Device object.
|
|
99
|
+
gds_layer : tuple[int, int], optional
|
|
100
|
+
A tuple specifying the layer and datatype to be used from the GDS file. Defaults
|
|
101
|
+
to (1, 0).
|
|
102
|
+
bounds : tuple[tuple[int, int], tuple[int, int]], optional
|
|
103
|
+
A tuple specifying the bounds for cropping the cell before conversion, formatted
|
|
104
|
+
as ((min_x, min_y), (max_x, max_y)), in units of the GDS file. If None, the
|
|
105
|
+
entire cell is used.
|
|
106
|
+
**kwargs
|
|
107
|
+
Additional keyword arguments to be passed to the Device constructor.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
Device
|
|
112
|
+
A Device object representing the specified cell from the GDS file, after
|
|
113
|
+
processing based on the specified layer.
|
|
114
|
+
"""
|
|
115
|
+
gdstk_library = gdstk.read_gds(gds_path)
|
|
116
|
+
gdstk_cell = gdstk_library[cell_name]
|
|
117
|
+
device_array = _gdstk_to_device_array(
|
|
118
|
+
gdstk_cell=gdstk_cell, gds_layer=gds_layer, bounds=bounds
|
|
119
|
+
)
|
|
120
|
+
return Device(device_array=device_array, **kwargs)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def from_gdstk(
|
|
124
|
+
gdstk_cell: gdstk.Cell,
|
|
125
|
+
gds_layer: tuple[int, int] = (1, 0),
|
|
126
|
+
bounds: tuple[tuple[int, int], tuple[int, int]] = None,
|
|
127
|
+
**kwargs,
|
|
128
|
+
):
|
|
129
|
+
"""
|
|
130
|
+
Create a Device from a gdstk cell.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
gdstk_cell : gdstk.Cell
|
|
135
|
+
The gdstk.Cell object to be converted into a Device object.
|
|
136
|
+
gds_layer : tuple[int, int], optional
|
|
137
|
+
A tuple specifying the layer and datatype to be used. Defaults to (1, 0).
|
|
138
|
+
bounds : tuple[tuple[int, int], tuple[int, int]], optional
|
|
139
|
+
A tuple specifying the bounds for cropping the cell before conversion, formatted
|
|
140
|
+
as ((min_x, min_y), (max_x, max_y)), in units of the GDS file. If None, the
|
|
141
|
+
entire cell is used.
|
|
142
|
+
**kwargs
|
|
143
|
+
Additional keyword arguments to be passed to the Device constructor.
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
Device
|
|
148
|
+
A Device object representing the gdstk.Cell, after processing based on the
|
|
149
|
+
specified layer.
|
|
150
|
+
"""
|
|
151
|
+
device_array = _gdstk_to_device_array(
|
|
152
|
+
gdstk_cell=gdstk_cell, gds_layer=gds_layer, bounds=bounds
|
|
153
|
+
)
|
|
154
|
+
return Device(device_array=device_array, **kwargs)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _gdstk_to_device_array(
|
|
158
|
+
gdstk_cell: gdstk.Cell,
|
|
159
|
+
gds_layer: tuple[int, int] = (1, 0),
|
|
160
|
+
bounds: tuple[tuple[int, int], tuple[int, int]] = None,
|
|
161
|
+
) -> np.ndarray:
|
|
162
|
+
polygons = gdstk_cell.get_polygons(layer=gds_layer[0], datatype=gds_layer[1])
|
|
163
|
+
if bounds:
|
|
164
|
+
polygons = gdstk.slice(
|
|
165
|
+
polygons, position=(bounds[0][0], bounds[1][0]), axis="x"
|
|
166
|
+
)[1]
|
|
167
|
+
polygons = gdstk.slice(
|
|
168
|
+
polygons, position=(bounds[0][1], bounds[1][1]), axis="y"
|
|
169
|
+
)[1]
|
|
170
|
+
bounds = tuple(tuple(x * 1000 for x in sub_tuple) for sub_tuple in bounds)
|
|
171
|
+
else:
|
|
172
|
+
bounds = tuple(
|
|
173
|
+
tuple(1000 * x for x in sub_tuple)
|
|
174
|
+
for sub_tuple in gdstk_cell.bounding_box()
|
|
175
|
+
)
|
|
176
|
+
contours = [
|
|
177
|
+
np.array(
|
|
178
|
+
[
|
|
179
|
+
[
|
|
180
|
+
[
|
|
181
|
+
int(1000 * vertex[0] - bounds[0][0]),
|
|
182
|
+
int(1000 * vertex[1] - bounds[0][1]),
|
|
183
|
+
]
|
|
184
|
+
]
|
|
185
|
+
for vertex in polygon.points
|
|
186
|
+
],
|
|
187
|
+
dtype=np.int32,
|
|
188
|
+
)
|
|
189
|
+
for polygon in polygons
|
|
190
|
+
]
|
|
191
|
+
device_array = np.zeros(
|
|
192
|
+
(int(bounds[1][1] - bounds[0][1]), int(bounds[1][0] - bounds[0][0]))
|
|
193
|
+
)
|
|
194
|
+
cv2.fillPoly(img=device_array, pts=contours, color=(1, 1, 1))
|
|
195
|
+
device_array = np.flipud(device_array)
|
|
196
|
+
return device_array
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def from_sem(
|
|
200
|
+
sem_path: str,
|
|
201
|
+
sem_resolution: float = None,
|
|
202
|
+
sem_resolution_key: str = None,
|
|
203
|
+
binarize: bool = True,
|
|
204
|
+
bounds: tuple[tuple[int, int], tuple[int, int]] = None,
|
|
205
|
+
**kwargs,
|
|
206
|
+
) -> Device:
|
|
207
|
+
"""
|
|
208
|
+
Create a Device from a scanning electron microscope (SEM) image file.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
sem_path : str
|
|
213
|
+
The file path to the SEM image.
|
|
214
|
+
sem_resolution : float, optional
|
|
215
|
+
The resolution of the SEM image in nanometers per pixel. If not provided, it
|
|
216
|
+
will be extracted from the image metadata using the `sem_resolution_key`.
|
|
217
|
+
sem_resolution_key : str, optional
|
|
218
|
+
The key to look for in the SEM image metadata to extract the resolution.
|
|
219
|
+
Required if `sem_resolution` is not provided.
|
|
220
|
+
binarize : bool, optional
|
|
221
|
+
If True, the SEM image will be binarized (converted to binary values) before
|
|
222
|
+
conversion to a Device object. This is needed for processing grayscale images
|
|
223
|
+
into binary masks. Defaults to True.
|
|
224
|
+
bounds : tuple[tuple[int, int], tuple[int, int]], optional
|
|
225
|
+
A tuple specifying the bounds for cropping the image before conversion,
|
|
226
|
+
formatted as ((min_x, min_y), (max_x, max_y)). If None, the entire image is
|
|
227
|
+
used.
|
|
228
|
+
**kwargs
|
|
229
|
+
Additional keyword arguments to be passed to the Device constructor.
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
Device
|
|
234
|
+
A Device object representing the processed SEM image.
|
|
235
|
+
|
|
236
|
+
Raises
|
|
237
|
+
------
|
|
238
|
+
ValueError
|
|
239
|
+
If neither `sem_resolution` nor `sem_resolution_key` is provided.
|
|
240
|
+
"""
|
|
241
|
+
if sem_resolution is None and sem_resolution_key is not None:
|
|
242
|
+
sem_resolution = get_sem_resolution(sem_path, sem_resolution_key)
|
|
243
|
+
elif sem_resolution is None:
|
|
244
|
+
raise ValueError("Either sem_resolution or resolution_key must be provided.")
|
|
245
|
+
|
|
246
|
+
device_array = cv2.imread(sem_path, flags=cv2.IMREAD_GRAYSCALE)
|
|
247
|
+
if sem_resolution is not None:
|
|
248
|
+
device_array = cv2.resize(
|
|
249
|
+
device_array, dsize=(0, 0), fx=sem_resolution, fy=sem_resolution
|
|
250
|
+
)
|
|
251
|
+
if bounds is not None:
|
|
252
|
+
device_array = device_array[
|
|
253
|
+
-bounds[1][1] : -bounds[0][1], bounds[0][0] : bounds[1][0]
|
|
254
|
+
]
|
|
255
|
+
if binarize:
|
|
256
|
+
device_array = geometry.binarize_sem(device_array)
|
|
257
|
+
return Device(device_array=device_array, **kwargs)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def get_sem_resolution(sem_path: str, sem_resolution_key: str) -> float:
|
|
261
|
+
"""
|
|
262
|
+
Extracts the resolution of a scanning electron microscope (SEM) image from its
|
|
263
|
+
metadata.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
sem_path : str
|
|
268
|
+
The file path to the SEM image.
|
|
269
|
+
sem_resolution_key : str
|
|
270
|
+
The key to look for in the SEM image metadata to extract the resolution.
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
float
|
|
275
|
+
The resolution of the SEM image in nanometers per pixel.
|
|
276
|
+
|
|
277
|
+
Raises
|
|
278
|
+
------
|
|
279
|
+
ValueError
|
|
280
|
+
If the resolution key is not found in the SEM image metadata.
|
|
281
|
+
"""
|
|
282
|
+
with open(sem_path, "rb") as file:
|
|
283
|
+
resolution_key_bytes = sem_resolution_key.encode("utf-8")
|
|
284
|
+
for line in file:
|
|
285
|
+
if resolution_key_bytes in line:
|
|
286
|
+
line_str = line.decode("utf-8")
|
|
287
|
+
match = re.search(r"-?\d+(\.\d+)?", line_str)
|
|
288
|
+
if match:
|
|
289
|
+
value = float(match.group())
|
|
290
|
+
if value > 100:
|
|
291
|
+
value /= 1000
|
|
292
|
+
return value
|
|
293
|
+
raise ValueError(f"Resolution key '{sem_resolution_key}' not found in {sem_path}.")
|