prefab 1.1.4__py3-none-any.whl → 1.1.6__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,8 +1,9 @@
1
1
  """Provides functions for manipulating ndarrays of device geometries."""
2
2
 
3
+ from typing import Optional
4
+
3
5
  import cv2
4
6
  import numpy as np
5
- from skimage.morphology import closing, disk, opening, square
6
7
 
7
8
 
8
9
  def normalize(device_array: np.ndarray) -> np.ndarray:
@@ -34,9 +35,9 @@ def binarize(
34
35
  ----------
35
36
  device_array : np.ndarray
36
37
  The input array to be binarized.
37
- eta : float, optional
38
+ eta : float
38
39
  The threshold value for binarization. Defaults to 0.5.
39
- beta : float, optional
40
+ beta : float
40
41
  The scaling factor for the binarization process. A higher value makes the
41
42
  transition sharper. Defaults to np.inf, which results in a hard threshold.
42
43
 
@@ -60,7 +61,7 @@ def binarize_hard(device_array: np.ndarray, eta: float = 0.5) -> np.ndarray:
60
61
  ----------
61
62
  device_array : np.ndarray
62
63
  The input array to be binarized.
63
- eta : float, optional
64
+ eta : float
64
65
  The threshold value for binarization. Defaults to 0.5.
65
66
 
66
67
  Returns
@@ -95,11 +96,12 @@ def binarize_sem(sem_array: np.ndarray) -> np.ndarray:
95
96
 
96
97
  def binarize_monte_carlo(
97
98
  device_array: np.ndarray,
98
- threshold_noise_std: float,
99
- threshold_blur_std: float,
99
+ noise_magnitude: float,
100
+ blur_radius: float,
100
101
  ) -> np.ndarray:
101
102
  """
102
- Binarize the input ndarray using a Monte Carlo approach with Gaussian blurring.
103
+ Binarize the input ndarray using a dynamic thresholding approach to simulate surface
104
+ roughness.
103
105
 
104
106
  This function applies a dynamic thresholding technique where the threshold value is
105
107
  determined by a base value perturbed by Gaussian-distributed random noise. The
@@ -107,14 +109,19 @@ def binarize_monte_carlo(
107
109
  simulating a potentially more realistic scenario where the threshold is not uniform
108
110
  across the device.
109
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
+
110
117
  Parameters
111
118
  ----------
112
119
  device_array : np.ndarray
113
120
  The input array to be binarized.
114
- threshold_noise_std : float
121
+ noise_magnitude : float
115
122
  The standard deviation of the Gaussian distribution used to generate noise for
116
123
  the threshold values. This controls the amount of randomness in the threshold.
117
- threshold_blur_std : float
124
+ blur_radius : float
118
125
  The standard deviation for the Gaussian kernel used in blurring the threshold
119
126
  map. This controls the spatial variation of the threshold across the array.
120
127
 
@@ -127,10 +134,10 @@ def binarize_monte_carlo(
127
134
  device_array = np.squeeze(device_array)
128
135
  base_threshold = np.random.normal(loc=0.5, scale=0.1)
129
136
  threshold_noise = np.random.normal(
130
- loc=0, scale=threshold_noise_std, size=device_array.shape
137
+ loc=0, scale=noise_magnitude, size=device_array.shape
131
138
  )
132
139
  spatial_threshold = cv2.GaussianBlur(
133
- threshold_noise, ksize=(0, 0), sigmaX=threshold_blur_std
140
+ threshold_noise, ksize=(0, 0), sigmaX=blur_radius
134
141
  )
135
142
  dynamic_threshold = base_threshold + spatial_threshold
136
143
  binarized_array = np.where(device_array < dynamic_threshold, 0.0, 1.0)
@@ -149,9 +156,9 @@ def ternarize(
149
156
  ----------
150
157
  device_array : np.ndarray
151
158
  The input array to be ternarized.
152
- eta1 : float, optional
159
+ eta1 : float
153
160
  The first threshold value for ternarization. Defaults to 1/3.
154
- eta2 : float, optional
161
+ eta2 : float
155
162
  The second threshold value for ternarization. Defaults to 2/3.
156
163
 
157
164
  Returns
@@ -162,7 +169,9 @@ def ternarize(
162
169
  return np.where(device_array < eta1, 0.0, np.where(device_array >= eta2, 1.0, 0.5))
163
170
 
164
171
 
165
- def trim(device_array: np.ndarray, buffer_thickness: dict = None) -> np.ndarray:
172
+ def trim(
173
+ device_array: np.ndarray, buffer_thickness: Optional[dict[str, int]] = None
174
+ ) -> np.ndarray:
166
175
  """
167
176
  Trim the input ndarray by removing rows and columns that are completely zero.
168
177
 
@@ -170,7 +179,7 @@ def trim(device_array: np.ndarray, buffer_thickness: dict = None) -> np.ndarray:
170
179
  ----------
171
180
  device_array : np.ndarray
172
181
  The input array to be trimmed.
173
- buffer_thickness : dict, optional
182
+ buffer_thickness : Optional[dict[str, int]]
174
183
  A dictionary specifying the thickness of the buffer to leave around the non-zero
175
184
  elements of the array. Should contain keys 'top', 'bottom', 'left', 'right'.
176
185
  Defaults to None, which means no buffer is added.
@@ -200,6 +209,30 @@ def trim(device_array: np.ndarray, buffer_thickness: dict = None) -> np.ndarray:
200
209
  ]
201
210
 
202
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
+
203
236
  def blur(device_array: np.ndarray, sigma: float = 1.0) -> np.ndarray:
204
237
  """
205
238
  Apply Gaussian blur to the input ndarray and normalize the result.
@@ -208,7 +241,7 @@ def blur(device_array: np.ndarray, sigma: float = 1.0) -> np.ndarray:
208
241
  ----------
209
242
  device_array : np.ndarray
210
243
  The input array to be blurred.
211
- sigma : float, optional
244
+ sigma : float
212
245
  The standard deviation for the Gaussian kernel. This controls the amount of
213
246
  blurring. Defaults to 1.0.
214
247
 
@@ -318,13 +351,18 @@ def enforce_feature_size(
318
351
  device geometry are at least the specified minimum size. It uses either a disk
319
352
  or square structuring element for the operations.
320
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
+
321
359
  Parameters
322
360
  ----------
323
361
  device_array : np.ndarray
324
362
  The input array representing the device geometry.
325
363
  min_feature_size : int
326
364
  The minimum feature size to enforce, in nanometers.
327
- strel : str, optional
365
+ strel : str
328
366
  The type of structuring element to use. Can be either "disk" or "square".
329
367
  Defaults to "disk".
330
368
 
@@ -339,12 +377,18 @@ def enforce_feature_size(
339
377
  If an invalid structuring element type is specified.
340
378
  """
341
379
  if strel == "disk":
342
- structuring_element = disk(radius=min_feature_size / 2)
380
+ kernel = cv2.getStructuringElement(
381
+ cv2.MORPH_ELLIPSE, (min_feature_size, min_feature_size)
382
+ )
343
383
  elif strel == "square":
344
- structuring_element = square(width=min_feature_size)
384
+ kernel = cv2.getStructuringElement(
385
+ cv2.MORPH_RECT, (min_feature_size, min_feature_size)
386
+ )
345
387
  else:
346
388
  raise ValueError(f"Invalid structuring element: {strel}")
347
389
 
348
- modified_geometry = closing(device_array[:, :, 0], structuring_element)
349
- modified_geometry = opening(modified_geometry, structuring_element)
350
- return np.expand_dims(modified_geometry, axis=-1)
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/predict.py CHANGED
@@ -1,4 +1,4 @@
1
- """Provides prediction functions for ndarrays of device geometries."""
1
+ """Prediction functions for ndarrays of device geometries."""
2
2
 
3
3
  import base64
4
4
  import io
@@ -16,7 +16,8 @@ from tqdm import tqdm
16
16
  from .geometry import binarize_hard
17
17
  from .models import Model
18
18
 
19
- BASE_URL = "https://prefab-photonics--predict"
19
+ BASE_ENDPOINT_URL = "https://prefab-photonics--predict"
20
+ ENDPOINT_VERSION = 1
20
21
 
21
22
 
22
23
  def predict_array(
@@ -46,15 +47,16 @@ def predict_array(
46
47
  creation and the data it was trained on, ensuring the prediction is tailored to
47
48
  specific fabrication parameters.
48
49
  model_type : str
49
- The type of model to use (e.g., 'p', 'c', 's').
50
+ The type of model to use (e.g., 'p' for prediction, 'c' for correction, or 's'
51
+ for SEMulate).
50
52
  binarize : bool
51
53
  If True, the predicted device geometry will be binarized using a threshold
52
54
  method. This is useful for converting probabilistic predictions into binary
53
55
  geometries.
54
- gpu : bool, optional
56
+ gpu : bool
55
57
  If True, the prediction will be performed on a GPU. Defaults to False. Note: The
56
- GPU option has more overhead and will take longer for small devices, but will be
57
- faster for larger devices.
58
+ GPU option has more startup overhead and will take longer for small devices, but
59
+ will be faster for larger devices.
58
60
 
59
61
  Returns
60
62
  -------
@@ -68,7 +70,11 @@ def predict_array(
68
70
  """
69
71
  headers = _prepare_headers()
70
72
  predict_data = _prepare_predict_data(device_array, model, model_type, binarize)
71
- endpoint_url = f"{BASE_URL}-gpu-v1.modal.run" if gpu else f"{BASE_URL}-v1.modal.run"
73
+ endpoint_url = (
74
+ f"{BASE_ENDPOINT_URL}-gpu-v{ENDPOINT_VERSION}.modal.run"
75
+ if gpu
76
+ else f"{BASE_ENDPOINT_URL}-v{ENDPOINT_VERSION}.modal.run"
77
+ )
72
78
 
73
79
  try:
74
80
  with requests.post(
@@ -78,7 +84,10 @@ def predict_array(
78
84
  stream=True,
79
85
  ) as response:
80
86
  response.raise_for_status()
81
- return _process_response(response, model_type, binarize)
87
+ result = _process_response(response, model_type, binarize)
88
+ if result is None:
89
+ raise RuntimeError("No prediction result received.")
90
+ return result
82
91
  except requests.RequestException as e:
83
92
  raise RuntimeError(f"Request failed: {e}") from e
84
93
 
@@ -86,9 +95,33 @@ def predict_array(
86
95
  def _predict_array_with_grad(
87
96
  device_array: np.ndarray, model: Model
88
97
  ) -> tuple[np.ndarray, np.ndarray]:
98
+ """
99
+ Predict the nanofabrication outcome of a device array and compute its gradient.
100
+
101
+ This function predicts the outcome of the nanofabrication process for a given
102
+ device array using a specified model. It also computes the gradient of the
103
+ prediction with respect to the input device array.
104
+
105
+ Notes
106
+ -----
107
+ This function is currently not used in the main `predict_array` function as
108
+ the main `predict_array` function (e.g., GPU support and progress bar) for now.
109
+
110
+ Parameters
111
+ ----------
112
+ device_array : np.ndarray
113
+ A 2D array representing the planar geometry of the device.
114
+ model : Model
115
+ The model to use for prediction, representing a specific fabrication process.
116
+
117
+ Returns
118
+ -------
119
+ tuple[np.ndarray, np.ndarray]
120
+ The predicted output array and gradient array.
121
+ """
89
122
  headers = _prepare_headers()
90
123
  predict_data = _prepare_predict_data(device_array, model, "p", False)
91
- endpoint_url = f"{BASE_URL}-with-grad-v1.modal.run"
124
+ endpoint_url = f"{BASE_ENDPOINT_URL}-with-grad-v{ENDPOINT_VERSION}.modal.run"
92
125
 
93
126
  response = requests.post(
94
127
  endpoint_url, data=json.dumps(predict_data), headers=headers
@@ -99,7 +132,6 @@ def _predict_array_with_grad(
99
132
  gradient_max = response.json()["gradient_max"]
100
133
  gradient_range = gradient_max - gradient_min
101
134
  gradient_array = gradient_array * gradient_range + gradient_min
102
-
103
135
  return (prediction_array, gradient_array)
104
136
 
105
137
 
@@ -110,7 +142,8 @@ def predict_array_with_grad(device_array: np.ndarray, model: Model) -> np.ndarra
110
142
 
111
143
  This function predicts the outcome of the nanofabrication process for a given
112
144
  device array using a specified model. It also computes the gradient of the
113
- prediction with respect to the input device array.
145
+ prediction with respect to the input device array, making it suitable for use in
146
+ automatic differentiation applications (e.g., autograd).
114
147
 
115
148
  Parameters
116
149
  ----------
@@ -127,32 +160,29 @@ def predict_array_with_grad(device_array: np.ndarray, model: Model) -> np.ndarra
127
160
  prediction_array, gradient_array = _predict_array_with_grad(
128
161
  device_array=device_array, model=model
129
162
  )
130
- predict_array_with_grad.gradient_array = gradient_array
163
+ predict_array_with_grad.gradient_array = gradient_array # type: ignore
131
164
  return prediction_array
132
165
 
133
166
 
134
- def predict_array_with_grad_vjp(ans: np.ndarray, x: np.ndarray, model: Model):
167
+ def predict_array_with_grad_vjp(ans: np.ndarray, device_array: np.ndarray, *args):
135
168
  """
136
169
  Define the vector-Jacobian product (VJP) for the prediction function.
137
170
 
138
- This function provides the VJP for the `predict_array_with_grad` function,
139
- which is used in reverse-mode automatic differentiation to compute gradients.
140
-
141
171
  Parameters
142
172
  ----------
143
173
  ans : np.ndarray
144
174
  The output of the `predict_array_with_grad` function.
145
- x : np.ndarray
175
+ device_array : np.ndarray
146
176
  The input device array for which the gradient is computed.
147
- model : Model
148
- The model used for prediction.
177
+ *args :
178
+ Additional arguments that aren't used in the VJP computation.
149
179
 
150
180
  Returns
151
181
  -------
152
182
  function
153
183
  A function that computes the VJP given an upstream gradient `g`.
154
184
  """
155
- grad_x = predict_array_with_grad.gradient_array
185
+ grad_x = predict_array_with_grad.gradient_array # type: ignore
156
186
 
157
187
  def vjp(g: np.ndarray) -> np.ndarray:
158
188
  return g * grad_x
@@ -195,7 +225,7 @@ def _read_tokens():
195
225
  "Could not validate user.\n"
196
226
  "Please update prefab using: pip install --upgrade prefab.\n"
197
227
  "Signup/login and generate a new token.\n"
198
- "See https://www.prefabphotonics.com/docs/guides/quickstart."
228
+ "See https://docs.prefabphotonics.com/."
199
229
  ) from None
200
230
 
201
231