prefab 1.3.0__py3-none-any.whl → 1.4.1__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,35 +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)
136
- base_threshold = np.clip(base_threshold, 0.2, 0.8)
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))
137
127
  threshold_noise = np.random.normal(
138
128
  loc=0, scale=noise_magnitude, size=device_array.shape
139
129
  )
140
- spatial_threshold = cv2.GaussianBlur(
130
+ spatial_threshold: npt.NDArray[np.float64] = cv2.GaussianBlur(
141
131
  threshold_noise, ksize=(0, 0), sigmaX=blur_radius
142
- )
143
- dynamic_threshold = base_threshold + spatial_threshold
132
+ ).astype(np.float64)
133
+ dynamic_threshold: npt.NDArray[np.float64] = base_threshold + spatial_threshold
144
134
  binarized_array = np.where(device_array < dynamic_threshold, 0.0, 1.0)
145
135
  binarized_array = np.expand_dims(binarized_array, axis=-1)
146
136
  return binarized_array
147
137
 
148
138
 
149
139
  def ternarize(
150
- device_array: np.ndarray, eta1: float = 1 / 3, eta2: float = 2 / 3
151
- ) -> np.ndarray:
140
+ device_array: npt.NDArray[np.float64], eta1: float = 1 / 3, eta2: float = 2 / 3
141
+ ) -> npt.NDArray[np.float64]:
152
142
  """
153
143
  Ternarize the input ndarray based on two thresholds. This function is useful for
154
144
  flattened devices with angled sidewalls (i.e., three segments).
155
145
 
156
146
  Parameters
157
147
  ----------
158
- device_array : np.ndarray
148
+ device_array : npt.NDArray[np.float64]
159
149
  The input array to be ternarized.
160
150
  eta1 : float
161
151
  The first threshold value for ternarization. Defaults to 1/3.
@@ -164,21 +154,22 @@ def ternarize(
164
154
 
165
155
  Returns
166
156
  -------
167
- np.ndarray
157
+ npt.NDArray[np.float64]
168
158
  The ternarized array with elements set to 0, 0.5, or 1 based on the thresholds.
169
159
  """
170
160
  return np.where(device_array < eta1, 0.0, np.where(device_array >= eta2, 1.0, 0.5))
171
161
 
172
162
 
173
163
  def trim(
174
- device_array: np.ndarray, buffer_thickness: Optional[dict[str, int]] = None
175
- ) -> np.ndarray:
164
+ device_array: npt.NDArray[np.float64],
165
+ buffer_thickness: dict[str, int] | None = None,
166
+ ) -> npt.NDArray[np.float64]:
176
167
  """
177
168
  Trim the input ndarray by removing rows and columns that are completely zero.
178
169
 
179
170
  Parameters
180
171
  ----------
181
- device_array : np.ndarray
172
+ device_array : npt.NDArray[np.float64]
182
173
  The input array to be trimmed.
183
174
  buffer_thickness : Optional[dict[str, int]]
184
175
  A dictionary specifying the thickness of the buffer to leave around the non-zero
@@ -187,22 +178,28 @@ def trim(
187
178
 
188
179
  Returns
189
180
  -------
190
- np.ndarray
181
+ npt.NDArray[np.float64]
191
182
  The trimmed array, potentially with a buffer around the non-zero elements.
192
183
  """
193
184
  if buffer_thickness is None:
194
185
  buffer_thickness = {"top": 0, "bottom": 0, "left": 0, "right": 0}
195
186
 
196
- nonzero_rows, nonzero_cols = np.nonzero(np.squeeze(device_array))
197
- 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)
198
197
  row_max = min(
199
- nonzero_rows.max() + buffer_thickness.get("bottom", 0) + 1,
200
- device_array.shape[0],
198
+ row_max_val + buffer_thickness.get("bottom", 0) + 1, device_array.shape[0]
201
199
  )
202
- 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)
203
201
  col_max = min(
204
- nonzero_cols.max() + buffer_thickness.get("right", 0) + 1,
205
- device_array.shape[1],
202
+ col_max_val + buffer_thickness.get("right", 0) + 1, device_array.shape[1]
206
203
  )
207
204
  return device_array[
208
205
  row_min:row_max,
@@ -210,20 +207,22 @@ def trim(
210
207
  ]
211
208
 
212
209
 
213
- 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]:
214
213
  """
215
214
  Pad the input ndarray uniformly with a specified width on all sides.
216
215
 
217
216
  Parameters
218
217
  ----------
219
- device_array : np.ndarray
218
+ device_array : npt.NDArray[np.float64]
220
219
  The input array to be padded.
221
220
  pad_width : int
222
221
  The number of pixels to pad on each side.
223
222
 
224
223
  Returns
225
224
  -------
226
- np.ndarray
225
+ npt.NDArray[np.float64]
227
226
  The padded array.
228
227
  """
229
228
  return np.pad(
@@ -234,13 +233,15 @@ def pad(device_array: np.ndarray, pad_width: int) -> np.ndarray:
234
233
  )
235
234
 
236
235
 
237
- 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]:
238
239
  """
239
240
  Apply Gaussian blur to the input ndarray and normalize the result.
240
241
 
241
242
  Parameters
242
243
  ----------
243
- device_array : np.ndarray
244
+ device_array : npt.NDArray[np.float64]
244
245
  The input array to be blurred.
245
246
  sigma : float
246
247
  The standard deviation for the Gaussian kernel. This controls the amount of
@@ -248,21 +249,28 @@ def blur(device_array: np.ndarray, sigma: float = 1.0) -> np.ndarray:
248
249
 
249
250
  Returns
250
251
  -------
251
- np.ndarray
252
+ npt.NDArray[np.float64]
252
253
  The blurred and normalized array with values scaled between 0 and 1.
253
254
  """
254
255
  return np.expand_dims(
255
- 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,
256
262
  )
257
263
 
258
264
 
259
- 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]:
260
268
  """
261
269
  Rotate the input ndarray by a given angle.
262
270
 
263
271
  Parameters
264
272
  ----------
265
- device_array : np.ndarray
273
+ device_array : npt.NDArray[np.float64]
266
274
  The input array to be rotated.
267
275
  angle : float
268
276
  The angle of rotation in degrees. Positive values mean counter-clockwise
@@ -270,7 +278,7 @@ def rotate(device_array: np.ndarray, angle: float) -> np.ndarray:
270
278
 
271
279
  Returns
272
280
  -------
273
- np.ndarray
281
+ npt.NDArray[np.float64]
274
282
  The rotated array.
275
283
  """
276
284
  center = (device_array.shape[1] / 2, device_array.shape[0] / 2)
@@ -280,116 +288,73 @@ def rotate(device_array: np.ndarray, angle: float) -> np.ndarray:
280
288
  device_array,
281
289
  M=rotation_matrix,
282
290
  dsize=(device_array.shape[1], device_array.shape[0]),
283
- ),
291
+ ).astype(np.float64),
284
292
  axis=-1,
285
293
  )
286
294
 
287
295
 
288
- 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]:
289
299
  """
290
300
  Erode the input ndarray using a specified kernel size and number of iterations.
291
301
 
292
302
  Parameters
293
303
  ----------
294
- device_array : np.ndarray
304
+ device_array : npt.NDArray[np.float64]
295
305
  The input array representing the device geometry to be eroded.
296
306
  kernel_size : int
297
307
  The size of the kernel used for erosion.
298
308
 
299
309
  Returns
300
310
  -------
301
- np.ndarray
311
+ npt.NDArray[np.float64]
302
312
  The eroded array.
303
313
  """
304
314
  kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
305
- 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
+ )
306
318
 
307
319
 
308
- 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]:
309
323
  """
310
324
  Dilate the input ndarray using a specified kernel size.
311
325
 
312
326
  Parameters
313
327
  ----------
314
- device_array : np.ndarray
328
+ device_array : npt.NDArray[np.float64]
315
329
  The input array representing the device geometry to be dilated.
316
330
  kernel_size : int
317
331
  The size of the kernel used for dilation.
318
332
 
319
333
  Returns
320
334
  -------
321
- np.ndarray
335
+ npt.NDArray[np.float64]
322
336
  The dilated array.
323
337
  """
324
338
  kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
325
- 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
+ )
326
342
 
327
343
 
328
- def flatten(device_array: np.ndarray) -> np.ndarray:
344
+ def flatten(device_array: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
329
345
  """
330
346
  Flatten the input ndarray by summing the vertical layers and normalizing the result.
331
347
 
332
348
  Parameters
333
349
  ----------
334
- device_array : np.ndarray
350
+ device_array : npt.NDArray[np.float64]
335
351
  The input array to be flattened.
336
352
 
337
353
  Returns
338
354
  -------
339
- np.ndarray
355
+ npt.NDArray[np.float64]
340
356
  The flattened array with values scaled between 0 and 1.
341
357
  """
342
- return normalize(np.sum(device_array, axis=-1, keepdims=True))
343
-
344
-
345
- def enforce_feature_size(
346
- device_array: np.ndarray, min_feature_size: int, strel: str = "disk"
347
- ) -> np.ndarray:
348
- """
349
- Enforce a minimum feature size on the device geometry.
350
-
351
- This function applies morphological operations to ensure that all features in the
352
- device geometry are at least the specified minimum size. It uses either a disk
353
- or square structuring element for the operations.
354
-
355
- Notes
356
- -----
357
- This function does not guarantee that the minimum feature size is enforced in all
358
- cases. A better process is needed.
359
-
360
- Parameters
361
- ----------
362
- device_array : np.ndarray
363
- The input array representing the device geometry.
364
- min_feature_size : int
365
- The minimum feature size to enforce, in nanometers.
366
- strel : str
367
- The type of structuring element to use. Can be either "disk" or "square".
368
- Defaults to "disk".
369
-
370
- Returns
371
- -------
372
- np.ndarray
373
- The modified device array with enforced feature size.
374
-
375
- Raises
376
- ------
377
- ValueError
378
- If an invalid structuring element type is specified.
379
- """
380
- if strel == "disk":
381
- kernel = cv2.getStructuringElement(
382
- cv2.MORPH_ELLIPSE, (min_feature_size, min_feature_size)
383
- )
384
- elif strel == "square":
385
- kernel = cv2.getStructuringElement(
386
- cv2.MORPH_RECT, (min_feature_size, min_feature_size)
387
- )
388
- else:
389
- raise ValueError(f"Invalid structuring element: {strel}")
390
-
391
- device_array_2d = (device_array[:, :, 0] * 255).astype(np.uint8)
392
- modified_geometry = cv2.morphologyEx(device_array_2d, cv2.MORPH_CLOSE, kernel)
393
- modified_geometry = cv2.morphologyEx(modified_geometry, cv2.MORPH_OPEN, kernel)
394
-
395
- 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
+ )
@@ -0,0 +1,57 @@
1
+ """
2
+ Fabrication process model definitions and configurations.
3
+
4
+ This module automatically discovers and loads all model definitions from
5
+ Python files in the models/ directory.
6
+ """
7
+
8
+ import importlib
9
+ from pathlib import Path
10
+
11
+ from .base import Fab, Model
12
+
13
+ models = {}
14
+
15
+
16
+ def _load_models_from_module(module):
17
+ """
18
+ Load all Model instances from a module into the models dict.
19
+
20
+ If the module defines a __models__ dict, use that for registration.
21
+ Otherwise, auto-register all Model instances using their variable names.
22
+
23
+ Parameters
24
+ ----------
25
+ module : module
26
+ Python module containing Model instances to register.
27
+ """
28
+ if hasattr(module, "__models__"):
29
+ models_dict = getattr(module, "__models__")
30
+ for name, obj in models_dict.items():
31
+ if isinstance(obj, Model):
32
+ models[name] = obj
33
+ else:
34
+ for name, obj in vars(module).items():
35
+ if isinstance(obj, Model):
36
+ models[name] = obj
37
+
38
+
39
+ models_dir = Path(__file__).parent
40
+
41
+ for model_file in models_dir.glob("*.py"):
42
+ if model_file.stem in ("__init__", "base"):
43
+ continue
44
+
45
+ module_name = f".{model_file.stem}"
46
+ try:
47
+ module = importlib.import_module(module_name, package=__package__)
48
+ _load_models_from_module(module)
49
+ except Exception:
50
+ pass
51
+
52
+
53
+ __all__ = [
54
+ "Fab",
55
+ "Model",
56
+ "models",
57
+ ]
prefab/models/base.py ADDED
@@ -0,0 +1,63 @@
1
+ """
2
+ Base model definitions for fabrication processes.
3
+
4
+ This module defines the core data structures for representing nanofabrication
5
+ processes and their associated models.
6
+ """
7
+
8
+ import json
9
+ from datetime import date
10
+
11
+ from pydantic import BaseModel
12
+
13
+
14
+ class Fab(BaseModel):
15
+ """
16
+ Represents a fabrication process in the PreFab model library.
17
+
18
+ Attributes
19
+ ----------
20
+ foundry : str
21
+ The name of the foundry where the fabrication process takes place.
22
+ process : str
23
+ The specific process used in the fabrication.
24
+ """
25
+
26
+ foundry: str
27
+ process: str
28
+
29
+
30
+ class Model(BaseModel):
31
+ """
32
+ Represents a model of a fabrication process including versioning and dataset detail.
33
+
34
+ Attributes
35
+ ----------
36
+ fab : Fab
37
+ An instance of the Fab class representing the fabrication details.
38
+ version : str
39
+ The version identifier of the model.
40
+ version_date : date
41
+ The release date of this version of the model.
42
+ dataset : str
43
+ The identifier for the dataset used in this model.
44
+ dataset_date : date
45
+ The date when the dataset was last updated or released.
46
+ tag : str
47
+ An optional tag for additional categorization or notes.
48
+
49
+ Methods
50
+ -------
51
+ to_json()
52
+ Serializes the model instance to a JSON formatted string.
53
+ """
54
+
55
+ fab: Fab
56
+ version: str
57
+ version_date: date
58
+ dataset: str
59
+ dataset_date: date
60
+ tag: str
61
+
62
+ def to_json(self):
63
+ return json.dumps(self.model_dump(), default=str)
@@ -0,0 +1,29 @@
1
+ """
2
+ Base evaluation models.
3
+
4
+ Pre-configured model instances for common fabrication processes.
5
+ """
6
+
7
+ from datetime import date
8
+
9
+ from .base import Fab, Model
10
+
11
+
12
+ Generic = Fab(
13
+ foundry="Generic",
14
+ process="SOI",
15
+ )
16
+
17
+ Generic_SOI_ANF1_d0 = Model(
18
+ fab=Generic,
19
+ version="ANF1",
20
+ version_date=date(2025, 11, 7),
21
+ dataset="d0",
22
+ dataset_date=date(2025, 11, 7),
23
+ tag="",
24
+ )
25
+
26
+ # Export models with user-facing names
27
+ __models__ = {
28
+ "Generic_SOI": Generic_SOI_ANF1_d0,
29
+ }