prefab 1.2.0__py3-none-any.whl → 1.4.0__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.")
@@ -242,7 +250,6 @@ class Device(BaseModel):
242
250
  self,
243
251
  model: Model,
244
252
  binarize: bool = False,
245
- gpu: bool = False,
246
253
  ) -> "Device":
247
254
  """
248
255
  Predict the nanofabrication outcome of the device using a specified model.
@@ -255,19 +262,14 @@ class Device(BaseModel):
255
262
  ----------
256
263
  model : Model
257
264
  The model to use for prediction, representing a specific fabrication process
258
- and dataset. This model encapsulates details about the fabrication foundry,
259
- process, material, technology, thickness, and sidewall presence, as defined
260
- in `models.py`. Each model is associated with a version and dataset that
261
- detail its creation and the data it was trained on, ensuring the prediction
262
- 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.
263
269
  binarize : bool
264
270
  If True, the predicted device geometry will be binarized using a threshold
265
271
  method. This is useful for converting probabilistic predictions into binary
266
272
  geometries. Defaults to False.
267
- gpu : bool
268
- If True, the prediction will be performed on a GPU. Defaults to False.
269
- Note: The GPU option has more overhead and will take longer for small
270
- devices, but will be faster for larger devices.
271
273
 
272
274
  Returns
273
275
  -------
@@ -285,7 +287,6 @@ class Device(BaseModel):
285
287
  model=model,
286
288
  model_type="p",
287
289
  binarize=binarize,
288
- gpu=gpu,
289
290
  )
290
291
  return self.model_copy(update={"device_array": prediction_array})
291
292
 
@@ -293,7 +294,6 @@ class Device(BaseModel):
293
294
  self,
294
295
  model: Model,
295
296
  binarize: bool = True,
296
- gpu: bool = False,
297
297
  ) -> "Device":
298
298
  """
299
299
  Correct the nanofabrication outcome of the device using a specified model.
@@ -308,19 +308,14 @@ class Device(BaseModel):
308
308
  ----------
309
309
  model : Model
310
310
  The model to use for correction, representing a specific fabrication process
311
- and dataset. This model encapsulates details about the fabrication foundry,
312
- process, material, technology, thickness, and sidewall presence, as defined
313
- in `models.py`. Each model is associated with a version and dataset that
314
- detail its creation and the data it was trained on, ensuring the correction
315
- 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.
316
315
  binarize : bool
317
316
  If True, the corrected device geometry will be binarized using a threshold
318
317
  method. This is useful for converting probabilistic corrections into binary
319
318
  geometries. Defaults to True.
320
- gpu : bool
321
- If True, the prediction will be performed on a GPU. Defaults to False.
322
- Note: The GPU option has more overhead and will take longer for small
323
- devices, but will be faster for larger devices.
324
319
 
325
320
  Returns
326
321
  -------
@@ -338,114 +333,10 @@ class Device(BaseModel):
338
333
  model=model,
339
334
  model_type="c",
340
335
  binarize=binarize,
341
- gpu=gpu,
342
336
  )
343
337
  return self.model_copy(update={"device_array": correction_array})
344
338
 
345
- def semulate(
346
- self,
347
- model: Model,
348
- gpu: bool = False,
349
- ) -> "Device":
350
- """
351
- Simulate the appearance of the device as if viewed under a scanning electron
352
- microscope (SEM).
353
-
354
- This method applies a specified machine learning model to transform the device
355
- geometry into a style that resembles an SEM image. This can be useful for
356
- visualizing how the device might appear under an SEM, which is often used for
357
- inspecting the surface and composition of materials at high magnification.
358
-
359
- Parameters
360
- ----------
361
- model : Model
362
- The model to use for SEMulation, representing a specific fabrication process
363
- and dataset. This model encapsulates details about the fabrication foundry,
364
- process, material, technology, thickness, and sidewall presence, as defined
365
- in `models.py`. Each model is associated with a version and dataset that
366
- detail its creation and the data it was trained on, ensuring the SEMulation
367
- is tailored to specific fabrication parameters.
368
- gpu : bool
369
- If True, the prediction will be performed on a GPU. Defaults to False.
370
- Note: The GPU option has more overhead and will take longer for small
371
- devices, but will be faster for larger devices.
372
-
373
- Notes
374
- -----
375
- The salt-and-pepper noise is added manually until the model is trained to
376
- generate this noise (not a big priority).
377
-
378
- Returns
379
- -------
380
- Device
381
- A new instance of the Device class with its geometry transformed to simulate
382
- an SEM image style.
383
-
384
- Raises
385
- ------
386
- RuntimeError
387
- If the prediction service returns an error or if the response from the
388
- service cannot be processed correctly.
389
- """
390
- semulated_array = predict_array(
391
- device_array=self.device_array,
392
- model=model,
393
- model_type="s",
394
- binarize=False,
395
- gpu=gpu,
396
- )
397
- semulated_array += np.random.normal(0, 0.03, semulated_array.shape)
398
- return self.model_copy(update={"device_array": semulated_array})
399
-
400
- def segment(
401
- self,
402
- model: Model,
403
- gpu: bool = False,
404
- ) -> "Device":
405
- """
406
- Segment a scanning electron microscope (SEM) image into a binary mask.
407
-
408
- This method applies a specified machine learning model to transform a grayscale
409
- SEM image into a binary mask, where 1 represents the device structure and 0
410
- represents the background. This is useful for extracting the device geometry
411
- from experimental SEM images for analysis or comparison with design intent.
412
-
413
- Parameters
414
- ----------
415
- model : Model
416
- The model to use for segmentation, representing a specific fabrication
417
- process and dataset. This model encapsulates details about the fabrication
418
- foundry, process, material, technology, thickness, and sidewall presence, as
419
- defined in `models.py`. Each model is associated with a version and dataset
420
- that detail its creation and the data it was trained on, ensuring the
421
- segmentation is tailored to specific fabrication parameters.
422
- gpu : bool
423
- If True, the prediction will be performed on a GPU. Defaults to False.
424
- Note: The GPU option has more overhead and will take longer for small
425
- devices, but will be faster for larger devices.
426
-
427
- Returns
428
- -------
429
- Device
430
- A new instance of the Device class with its geometry transformed into a
431
- binary mask.
432
-
433
- Raises
434
- ------
435
- RuntimeError
436
- If the prediction service returns an error or if the response from the
437
- service cannot be processed correctly.
438
- """
439
- segmented_array = predict_array(
440
- device_array=self.normalize().device_array,
441
- model=model,
442
- model_type="b",
443
- binarize=False,
444
- gpu=gpu,
445
- )
446
- return self.model_copy(update={"device_array": segmented_array})
447
-
448
- def to_ndarray(self) -> np.ndarray:
339
+ def to_ndarray(self) -> npt.NDArray[Any]:
449
340
  """
450
341
  Converts the device geometry to an ndarray.
451
342
 
@@ -473,7 +364,11 @@ class Device(BaseModel):
473
364
  ]
474
365
  return ndarray
475
366
 
476
- 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:
477
372
  """
478
373
  Exports the device geometry as an image file.
479
374
 
@@ -486,8 +381,49 @@ class Device(BaseModel):
486
381
  img_path : str
487
382
  The path where the image file will be saved. If not specified, the image is
488
383
  saved as "prefab_device.png" in the current directory.
489
- """
490
- 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))
491
427
  print(f"Saved Device image to '{img_path}'")
492
428
 
493
429
  def to_gds(
@@ -497,7 +433,7 @@ class Device(BaseModel):
497
433
  gds_layer: tuple[int, int] = (1, 0),
498
434
  contour_approx_mode: int = 2,
499
435
  origin: tuple[float, float] = (0.0, 0.0),
500
- ):
436
+ ) -> None:
501
437
  """
502
438
  Exports the device geometry as a GDSII file.
503
439
 
@@ -531,7 +467,7 @@ class Device(BaseModel):
531
467
  )
532
468
  print(f"Saving GDS to '{gds_path}'...")
533
469
  gdstk_library = gdstk.Library()
534
- gdstk_library.add(gdstk_cell)
470
+ _ = gdstk_library.add(gdstk_cell)
535
471
  gdstk_library.write_gds(outfile=gds_path, max_points=8190)
536
472
 
537
473
  def to_gdstk(
@@ -540,7 +476,7 @@ class Device(BaseModel):
540
476
  gds_layer: tuple[int, int] = (1, 0),
541
477
  contour_approx_mode: int = 2,
542
478
  origin: tuple[float, float] = (0.0, 0.0),
543
- ):
479
+ ) -> gdstk.Cell:
544
480
  """
545
481
  Converts the device geometry to a GDSTK cell object.
546
482
 
@@ -652,85 +588,11 @@ class Device(BaseModel):
652
588
  datatype=gds_layer[1],
653
589
  )
654
590
  for polygon in processed_polygons:
655
- cell.add(polygon)
591
+ _ = cell.add(polygon)
656
592
 
657
593
  return cell
658
594
 
659
- def to_gdsfactory(self) -> "gf.Component":
660
- """
661
- Convert the device geometry to a gdsfactory Component.
662
-
663
- Returns
664
- -------
665
- gf.Component
666
- A gdsfactory Component object representing the device geometry.
667
-
668
- Raises
669
- ------
670
- ImportError
671
- If the gdsfactory package is not installed.
672
- """
673
- try:
674
- import gdsfactory as gf
675
- except ImportError:
676
- raise ImportError(
677
- "The gdsfactory package is required to use this function; "
678
- "try `pip install gdsfactory`."
679
- ) from None
680
-
681
- device_array = np.rot90(self.to_ndarray(), k=-1)
682
- return gf.read.from_np(device_array, nm_per_pixel=1)
683
-
684
- def to_tidy3d(
685
- self,
686
- eps0: float,
687
- thickness: float,
688
- ) -> "td.Structure":
689
- """
690
- Convert the device geometry to a Tidy3D Structure.
691
-
692
- Parameters
693
- ----------
694
- eps0 : float
695
- The permittivity value to assign to the device array.
696
- thickness : float
697
- The thickness of the device in the z-direction.
698
-
699
- Returns
700
- -------
701
- td.Structure
702
- A Tidy3D Structure object representing the device geometry.
703
-
704
- Raises
705
- ------
706
- ImportError
707
- If the tidy3d package is not installed.
708
- """
709
- try:
710
- from tidy3d import Box, CustomMedium, SpatialDataArray, Structure, inf
711
- except ImportError:
712
- raise ImportError(
713
- "The tidy3d package is required to use this function; "
714
- "try `pip install tidy3d`."
715
- ) from None
716
-
717
- X = np.linspace(-self.shape[1] / 2000, self.shape[1] / 2000, self.shape[1])
718
- Y = np.linspace(-self.shape[0] / 2000, self.shape[0] / 2000, self.shape[0])
719
- Z = np.array([0])
720
-
721
- device_array = np.rot90(np.fliplr(self.device_array), k=1)
722
- eps_array = np.where(device_array >= 1.0, eps0, device_array)
723
- eps_array = np.where(eps_array < 1.0, 1.0, eps_array)
724
- eps_dataset = SpatialDataArray(eps_array, coords=dict(x=X, y=Y, z=Z))
725
- medium = CustomMedium.from_eps_raw(eps_dataset)
726
- return Structure(
727
- geometry=Box(center=(0, 0, 0), size=(inf, inf, thickness), attrs={}),
728
- medium=medium,
729
- name="device",
730
- attrs={},
731
- )
732
-
733
- def to_3d(self, thickness_nm: int) -> np.ndarray:
595
+ def to_3d(self, thickness_nm: int) -> npt.NDArray[Any]:
734
596
  """
735
597
  Convert the 2D device geometry into a 3D representation.
736
598
 
@@ -764,62 +626,33 @@ class Device(BaseModel):
764
626
  layered_array[:, :, i] = dt_interp >= 0
765
627
  return layered_array
766
628
 
767
- def to_stl(self, thickness_nm: int, filename: str = "prefab_device.stl"):
768
- """
769
- Export the device geometry as an STL file.
770
-
771
- Parameters
772
- ----------
773
- thickness_nm : int
774
- The thickness of the 3D representation in nanometers.
775
- filename : str
776
- The name of the STL file to save. Defaults to "prefab_device.stl".
777
-
778
- Raises
779
- ------
780
- ValueError
781
- If the thickness is not a positive integer.
782
- ImportError
783
- If the numpy-stl package is not installed.
784
- """
785
- try:
786
- from stl import mesh # type: ignore
787
- except ImportError:
788
- raise ImportError(
789
- "The stl package is required to use this function; "
790
- "try `pip install numpy-stl`."
791
- ) from None
792
-
793
- if thickness_nm <= 0:
794
- raise ValueError("Thickness must be a positive integer.")
795
-
796
- layered_array = self.to_3d(thickness_nm)
797
- layered_array = np.pad(
798
- layered_array, ((0, 0), (0, 0), (10, 10)), mode="constant"
799
- )
800
- verts, faces, _, _ = measure.marching_cubes(layered_array, level=0.5)
801
- cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
802
- for i, f in enumerate(faces):
803
- for j in range(3):
804
- cube.vectors[i][j] = verts[f[j], :]
805
- cube.save(filename)
806
- print(f"Saved Device to '{filename}'")
807
-
808
629
  def _plot_base(
809
630
  self,
810
- plot_array: np.ndarray,
631
+ plot_array: npt.NDArray[Any],
811
632
  show_buffer: bool,
812
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]],
813
- ax: Optional[Axes],
814
- **kwargs,
633
+ bounds: tuple[tuple[int, int], tuple[int, int]] | None,
634
+ ax: Axes | None,
635
+ **kwargs: Any,
815
636
  ) -> tuple[plt.cm.ScalarMappable, Axes]:
816
637
  if ax is None:
817
638
  _, ax = plt.subplots()
818
- ax.set_ylabel("y (nm)")
819
- ax.set_xlabel("x (nm)")
639
+ _ = ax.set_ylabel("y (nm)")
640
+ _ = ax.set_xlabel("x (nm)")
820
641
 
821
642
  min_x, min_y = (0, 0) if bounds is None else bounds[0]
822
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
823
656
  min_x = max(min_x, 0)
824
657
  min_y = max(min_y, 0)
825
658
  max_x = "end" if max_x == "end" else min(max_x, plot_array.shape[1])
@@ -872,10 +705,10 @@ class Device(BaseModel):
872
705
  def plot(
873
706
  self,
874
707
  show_buffer: bool = True,
875
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
876
- level: Optional[int] = None,
877
- ax: Optional[Axes] = None,
878
- **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,
879
712
  ) -> Axes:
880
713
  """
881
714
  Visualizes the device geometry.
@@ -891,9 +724,10 @@ class Device(BaseModel):
891
724
  If True, visualizes the buffer zones around the device. Defaults to True.
892
725
  bounds : Optional[tuple[tuple[int, int], tuple[int, int]]], optional
893
726
  Specifies the bounds for zooming into the device geometry, formatted as
894
- ((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
895
- will be replaced with the corresponding dimension size of the device array.
896
- 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.
897
731
  level : int
898
732
  The vertical layer to plot. If None, the device geometry is flattened.
899
733
  Defaults to None.
@@ -924,14 +758,14 @@ class Device(BaseModel):
924
758
 
925
759
  def plot_contour(
926
760
  self,
927
- linewidth: Optional[int] = None,
928
- # label: Optional[str] = "Device contour",
761
+ linewidth: int | None = None,
762
+ # label: str | None = "Device contour",
929
763
  show_buffer: bool = True,
930
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
931
- level: Optional[int] = None,
932
- ax: Optional[Axes] = None,
933
- **kwargs,
934
- ):
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:
935
769
  """
936
770
  Visualizes the contour of the device geometry.
937
771
 
@@ -950,9 +784,10 @@ class Device(BaseModel):
950
784
  it is set to True.
951
785
  bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
952
786
  Specifies the bounds for zooming into the device geometry, formatted as
953
- ((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
954
- will be replaced with the corresponding dimension size of the device array.
955
- 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.
956
791
  level : int
957
792
  The vertical layer to plot. If None, the device geometry is flattened.
958
793
  Defaults to None.
@@ -974,8 +809,9 @@ class Device(BaseModel):
974
809
  device_array = self.device_array[:, :, level]
975
810
 
976
811
  kwargs.setdefault("cmap", "spring")
977
- if linewidth is None:
978
- linewidth = device_array.shape[0] // 100
812
+ linewidth_value = (
813
+ linewidth if linewidth is not None else device_array.shape[0] // 100
814
+ )
979
815
 
980
816
  contours, _ = cv2.findContours(
981
817
  geometry.binarize_hard(device_array).astype(np.uint8),
@@ -983,7 +819,7 @@ class Device(BaseModel):
983
819
  cv2.CHAIN_APPROX_SIMPLE,
984
820
  )
985
821
  contour_array = np.zeros_like(device_array, dtype=np.uint8)
986
- cv2.drawContours(contour_array, contours, -1, (255,), linewidth)
822
+ _ = cv2.drawContours(contour_array, contours, -1, (255,), linewidth_value)
987
823
  contour_array = np.ma.masked_equal(contour_array, 0)
988
824
 
989
825
  _, ax = self._plot_base(
@@ -1001,11 +837,11 @@ class Device(BaseModel):
1001
837
  def plot_uncertainty(
1002
838
  self,
1003
839
  show_buffer: bool = True,
1004
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
1005
- level: Optional[int] = None,
1006
- ax: Optional[Axes] = None,
1007
- **kwargs,
1008
- ):
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:
1009
845
  """
1010
846
  Visualizes the uncertainty in the edge positions of the predicted device.
1011
847
 
@@ -1023,9 +859,10 @@ class Device(BaseModel):
1023
859
  default, it is set to True.
1024
860
  bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
1025
861
  Specifies the bounds for zooming into the device geometry, formatted as
1026
- ((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
1027
- will be replaced with the corresponding dimension size of the device array.
1028
- 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.
1029
866
  level : int
1030
867
  The vertical layer to plot. If None, the device geometry is flattened.
1031
868
  Defaults to None.
@@ -1064,10 +901,10 @@ class Device(BaseModel):
1064
901
  self,
1065
902
  ref_device: "Device",
1066
903
  show_buffer: bool = True,
1067
- bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
1068
- level: Optional[int] = None,
1069
- ax: Optional[Axes] = None,
1070
- **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,
1071
908
  ) -> Axes:
1072
909
  """
1073
910
  Visualizes the comparison between the current device geometry and a reference
@@ -1085,9 +922,10 @@ class Device(BaseModel):
1085
922
  If True, visualizes the buffer zones around the device. Defaults to True.
1086
923
  bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
1087
924
  Specifies the bounds for zooming into the device geometry, formatted as
1088
- ((min_x, min_y), (max_x, max_y)). If 'max_x' or 'max_y' is set to "end", it
1089
- will be replaced with the corresponding dimension size of the device array.
1090
- 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.
1091
929
  level : int
1092
930
  The vertical layer to plot. If None, the device geometry is flattened.
1093
931
  Defaults to None.
@@ -1123,7 +961,7 @@ class Device(BaseModel):
1123
961
  cbar.set_label("Added (a.u.) Removed (a.u.)")
1124
962
  return ax
1125
963
 
1126
- def _add_buffer_visualization(self, ax: Axes):
964
+ def _add_buffer_visualization(self, ax: Axes) -> None:
1127
965
  plot_array = self.device_array
1128
966
 
1129
967
  buffer_thickness = self.buffer_spec.thickness
@@ -1138,7 +976,7 @@ class Device(BaseModel):
1138
976
  edgecolor="black",
1139
977
  linewidth=1,
1140
978
  )
1141
- ax.add_patch(mid_rect)
979
+ _ = ax.add_patch(mid_rect)
1142
980
 
1143
981
  top_rect = Rectangle(
1144
982
  (0, 0),
@@ -1147,7 +985,7 @@ class Device(BaseModel):
1147
985
  facecolor=buffer_fill,
1148
986
  hatch=buffer_hatch,
1149
987
  )
1150
- ax.add_patch(top_rect)
988
+ _ = ax.add_patch(top_rect)
1151
989
 
1152
990
  bottom_rect = Rectangle(
1153
991
  (0, plot_array.shape[0] - buffer_thickness["bottom"]),
@@ -1156,7 +994,7 @@ class Device(BaseModel):
1156
994
  facecolor=buffer_fill,
1157
995
  hatch=buffer_hatch,
1158
996
  )
1159
- ax.add_patch(bottom_rect)
997
+ _ = ax.add_patch(bottom_rect)
1160
998
 
1161
999
  left_rect = Rectangle(
1162
1000
  (0, buffer_thickness["top"]),
@@ -1165,7 +1003,7 @@ class Device(BaseModel):
1165
1003
  facecolor=buffer_fill,
1166
1004
  hatch=buffer_hatch,
1167
1005
  )
1168
- ax.add_patch(left_rect)
1006
+ _ = ax.add_patch(left_rect)
1169
1007
 
1170
1008
  right_rect = Rectangle(
1171
1009
  (
@@ -1177,7 +1015,7 @@ class Device(BaseModel):
1177
1015
  facecolor=buffer_fill,
1178
1016
  hatch=buffer_hatch,
1179
1017
  )
1180
- ax.add_patch(right_rect)
1018
+ _ = ax.add_patch(right_rect)
1181
1019
 
1182
1020
  def normalize(self) -> "Device":
1183
1021
  """
@@ -1238,7 +1076,7 @@ class Device(BaseModel):
1238
1076
  update={"device_array": binarized_device_array.astype(np.uint8)}
1239
1077
  )
1240
1078
 
1241
- def binarize_monte_carlo(
1079
+ def binarize_with_roughness(
1242
1080
  self,
1243
1081
  noise_magnitude: float = 2.0,
1244
1082
  blur_radius: float = 8.0,
@@ -1274,7 +1112,7 @@ class Device(BaseModel):
1274
1112
  Device
1275
1113
  A new instance of the Device with the binarized geometry.
1276
1114
  """
1277
- binarized_device_array = geometry.binarize_monte_carlo(
1115
+ binarized_device_array = geometry.binarize_with_roughness(
1278
1116
  device_array=self.device_array,
1279
1117
  noise_magnitude=noise_magnitude,
1280
1118
  blur_radius=blur_radius,
@@ -1418,7 +1256,7 @@ class Device(BaseModel):
1418
1256
  flattened_device_array = geometry.flatten(device_array=self.device_array)
1419
1257
  return self.model_copy(update={"device_array": flattened_device_array})
1420
1258
 
1421
- def get_uncertainty(self) -> np.ndarray:
1259
+ def get_uncertainty(self) -> npt.NDArray[Any]:
1422
1260
  """
1423
1261
  Calculate the uncertainty in the edge positions of the predicted device.
1424
1262
 
@@ -1434,96 +1272,3 @@ class Device(BaseModel):
1434
1272
  with higher values indicating greater uncertainty.
1435
1273
  """
1436
1274
  return 1 - 2 * np.abs(0.5 - self.device_array)
1437
-
1438
- def enforce_feature_size(
1439
- self, min_feature_size: int, strel: str = "disk"
1440
- ) -> "Device":
1441
- """
1442
- Enforce a minimum feature size on the device geometry.
1443
-
1444
- This method applies morphological operations to ensure that all features in the
1445
- device geometry are at least the specified minimum size. It uses either a disk
1446
- or square structuring element for the operations.
1447
-
1448
- Notes
1449
- -----
1450
- This function does not guarantee that the minimum feature size is enforced in
1451
- all cases. A better process is needed.
1452
-
1453
- Parameters
1454
- ----------
1455
- min_feature_size : int
1456
- The minimum feature size to enforce, in nanometers.
1457
- strel : str
1458
- The type of structuring element to use. Can be either "disk" or "square".
1459
- Defaults to "disk".
1460
-
1461
- Returns
1462
- -------
1463
- Device
1464
- A new instance of the Device with the modified geometry.
1465
-
1466
- Raises
1467
- ------
1468
- ValueError
1469
- If an invalid structuring element type is specified.
1470
- """
1471
- modified_geometry = geometry.enforce_feature_size(
1472
- device_array=self.device_array,
1473
- min_feature_size=min_feature_size,
1474
- strel=strel,
1475
- )
1476
- return self.model_copy(update={"device_array": modified_geometry})
1477
-
1478
- def check_feature_size(self, min_feature_size: int, strel: str = "disk"):
1479
- """
1480
- Check and visualize the effect of enforcing a minimum feature size on the device
1481
- geometry.
1482
-
1483
- This method enforces a minimum feature size on the device geometry using the
1484
- specified structuring element, compares the modified geometry with the original,
1485
- and plots the differences. It also calculates and prints the Hamming distance
1486
- between the original and modified geometries, providing a measure of the changes
1487
- introduced by the feature size enforcement.
1488
-
1489
- Notes
1490
- -----
1491
- This is not a design-rule-checking function, but it can be useful for quick
1492
- checks.
1493
-
1494
- Parameters
1495
- ----------
1496
- min_feature_size : int
1497
- The minimum feature size to enforce, in nanometers.
1498
- strel : str
1499
- The type of structuring element to use. Can be either "disk" or "square".
1500
- Defaults to "disk".
1501
-
1502
- Raises
1503
- ------
1504
- ValueError
1505
- If an invalid structuring element type is specified or if min_feature_size
1506
- is not a positive integer.
1507
- """
1508
- if min_feature_size <= 0:
1509
- raise ValueError("min_feature_size must be a positive integer.")
1510
-
1511
- enforced_device = self.enforce_feature_size(min_feature_size, strel)
1512
-
1513
- difference = np.abs(
1514
- enforced_device.device_array[:, :, 0] - self.device_array[:, :, 0]
1515
- )
1516
- _, ax = self._plot_base(
1517
- plot_array=difference,
1518
- show_buffer=False,
1519
- ax=None,
1520
- bounds=None,
1521
- cmap="jet",
1522
- )
1523
-
1524
- hamming_distance = compare.hamming_distance(self, enforced_device)
1525
- print(
1526
- f"Feature size check with minimum size {min_feature_size} "
1527
- f"using '{strel}' structuring element resulted in a Hamming "
1528
- f"distance of: {hamming_distance}"
1529
- )