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/device.py CHANGED
@@ -1,26 +1,31 @@
1
- """Provides the Device class for representing photonic devices."""
1
+ """
2
+ Core device representation and manipulation for photonic geometries.
2
3
 
3
- from typing import TYPE_CHECKING, Literal, Optional
4
+ This module provides the Device class for representing planar photonic device
5
+ geometries and performing operations on them. It includes buffer zone management,
6
+ prediction and correction of nanofabrication outcomes, visualization capabilities,
7
+ and export functions to various formats. The module also supports geometric
8
+ transformations (rotation, dilation, erosion), image processing operations (blur,
9
+ binarization), and comparison tools for analyzing fabrication deviations.
10
+ """
11
+
12
+ from typing import Any, Literal
4
13
 
5
14
  import cv2
6
15
  import gdstk
7
16
  import matplotlib.pyplot as plt
8
17
  import numpy as np
18
+ import numpy.typing as npt
9
19
  from matplotlib.axes import Axes
10
20
  from matplotlib.patches import Rectangle
11
21
  from PIL import Image
12
- from pydantic import BaseModel, Field, model_validator, validator
22
+ from pydantic import BaseModel, Field, field_validator, model_validator
13
23
  from scipy.ndimage import distance_transform_edt
14
- from skimage import measure
15
24
 
16
- from . import compare, geometry
25
+ from . import geometry
17
26
  from .models import Model
18
27
  from .predict import predict_array
19
28
 
20
- if TYPE_CHECKING:
21
- import gdsfactory as gf
22
- import tidy3d as td
23
-
24
29
  Image.MAX_IMAGE_PIXELS = None
25
30
 
26
31
 
@@ -33,7 +38,7 @@ class BufferSpec(BaseModel):
33
38
  providing extra space for device fabrication processes or for ensuring that the
34
39
  device is isolated from surrounding structures.
35
40
 
36
- Parameters
41
+ Attributes
37
42
  ----------
38
43
  mode : dict[str, str]
39
44
  A dictionary that defines the buffer mode for each side of the device
@@ -53,8 +58,8 @@ class BufferSpec(BaseModel):
53
58
  allowed values ('constant', 'edge', 'none'). Or if any of the thickness values
54
59
  are negative.
55
60
 
56
- Example
57
- -------
61
+ Examples
62
+ --------
58
63
  import prefab as pf
59
64
 
60
65
  buffer_spec = pf.BufferSpec(
@@ -90,23 +95,25 @@ class BufferSpec(BaseModel):
90
95
  }
91
96
  )
92
97
 
93
- @validator("mode", pre=True)
94
- def check_mode(cls, v):
98
+ @field_validator("mode", mode="before")
99
+ @classmethod
100
+ def check_mode(cls, v: dict[str, str]) -> dict[str, str]:
95
101
  allowed_modes = ["constant", "edge", "none"]
96
102
  if not all(mode in allowed_modes for mode in v.values()):
97
103
  raise ValueError(f"Buffer mode must be one of {allowed_modes}, got '{v}'.")
98
104
  return v
99
105
 
100
- @validator("thickness")
101
- def check_thickness(cls, v):
106
+ @field_validator("thickness")
107
+ @classmethod
108
+ def check_thickness(cls, v: dict[str, int]) -> dict[str, int]:
102
109
  if not all(t >= 0 for t in v.values()):
103
110
  raise ValueError("All thickness values must be greater than or equal to 0.")
104
111
  return v
105
112
 
106
113
  @model_validator(mode="after")
107
- def check_none_thickness(cls, values):
108
- mode = values.mode
109
- thickness = values.thickness
114
+ def check_none_thickness(self) -> "BufferSpec":
115
+ mode = self.mode
116
+ thickness = self.thickness
110
117
  for side in mode:
111
118
  if mode[side] == "none" and thickness[side] != 0:
112
119
  raise ValueError(
@@ -116,22 +123,22 @@ class BufferSpec(BaseModel):
116
123
  raise ValueError(
117
124
  f"Mode must be 'none' when thickness is 0 for {side} side"
118
125
  )
119
- return values
126
+ return self
120
127
 
121
128
 
122
129
  class Device(BaseModel):
123
- device_array: np.ndarray = Field(...)
130
+ device_array: npt.NDArray[Any] = Field(...)
124
131
  buffer_spec: BufferSpec = Field(default_factory=BufferSpec)
125
132
 
126
133
  class Config:
127
- arbitrary_types_allowed = True
134
+ arbitrary_types_allowed: bool = True
128
135
 
129
136
  @property
130
137
  def shape(self) -> tuple[int, ...]:
131
138
  return tuple(self.device_array.shape)
132
139
 
133
140
  def __init__(
134
- self, device_array: np.ndarray, buffer_spec: Optional[BufferSpec] = None
141
+ self, device_array: npt.NDArray[Any], buffer_spec: BufferSpec | None = None
135
142
  ):
136
143
  """
137
144
  Represents the planar geometry of a photonic device design that will have its
@@ -174,10 +181,10 @@ class Device(BaseModel):
174
181
  )
175
182
  self._initial_processing()
176
183
 
177
- def __call__(self, *args, **kwargs):
184
+ def __call__(self, *args: Any, **kwargs: Any) -> Axes:
178
185
  return self.plot(*args, **kwargs)
179
186
 
180
- def _initial_processing(self):
187
+ def _initial_processing(self) -> None:
181
188
  buffer_thickness = self.buffer_spec.thickness
182
189
  buffer_mode = self.buffer_spec.mode
183
190
 
@@ -212,7 +219,8 @@ class Device(BaseModel):
212
219
  self.device_array = np.expand_dims(self.device_array, axis=-1)
213
220
 
214
221
  @model_validator(mode="before")
215
- def check_device_array(cls, values):
222
+ @classmethod
223
+ def check_device_array(cls, values: dict[str, Any]) -> dict[str, Any]:
216
224
  device_array = values.get("device_array")
217
225
  if not isinstance(device_array, np.ndarray):
218
226
  raise ValueError("device_array must be a numpy ndarray.")
@@ -254,11 +262,10 @@ class Device(BaseModel):
254
262
  ----------
255
263
  model : Model
256
264
  The model to use for prediction, representing a specific fabrication process
257
- and dataset. This model encapsulates details about the fabrication foundry,
258
- process, material, technology, thickness, and sidewall presence, as defined
259
- in `models.py`. Each model is associated with a version and dataset that
260
- detail its creation and the data it was trained on, ensuring the prediction
261
- is tailored to specific fabrication parameters.
265
+ and dataset. This model encapsulates details about the fabrication foundry
266
+ and process, as defined in `models.py`. Each model is associated with a
267
+ version and dataset that detail its creation and the data it was trained on,
268
+ ensuring the prediction is tailored to specific fabrication parameters.
262
269
  binarize : bool
263
270
  If True, the predicted device geometry will be binarized using a threshold
264
271
  method. This is useful for converting probabilistic predictions into binary
@@ -301,11 +308,10 @@ class Device(BaseModel):
301
308
  ----------
302
309
  model : Model
303
310
  The model to use for correction, representing a specific fabrication process
304
- and dataset. This model encapsulates details about the fabrication foundry,
305
- process, material, technology, thickness, and sidewall presence, as defined
306
- in `models.py`. Each model is associated with a version and dataset that
307
- detail its creation and the data it was trained on, ensuring the correction
308
- is tailored to specific fabrication parameters.
311
+ and dataset. This model encapsulates details about the fabrication foundry
312
+ and process, as defined in `models.py`. Each model is associated with a
313
+ version and dataset that detail its creation and the data it was trained on,
314
+ ensuring the correction is tailored to specific fabrication parameters.
309
315
  binarize : bool
310
316
  If True, the corrected device geometry will be binarized using a threshold
311
317
  method. This is useful for converting probabilistic corrections into binary
@@ -330,98 +336,7 @@ class Device(BaseModel):
330
336
  )
331
337
  return self.model_copy(update={"device_array": correction_array})
332
338
 
333
- def semulate(
334
- self,
335
- model: Model,
336
- ) -> "Device":
337
- """
338
- Simulate the appearance of the device as if viewed under a scanning electron
339
- microscope (SEM).
340
-
341
- This method applies a specified machine learning model to transform the device
342
- geometry into a style that resembles an SEM image. This can be useful for
343
- visualizing how the device might appear under an SEM, which is often used for
344
- inspecting the surface and composition of materials at high magnification.
345
-
346
- Parameters
347
- ----------
348
- model : Model
349
- The model to use for SEMulation, representing a specific fabrication process
350
- and dataset. This model encapsulates details about the fabrication foundry,
351
- process, material, technology, thickness, and sidewall presence, as defined
352
- in `models.py`. Each model is associated with a version and dataset that
353
- detail its creation and the data it was trained on, ensuring the SEMulation
354
- is tailored to specific fabrication parameters.
355
-
356
- Notes
357
- -----
358
- The salt-and-pepper noise is added manually until the model is trained to
359
- generate this noise (not a big priority).
360
-
361
- Returns
362
- -------
363
- Device
364
- A new instance of the Device class with its geometry transformed to simulate
365
- an SEM image style.
366
-
367
- Raises
368
- ------
369
- RuntimeError
370
- If the prediction service returns an error or if the response from the
371
- service cannot be processed correctly.
372
- """
373
- semulated_array = predict_array(
374
- device_array=self.device_array,
375
- model=model,
376
- model_type="s",
377
- binarize=False,
378
- )
379
- semulated_array += np.random.normal(0, 0.03, semulated_array.shape)
380
- return self.model_copy(update={"device_array": semulated_array})
381
-
382
- def segment(
383
- self,
384
- model: Model,
385
- ) -> "Device":
386
- """
387
- Segment a scanning electron microscope (SEM) image into a binary mask.
388
-
389
- This method applies a specified machine learning model to transform a grayscale
390
- SEM image into a binary mask, where 1 represents the device structure and 0
391
- represents the background. This is useful for extracting the device geometry
392
- from experimental SEM images for analysis or comparison with design intent.
393
-
394
- Parameters
395
- ----------
396
- model : Model
397
- The model to use for segmentation, representing a specific fabrication
398
- process and dataset. This model encapsulates details about the fabrication
399
- foundry, process, material, technology, thickness, and sidewall presence, as
400
- defined in `models.py`. Each model is associated with a version and dataset
401
- that detail its creation and the data it was trained on, ensuring the
402
- segmentation is tailored to specific fabrication parameters.
403
-
404
- Returns
405
- -------
406
- Device
407
- A new instance of the Device class with its geometry transformed into a
408
- binary mask.
409
-
410
- Raises
411
- ------
412
- RuntimeError
413
- If the prediction service returns an error or if the response from the
414
- service cannot be processed correctly.
415
- """
416
- segmented_array = predict_array(
417
- device_array=self.normalize().device_array,
418
- model=model,
419
- model_type="b",
420
- binarize=False,
421
- )
422
- return self.model_copy(update={"device_array": segmented_array})
423
-
424
- def to_ndarray(self) -> np.ndarray:
339
+ def to_ndarray(self) -> npt.NDArray[Any]:
425
340
  """
426
341
  Converts the device geometry to an ndarray.
427
342
 
@@ -449,7 +364,11 @@ class Device(BaseModel):
449
364
  ]
450
365
  return ndarray
451
366
 
452
- def to_img(self, img_path: str = "prefab_device.png"):
367
+ def to_img(
368
+ self,
369
+ img_path: str = "prefab_device.png",
370
+ bounds: tuple[tuple[int, int], tuple[int, int]] | None = None,
371
+ ) -> None:
453
372
  """
454
373
  Exports the device geometry as an image file.
455
374
 
@@ -462,8 +381,49 @@ class Device(BaseModel):
462
381
  img_path : str
463
382
  The path where the image file will be saved. If not specified, the image is
464
383
  saved as "prefab_device.png" in the current directory.
465
- """
466
- cv2.imwrite(img_path, 255 * self.flatten().to_ndarray())
384
+ bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
385
+ Specifies the bounds for cropping the device geometry, formatted as
386
+ ((min_x, min_y), (max_x, max_y)). Negative values count from the end
387
+ (e.g., -50 means 50 pixels from the edge). If 'max_x' or 'max_y' is set
388
+ to "end", it will be replaced with the corresponding dimension size of the
389
+ device array. If None, the entire device geometry is exported.
390
+ """
391
+ device_array = self.flatten().to_ndarray()
392
+
393
+ if bounds is not None:
394
+ min_x, min_y = bounds[0]
395
+ max_x, max_y = bounds[1]
396
+
397
+ # Handle negative indices (count from end)
398
+ if min_x < 0:
399
+ min_x = device_array.shape[1] + min_x
400
+ if min_y < 0:
401
+ min_y = device_array.shape[0] + min_y
402
+ if max_x != "end" and max_x < 0:
403
+ max_x = device_array.shape[1] + max_x
404
+ if max_y != "end" and max_y < 0:
405
+ max_y = device_array.shape[0] + max_y
406
+
407
+ # Clamp to valid range
408
+ min_x = max(min_x, 0)
409
+ min_y = max(min_y, 0)
410
+ max_x = "end" if max_x == "end" else min(max_x, device_array.shape[1])
411
+ max_y = "end" if max_y == "end" else min(max_y, device_array.shape[0])
412
+ max_x = device_array.shape[1] if max_x == "end" else max_x
413
+ max_y = device_array.shape[0] if max_y == "end" else max_y
414
+
415
+ if min_x >= max_x or min_y >= max_y:
416
+ raise ValueError(
417
+ "Invalid bounds: min values must be less than max values. "
418
+ + f"Got min_x={min_x}, max_x={max_x}, min_y={min_y}, max_y={max_y}"
419
+ )
420
+
421
+ device_array = device_array[
422
+ device_array.shape[0] - max_y : device_array.shape[0] - min_y,
423
+ min_x:max_x,
424
+ ]
425
+
426
+ _ = cv2.imwrite(img_path, (255 * device_array).astype(np.uint8))
467
427
  print(f"Saved Device image to '{img_path}'")
468
428
 
469
429
  def to_gds(
@@ -473,7 +433,7 @@ class Device(BaseModel):
473
433
  gds_layer: tuple[int, int] = (1, 0),
474
434
  contour_approx_mode: int = 2,
475
435
  origin: tuple[float, float] = (0.0, 0.0),
476
- ):
436
+ ) -> None:
477
437
  """
478
438
  Exports the device geometry as a GDSII file.
479
439
 
@@ -507,7 +467,7 @@ class Device(BaseModel):
507
467
  )
508
468
  print(f"Saving GDS to '{gds_path}'...")
509
469
  gdstk_library = gdstk.Library()
510
- gdstk_library.add(gdstk_cell)
470
+ _ = gdstk_library.add(gdstk_cell)
511
471
  gdstk_library.write_gds(outfile=gds_path, max_points=8190)
512
472
 
513
473
  def to_gdstk(
@@ -516,7 +476,7 @@ class Device(BaseModel):
516
476
  gds_layer: tuple[int, int] = (1, 0),
517
477
  contour_approx_mode: int = 2,
518
478
  origin: tuple[float, float] = (0.0, 0.0),
519
- ):
479
+ ) -> gdstk.Cell:
520
480
  """
521
481
  Converts the device geometry to a GDSTK cell object.
522
482
 
@@ -628,85 +588,11 @@ class Device(BaseModel):
628
588
  datatype=gds_layer[1],
629
589
  )
630
590
  for polygon in processed_polygons:
631
- cell.add(polygon)
591
+ _ = cell.add(polygon)
632
592
 
633
593
  return cell
634
594
 
635
- def to_gdsfactory(self) -> "gf.Component":
636
- """
637
- Convert the device geometry to a gdsfactory Component.
638
-
639
- Returns
640
- -------
641
- gf.Component
642
- A gdsfactory Component object representing the device geometry.
643
-
644
- Raises
645
- ------
646
- ImportError
647
- If the gdsfactory package is not installed.
648
- """
649
- try:
650
- import gdsfactory as gf
651
- except ImportError:
652
- raise ImportError(
653
- "The gdsfactory package is required to use this function; "
654
- "try `pip install gdsfactory`."
655
- ) from None
656
-
657
- device_array = np.rot90(self.to_ndarray(), k=-1)
658
- return gf.read.from_np(device_array, nm_per_pixel=1)
659
-
660
- def to_tidy3d(
661
- self,
662
- eps0: float,
663
- thickness: float,
664
- ) -> "td.Structure":
665
- """
666
- Convert the device geometry to a Tidy3D Structure.
667
-
668
- Parameters
669
- ----------
670
- eps0 : float
671
- The permittivity value to assign to the device array.
672
- thickness : float
673
- The thickness of the device in the z-direction.
674
-
675
- Returns
676
- -------
677
- td.Structure
678
- A Tidy3D Structure object representing the device geometry.
679
-
680
- Raises
681
- ------
682
- ImportError
683
- If the tidy3d package is not installed.
684
- """
685
- try:
686
- from tidy3d import Box, CustomMedium, SpatialDataArray, Structure, inf
687
- except ImportError:
688
- raise ImportError(
689
- "The tidy3d package is required to use this function; "
690
- "try `pip install tidy3d`."
691
- ) from None
692
-
693
- X = np.linspace(-self.shape[1] / 2000, self.shape[1] / 2000, self.shape[1])
694
- Y = np.linspace(-self.shape[0] / 2000, self.shape[0] / 2000, self.shape[0])
695
- Z = np.array([0])
696
-
697
- device_array = np.rot90(np.fliplr(self.device_array), k=1)
698
- eps_array = np.where(device_array >= 1.0, eps0, device_array)
699
- eps_array = np.where(eps_array < 1.0, 1.0, eps_array)
700
- eps_dataset = SpatialDataArray(eps_array, coords=dict(x=X, y=Y, z=Z))
701
- medium = CustomMedium.from_eps_raw(eps_dataset)
702
- return Structure(
703
- geometry=Box(center=(0, 0, 0), size=(inf, inf, thickness), attrs={}),
704
- medium=medium,
705
- name="device",
706
- attrs={},
707
- )
708
-
709
- def to_3d(self, thickness_nm: int) -> np.ndarray:
595
+ def to_3d(self, thickness_nm: int) -> npt.NDArray[Any]:
710
596
  """
711
597
  Convert the 2D device geometry into a 3D representation.
712
598
 
@@ -740,62 +626,33 @@ class Device(BaseModel):
740
626
  layered_array[:, :, i] = dt_interp >= 0
741
627
  return layered_array
742
628
 
743
- def to_stl(self, thickness_nm: int, filename: str = "prefab_device.stl"):
744
- """
745
- Export the device geometry as an STL file.
746
-
747
- Parameters
748
- ----------
749
- thickness_nm : int
750
- The thickness of the 3D representation in nanometers.
751
- filename : str
752
- The name of the STL file to save. Defaults to "prefab_device.stl".
753
-
754
- Raises
755
- ------
756
- ValueError
757
- If the thickness is not a positive integer.
758
- ImportError
759
- If the numpy-stl package is not installed.
760
- """
761
- try:
762
- from stl import mesh # type: ignore
763
- except ImportError:
764
- raise ImportError(
765
- "The stl package is required to use this function; "
766
- "try `pip install numpy-stl`."
767
- ) from None
768
-
769
- if thickness_nm <= 0:
770
- raise ValueError("Thickness must be a positive integer.")
771
-
772
- layered_array = self.to_3d(thickness_nm)
773
- layered_array = np.pad(
774
- layered_array, ((0, 0), (0, 0), (10, 10)), mode="constant"
775
- )
776
- verts, faces, _, _ = measure.marching_cubes(layered_array, level=0.5)
777
- cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
778
- for i, f in enumerate(faces):
779
- for j in range(3):
780
- cube.vectors[i][j] = verts[f[j], :]
781
- cube.save(filename)
782
- print(f"Saved Device to '{filename}'")
783
-
784
629
  def _plot_base(
785
630
  self,
786
- plot_array: np.ndarray,
631
+ plot_array: npt.NDArray[Any],
787
632
  show_buffer: bool,
788
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]],
789
- ax: Optional[Axes],
790
- **kwargs,
633
+ bounds: tuple[tuple[int, int], tuple[int, int]] | None,
634
+ ax: Axes | None,
635
+ **kwargs: Any,
791
636
  ) -> tuple[plt.cm.ScalarMappable, Axes]:
792
637
  if ax is None:
793
638
  _, ax = plt.subplots()
794
- ax.set_ylabel("y (nm)")
795
- ax.set_xlabel("x (nm)")
639
+ _ = ax.set_ylabel("y (nm)")
640
+ _ = ax.set_xlabel("x (nm)")
796
641
 
797
642
  min_x, min_y = (0, 0) if bounds is None else bounds[0]
798
643
  max_x, max_y = plot_array.shape[::-1] if bounds is None else bounds[1]
644
+
645
+ # Handle negative indices (count from end)
646
+ if min_x < 0:
647
+ min_x = plot_array.shape[1] + min_x
648
+ if min_y < 0:
649
+ min_y = plot_array.shape[0] + min_y
650
+ if max_x != "end" and max_x < 0:
651
+ max_x = plot_array.shape[1] + max_x
652
+ if max_y != "end" and max_y < 0:
653
+ max_y = plot_array.shape[0] + max_y
654
+
655
+ # Clamp to valid range
799
656
  min_x = max(min_x, 0)
800
657
  min_y = max(min_y, 0)
801
658
  max_x = "end" if max_x == "end" else min(max_x, plot_array.shape[1])
@@ -848,10 +705,10 @@ class Device(BaseModel):
848
705
  def plot(
849
706
  self,
850
707
  show_buffer: bool = True,
851
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
852
- level: Optional[int] = None,
853
- ax: Optional[Axes] = None,
854
- **kwargs,
708
+ bounds: tuple[tuple[int, int], tuple[int, int]] | None = None,
709
+ level: int | None = None,
710
+ ax: Axes | None = None,
711
+ **kwargs: Any,
855
712
  ) -> Axes:
856
713
  """
857
714
  Visualizes the device geometry.
@@ -867,9 +724,10 @@ class Device(BaseModel):
867
724
  If True, visualizes the buffer zones around the device. Defaults to True.
868
725
  bounds : Optional[tuple[tuple[int, int], tuple[int, int]]], optional
869
726
  Specifies the bounds for zooming into the device geometry, formatted as
870
- ((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
871
- will be replaced with the corresponding dimension size of the device array.
872
- If None, the entire device geometry is visualized.
727
+ ((min_x, min_y), (max_x, max_y)). Negative values count from the end
728
+ (e.g., -50 means 50 pixels from the edge). If 'max_x' or 'max_y' is set
729
+ to "end", it will be replaced with the corresponding dimension size of the
730
+ device array. If None, the entire device geometry is visualized.
873
731
  level : int
874
732
  The vertical layer to plot. If None, the device geometry is flattened.
875
733
  Defaults to None.
@@ -900,14 +758,14 @@ class Device(BaseModel):
900
758
 
901
759
  def plot_contour(
902
760
  self,
903
- linewidth: Optional[int] = None,
904
- # label: Optional[str] = "Device contour",
761
+ linewidth: int | None = None,
762
+ # label: str | None = "Device contour",
905
763
  show_buffer: bool = True,
906
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
907
- level: Optional[int] = None,
908
- ax: Optional[Axes] = None,
909
- **kwargs,
910
- ):
764
+ bounds: tuple[tuple[int, int], tuple[int, int]] | None = None,
765
+ level: int | None = None,
766
+ ax: Axes | None = None,
767
+ **kwargs: Any,
768
+ ) -> Axes:
911
769
  """
912
770
  Visualizes the contour of the device geometry.
913
771
 
@@ -926,9 +784,10 @@ class Device(BaseModel):
926
784
  it is set to True.
927
785
  bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
928
786
  Specifies the bounds for zooming into the device geometry, formatted as
929
- ((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
930
- will be replaced with the corresponding dimension size of the device array.
931
- If None, the entire device geometry is visualized.
787
+ ((min_x, min_y), (max_x, max_y)). Negative values count from the end
788
+ (e.g., -50 means 50 pixels from the edge). If 'max_x' or 'max_y' is set
789
+ to "end", it will be replaced with the corresponding dimension size of the
790
+ device array. If None, the entire device geometry is visualized.
932
791
  level : int
933
792
  The vertical layer to plot. If None, the device geometry is flattened.
934
793
  Defaults to None.
@@ -950,8 +809,9 @@ class Device(BaseModel):
950
809
  device_array = self.device_array[:, :, level]
951
810
 
952
811
  kwargs.setdefault("cmap", "spring")
953
- if linewidth is None:
954
- linewidth = device_array.shape[0] // 100
812
+ linewidth_value = (
813
+ linewidth if linewidth is not None else device_array.shape[0] // 100
814
+ )
955
815
 
956
816
  contours, _ = cv2.findContours(
957
817
  geometry.binarize_hard(device_array).astype(np.uint8),
@@ -959,7 +819,7 @@ class Device(BaseModel):
959
819
  cv2.CHAIN_APPROX_SIMPLE,
960
820
  )
961
821
  contour_array = np.zeros_like(device_array, dtype=np.uint8)
962
- cv2.drawContours(contour_array, contours, -1, (255,), linewidth)
822
+ _ = cv2.drawContours(contour_array, contours, -1, (255,), linewidth_value)
963
823
  contour_array = np.ma.masked_equal(contour_array, 0)
964
824
 
965
825
  _, ax = self._plot_base(
@@ -977,11 +837,11 @@ class Device(BaseModel):
977
837
  def plot_uncertainty(
978
838
  self,
979
839
  show_buffer: bool = True,
980
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
981
- level: Optional[int] = None,
982
- ax: Optional[Axes] = None,
983
- **kwargs,
984
- ):
840
+ bounds: tuple[tuple[int, int], tuple[int, int]] | None = None,
841
+ level: int | None = None,
842
+ ax: Axes | None = None,
843
+ **kwargs: Any,
844
+ ) -> Axes:
985
845
  """
986
846
  Visualizes the uncertainty in the edge positions of the predicted device.
987
847
 
@@ -999,9 +859,10 @@ class Device(BaseModel):
999
859
  default, it is set to True.
1000
860
  bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
1001
861
  Specifies the bounds for zooming into the device geometry, formatted as
1002
- ((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
1003
- will be replaced with the corresponding dimension size of the device array.
1004
- If None, the entire device geometry is visualized.
862
+ ((min_x, min_y), (max_x, max_y)). Negative values count from the end
863
+ (e.g., -50 means 50 pixels from the edge). If 'max_x' or 'max_y' is set
864
+ to "end", it will be replaced with the corresponding dimension size of the
865
+ device array. If None, the entire device geometry is visualized.
1005
866
  level : int
1006
867
  The vertical layer to plot. If None, the device geometry is flattened.
1007
868
  Defaults to None.
@@ -1040,10 +901,10 @@ class Device(BaseModel):
1040
901
  self,
1041
902
  ref_device: "Device",
1042
903
  show_buffer: bool = True,
1043
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
1044
- level: Optional[int] = None,
1045
- ax: Optional[Axes] = None,
1046
- **kwargs,
904
+ bounds: tuple[tuple[int, int], tuple[int, int]] | None = None,
905
+ level: int | None = None,
906
+ ax: Axes | None = None,
907
+ **kwargs: Any,
1047
908
  ) -> Axes:
1048
909
  """
1049
910
  Visualizes the comparison between the current device geometry and a reference
@@ -1061,9 +922,10 @@ class Device(BaseModel):
1061
922
  If True, visualizes the buffer zones around the device. Defaults to True.
1062
923
  bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
1063
924
  Specifies the bounds for zooming into the device geometry, formatted as
1064
- ((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
1065
- will be replaced with the corresponding dimension size of the device array.
1066
- If None, the entire device geometry is visualized.
925
+ ((min_x, min_y), (max_x, max_y)). Negative values count from the end
926
+ (e.g., -50 means 50 pixels from the edge). If 'max_x' or 'max_y' is set
927
+ to "end", it will be replaced with the corresponding dimension size of the
928
+ device array. If None, the entire device geometry is visualized.
1067
929
  level : int
1068
930
  The vertical layer to plot. If None, the device geometry is flattened.
1069
931
  Defaults to None.
@@ -1099,7 +961,7 @@ class Device(BaseModel):
1099
961
  cbar.set_label("Added (a.u.) Removed (a.u.)")
1100
962
  return ax
1101
963
 
1102
- def _add_buffer_visualization(self, ax: Axes):
964
+ def _add_buffer_visualization(self, ax: Axes) -> None:
1103
965
  plot_array = self.device_array
1104
966
 
1105
967
  buffer_thickness = self.buffer_spec.thickness
@@ -1114,7 +976,7 @@ class Device(BaseModel):
1114
976
  edgecolor="black",
1115
977
  linewidth=1,
1116
978
  )
1117
- ax.add_patch(mid_rect)
979
+ _ = ax.add_patch(mid_rect)
1118
980
 
1119
981
  top_rect = Rectangle(
1120
982
  (0, 0),
@@ -1123,7 +985,7 @@ class Device(BaseModel):
1123
985
  facecolor=buffer_fill,
1124
986
  hatch=buffer_hatch,
1125
987
  )
1126
- ax.add_patch(top_rect)
988
+ _ = ax.add_patch(top_rect)
1127
989
 
1128
990
  bottom_rect = Rectangle(
1129
991
  (0, plot_array.shape[0] - buffer_thickness["bottom"]),
@@ -1132,7 +994,7 @@ class Device(BaseModel):
1132
994
  facecolor=buffer_fill,
1133
995
  hatch=buffer_hatch,
1134
996
  )
1135
- ax.add_patch(bottom_rect)
997
+ _ = ax.add_patch(bottom_rect)
1136
998
 
1137
999
  left_rect = Rectangle(
1138
1000
  (0, buffer_thickness["top"]),
@@ -1141,7 +1003,7 @@ class Device(BaseModel):
1141
1003
  facecolor=buffer_fill,
1142
1004
  hatch=buffer_hatch,
1143
1005
  )
1144
- ax.add_patch(left_rect)
1006
+ _ = ax.add_patch(left_rect)
1145
1007
 
1146
1008
  right_rect = Rectangle(
1147
1009
  (
@@ -1153,7 +1015,7 @@ class Device(BaseModel):
1153
1015
  facecolor=buffer_fill,
1154
1016
  hatch=buffer_hatch,
1155
1017
  )
1156
- ax.add_patch(right_rect)
1018
+ _ = ax.add_patch(right_rect)
1157
1019
 
1158
1020
  def normalize(self) -> "Device":
1159
1021
  """
@@ -1214,7 +1076,7 @@ class Device(BaseModel):
1214
1076
  update={"device_array": binarized_device_array.astype(np.uint8)}
1215
1077
  )
1216
1078
 
1217
- def binarize_monte_carlo(
1079
+ def binarize_with_roughness(
1218
1080
  self,
1219
1081
  noise_magnitude: float = 2.0,
1220
1082
  blur_radius: float = 8.0,
@@ -1250,7 +1112,7 @@ class Device(BaseModel):
1250
1112
  Device
1251
1113
  A new instance of the Device with the binarized geometry.
1252
1114
  """
1253
- binarized_device_array = geometry.binarize_monte_carlo(
1115
+ binarized_device_array = geometry.binarize_with_roughness(
1254
1116
  device_array=self.device_array,
1255
1117
  noise_magnitude=noise_magnitude,
1256
1118
  blur_radius=blur_radius,
@@ -1394,7 +1256,7 @@ class Device(BaseModel):
1394
1256
  flattened_device_array = geometry.flatten(device_array=self.device_array)
1395
1257
  return self.model_copy(update={"device_array": flattened_device_array})
1396
1258
 
1397
- def get_uncertainty(self) -> np.ndarray:
1259
+ def get_uncertainty(self) -> npt.NDArray[Any]:
1398
1260
  """
1399
1261
  Calculate the uncertainty in the edge positions of the predicted device.
1400
1262
 
@@ -1410,96 +1272,3 @@ class Device(BaseModel):
1410
1272
  with higher values indicating greater uncertainty.
1411
1273
  """
1412
1274
  return 1 - 2 * np.abs(0.5 - self.device_array)
1413
-
1414
- def enforce_feature_size(
1415
- self, min_feature_size: int, strel: str = "disk"
1416
- ) -> "Device":
1417
- """
1418
- Enforce a minimum feature size on the device geometry.
1419
-
1420
- This method applies morphological operations to ensure that all features in the
1421
- device geometry are at least the specified minimum size. It uses either a disk
1422
- or square structuring element for the operations.
1423
-
1424
- Notes
1425
- -----
1426
- This function does not guarantee that the minimum feature size is enforced in
1427
- all cases. A better process is needed.
1428
-
1429
- Parameters
1430
- ----------
1431
- min_feature_size : int
1432
- The minimum feature size to enforce, in nanometers.
1433
- strel : str
1434
- The type of structuring element to use. Can be either "disk" or "square".
1435
- Defaults to "disk".
1436
-
1437
- Returns
1438
- -------
1439
- Device
1440
- A new instance of the Device with the modified geometry.
1441
-
1442
- Raises
1443
- ------
1444
- ValueError
1445
- If an invalid structuring element type is specified.
1446
- """
1447
- modified_geometry = geometry.enforce_feature_size(
1448
- device_array=self.device_array,
1449
- min_feature_size=min_feature_size,
1450
- strel=strel,
1451
- )
1452
- return self.model_copy(update={"device_array": modified_geometry})
1453
-
1454
- def check_feature_size(self, min_feature_size: int, strel: str = "disk"):
1455
- """
1456
- Check and visualize the effect of enforcing a minimum feature size on the device
1457
- geometry.
1458
-
1459
- This method enforces a minimum feature size on the device geometry using the
1460
- specified structuring element, compares the modified geometry with the original,
1461
- and plots the differences. It also calculates and prints the Hamming distance
1462
- between the original and modified geometries, providing a measure of the changes
1463
- introduced by the feature size enforcement.
1464
-
1465
- Notes
1466
- -----
1467
- This is not a design-rule-checking function, but it can be useful for quick
1468
- checks.
1469
-
1470
- Parameters
1471
- ----------
1472
- min_feature_size : int
1473
- The minimum feature size to enforce, in nanometers.
1474
- strel : str
1475
- The type of structuring element to use. Can be either "disk" or "square".
1476
- Defaults to "disk".
1477
-
1478
- Raises
1479
- ------
1480
- ValueError
1481
- If an invalid structuring element type is specified or if min_feature_size
1482
- is not a positive integer.
1483
- """
1484
- if min_feature_size <= 0:
1485
- raise ValueError("min_feature_size must be a positive integer.")
1486
-
1487
- enforced_device = self.enforce_feature_size(min_feature_size, strel)
1488
-
1489
- difference = np.abs(
1490
- enforced_device.device_array[:, :, 0] - self.device_array[:, :, 0]
1491
- )
1492
- _, ax = self._plot_base(
1493
- plot_array=difference,
1494
- show_buffer=False,
1495
- ax=None,
1496
- bounds=None,
1497
- cmap="jet",
1498
- )
1499
-
1500
- hamming_distance = compare.hamming_distance(self, enforced_device)
1501
- print(
1502
- f"Feature size check with minimum size {min_feature_size} "
1503
- f"using '{strel}' structuring element resulted in a Hamming "
1504
- f"distance of: {hamming_distance}"
1505
- )