prefab 0.5.2__py3-none-any.whl → 1.1.7__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/geometry.py ADDED
@@ -0,0 +1,394 @@
1
+ """Provides functions for manipulating ndarrays of device geometries."""
2
+
3
+ from typing import Optional
4
+
5
+ import cv2
6
+ import numpy as np
7
+
8
+
9
+ def normalize(device_array: np.ndarray) -> np.ndarray:
10
+ """
11
+ Normalize the input ndarray to have values between 0 and 1.
12
+
13
+ Parameters
14
+ ----------
15
+ device_array : np.ndarray
16
+ The input array to be normalized.
17
+
18
+ Returns
19
+ -------
20
+ np.ndarray
21
+ The normalized array with values scaled between 0 and 1.
22
+ """
23
+ return (device_array - np.min(device_array)) / (
24
+ np.max(device_array) - np.min(device_array)
25
+ )
26
+
27
+
28
+ def binarize(
29
+ device_array: np.ndarray, eta: float = 0.5, beta: float = np.inf
30
+ ) -> np.ndarray:
31
+ """
32
+ Binarize the input ndarray based on a threshold and a scaling factor.
33
+
34
+ Parameters
35
+ ----------
36
+ device_array : np.ndarray
37
+ The input array to be binarized.
38
+ eta : float
39
+ The threshold value for binarization. Defaults to 0.5.
40
+ beta : float
41
+ The scaling factor for the binarization process. A higher value makes the
42
+ transition sharper. Defaults to np.inf, which results in a hard threshold.
43
+
44
+ Returns
45
+ -------
46
+ np.ndarray
47
+ The binarized array with elements scaled to 0 or 1.
48
+ """
49
+ return (np.tanh(beta * eta) + np.tanh(beta * (device_array - eta))) / (
50
+ np.tanh(beta * eta) + np.tanh(beta * (1 - eta))
51
+ )
52
+
53
+
54
+ def binarize_hard(device_array: np.ndarray, eta: float = 0.5) -> np.ndarray:
55
+ """
56
+ Apply a hard threshold to binarize the input ndarray. The `binarize` function is
57
+ generally preferred for most use cases, but it can create numerical artifacts for
58
+ large beta values.
59
+
60
+ Parameters
61
+ ----------
62
+ device_array : np.ndarray
63
+ The input array to be binarized.
64
+ eta : float
65
+ The threshold value for binarization. Defaults to 0.5.
66
+
67
+ Returns
68
+ -------
69
+ np.ndarray
70
+ The binarized array with elements set to 0 or 1 based on the threshold.
71
+ """
72
+ return np.where(device_array < eta, 0.0, 1.0)
73
+
74
+
75
+ def binarize_sem(sem_array: np.ndarray) -> np.ndarray:
76
+ """
77
+ Binarize a grayscale scanning electron microscope (SEM) image.
78
+
79
+ This function applies Otsu's method to automatically determine the optimal threshold
80
+ value for binarization of a grayscale SEM image.
81
+
82
+ Parameters
83
+ ----------
84
+ sem_array : np.ndarray
85
+ The input SEM image array to be binarized.
86
+
87
+ Returns
88
+ -------
89
+ np.ndarray
90
+ The binarized SEM image array with elements scaled to 0 or 1.
91
+ """
92
+ return cv2.threshold(
93
+ sem_array.astype("uint8"), 0, 1, cv2.THRESH_BINARY + cv2.THRESH_OTSU
94
+ )[1]
95
+
96
+
97
+ def binarize_monte_carlo(
98
+ device_array: np.ndarray,
99
+ noise_magnitude: float,
100
+ blur_radius: float,
101
+ ) -> np.ndarray:
102
+ """
103
+ Binarize the input ndarray using a dynamic thresholding approach to simulate surface
104
+ roughness.
105
+
106
+ This function applies a dynamic thresholding technique where the threshold value is
107
+ determined by a base value perturbed by Gaussian-distributed random noise. The
108
+ threshold is then spatially varied across the array using Gaussian blurring,
109
+ simulating a potentially more realistic scenario where the threshold is not uniform
110
+ across the device.
111
+
112
+ Notes
113
+ -----
114
+ This is a temporary solution, where the defaults are chosen based on what looks
115
+ good. A better, data-driven approach is needed.
116
+
117
+ Parameters
118
+ ----------
119
+ device_array : np.ndarray
120
+ The input array to be binarized.
121
+ noise_magnitude : float
122
+ The standard deviation of the Gaussian distribution used to generate noise for
123
+ the threshold values. This controls the amount of randomness in the threshold.
124
+ blur_radius : float
125
+ The standard deviation for the Gaussian kernel used in blurring the threshold
126
+ map. This controls the spatial variation of the threshold across the array.
127
+
128
+ Returns
129
+ -------
130
+ np.ndarray
131
+ The binarized array with elements set to 0 or 1 based on the dynamically
132
+ generated threshold.
133
+ """
134
+ device_array = np.squeeze(device_array)
135
+ base_threshold = np.random.normal(loc=0.5, scale=0.1)
136
+ threshold_noise = np.random.normal(
137
+ loc=0, scale=noise_magnitude, size=device_array.shape
138
+ )
139
+ spatial_threshold = cv2.GaussianBlur(
140
+ threshold_noise, ksize=(0, 0), sigmaX=blur_radius
141
+ )
142
+ dynamic_threshold = base_threshold + spatial_threshold
143
+ binarized_array = np.where(device_array < dynamic_threshold, 0.0, 1.0)
144
+ binarized_array = np.expand_dims(binarized_array, axis=-1)
145
+ return binarized_array
146
+
147
+
148
+ def ternarize(
149
+ device_array: np.ndarray, eta1: float = 1 / 3, eta2: float = 2 / 3
150
+ ) -> np.ndarray:
151
+ """
152
+ Ternarize the input ndarray based on two thresholds. This function is useful for
153
+ flattened devices with angled sidewalls (i.e., three segments).
154
+
155
+ Parameters
156
+ ----------
157
+ device_array : np.ndarray
158
+ The input array to be ternarized.
159
+ eta1 : float
160
+ The first threshold value for ternarization. Defaults to 1/3.
161
+ eta2 : float
162
+ The second threshold value for ternarization. Defaults to 2/3.
163
+
164
+ Returns
165
+ -------
166
+ np.ndarray
167
+ The ternarized array with elements set to 0, 0.5, or 1 based on the thresholds.
168
+ """
169
+ return np.where(device_array < eta1, 0.0, np.where(device_array >= eta2, 1.0, 0.5))
170
+
171
+
172
+ def trim(
173
+ device_array: np.ndarray, buffer_thickness: Optional[dict[str, int]] = None
174
+ ) -> np.ndarray:
175
+ """
176
+ Trim the input ndarray by removing rows and columns that are completely zero.
177
+
178
+ Parameters
179
+ ----------
180
+ device_array : np.ndarray
181
+ The input array to be trimmed.
182
+ buffer_thickness : Optional[dict[str, int]]
183
+ A dictionary specifying the thickness of the buffer to leave around the non-zero
184
+ elements of the array. Should contain keys 'top', 'bottom', 'left', 'right'.
185
+ Defaults to None, which means no buffer is added.
186
+
187
+ Returns
188
+ -------
189
+ np.ndarray
190
+ The trimmed array, potentially with a buffer around the non-zero elements.
191
+ """
192
+ if buffer_thickness is None:
193
+ buffer_thickness = {"top": 0, "bottom": 0, "left": 0, "right": 0}
194
+
195
+ nonzero_rows, nonzero_cols = np.nonzero(np.squeeze(device_array))
196
+ row_min = max(nonzero_rows.min() - buffer_thickness.get("top", 0), 0)
197
+ row_max = min(
198
+ nonzero_rows.max() + buffer_thickness.get("bottom", 0) + 1,
199
+ device_array.shape[0],
200
+ )
201
+ col_min = max(nonzero_cols.min() - buffer_thickness.get("left", 0), 0)
202
+ col_max = min(
203
+ nonzero_cols.max() + buffer_thickness.get("right", 0) + 1,
204
+ device_array.shape[1],
205
+ )
206
+ return device_array[
207
+ row_min:row_max,
208
+ col_min:col_max,
209
+ ]
210
+
211
+
212
+ def pad(device_array: np.ndarray, pad_width: int) -> np.ndarray:
213
+ """
214
+ Pad the input ndarray uniformly with a specified width on all sides.
215
+
216
+ Parameters
217
+ ----------
218
+ device_array : np.ndarray
219
+ The input array to be padded.
220
+ pad_width : int
221
+ The number of pixels to pad on each side.
222
+
223
+ Returns
224
+ -------
225
+ np.ndarray
226
+ The padded array.
227
+ """
228
+ return np.pad(
229
+ device_array,
230
+ pad_width=((pad_width, pad_width), (pad_width, pad_width), (0, 0)),
231
+ mode="constant",
232
+ constant_values=0,
233
+ )
234
+
235
+
236
+ def blur(device_array: np.ndarray, sigma: float = 1.0) -> np.ndarray:
237
+ """
238
+ Apply Gaussian blur to the input ndarray and normalize the result.
239
+
240
+ Parameters
241
+ ----------
242
+ device_array : np.ndarray
243
+ The input array to be blurred.
244
+ sigma : float
245
+ The standard deviation for the Gaussian kernel. This controls the amount of
246
+ blurring. Defaults to 1.0.
247
+
248
+ Returns
249
+ -------
250
+ np.ndarray
251
+ The blurred and normalized array with values scaled between 0 and 1.
252
+ """
253
+ return np.expand_dims(
254
+ normalize(cv2.GaussianBlur(device_array, ksize=(0, 0), sigmaX=sigma)), axis=-1
255
+ )
256
+
257
+
258
+ def rotate(device_array: np.ndarray, angle: float) -> np.ndarray:
259
+ """
260
+ Rotate the input ndarray by a given angle.
261
+
262
+ Parameters
263
+ ----------
264
+ device_array : np.ndarray
265
+ The input array to be rotated.
266
+ angle : float
267
+ The angle of rotation in degrees. Positive values mean counter-clockwise
268
+ rotation.
269
+
270
+ Returns
271
+ -------
272
+ np.ndarray
273
+ The rotated array.
274
+ """
275
+ center = (device_array.shape[1] / 2, device_array.shape[0] / 2)
276
+ rotation_matrix = cv2.getRotationMatrix2D(center=center, angle=angle, scale=1)
277
+ return np.expand_dims(
278
+ cv2.warpAffine(
279
+ device_array,
280
+ M=rotation_matrix,
281
+ dsize=(device_array.shape[1], device_array.shape[0]),
282
+ ),
283
+ axis=-1,
284
+ )
285
+
286
+
287
+ def erode(device_array: np.ndarray, kernel_size: int) -> np.ndarray:
288
+ """
289
+ Erode the input ndarray using a specified kernel size and number of iterations.
290
+
291
+ Parameters
292
+ ----------
293
+ device_array : np.ndarray
294
+ The input array representing the device geometry to be eroded.
295
+ kernel_size : int
296
+ The size of the kernel used for erosion.
297
+
298
+ Returns
299
+ -------
300
+ np.ndarray
301
+ The eroded array.
302
+ """
303
+ kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
304
+ return np.expand_dims(cv2.erode(device_array, kernel=kernel), axis=-1)
305
+
306
+
307
+ def dilate(device_array: np.ndarray, kernel_size: int) -> np.ndarray:
308
+ """
309
+ Dilate the input ndarray using a specified kernel size.
310
+
311
+ Parameters
312
+ ----------
313
+ device_array : np.ndarray
314
+ The input array representing the device geometry to be dilated.
315
+ kernel_size : int
316
+ The size of the kernel used for dilation.
317
+
318
+ Returns
319
+ -------
320
+ np.ndarray
321
+ The dilated array.
322
+ """
323
+ kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
324
+ return np.expand_dims(cv2.dilate(device_array, kernel=kernel), axis=-1)
325
+
326
+
327
+ def flatten(device_array: np.ndarray) -> np.ndarray:
328
+ """
329
+ Flatten the input ndarray by summing the vertical layers and normalizing the result.
330
+
331
+ Parameters
332
+ ----------
333
+ device_array : np.ndarray
334
+ The input array to be flattened.
335
+
336
+ Returns
337
+ -------
338
+ np.ndarray
339
+ The flattened array with values scaled between 0 and 1.
340
+ """
341
+ return normalize(np.sum(device_array, axis=-1, keepdims=True))
342
+
343
+
344
+ def enforce_feature_size(
345
+ device_array: np.ndarray, min_feature_size: int, strel: str = "disk"
346
+ ) -> np.ndarray:
347
+ """
348
+ Enforce a minimum feature size on the device geometry.
349
+
350
+ This function applies morphological operations to ensure that all features in the
351
+ device geometry are at least the specified minimum size. It uses either a disk
352
+ or square structuring element for the operations.
353
+
354
+ Notes
355
+ -----
356
+ This function does not guarantee that the minimum feature size is enforced in all
357
+ cases. A better process is needed.
358
+
359
+ Parameters
360
+ ----------
361
+ device_array : np.ndarray
362
+ The input array representing the device geometry.
363
+ min_feature_size : int
364
+ The minimum feature size to enforce, in nanometers.
365
+ strel : str
366
+ The type of structuring element to use. Can be either "disk" or "square".
367
+ Defaults to "disk".
368
+
369
+ Returns
370
+ -------
371
+ np.ndarray
372
+ The modified device array with enforced feature size.
373
+
374
+ Raises
375
+ ------
376
+ ValueError
377
+ If an invalid structuring element type is specified.
378
+ """
379
+ if strel == "disk":
380
+ kernel = cv2.getStructuringElement(
381
+ cv2.MORPH_ELLIPSE, (min_feature_size, min_feature_size)
382
+ )
383
+ elif strel == "square":
384
+ kernel = cv2.getStructuringElement(
385
+ cv2.MORPH_RECT, (min_feature_size, min_feature_size)
386
+ )
387
+ else:
388
+ raise ValueError(f"Invalid structuring element: {strel}")
389
+
390
+ device_array_2d = (device_array[:, :, 0] * 255).astype(np.uint8)
391
+ modified_geometry = cv2.morphologyEx(device_array_2d, cv2.MORPH_CLOSE, kernel)
392
+ modified_geometry = cv2.morphologyEx(modified_geometry, cv2.MORPH_OPEN, kernel)
393
+
394
+ return np.expand_dims(modified_geometry.astype(float) / 255, axis=-1)
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
+ Attributes
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_d10 = Model(
92
+ fab=ANT_NanoSOI,
93
+ version="ANF1",
94
+ version_date=date(2024, 5, 6),
95
+ dataset="d10",
96
+ dataset_date=date(2024, 6, 8),
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_d10,
111
+ ANT_NanoSOI_ANF1_d10=ANT_NanoSOI_ANF1_d10,
112
+ ANT_SiN=ANT_SiN_ANF1_d1,
113
+ ANT_SiN_ANF1_d1=ANT_SiN_ANF1_d1,
114
+ )