prefab 1.0.2__py3-none-any.whl → 1.0.4__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 CHANGED
@@ -5,7 +5,9 @@ Usage:
5
5
  import prefab as pf
6
6
  """
7
7
 
8
- from . import compare, geometry, read
8
+ __version__ = "1.0.4"
9
+
10
+ from . import compare, geometry, read, shapes
9
11
  from .device import BufferSpec, Device
10
12
  from .models import models
11
13
 
@@ -14,6 +16,8 @@ __all__ = [
14
16
  "BufferSpec",
15
17
  "geometry",
16
18
  "read",
19
+ "shapes",
17
20
  "compare",
18
21
  "models",
22
+ "__version__",
19
23
  ]
prefab/compare.py CHANGED
@@ -7,40 +7,40 @@ from .device import Device
7
7
 
8
8
  def mean_squared_error(device_a: Device, device_b: Device) -> float:
9
9
  """
10
- Calculate the mean squared error (MSE) between two non-binarized devices.
10
+ Calculate the mean squared error (MSE) between two non-binarized devices. A lower
11
+ value indicates more similarity.
11
12
 
12
13
  Parameters
13
14
  ----------
14
15
  device_a : Device
15
- The first device.
16
+ The first device (non-binarized).
16
17
  device_b : Device
17
- The second device.
18
+ The second device (non-binarized).
18
19
 
19
20
  Returns
20
21
  -------
21
22
  float
22
- The mean squared error between two devices. A lower value indicates more
23
- similarity.
23
+ The mean squared error between two devices.
24
24
  """
25
25
  return np.mean((device_a.device_array - device_b.device_array) ** 2)
26
26
 
27
27
 
28
28
  def intersection_over_union(device_a: Device, device_b: Device) -> float:
29
29
  """
30
- Calculates the Intersection over Union (IoU) between two binary devices.
30
+ Calculates the Intersection over Union (IoU) between two binary devices. A value
31
+ closer to 1 indicates more similarity (more overlap).
31
32
 
32
33
  Parameters
33
34
  ----------
34
35
  device_a : Device
35
- The first device.
36
+ The first device (binarized).
36
37
  device_b : Device
37
- The second device.
38
+ The second device (binarized).
38
39
 
39
40
  Returns
40
41
  -------
41
42
  float
42
- The Intersection over Union between two devices. A value closer to 1 indicates
43
- more similarity (more overlap).
43
+ The Intersection over Union between two devices.
44
44
  """
45
45
  return np.sum(
46
46
  np.logical_and(device_a.device_array, device_b.device_array)
@@ -49,40 +49,42 @@ def intersection_over_union(device_a: Device, device_b: Device) -> float:
49
49
 
50
50
  def hamming_distance(device_a: Device, device_b: Device) -> int:
51
51
  """
52
- Calculates the Hamming distance between two binary devices.
52
+ Calculates the Hamming distance between two binary devices. A lower value indicates
53
+ more similarity. The Hamming distance is calculated as the number of positions at
54
+ which the corresponding pixels are different.
53
55
 
54
56
  Parameters
55
57
  ----------
56
58
  device_a : Device
57
- The first device.
59
+ The first device (binarized).
58
60
  device_b : Device
59
- The second device.
61
+ The second device (binarized).
60
62
 
61
63
  Returns
62
64
  -------
63
65
  int
64
- The Hamming distance between two devices. A lower value indicates more
65
- similarity.
66
+ The Hamming distance between two devices.
66
67
  """
67
68
  return np.sum(device_a.device_array != device_b.device_array)
68
69
 
69
70
 
70
71
  def dice_coefficient(device_a: Device, device_b: Device) -> float:
71
72
  """
72
- Calculates the Dice coefficient between two binary devices.
73
+ Calculates the Dice coefficient between two binary devices. A value closer to 1
74
+ indicates more similarity. The Dice coefficient is calculated as twice the number of
75
+ pixels in common divided by the total number of pixels in the two devices.
73
76
 
74
77
  Parameters
75
78
  ----------
76
79
  device_a : Device
77
- The first device.
80
+ The first device (binarized).
78
81
  device_b : Device
79
- The second device.
82
+ The second device (binarized).
80
83
 
81
84
  Returns
82
85
  -------
83
86
  float
84
- The Dice coefficient between two devices. A value closer to 1 indicates more
85
- similarity.
87
+ The Dice coefficient between two devices.
86
88
  """
87
89
  intersection = 2.0 * np.sum(
88
90
  np.logical_and(device_a.device_array, device_b.device_array)
prefab/device.py CHANGED
@@ -40,14 +40,16 @@ class BufferSpec(BaseModel):
40
40
  ('top', 'bottom', 'left', 'right'), where 'constant' is used for isolated
41
41
  structures and 'edge' is utilized for preserving the edge, such as for waveguide
42
42
  connections.
43
- thickness : conint(gt=0)
44
- The thickness of the buffer zone around the device. Must be greater than 0.
43
+ thickness : dict[str, conint(gt=0)]
44
+ A dictionary that defines the thickness of the buffer zone for each side of the
45
+ device ('top', 'bottom', 'left', 'right'). Each value must be greater than 0.
45
46
 
46
47
  Raises
47
48
  ------
48
49
  ValueError
49
50
  If any of the modes specified in the 'mode' dictionary are not one of the
50
- allowed values ('constant', 'edge'). Or if the thickness is not greater than 0.
51
+ allowed values ('constant', 'edge'). Or if any of the thickness values are not
52
+ greater than 0.
51
53
 
52
54
  Example
53
55
  -------
@@ -60,7 +62,12 @@ class BufferSpec(BaseModel):
60
62
  "left": "constant",
61
63
  "right": "edge",
62
64
  },
63
- thickness=150,
65
+ thickness={
66
+ "top": 150,
67
+ "bottom": 100,
68
+ "left": 200,
69
+ "right": 250,
70
+ },
64
71
  )
65
72
  """
66
73
 
@@ -72,7 +79,14 @@ class BufferSpec(BaseModel):
72
79
  "right": "constant",
73
80
  }
74
81
  )
75
- thickness: conint(gt=0) = 128
82
+ thickness: dict[str, conint(gt=0)] = Field(
83
+ default_factory=lambda: {
84
+ "top": 128,
85
+ "bottom": 128,
86
+ "left": 128,
87
+ "right": 128,
88
+ }
89
+ )
76
90
 
77
91
  @validator("mode", pre=True)
78
92
  def check_mode(cls, v):
@@ -146,28 +160,26 @@ class Device(BaseModel):
146
160
 
147
161
  self.device_array = np.pad(
148
162
  self.device_array,
149
- pad_width=((buffer_thickness, 0), (0, 0)),
163
+ pad_width=((buffer_thickness["top"], 0), (0, 0)),
150
164
  mode=buffer_mode["top"],
151
165
  )
152
166
  self.device_array = np.pad(
153
167
  self.device_array,
154
- pad_width=((0, buffer_thickness), (0, 0)),
168
+ pad_width=((0, buffer_thickness["bottom"]), (0, 0)),
155
169
  mode=buffer_mode["bottom"],
156
170
  )
157
171
  self.device_array = np.pad(
158
172
  self.device_array,
159
- pad_width=((0, 0), (buffer_thickness, 0)),
173
+ pad_width=((0, 0), (buffer_thickness["left"], 0)),
160
174
  mode=buffer_mode["left"],
161
175
  )
162
176
  self.device_array = np.pad(
163
177
  self.device_array,
164
- pad_width=((0, 0), (0, buffer_thickness)),
178
+ pad_width=((0, 0), (0, buffer_thickness["right"])),
165
179
  mode=buffer_mode["right"],
166
180
  )
167
181
 
168
- self.device_array = np.expand_dims(
169
- self.device_array.astype(np.float32), axis=-1
170
- )
182
+ self.device_array = np.expand_dims(self.device_array, axis=-1)
171
183
 
172
184
  @root_validator(pre=True)
173
185
  def check_device_array(cls, values):
@@ -212,6 +224,7 @@ class Device(BaseModel):
212
224
  model: Model,
213
225
  model_type: str,
214
226
  binarize: bool,
227
+ gpu: bool = False,
215
228
  ) -> "Device":
216
229
  try:
217
230
  with open(os.path.expanduser("~/.prefab.toml")) as file:
@@ -233,6 +246,7 @@ class Device(BaseModel):
233
246
  "Signup/login and generate a new token.\n"
234
247
  "See https://www.prefabphotonics.com/docs/guides/quickstart."
235
248
  ) from None
249
+
236
250
  headers = {
237
251
  "Authorization": f"Bearer {access_token}",
238
252
  "X-Refresh-Token": refresh_token,
@@ -246,84 +260,99 @@ class Device(BaseModel):
246
260
  }
247
261
  json_data = json.dumps(predict_data)
248
262
 
249
- endpoint_url = "https://prefab-photonics--predict-v1.modal.run"
250
-
251
- with requests.post(
252
- endpoint_url, data=json_data, headers=headers, stream=True
253
- ) as response:
254
- response.raise_for_status()
255
- event_type = None
256
- model_descriptions = {"p": "Prediction", "c": "Correction", "s": "SEMulate"}
257
- progress_bar = tqdm(
258
- total=100,
259
- desc=f"{model_descriptions[model_type]}",
260
- unit="%",
261
- colour="green",
262
- bar_format="{l_bar}{bar:30}{r_bar}{bar:-10b}",
263
- )
263
+ endpoint_url = (
264
+ "https://prefab-photonics--predict-gpu-v1.modal.run"
265
+ if gpu
266
+ else "https://prefab-photonics--predict-v1.modal.run"
267
+ )
264
268
 
265
- for line in response.iter_lines():
266
- if line:
267
- decoded_line = line.decode("utf-8").strip()
268
- if decoded_line.startswith("event:"):
269
- event_type = decoded_line.split(":")[1].strip()
270
- elif decoded_line.startswith("data:"):
271
- try:
272
- data_content = json.loads(decoded_line.split("data: ")[1])
273
- if event_type == "progress":
274
- progress = round(100 * data_content["progress"])
275
- progress_bar.update(progress - progress_bar.n)
276
- elif event_type == "result":
277
- results = []
278
- for key in sorted(data_content.keys()):
279
- if key.startswith("result"):
280
- decoded_image = self._decode_array(
281
- data_content[key]
282
- )
283
- results.append(decoded_image)
269
+ try:
270
+ with requests.post(
271
+ endpoint_url, data=json_data, headers=headers, stream=True
272
+ ) as response:
273
+ response.raise_for_status()
274
+ event_type = None
275
+ model_descriptions = {
276
+ "p": "Prediction",
277
+ "c": "Correction",
278
+ "s": "SEMulate",
279
+ }
280
+ progress_bar = tqdm(
281
+ total=100,
282
+ desc=f"{model_descriptions[model_type]}",
283
+ unit="%",
284
+ colour="green",
285
+ bar_format="{l_bar}{bar:30}{r_bar}{bar:-10b}",
286
+ )
284
287
 
285
- if results:
286
- prediction = np.stack(results, axis=-1)
287
- if binarize:
288
- prediction = geometry.binarize_hard(prediction)
288
+ for line in response.iter_lines():
289
+ if line:
290
+ decoded_line = line.decode("utf-8").strip()
291
+ if decoded_line.startswith("event:"):
292
+ event_type = decoded_line.split(":")[1].strip()
293
+ elif decoded_line.startswith("data:"):
294
+ try:
295
+ data_content = json.loads(
296
+ decoded_line.split("data: ")[1]
297
+ )
298
+ if event_type == "progress":
299
+ progress = round(100 * data_content["progress"])
300
+ progress_bar.update(progress - progress_bar.n)
301
+ elif event_type == "result":
302
+ results = []
303
+ for key in sorted(data_content.keys()):
304
+ if key.startswith("result"):
305
+ decoded_image = self._decode_array(
306
+ data_content[key]
307
+ )
308
+ results.append(decoded_image)
309
+
310
+ if results:
311
+ prediction = np.stack(results, axis=-1)
312
+ if binarize:
313
+ prediction = geometry.binarize_hard(
314
+ prediction
315
+ )
316
+ progress_bar.close()
317
+ return prediction
318
+ elif event_type == "end":
319
+ print("Stream ended.")
289
320
  progress_bar.close()
290
- return prediction
291
- elif event_type == "end":
292
- print("Stream ended.")
293
- progress_bar.close()
294
- break
295
- elif event_type == "auth":
296
- if "new_refresh_token" in data_content["auth"]:
297
- prefab_file_path = os.path.expanduser(
298
- "~/.prefab.toml"
299
- )
300
- with open(
301
- prefab_file_path, "w", encoding="utf-8"
302
- ) as toml_file:
303
- toml.dump(
304
- {
305
- "access_token": data_content["auth"][
306
- "new_access_token"
307
- ],
308
- "refresh_token": data_content["auth"][
309
- "new_refresh_token"
310
- ],
311
- },
312
- toml_file,
321
+ break
322
+ elif event_type == "auth":
323
+ if "new_refresh_token" in data_content["auth"]:
324
+ prefab_file_path = os.path.expanduser(
325
+ "~/.prefab.toml"
313
326
  )
314
- elif event_type == "error":
315
- print(f"Error: {data_content['error']}")
316
- progress_bar.close()
317
- except json.JSONDecodeError:
318
- print(
319
- "Failed to decode JSON:",
320
- decoded_line.split("data: ")[1],
321
- )
327
+ with open(
328
+ prefab_file_path, "w", encoding="utf-8"
329
+ ) as toml_file:
330
+ toml.dump(
331
+ {
332
+ "access_token": data_content[
333
+ "auth"
334
+ ]["new_access_token"],
335
+ "refresh_token": data_content[
336
+ "auth"
337
+ ]["new_refresh_token"],
338
+ },
339
+ toml_file,
340
+ )
341
+ elif event_type == "error":
342
+ raise ValueError(f"{data_content['error']}")
343
+ except json.JSONDecodeError:
344
+ raise ValueError(
345
+ "Failed to decode JSON:",
346
+ decoded_line.split("data: ")[1],
347
+ ) from None
348
+ except requests.RequestException as e:
349
+ raise RuntimeError(f"Request failed: {e}") from e
322
350
 
323
351
  def predict(
324
352
  self,
325
353
  model: Model,
326
354
  binarize: bool = False,
355
+ gpu: bool = False,
327
356
  ) -> "Device":
328
357
  """
329
358
  Predict the nanofabrication outcome of the device using a specified model.
@@ -345,6 +374,10 @@ class Device(BaseModel):
345
374
  If True, the predicted device geometry will be binarized using a threshold
346
375
  method. This is useful for converting probabilistic predictions into binary
347
376
  geometries. Defaults to False.
377
+ gpu : bool, optional
378
+ If True, the prediction will be performed on a GPU. Defaults to False.
379
+ Note: The GPU option has more overhead and will take longer for small
380
+ devices, but will be faster for larger devices.
348
381
 
349
382
  Returns
350
383
  -------
@@ -361,6 +394,7 @@ class Device(BaseModel):
361
394
  model=model,
362
395
  model_type="p",
363
396
  binarize=binarize,
397
+ gpu=gpu,
364
398
  )
365
399
  return self.model_copy(update={"device_array": prediction_array})
366
400
 
@@ -368,6 +402,7 @@ class Device(BaseModel):
368
402
  self,
369
403
  model: Model,
370
404
  binarize: bool = True,
405
+ gpu: bool = False,
371
406
  ) -> "Device":
372
407
  """
373
408
  Correct the nanofabrication outcome of the device using a specified model.
@@ -391,6 +426,10 @@ class Device(BaseModel):
391
426
  If True, the corrected device geometry will be binarized using a threshold
392
427
  method. This is useful for converting probabilistic corrections into binary
393
428
  geometries. Defaults to True.
429
+ gpu : bool, optional
430
+ If True, the prediction will be performed on a GPU. Defaults to False.
431
+ Note: The GPU option has more overhead and will take longer for small
432
+ devices, but will be faster for larger devices.
394
433
 
395
434
  Returns
396
435
  -------
@@ -407,12 +446,14 @@ class Device(BaseModel):
407
446
  model=model,
408
447
  model_type="c",
409
448
  binarize=binarize,
449
+ gpu=gpu,
410
450
  )
411
451
  return self.model_copy(update={"device_array": correction_array})
412
452
 
413
453
  def semulate(
414
454
  self,
415
455
  model: Model,
456
+ gpu: bool = False,
416
457
  ) -> "Device":
417
458
  """
418
459
  Simulate the appearance of the device as if viewed under a scanning electron
@@ -432,6 +473,10 @@ class Device(BaseModel):
432
473
  in `models.py`. Each model is associated with a version and dataset that
433
474
  detail its creation and the data it was trained on, ensuring the SEMulation
434
475
  is tailored to specific fabrication parameters.
476
+ gpu : bool, optional
477
+ If True, the prediction will be performed on a GPU. Defaults to False.
478
+ Note: The GPU option has more overhead and will take longer for small
479
+ devices, but will be faster for larger devices.
435
480
 
436
481
  Returns
437
482
  -------
@@ -443,6 +488,7 @@ class Device(BaseModel):
443
488
  model=model,
444
489
  model_type="s",
445
490
  binarize=False,
491
+ gpu=gpu,
446
492
  )
447
493
  return self.model_copy(update={"device_array": semulated_array})
448
494
 
@@ -464,10 +510,12 @@ class Device(BaseModel):
464
510
  buffer_thickness = self.buffer_spec.thickness
465
511
  buffer_mode = self.buffer_spec.mode
466
512
 
467
- crop_top = buffer_thickness if buffer_mode["top"] == "edge" else 0
468
- crop_bottom = buffer_thickness if buffer_mode["bottom"] == "edge" else 0
469
- crop_left = buffer_thickness if buffer_mode["left"] == "edge" else 0
470
- crop_right = buffer_thickness if buffer_mode["right"] == "edge" else 0
513
+ crop_top = buffer_thickness["top"] if buffer_mode["top"] == "edge" else 0
514
+ crop_bottom = (
515
+ buffer_thickness["bottom"] if buffer_mode["bottom"] == "edge" else 0
516
+ )
517
+ crop_left = buffer_thickness["left"] if buffer_mode["left"] == "edge" else 0
518
+ crop_right = buffer_thickness["right"] if buffer_mode["right"] == "edge" else 0
471
519
 
472
520
  ndarray = device_array[
473
521
  crop_top : device_array.shape[0] - crop_bottom,
@@ -629,7 +677,7 @@ class Device(BaseModel):
629
677
  bounds: Optional[tuple[tuple[int, int], tuple[int, int]]],
630
678
  ax: Optional[Axes],
631
679
  **kwargs,
632
- ) -> Axes:
680
+ ) -> tuple[plt.cm.ScalarMappable, Axes]:
633
681
  if ax is None:
634
682
  _, ax = plt.subplots()
635
683
  ax.set_ylabel("y (nm)")
@@ -673,6 +721,13 @@ class Device(BaseModel):
673
721
  if show_buffer:
674
722
  self._add_buffer_visualization(ax)
675
723
 
724
+ # # Adjust colorbar font size if a colorbar is added
725
+ # if "cmap" in kwargs:
726
+ # cbar = plt.colorbar(mappable, ax=ax)
727
+ # cbar.ax.tick_params(labelsize=14)
728
+ # if "label" in kwargs:
729
+ # cbar.set_label(kwargs["label"], fontsize=16)
730
+
676
731
  return mappable, ax
677
732
 
678
733
  def plot(
@@ -937,9 +992,9 @@ class Device(BaseModel):
937
992
  buffer_hatch = "/"
938
993
 
939
994
  mid_rect = Rectangle(
940
- (buffer_thickness, buffer_thickness),
941
- plot_array.shape[1] - 2 * buffer_thickness,
942
- plot_array.shape[0] - 2 * buffer_thickness,
995
+ (buffer_thickness["left"], buffer_thickness["top"]),
996
+ plot_array.shape[1] - buffer_thickness["left"] - buffer_thickness["right"],
997
+ plot_array.shape[0] - buffer_thickness["top"] - buffer_thickness["bottom"],
943
998
  facecolor="none",
944
999
  edgecolor="black",
945
1000
  linewidth=1,
@@ -949,25 +1004,25 @@ class Device(BaseModel):
949
1004
  top_rect = Rectangle(
950
1005
  (0, 0),
951
1006
  plot_array.shape[1],
952
- buffer_thickness,
1007
+ buffer_thickness["top"],
953
1008
  facecolor=buffer_fill,
954
1009
  hatch=buffer_hatch,
955
1010
  )
956
1011
  ax.add_patch(top_rect)
957
1012
 
958
1013
  bottom_rect = Rectangle(
959
- (0, plot_array.shape[0] - buffer_thickness),
1014
+ (0, plot_array.shape[0] - buffer_thickness["bottom"]),
960
1015
  plot_array.shape[1],
961
- buffer_thickness,
1016
+ buffer_thickness["bottom"],
962
1017
  facecolor=buffer_fill,
963
1018
  hatch=buffer_hatch,
964
1019
  )
965
1020
  ax.add_patch(bottom_rect)
966
1021
 
967
1022
  left_rect = Rectangle(
968
- (0, buffer_thickness),
969
- buffer_thickness,
970
- plot_array.shape[0] - 2 * buffer_thickness,
1023
+ (0, buffer_thickness["top"]),
1024
+ buffer_thickness["left"],
1025
+ plot_array.shape[0] - buffer_thickness["top"] - buffer_thickness["bottom"],
971
1026
  facecolor=buffer_fill,
972
1027
  hatch=buffer_hatch,
973
1028
  )
@@ -975,11 +1030,11 @@ class Device(BaseModel):
975
1030
 
976
1031
  right_rect = Rectangle(
977
1032
  (
978
- plot_array.shape[1] - buffer_thickness,
979
- buffer_thickness,
1033
+ plot_array.shape[1] - buffer_thickness["right"],
1034
+ buffer_thickness["top"],
980
1035
  ),
981
- buffer_thickness,
982
- plot_array.shape[0] - 2 * buffer_thickness,
1036
+ buffer_thickness["right"],
1037
+ plot_array.shape[0] - buffer_thickness["top"] - buffer_thickness["bottom"],
983
1038
  facecolor=buffer_fill,
984
1039
  hatch=buffer_hatch,
985
1040
  )
@@ -1017,7 +1072,9 @@ class Device(BaseModel):
1017
1072
  binarized_device_array = geometry.binarize(
1018
1073
  device_array=self.device_array, eta=eta, beta=beta
1019
1074
  )
1020
- return self.model_copy(update={"device_array": binarized_device_array})
1075
+ return self.model_copy(
1076
+ update={"device_array": binarized_device_array.astype(np.uint8)}
1077
+ )
1021
1078
 
1022
1079
  def binarize_hard(self, eta: float = 0.5) -> "Device":
1023
1080
  """
@@ -1038,12 +1095,14 @@ class Device(BaseModel):
1038
1095
  binarized_device_array = geometry.binarize_hard(
1039
1096
  device_array=self.device_array, eta=eta
1040
1097
  )
1041
- return self.model_copy(update={"device_array": binarized_device_array})
1098
+ return self.model_copy(
1099
+ update={"device_array": binarized_device_array.astype(np.uint8)}
1100
+ )
1042
1101
 
1043
1102
  def binarize_monte_carlo(
1044
1103
  self,
1045
1104
  threshold_noise_std: float = 2.0,
1046
- threshold_blur_std: float = 9.0,
1105
+ threshold_blur_std: float = 8.0,
1047
1106
  ) -> "Device":
1048
1107
  """
1049
1108
  Binarize the device geometry using a Monte Carlo approach with Gaussian
@@ -1106,9 +1165,10 @@ class Device(BaseModel):
1106
1165
 
1107
1166
  Parameters
1108
1167
  ----------
1109
- buffer_thickness : int, optional
1110
- The thickness of the buffer to leave around the empty space. Defaults to 0,
1111
- which means no buffer is added.
1168
+ buffer_thickness : dict, optional
1169
+ A dictionary specifying the thickness of the buffer to leave around the
1170
+ non-zero elements of the array. Should contain keys 'top', 'bottom', 'left',
1171
+ 'right'. Defaults to None, which means no buffer is added.
1112
1172
 
1113
1173
  Returns
1114
1174
  -------
@@ -1233,3 +1293,5 @@ class Device(BaseModel):
1233
1293
  with higher values indicating greater uncertainty.
1234
1294
  """
1235
1295
  return 1 - 2 * np.abs(0.5 - self.device_array)
1296
+
1297
+ return 1 - 2 * np.abs(0.5 - self.device_array)