prefab 1.2.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prefab/geometry.py CHANGED
@@ -1,23 +1,31 @@
1
- """Provides functions for manipulating ndarrays of device geometries."""
1
+ """
2
+ Functions for manipulating and transforming device geometry arrays.
2
3
 
3
- from typing import Optional
4
+ This module provides utilities for common geometric operations on numpy arrays
5
+ representing device geometries, including normalization, binarization, trimming,
6
+ padding, blurring, rotation, morphological operations (erosion/dilation), and
7
+ flattening. All functions operate on npt.NDArray[np.float64] arrays.
8
+ """
9
+
10
+ from typing import cast
4
11
 
5
12
  import cv2
6
13
  import numpy as np
14
+ import numpy.typing as npt
7
15
 
8
16
 
9
- def normalize(device_array: np.ndarray) -> np.ndarray:
17
+ def normalize(device_array: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
10
18
  """
11
19
  Normalize the input ndarray to have values between 0 and 1.
12
20
 
13
21
  Parameters
14
22
  ----------
15
- device_array : np.ndarray
23
+ device_array : npt.NDArray[np.float64]
16
24
  The input array to be normalized.
17
25
 
18
26
  Returns
19
27
  -------
20
- np.ndarray
28
+ npt.NDArray[np.float64]
21
29
  The normalized array with values scaled between 0 and 1.
22
30
  """
23
31
  return (device_array - np.min(device_array)) / (
@@ -26,14 +34,14 @@ def normalize(device_array: np.ndarray) -> np.ndarray:
26
34
 
27
35
 
28
36
  def binarize(
29
- device_array: np.ndarray, eta: float = 0.5, beta: float = np.inf
30
- ) -> np.ndarray:
37
+ device_array: npt.NDArray[np.float64], eta: float = 0.5, beta: float = np.inf
38
+ ) -> npt.NDArray[np.float64]:
31
39
  """
32
40
  Binarize the input ndarray based on a threshold and a scaling factor.
33
41
 
34
42
  Parameters
35
43
  ----------
36
- device_array : np.ndarray
44
+ device_array : npt.NDArray[np.float64]
37
45
  The input array to be binarized.
38
46
  eta : float
39
47
  The threshold value for binarization. Defaults to 0.5.
@@ -43,15 +51,19 @@ def binarize(
43
51
 
44
52
  Returns
45
53
  -------
46
- np.ndarray
54
+ npt.NDArray[np.float64]
47
55
  The binarized array with elements scaled to 0 or 1.
48
56
  """
49
- return (np.tanh(beta * eta) + np.tanh(beta * (device_array - eta))) / (
50
- np.tanh(beta * eta) + np.tanh(beta * (1 - eta))
57
+ return cast(
58
+ npt.NDArray[np.float64],
59
+ (np.tanh(beta * eta) + np.tanh(beta * (device_array - eta)))
60
+ / (np.tanh(beta * eta) + np.tanh(beta * (1 - eta))),
51
61
  )
52
62
 
53
63
 
54
- def binarize_hard(device_array: np.ndarray, eta: float = 0.5) -> np.ndarray:
64
+ def binarize_hard(
65
+ device_array: npt.NDArray[np.float64], eta: float = 0.5
66
+ ) -> npt.NDArray[np.float64]:
55
67
  """
56
68
  Apply a hard threshold to binarize the input ndarray. The `binarize` function is
57
69
  generally preferred for most use cases, but it can create numerical artifacts for
@@ -59,46 +71,24 @@ def binarize_hard(device_array: np.ndarray, eta: float = 0.5) -> np.ndarray:
59
71
 
60
72
  Parameters
61
73
  ----------
62
- device_array : np.ndarray
74
+ device_array : npt.NDArray[np.float64]
63
75
  The input array to be binarized.
64
76
  eta : float
65
77
  The threshold value for binarization. Defaults to 0.5.
66
78
 
67
79
  Returns
68
80
  -------
69
- np.ndarray
81
+ npt.NDArray[np.float64]
70
82
  The binarized array with elements set to 0 or 1 based on the threshold.
71
83
  """
72
84
  return np.where(device_array < eta, 0.0, 1.0)
73
85
 
74
86
 
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,
87
+ def binarize_with_roughness(
88
+ device_array: npt.NDArray[np.float64],
99
89
  noise_magnitude: float,
100
90
  blur_radius: float,
101
- ) -> np.ndarray:
91
+ ) -> npt.NDArray[np.float64]:
102
92
  """
103
93
  Binarize the input ndarray using a dynamic thresholding approach to simulate surface
104
94
  roughness.
@@ -116,7 +106,7 @@ def binarize_monte_carlo(
116
106
 
117
107
  Parameters
118
108
  ----------
119
- device_array : np.ndarray
109
+ device_array : npt.NDArray[np.float64]
120
110
  The input array to be binarized.
121
111
  noise_magnitude : float
122
112
  The standard deviation of the Gaussian distribution used to generate noise for
@@ -127,34 +117,35 @@ def binarize_monte_carlo(
127
117
 
128
118
  Returns
129
119
  -------
130
- np.ndarray
120
+ npt.NDArray[np.float64]
131
121
  The binarized array with elements set to 0 or 1 based on the dynamically
132
122
  generated threshold.
133
123
  """
134
124
  device_array = np.squeeze(device_array)
135
- base_threshold = np.random.normal(loc=0.5, scale=0.1)
125
+ base_threshold_raw = float(np.random.normal(loc=0.5, scale=0.1))
126
+ base_threshold = max(0.2, min(base_threshold_raw, 0.8))
136
127
  threshold_noise = np.random.normal(
137
128
  loc=0, scale=noise_magnitude, size=device_array.shape
138
129
  )
139
- spatial_threshold = cv2.GaussianBlur(
130
+ spatial_threshold: npt.NDArray[np.float64] = cv2.GaussianBlur(
140
131
  threshold_noise, ksize=(0, 0), sigmaX=blur_radius
141
- )
142
- dynamic_threshold = base_threshold + spatial_threshold
132
+ ).astype(np.float64)
133
+ dynamic_threshold: npt.NDArray[np.float64] = base_threshold + spatial_threshold
143
134
  binarized_array = np.where(device_array < dynamic_threshold, 0.0, 1.0)
144
135
  binarized_array = np.expand_dims(binarized_array, axis=-1)
145
136
  return binarized_array
146
137
 
147
138
 
148
139
  def ternarize(
149
- device_array: np.ndarray, eta1: float = 1 / 3, eta2: float = 2 / 3
150
- ) -> np.ndarray:
140
+ device_array: npt.NDArray[np.float64], eta1: float = 1 / 3, eta2: float = 2 / 3
141
+ ) -> npt.NDArray[np.float64]:
151
142
  """
152
143
  Ternarize the input ndarray based on two thresholds. This function is useful for
153
144
  flattened devices with angled sidewalls (i.e., three segments).
154
145
 
155
146
  Parameters
156
147
  ----------
157
- device_array : np.ndarray
148
+ device_array : npt.NDArray[np.float64]
158
149
  The input array to be ternarized.
159
150
  eta1 : float
160
151
  The first threshold value for ternarization. Defaults to 1/3.
@@ -163,21 +154,22 @@ def ternarize(
163
154
 
164
155
  Returns
165
156
  -------
166
- np.ndarray
157
+ npt.NDArray[np.float64]
167
158
  The ternarized array with elements set to 0, 0.5, or 1 based on the thresholds.
168
159
  """
169
160
  return np.where(device_array < eta1, 0.0, np.where(device_array >= eta2, 1.0, 0.5))
170
161
 
171
162
 
172
163
  def trim(
173
- device_array: np.ndarray, buffer_thickness: Optional[dict[str, int]] = None
174
- ) -> np.ndarray:
164
+ device_array: npt.NDArray[np.float64],
165
+ buffer_thickness: dict[str, int] | None = None,
166
+ ) -> npt.NDArray[np.float64]:
175
167
  """
176
168
  Trim the input ndarray by removing rows and columns that are completely zero.
177
169
 
178
170
  Parameters
179
171
  ----------
180
- device_array : np.ndarray
172
+ device_array : npt.NDArray[np.float64]
181
173
  The input array to be trimmed.
182
174
  buffer_thickness : Optional[dict[str, int]]
183
175
  A dictionary specifying the thickness of the buffer to leave around the non-zero
@@ -186,22 +178,28 @@ def trim(
186
178
 
187
179
  Returns
188
180
  -------
189
- np.ndarray
181
+ npt.NDArray[np.float64]
190
182
  The trimmed array, potentially with a buffer around the non-zero elements.
191
183
  """
192
184
  if buffer_thickness is None:
193
185
  buffer_thickness = {"top": 0, "bottom": 0, "left": 0, "right": 0}
194
186
 
195
- nonzero_rows, nonzero_cols = np.nonzero(np.squeeze(device_array))
196
- row_min = max(nonzero_rows.min() - buffer_thickness.get("top", 0), 0)
187
+ nonzero_indices = np.nonzero(np.squeeze(device_array))
188
+ nonzero_rows = nonzero_indices[0]
189
+ nonzero_cols = nonzero_indices[1]
190
+
191
+ row_min_val = int(nonzero_rows.min())
192
+ row_max_val = int(nonzero_rows.max())
193
+ col_min_val = int(nonzero_cols.min())
194
+ col_max_val = int(nonzero_cols.max())
195
+
196
+ row_min = max(row_min_val - buffer_thickness.get("top", 0), 0)
197
197
  row_max = min(
198
- nonzero_rows.max() + buffer_thickness.get("bottom", 0) + 1,
199
- device_array.shape[0],
198
+ row_max_val + buffer_thickness.get("bottom", 0) + 1, device_array.shape[0]
200
199
  )
201
- col_min = max(nonzero_cols.min() - buffer_thickness.get("left", 0), 0)
200
+ col_min = max(col_min_val - buffer_thickness.get("left", 0), 0)
202
201
  col_max = min(
203
- nonzero_cols.max() + buffer_thickness.get("right", 0) + 1,
204
- device_array.shape[1],
202
+ col_max_val + buffer_thickness.get("right", 0) + 1, device_array.shape[1]
205
203
  )
206
204
  return device_array[
207
205
  row_min:row_max,
@@ -209,20 +207,22 @@ def trim(
209
207
  ]
210
208
 
211
209
 
212
- def pad(device_array: np.ndarray, pad_width: int) -> np.ndarray:
210
+ def pad(
211
+ device_array: npt.NDArray[np.float64], pad_width: int
212
+ ) -> npt.NDArray[np.float64]:
213
213
  """
214
214
  Pad the input ndarray uniformly with a specified width on all sides.
215
215
 
216
216
  Parameters
217
217
  ----------
218
- device_array : np.ndarray
218
+ device_array : npt.NDArray[np.float64]
219
219
  The input array to be padded.
220
220
  pad_width : int
221
221
  The number of pixels to pad on each side.
222
222
 
223
223
  Returns
224
224
  -------
225
- np.ndarray
225
+ npt.NDArray[np.float64]
226
226
  The padded array.
227
227
  """
228
228
  return np.pad(
@@ -233,13 +233,15 @@ def pad(device_array: np.ndarray, pad_width: int) -> np.ndarray:
233
233
  )
234
234
 
235
235
 
236
- def blur(device_array: np.ndarray, sigma: float = 1.0) -> np.ndarray:
236
+ def blur(
237
+ device_array: npt.NDArray[np.float64], sigma: float = 1.0
238
+ ) -> npt.NDArray[np.float64]:
237
239
  """
238
240
  Apply Gaussian blur to the input ndarray and normalize the result.
239
241
 
240
242
  Parameters
241
243
  ----------
242
- device_array : np.ndarray
244
+ device_array : npt.NDArray[np.float64]
243
245
  The input array to be blurred.
244
246
  sigma : float
245
247
  The standard deviation for the Gaussian kernel. This controls the amount of
@@ -247,21 +249,28 @@ def blur(device_array: np.ndarray, sigma: float = 1.0) -> np.ndarray:
247
249
 
248
250
  Returns
249
251
  -------
250
- np.ndarray
252
+ npt.NDArray[np.float64]
251
253
  The blurred and normalized array with values scaled between 0 and 1.
252
254
  """
253
255
  return np.expand_dims(
254
- normalize(cv2.GaussianBlur(device_array, ksize=(0, 0), sigmaX=sigma)), axis=-1
256
+ normalize(
257
+ cv2.GaussianBlur(device_array, ksize=(0, 0), sigmaX=sigma).astype(
258
+ np.float64
259
+ )
260
+ ),
261
+ axis=-1,
255
262
  )
256
263
 
257
264
 
258
- def rotate(device_array: np.ndarray, angle: float) -> np.ndarray:
265
+ def rotate(
266
+ device_array: npt.NDArray[np.float64], angle: float
267
+ ) -> npt.NDArray[np.float64]:
259
268
  """
260
269
  Rotate the input ndarray by a given angle.
261
270
 
262
271
  Parameters
263
272
  ----------
264
- device_array : np.ndarray
273
+ device_array : npt.NDArray[np.float64]
265
274
  The input array to be rotated.
266
275
  angle : float
267
276
  The angle of rotation in degrees. Positive values mean counter-clockwise
@@ -269,7 +278,7 @@ def rotate(device_array: np.ndarray, angle: float) -> np.ndarray:
269
278
 
270
279
  Returns
271
280
  -------
272
- np.ndarray
281
+ npt.NDArray[np.float64]
273
282
  The rotated array.
274
283
  """
275
284
  center = (device_array.shape[1] / 2, device_array.shape[0] / 2)
@@ -279,116 +288,73 @@ def rotate(device_array: np.ndarray, angle: float) -> np.ndarray:
279
288
  device_array,
280
289
  M=rotation_matrix,
281
290
  dsize=(device_array.shape[1], device_array.shape[0]),
282
- ),
291
+ ).astype(np.float64),
283
292
  axis=-1,
284
293
  )
285
294
 
286
295
 
287
- def erode(device_array: np.ndarray, kernel_size: int) -> np.ndarray:
296
+ def erode(
297
+ device_array: npt.NDArray[np.float64], kernel_size: int
298
+ ) -> npt.NDArray[np.float64]:
288
299
  """
289
300
  Erode the input ndarray using a specified kernel size and number of iterations.
290
301
 
291
302
  Parameters
292
303
  ----------
293
- device_array : np.ndarray
304
+ device_array : npt.NDArray[np.float64]
294
305
  The input array representing the device geometry to be eroded.
295
306
  kernel_size : int
296
307
  The size of the kernel used for erosion.
297
308
 
298
309
  Returns
299
310
  -------
300
- np.ndarray
311
+ npt.NDArray[np.float64]
301
312
  The eroded array.
302
313
  """
303
314
  kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
304
- return np.expand_dims(cv2.erode(device_array, kernel=kernel), axis=-1)
315
+ return np.expand_dims(
316
+ cv2.erode(device_array, kernel=kernel).astype(np.float64), axis=-1
317
+ )
305
318
 
306
319
 
307
- def dilate(device_array: np.ndarray, kernel_size: int) -> np.ndarray:
320
+ def dilate(
321
+ device_array: npt.NDArray[np.float64], kernel_size: int
322
+ ) -> npt.NDArray[np.float64]:
308
323
  """
309
324
  Dilate the input ndarray using a specified kernel size.
310
325
 
311
326
  Parameters
312
327
  ----------
313
- device_array : np.ndarray
328
+ device_array : npt.NDArray[np.float64]
314
329
  The input array representing the device geometry to be dilated.
315
330
  kernel_size : int
316
331
  The size of the kernel used for dilation.
317
332
 
318
333
  Returns
319
334
  -------
320
- np.ndarray
335
+ npt.NDArray[np.float64]
321
336
  The dilated array.
322
337
  """
323
338
  kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
324
- return np.expand_dims(cv2.dilate(device_array, kernel=kernel), axis=-1)
339
+ return np.expand_dims(
340
+ cv2.dilate(device_array, kernel=kernel).astype(np.float64), axis=-1
341
+ )
325
342
 
326
343
 
327
- def flatten(device_array: np.ndarray) -> np.ndarray:
344
+ def flatten(device_array: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
328
345
  """
329
346
  Flatten the input ndarray by summing the vertical layers and normalizing the result.
330
347
 
331
348
  Parameters
332
349
  ----------
333
- device_array : np.ndarray
350
+ device_array : npt.NDArray[np.float64]
334
351
  The input array to be flattened.
335
352
 
336
353
  Returns
337
354
  -------
338
- np.ndarray
355
+ npt.NDArray[np.float64]
339
356
  The flattened array with values scaled between 0 and 1.
340
357
  """
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)
358
+ return normalize(
359
+ cast(npt.NDArray[np.float64], np.sum(device_array, axis=-1, keepdims=True))
360
+ )
prefab/models.py CHANGED
@@ -1,4 +1,12 @@
1
- """Models for the PreFab library."""
1
+ """
2
+ Fabrication process model definitions and configurations.
3
+
4
+ This module defines the data structures for representing nanofabrication processes
5
+ and their associated machine learning models. It includes Pydantic models for
6
+ fabrication specifications (foundry, process) and versioned model configurations
7
+ (dataset, version, release dates). Pre-configured model instances are provided
8
+ for common fabrication processes.
9
+ """
2
10
 
3
11
  import json
4
12
  from datetime import date
@@ -16,22 +24,10 @@ class Fab(BaseModel):
16
24
  The name of the foundry where the fabrication process takes place.
17
25
  process : str
18
26
  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
27
  """
28
28
 
29
29
  foundry: str
30
30
  process: str
31
- material: str
32
- technology: str
33
- thickness: int
34
- has_sidewall: bool
35
31
 
36
32
 
37
33
  class Model(BaseModel):
@@ -67,48 +63,23 @@ class Model(BaseModel):
67
63
  tag: str
68
64
 
69
65
  def to_json(self):
70
- return json.dumps(self.dict(), default=str)
71
-
66
+ return json.dumps(self.model_dump(), default=str)
72
67
 
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
68
 
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="",
69
+ Generic = Fab(
70
+ foundry="Generic",
71
+ process="SOI",
98
72
  )
99
73
 
100
- ANT_SiN_ANF1_d1 = Model(
101
- fab=ANT_SiN,
74
+ Generic_SOI_ANF1_d0 = Model(
75
+ fab=Generic,
102
76
  version="ANF1",
103
- version_date=date(2024, 5, 6),
104
- dataset="d1",
105
- dataset_date=date(2024, 1, 31),
77
+ version_date=date(2025, 11, 7),
78
+ dataset="d0",
79
+ dataset_date=date(2025, 11, 7),
106
80
  tag="",
107
81
  )
108
82
 
109
83
  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,
84
+ Generic_SOI=Generic_SOI_ANF1_d0,
114
85
  )