prefab 1.1.1__py3-none-any.whl → 1.1.3__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,9 +5,9 @@ Usage:
5
5
  import prefab as pf
6
6
  """
7
7
 
8
- __version__ = "1.1.1"
8
+ __version__ = "1.1.3"
9
9
 
10
- from . import compare, geometry, read, shapes
10
+ from . import compare, geometry, predict, read, shapes
11
11
  from .device import BufferSpec, Device
12
12
  from .models import models
13
13
 
@@ -15,6 +15,7 @@ __all__ = [
15
15
  "Device",
16
16
  "BufferSpec",
17
17
  "geometry",
18
+ "predict",
18
19
  "read",
19
20
  "shapes",
20
21
  "compare",
prefab/compare.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """Provides functions to measure the similarity between devices."""
2
2
 
3
+ import warnings
4
+
3
5
  import numpy as np
4
6
 
5
7
  from .device import Device
@@ -42,6 +44,11 @@ def intersection_over_union(device_a: Device, device_b: Device) -> float:
42
44
  float
43
45
  The Intersection over Union between two devices.
44
46
  """
47
+ if not device_a.is_binary or not device_b.is_binary:
48
+ warnings.warn(
49
+ "One or both devices are not binarized.", UserWarning, stacklevel=2
50
+ )
51
+
45
52
  return np.sum(
46
53
  np.logical_and(device_a.device_array, device_b.device_array)
47
54
  ) / np.sum(np.logical_or(device_a.device_array, device_b.device_array))
@@ -65,6 +72,11 @@ def hamming_distance(device_a: Device, device_b: Device) -> int:
65
72
  int
66
73
  The Hamming distance between two devices.
67
74
  """
75
+ if not device_a.is_binary or not device_b.is_binary:
76
+ warnings.warn(
77
+ "One or both devices are not binarized.", UserWarning, stacklevel=2
78
+ )
79
+
68
80
  return np.sum(device_a.device_array != device_b.device_array)
69
81
 
70
82
 
@@ -86,6 +98,11 @@ def dice_coefficient(device_a: Device, device_b: Device) -> float:
86
98
  float
87
99
  The Dice coefficient between two devices.
88
100
  """
101
+ if not device_a.is_binary or not device_b.is_binary:
102
+ warnings.warn(
103
+ "One or both devices are not binarized.", UserWarning, stacklevel=2
104
+ )
105
+
89
106
  intersection = 2.0 * np.sum(
90
107
  np.logical_and(device_a.device_array, device_b.device_array)
91
108
  )
prefab/device.py CHANGED
@@ -1,28 +1,21 @@
1
1
  """Provides the Device class for representing photonic devices."""
2
2
 
3
- import base64
4
- import io
5
- import json
6
- import os
7
3
  from typing import Optional
8
4
 
9
5
  import cv2
10
6
  import gdstk
11
7
  import matplotlib.pyplot as plt
12
8
  import numpy as np
13
- import requests
14
- import toml
15
9
  from matplotlib.axes import Axes
16
10
  from matplotlib.patches import Rectangle
17
11
  from PIL import Image
18
12
  from pydantic import BaseModel, Field, conint, root_validator, validator
19
13
  from scipy.ndimage import distance_transform_edt
20
14
  from skimage import measure
21
- from skimage.morphology import closing, disk, opening, square
22
- from tqdm import tqdm
23
15
 
24
16
  from . import compare, geometry
25
17
  from .models import Model
18
+ from .predict import predict_array
26
19
 
27
20
  Image.MAX_IMAGE_PIXELS = None
28
21
 
@@ -193,6 +186,7 @@ class Device(BaseModel):
193
186
  raise ValueError("device_array must be a 2D array.")
194
187
  return values
195
188
 
189
+ @property
196
190
  def is_binary(self) -> bool:
197
191
  """
198
192
  Check if the device geometry is binary.
@@ -210,147 +204,6 @@ class Device(BaseModel):
210
204
  or np.array_equal(unique_values, [1])
211
205
  )
212
206
 
213
- def _encode_array(self, array):
214
- image = Image.fromarray(np.uint8(array * 255))
215
- buffered = io.BytesIO()
216
- image.save(buffered, format="PNG")
217
- encoded_png = base64.b64encode(buffered.getvalue()).decode("utf-8")
218
- return encoded_png
219
-
220
- def _decode_array(self, encoded_png):
221
- binary_data = base64.b64decode(encoded_png)
222
- image = Image.open(io.BytesIO(binary_data))
223
- return np.array(image) / 255
224
-
225
- def _predict_array(
226
- self,
227
- model: Model,
228
- model_type: str,
229
- binarize: bool,
230
- gpu: bool = False,
231
- ) -> "Device":
232
- try:
233
- with open(os.path.expanduser("~/.prefab.toml")) as file:
234
- content = file.readlines()
235
- access_token = None
236
- refresh_token = None
237
- for line in content:
238
- if "access_token" in line:
239
- access_token = line.split("=")[1].strip().strip('"')
240
- if "refresh_token" in line:
241
- refresh_token = line.split("=")[1].strip().strip('"')
242
- break
243
- if not access_token or not refresh_token:
244
- raise ValueError("Token not found in the configuration file.")
245
- except FileNotFoundError:
246
- raise FileNotFoundError(
247
- "Could not validate user.\n"
248
- "Please update prefab using: pip install --upgrade prefab.\n"
249
- "Signup/login and generate a new token.\n"
250
- "See https://www.prefabphotonics.com/docs/guides/quickstart."
251
- ) from None
252
-
253
- headers = {
254
- "Authorization": f"Bearer {access_token}",
255
- "X-Refresh-Token": refresh_token,
256
- }
257
-
258
- predict_data = {
259
- "device_array": self._encode_array(self.device_array[:, :, 0]),
260
- "model": model.to_json(),
261
- "model_type": model_type,
262
- "binary": binarize,
263
- }
264
- json_data = json.dumps(predict_data)
265
-
266
- endpoint_url = (
267
- "https://prefab-photonics--predict-gpu-v1.modal.run"
268
- if gpu
269
- else "https://prefab-photonics--predict-v1.modal.run"
270
- )
271
-
272
- try:
273
- with requests.post(
274
- endpoint_url, data=json_data, headers=headers, stream=True
275
- ) as response:
276
- response.raise_for_status()
277
- event_type = None
278
- model_descriptions = {
279
- "p": "Prediction",
280
- "c": "Correction",
281
- "s": "SEMulate",
282
- }
283
- progress_bar = tqdm(
284
- total=100,
285
- desc=f"{model_descriptions[model_type]}",
286
- unit="%",
287
- colour="green",
288
- bar_format="{l_bar}{bar:30}{r_bar}{bar:-10b}",
289
- )
290
-
291
- for line in response.iter_lines():
292
- if line:
293
- decoded_line = line.decode("utf-8").strip()
294
- if decoded_line.startswith("event:"):
295
- event_type = decoded_line.split(":")[1].strip()
296
- elif decoded_line.startswith("data:"):
297
- try:
298
- data_content = json.loads(
299
- decoded_line.split("data: ")[1]
300
- )
301
- if event_type == "progress":
302
- progress = round(100 * data_content["progress"])
303
- progress_bar.update(progress - progress_bar.n)
304
- elif event_type == "result":
305
- results = []
306
- for key in sorted(data_content.keys()):
307
- if key.startswith("result"):
308
- decoded_image = self._decode_array(
309
- data_content[key]
310
- )
311
- results.append(decoded_image)
312
-
313
- if results:
314
- prediction = np.stack(results, axis=-1)
315
- if binarize:
316
- prediction = geometry.binarize_hard(
317
- prediction
318
- )
319
- progress_bar.close()
320
- return prediction
321
- elif event_type == "end":
322
- print("Stream ended.")
323
- progress_bar.close()
324
- break
325
- elif event_type == "auth":
326
- if "new_refresh_token" in data_content["auth"]:
327
- prefab_file_path = os.path.expanduser(
328
- "~/.prefab.toml"
329
- )
330
- with open(
331
- prefab_file_path, "w", encoding="utf-8"
332
- ) as toml_file:
333
- toml.dump(
334
- {
335
- "access_token": data_content[
336
- "auth"
337
- ]["new_access_token"],
338
- "refresh_token": data_content[
339
- "auth"
340
- ]["new_refresh_token"],
341
- },
342
- toml_file,
343
- )
344
- elif event_type == "error":
345
- raise ValueError(f"{data_content['error']}")
346
- except json.JSONDecodeError:
347
- raise ValueError(
348
- "Failed to decode JSON:",
349
- decoded_line.split("data: ")[1],
350
- ) from None
351
- except requests.RequestException as e:
352
- raise RuntimeError(f"Request failed: {e}") from e
353
-
354
207
  def predict(
355
208
  self,
356
209
  model: Model,
@@ -393,7 +246,8 @@ class Device(BaseModel):
393
246
  If the prediction service returns an error or if the response from the
394
247
  service cannot be processed correctly.
395
248
  """
396
- prediction_array = self._predict_array(
249
+ prediction_array = predict_array(
250
+ device_array=self.device_array,
397
251
  model=model,
398
252
  model_type="p",
399
253
  binarize=binarize,
@@ -445,7 +299,8 @@ class Device(BaseModel):
445
299
  If the correction service returns an error or if the response from the
446
300
  service cannot be processed correctly.
447
301
  """
448
- correction_array = self._predict_array(
302
+ correction_array = predict_array(
303
+ device_array=self.device_array,
449
304
  model=model,
450
305
  model_type="c",
451
306
  binarize=binarize,
@@ -487,7 +342,8 @@ class Device(BaseModel):
487
342
  A new instance of the Device class with its geometry transformed to simulate
488
343
  an SEM image style.
489
344
  """
490
- semulated_array = self._predict_array(
345
+ semulated_array = predict_array(
346
+ device_array=self.device_array,
491
347
  model=model,
492
348
  model_type="s",
493
349
  binarize=False,
@@ -550,6 +406,7 @@ class Device(BaseModel):
550
406
  cell_name: str = "prefab_device",
551
407
  gds_layer: tuple[int, int] = (1, 0),
552
408
  contour_approx_mode: int = 2,
409
+ origin: tuple[float, float] = (0.0, 0.0),
553
410
  ):
554
411
  """
555
412
  Exports the device geometry as a GDSII file.
@@ -572,11 +429,15 @@ class Device(BaseModel):
572
429
  The mode of contour approximation used during the conversion. Defaults to 2,
573
430
  which corresponds to `cv2.CHAIN_APPROX_SIMPLE`, a method that compresses
574
431
  horizontal, vertical, and diagonal segments and leaves only their endpoints.
432
+ origin : tuple[float, float], optional
433
+ The x and y coordinates of the origin for the GDSII export. Defaults to
434
+ (0.0, 0.0).
575
435
  """
576
436
  gdstk_cell = self.flatten()._device_to_gdstk(
577
437
  cell_name=cell_name,
578
438
  gds_layer=gds_layer,
579
439
  contour_approx_mode=contour_approx_mode,
440
+ origin=origin,
580
441
  )
581
442
  print(f"Saving GDS to '{gds_path}'...")
582
443
  gdstk_library = gdstk.Library()
@@ -588,6 +449,7 @@ class Device(BaseModel):
588
449
  cell_name: str = "prefab_device",
589
450
  gds_layer: tuple[int, int] = (1, 0),
590
451
  contour_approx_mode: int = 2,
452
+ origin: tuple[float, float] = (0.0, 0.0),
591
453
  ):
592
454
  """
593
455
  Converts the device geometry to a GDSTK cell object.
@@ -607,6 +469,9 @@ class Device(BaseModel):
607
469
  The mode of contour approximation used during the conversion. Defaults to 2,
608
470
  which corresponds to `cv2.CHAIN_APPROX_SIMPLE`, a method that compresses
609
471
  horizontal, vertical, and diagonal segments and leaves only their endpoints.
472
+ origin : tuple[float, float], optional
473
+ The x and y coordinates of the origin for the GDSTK cell. Defaults to
474
+ (0.0, 0.0).
610
475
 
611
476
  Returns
612
477
  -------
@@ -618,6 +483,7 @@ class Device(BaseModel):
618
483
  cell_name=cell_name,
619
484
  gds_layer=gds_layer,
620
485
  contour_approx_mode=contour_approx_mode,
486
+ origin=origin,
621
487
  )
622
488
  return gdstk_cell
623
489
 
@@ -626,6 +492,7 @@ class Device(BaseModel):
626
492
  cell_name: str,
627
493
  gds_layer: tuple[int, int],
628
494
  contour_approx_mode: int,
495
+ origin: tuple[float, float],
629
496
  ) -> gdstk.Cell:
630
497
  approx_mode_mapping = {
631
498
  1: cv2.CHAIN_APPROX_NONE,
@@ -662,8 +529,21 @@ class Device(BaseModel):
662
529
  polygons_to_process = hierarchy_polygons[level]
663
530
 
664
531
  if polygons_to_process:
532
+ center_x_nm = self.device_array.shape[1] / 2
533
+ center_y_nm = self.device_array.shape[0] / 2
534
+
535
+ center_x_um = center_x_nm / 1000
536
+ center_y_um = center_y_nm / 1000
537
+
538
+ adjusted_polygons = [
539
+ [
540
+ (x - center_x_um + origin[0], y - center_y_um + origin[1])
541
+ for x, y in polygon
542
+ ]
543
+ for polygon in polygons_to_process
544
+ ]
665
545
  processed_polygons = gdstk.boolean(
666
- polygons_to_process,
546
+ adjusted_polygons,
667
547
  processed_polygons,
668
548
  operation,
669
549
  layer=gds_layer[0],
@@ -1313,13 +1193,6 @@ class Device(BaseModel):
1313
1193
  """
1314
1194
  Trim the device geometry by removing empty space around it.
1315
1195
 
1316
- Parameters
1317
- ----------
1318
- buffer_thickness : dict, optional
1319
- A dictionary specifying the thickness of the buffer to leave around the
1320
- non-zero elements of the array. Should contain keys 'top', 'bottom', 'left',
1321
- 'right'. Defaults to None, which means no buffer is added.
1322
-
1323
1196
  Returns
1324
1197
  -------
1325
1198
  Device
@@ -1414,15 +1287,10 @@ class Device(BaseModel):
1414
1287
  Flatten the device geometry by summing the vertical layers and normalizing the
1415
1288
  result.
1416
1289
 
1417
- Parameters
1418
- ----------
1419
- device_array : np.ndarray
1420
- The input array to be flattened.
1421
-
1422
1290
  Returns
1423
1291
  -------
1424
- np.ndarray
1425
- The flattened array with values scaled between 0 and 1.
1292
+ Device
1293
+ A new instance of the Device with the flattened geometry.
1426
1294
  """
1427
1295
  flattened_device_array = geometry.flatten(device_array=self.device_array)
1428
1296
  return self.model_copy(update={"device_array": flattened_device_array})
@@ -1472,16 +1340,11 @@ class Device(BaseModel):
1472
1340
  ValueError
1473
1341
  If an invalid structuring element type is specified.
1474
1342
  """
1475
- if strel == "disk":
1476
- structuring_element = disk(radius=min_feature_size / 2)
1477
- elif strel == "square":
1478
- structuring_element = square(width=min_feature_size)
1479
- else:
1480
- raise ValueError(f"Invalid structuring element: {strel}")
1481
-
1482
- modified_geometry = closing(self.device_array[:, :, 0], structuring_element)
1483
- modified_geometry = opening(modified_geometry, structuring_element)
1484
- modified_geometry = np.expand_dims(modified_geometry, axis=-1)
1343
+ modified_geometry = geometry.enforce_feature_size(
1344
+ device_array=self.device_array,
1345
+ min_feature_size=min_feature_size,
1346
+ strel=strel,
1347
+ )
1485
1348
  return self.model_copy(update={"device_array": modified_geometry})
1486
1349
 
1487
1350
  def check_feature_size(self, min_feature_size: int, strel: str = "disk"):
prefab/geometry.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import cv2
4
4
  import numpy as np
5
+ from skimage.morphology import closing, disk, opening, square
5
6
 
6
7
 
7
8
  def normalize(device_array: np.ndarray) -> np.ndarray:
@@ -248,7 +249,6 @@ def rotate(device_array: np.ndarray, angle: float) -> np.ndarray:
248
249
  ),
249
250
  axis=-1,
250
251
  )
251
- return np.expand_dims(rotated_device_array, axis=-1)
252
252
 
253
253
 
254
254
  def erode(device_array: np.ndarray, kernel_size: int) -> np.ndarray:
@@ -306,3 +306,45 @@ def flatten(device_array: np.ndarray) -> np.ndarray:
306
306
  The flattened array with values scaled between 0 and 1.
307
307
  """
308
308
  return normalize(np.sum(device_array, axis=-1, keepdims=True))
309
+
310
+
311
+ def enforce_feature_size(
312
+ device_array: np.ndarray, min_feature_size: int, strel: str = "disk"
313
+ ) -> np.ndarray:
314
+ """
315
+ Enforce a minimum feature size on the device geometry.
316
+
317
+ This function applies morphological operations to ensure that all features in the
318
+ device geometry are at least the specified minimum size. It uses either a disk
319
+ or square structuring element for the operations.
320
+
321
+ Parameters
322
+ ----------
323
+ device_array : np.ndarray
324
+ The input array representing the device geometry.
325
+ min_feature_size : int
326
+ The minimum feature size to enforce, in nanometers.
327
+ strel : str, optional
328
+ The type of structuring element to use. Can be either "disk" or "square".
329
+ Defaults to "disk".
330
+
331
+ Returns
332
+ -------
333
+ np.ndarray
334
+ The modified device array with enforced feature size.
335
+
336
+ Raises
337
+ ------
338
+ ValueError
339
+ If an invalid structuring element type is specified.
340
+ """
341
+ if strel == "disk":
342
+ structuring_element = disk(radius=min_feature_size / 2)
343
+ elif strel == "square":
344
+ structuring_element = square(width=min_feature_size)
345
+ else:
346
+ raise ValueError(f"Invalid structuring element: {strel}")
347
+
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)
prefab/models.py CHANGED
@@ -10,7 +10,7 @@ class Fab(BaseModel):
10
10
  """
11
11
  Represents a fabrication process in the PreFab model library.
12
12
 
13
- Parameters
13
+ Attributes
14
14
  ----------
15
15
  foundry : str
16
16
  The name of the foundry where the fabrication process takes place.