prefab 1.0.4__py3-none-any.whl → 1.1.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/__init__.py CHANGED
@@ -5,7 +5,7 @@ Usage:
5
5
  import prefab as pf
6
6
  """
7
7
 
8
- __version__ = "1.0.4"
8
+ __version__ = "1.1.0"
9
9
 
10
10
  from . import compare, geometry, read, shapes
11
11
  from .device import BufferSpec, Device
prefab/device.py CHANGED
@@ -16,9 +16,12 @@ from matplotlib.axes import Axes
16
16
  from matplotlib.patches import Rectangle
17
17
  from PIL import Image
18
18
  from pydantic import BaseModel, Field, conint, root_validator, validator
19
+ from scipy.ndimage import distance_transform_edt
20
+ from skimage import measure
21
+ from skimage.morphology import closing, disk, opening, square
19
22
  from tqdm import tqdm
20
23
 
21
- from . import geometry
24
+ from . import compare, geometry
22
25
  from .models import Model
23
26
 
24
27
  Image.MAX_IMAGE_PIXELS = None
@@ -670,6 +673,149 @@ class Device(BaseModel):
670
673
 
671
674
  return cell
672
675
 
676
+ def to_gdsfactory(self) -> "gf.Component": # noqa: F821
677
+ """
678
+ Convert the device geometry to a gdsfactory Component.
679
+
680
+ Returns
681
+ -------
682
+ gf.Component
683
+ A gdsfactory Component object representing the device geometry.
684
+
685
+ Raises
686
+ ------
687
+ ImportError
688
+ If the gdsfactory package is not installed.
689
+ """
690
+ try:
691
+ import gdsfactory as gf
692
+ except ImportError:
693
+ raise ImportError(
694
+ "The gdsfactory package is required to use this function; "
695
+ "try `pip install gdsfactory`."
696
+ ) from None
697
+
698
+ device_array = np.rot90(self.device_array, k=-1)
699
+ return gf.read.from_np(device_array[:, :, 0], nm_per_pixel=1)
700
+
701
+ def to_tidy3d(
702
+ self,
703
+ eps0: float,
704
+ thickness: float,
705
+ ) -> "td.Structure": # noqa: F821
706
+ """
707
+ Convert the device geometry to a Tidy3D Structure.
708
+
709
+ Parameters
710
+ ----------
711
+ eps0 : float
712
+ The permittivity value to assign to the device array.
713
+ thickness : float
714
+ The thickness of the device in the z-direction.
715
+
716
+ Returns
717
+ -------
718
+ td.Structure
719
+ A Tidy3D Structure object representing the device geometry.
720
+
721
+ Raises
722
+ ------
723
+ ImportError
724
+ If the tidy3d package is not installed.
725
+ """
726
+ try:
727
+ from tidy3d import Box, CustomMedium, SpatialDataArray, Structure, inf
728
+ except ImportError:
729
+ raise ImportError(
730
+ "The tidy3d package is required to use this function; "
731
+ "try `pip install tidy3d`."
732
+ ) from None
733
+
734
+ X = np.linspace(-self.shape[1] / 2000, self.shape[1] / 2000, self.shape[1])
735
+ Y = np.linspace(-self.shape[0] / 2000, self.shape[0] / 2000, self.shape[0])
736
+ Z = np.array([0])
737
+
738
+ device_array = np.rot90(np.fliplr(self.device_array), k=1)
739
+ eps_array = np.where(device_array >= 1.0, eps0, device_array)
740
+ eps_array = np.where(eps_array < 1.0, 1.0, eps_array)
741
+ eps_dataset = SpatialDataArray(eps_array, coords=dict(x=X, y=Y, z=Z))
742
+ medium = CustomMedium.from_eps_raw(eps_dataset)
743
+ return Structure(
744
+ geometry=Box(center=(0, 0, 0), size=(inf, inf, thickness)), medium=medium
745
+ )
746
+
747
+ def to_3d(self, thickness_nm: int) -> np.ndarray:
748
+ """
749
+ Convert the 2D device geometry into a 3D representation.
750
+
751
+ This method creates a 3D array by interpolating between the bottom and top
752
+ layers of the device geometry. The interpolation is linear.
753
+
754
+ Parameters
755
+ ----------
756
+ thickness_nm : int
757
+ The thickness of the 3D representation in nanometers.
758
+
759
+ Returns
760
+ -------
761
+ np.ndarray
762
+ A 3D narray representing the device geometry with the specified thickness.
763
+ """
764
+ bottom_layer = self.device_array[:, :, 0]
765
+ top_layer = self.device_array[:, :, -1]
766
+ dt_bottom = distance_transform_edt(bottom_layer) - distance_transform_edt(
767
+ 1 - bottom_layer
768
+ )
769
+ dt_top = distance_transform_edt(top_layer) - distance_transform_edt(
770
+ 1 - top_layer
771
+ )
772
+ weights = np.linspace(0, 1, thickness_nm)
773
+ layered_array = np.zeros(
774
+ (bottom_layer.shape[0], bottom_layer.shape[1], thickness_nm)
775
+ )
776
+ for i, w in enumerate(weights):
777
+ dt_interp = (1 - w) * dt_bottom + w * dt_top
778
+ layered_array[:, :, i] = dt_interp >= 0
779
+ return layered_array
780
+
781
+ def to_stl(self, thickness_nm: int, filename: str = "prefab_device.stl"):
782
+ """
783
+ Export the device geometry as an STL file.
784
+
785
+ Parameters
786
+ ----------
787
+ thickness_nm : int
788
+ The thickness of the 3D representation in nanometers.
789
+ filename : str, optional
790
+ The name of the STL file to save. Defaults to "prefab_device.stl".
791
+
792
+ Raises
793
+ ------
794
+ ValueError
795
+ If the thickness is not a positive integer.
796
+ ImportError
797
+ If the numpy-stl package is not installed.
798
+ """
799
+ try:
800
+ from stl import mesh
801
+ except ImportError:
802
+ raise ImportError(
803
+ "The stl package is required to use this function; "
804
+ "try `pip install numpy-stl`."
805
+ ) from None
806
+
807
+ if thickness_nm <= 0:
808
+ raise ValueError("Thickness must be a positive integer.")
809
+
810
+ layered_array = self.to_3d(thickness_nm)
811
+ verts, faces, _, _ = measure.marching_cubes(layered_array, level=0.5)
812
+ cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
813
+ for i, f in enumerate(faces):
814
+ for j in range(3):
815
+ cube.vectors[i][j] = verts[f[j], :]
816
+ cube.save(filename)
817
+ print(f"Saved Device to '{filename}'")
818
+
673
819
  def _plot_base(
674
820
  self,
675
821
  plot_array: np.ndarray,
@@ -1294,4 +1440,90 @@ class Device(BaseModel):
1294
1440
  """
1295
1441
  return 1 - 2 * np.abs(0.5 - self.device_array)
1296
1442
 
1297
- return 1 - 2 * np.abs(0.5 - self.device_array)
1443
+ def enforce_feature_size(
1444
+ self, min_feature_size: int, strel: str = "disk"
1445
+ ) -> "Device":
1446
+ """
1447
+ Enforce a minimum feature size on the device geometry.
1448
+
1449
+ This method applies morphological operations to ensure that all features in the
1450
+ device geometry are at least the specified minimum size. It uses either a disk
1451
+ or square structuring element for the operations.
1452
+
1453
+ Parameters
1454
+ ----------
1455
+ min_feature_size : int
1456
+ The minimum feature size to enforce, in nanometers.
1457
+ strel : str, optional
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
+ if strel == "disk":
1472
+ structuring_element = disk(radius=min_feature_size / 2)
1473
+ elif strel == "square":
1474
+ structuring_element = square(width=min_feature_size)
1475
+ else:
1476
+ raise ValueError(f"Invalid structuring element: {strel}")
1477
+
1478
+ modified_geometry = closing(self.device_array[:, :, 0], structuring_element)
1479
+ modified_geometry = opening(modified_geometry, structuring_element)
1480
+ modified_geometry = np.expand_dims(modified_geometry, axis=-1)
1481
+ return self.model_copy(update={"device_array": modified_geometry})
1482
+
1483
+ def check_feature_size(self, min_feature_size: int, strel: str = "disk"):
1484
+ """
1485
+ Check and visualize the effect of enforcing a minimum feature size on the device
1486
+ geometry.
1487
+
1488
+ This method enforces a minimum feature size on the device geometry using the
1489
+ specified structuring element, compares the modified geometry with the original,
1490
+ and plots the differences. It also calculates and prints the Hamming distance
1491
+ between the original and modified geometries, providing a measure of the changes
1492
+ introduced by the feature size enforcement.
1493
+
1494
+ Parameters
1495
+ ----------
1496
+ min_feature_size : int
1497
+ The minimum feature size to enforce, in nanometers.
1498
+ strel : str, optional
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
+ )
prefab/read.py CHANGED
@@ -219,6 +219,73 @@ def _gdstk_to_device_array(
219
219
  return device_array
220
220
 
221
221
 
222
+ def from_gdsfactory(
223
+ component: "gf.Component", # noqa: F821
224
+ **kwargs,
225
+ ) -> Device:
226
+ """
227
+ Create a Device from a gdsfactory component.
228
+
229
+ Parameters
230
+ ----------
231
+ component : gf.Component
232
+ The gdsfactory component to be converted into a Device object.
233
+ **kwargs
234
+ Additional keyword arguments to be passed to the Device constructor.
235
+
236
+ Returns
237
+ -------
238
+ Device
239
+ A Device object representing the gdsfactory component.
240
+
241
+ Raises
242
+ ------
243
+ ImportError
244
+ If the gdsfactory package is not installed.
245
+ """
246
+ try:
247
+ import gdsfactory as gf
248
+ except ImportError:
249
+ raise ImportError(
250
+ "The gdsfactory package is required to use this function; "
251
+ "try `pip install gdsfactory`."
252
+ ) from None
253
+
254
+ bounds = (
255
+ (component.xmin * 1000, component.ymin * 1000),
256
+ (component.xmax * 1000, component.ymax * 1000),
257
+ )
258
+
259
+ polygons = [
260
+ polygon
261
+ for polygons_list in component.get_polygons_points().values()
262
+ for polygon in polygons_list
263
+ ]
264
+
265
+ contours = [
266
+ np.array(
267
+ [
268
+ [
269
+ [
270
+ int(1000 * vertex[0] - bounds[0][0]),
271
+ int(1000 * vertex[1] - bounds[0][1]),
272
+ ]
273
+ ]
274
+ for vertex in polygon
275
+ ]
276
+ )
277
+ for polygon in polygons
278
+ ]
279
+
280
+ device_array = np.zeros(
281
+ (int(bounds[1][1] - bounds[0][1]), int(bounds[1][0] - bounds[0][0])),
282
+ dtype=np.uint8,
283
+ )
284
+ cv2.fillPoly(img=device_array, pts=contours, color=(1, 1, 1))
285
+ device_array = np.flipud(device_array)
286
+ return Device(device_array=device_array, **kwargs)
287
+
288
+
222
289
  def from_sem(
223
290
  sem_path: str,
224
291
  sem_resolution: float = None,
@@ -314,3 +381,73 @@ def get_sem_resolution(sem_path: str, sem_resolution_key: str) -> float:
314
381
  value /= 1000
315
382
  return value
316
383
  raise ValueError(f"Resolution key '{sem_resolution_key}' not found in {sem_path}.")
384
+
385
+
386
+ def from_tidy3d(
387
+ tidy3d_sim: "tidy3d.Simulation", # noqa: F821
388
+ eps_threshold: float,
389
+ z: float,
390
+ **kwargs,
391
+ ) -> Device:
392
+ """
393
+ Create a Device from a Tidy3D simulation.
394
+
395
+ Parameters
396
+ ----------
397
+ tidy3d_sim : tidy3d.Simulation
398
+ The Tidy3D simulation object.
399
+ eps_threshold : float
400
+ The threshold value for the permittivity to binarize the device array.
401
+ z : float
402
+ The z-coordinate at which to extract the permittivity.
403
+ **kwargs
404
+ Additional keyword arguments to be passed to the Device constructor.
405
+
406
+ Returns
407
+ -------
408
+ Device
409
+ A Device object representing the permittivity cross-section at the specified
410
+ z-coordinate for the Tidy3D simulation.
411
+
412
+ Raises
413
+ ------
414
+ ValueError
415
+ If the z-coordinate is outside the bounds of the simulation size in the
416
+ z-direction.
417
+ ImportError
418
+ If the tidy3d package is not installed.
419
+ """
420
+ try:
421
+ from tidy3d import Coords, Grid
422
+ except ImportError:
423
+ raise ImportError(
424
+ "The tidy3d package is required to use this function; "
425
+ "try `pip install tidy3d`."
426
+ ) from None
427
+
428
+ if not (
429
+ tidy3d_sim.center[2] - tidy3d_sim.size[2] / 2
430
+ <= z
431
+ <= tidy3d_sim.center[2] + tidy3d_sim.size[2] / 2
432
+ ):
433
+ raise ValueError(
434
+ f"z={z} is outside the bounds of the simulation size in the z-direction."
435
+ )
436
+
437
+ x = np.arange(
438
+ tidy3d_sim.center[0] - tidy3d_sim.size[0] / 2,
439
+ tidy3d_sim.center[0] + tidy3d_sim.size[0] / 2,
440
+ 0.001,
441
+ )
442
+ y = np.arange(
443
+ tidy3d_sim.center[1] - tidy3d_sim.size[1] / 2,
444
+ tidy3d_sim.center[1] + tidy3d_sim.size[1] / 2,
445
+ 0.001,
446
+ )
447
+ z = np.array([z])
448
+
449
+ grid = Grid(boundaries=Coords(x=x, y=y, z=z))
450
+ eps = np.real(tidy3d_sim.epsilon_on_grid(grid=grid, coord_key="boundaries").values)
451
+ device_array = geometry.binarize_hard(device_array=eps, eta=eps_threshold)[:, :, 0]
452
+ device_array = np.fliplr(np.rot90(device_array, k=-1))
453
+ return Device(device_array=device_array, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: prefab
3
- Version: 1.0.4
3
+ Version: 1.1.0
4
4
  Summary: Artificial nanofabrication of integrated photonic circuits using deep learning
5
5
  Project-URL: Homepage, https://prefabphotonics.com
6
6
  Project-URL: Repository, https://github.com/PreFab-Photonics/PreFab
@@ -524,6 +524,7 @@ Requires-Dist: pillow
524
524
  Requires-Dist: pydantic
525
525
  Requires-Dist: requests
526
526
  Requires-Dist: scikit-image
527
+ Requires-Dist: scipy
527
528
  Requires-Dist: toml
528
529
  Requires-Dist: tqdm
529
530
  Description-Content-Type: text/markdown
@@ -0,0 +1,12 @@
1
+ prefab/__init__.py,sha256=Tx4t6ODZeL-V-ZPuQXjEHHtwb7rjqCb2ApLFDniQ5fA,401
2
+ prefab/__main__.py,sha256=aAgt1WXa44k1nJqsiSD3uAfNeGpwtjWqMUYCHN5_Qrw,2759
3
+ prefab/compare.py,sha256=AfLJ69DTqvt0mkMqNCTdn2KWKoclt_0htGVXvarEhkY,2709
4
+ prefab/device.py,sha256=lklENpvqROifkEgNCT4BwU9xnsLt1nwdLksh5VDgpKU,58507
5
+ prefab/geometry.py,sha256=pXsVeu4Ycnq60bG6WqFNoUWnyiNStIaYQgbMIrEyndM,9614
6
+ prefab/models.py,sha256=UMzYZzKouroxlwkXCMKIYozmQCMhNhvt8kQrZmwmZB4,3671
7
+ prefab/read.py,sha256=Rzj9GGrszsKWdY8GxtmpfYcA4nsiuxgwSdEDVvfHN80,14624
8
+ prefab/shapes.py,sha256=Hc6dc2Y5Wmb2mZAxhdjBNEJF7C7tF2o460HNvcqcQdo,24856
9
+ prefab-1.1.0.dist-info/METADATA,sha256=tHahGccx-TKDbeo2ZMKTjC_JYPWesvovFyUuTONF_YA,34817
10
+ prefab-1.1.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
11
+ prefab-1.1.0.dist-info/licenses/LICENSE,sha256=IMF9i4xIpgCADf0U-V1cuf9HBmqWQd3qtI3FSuyW4zE,26526
12
+ prefab-1.1.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- prefab/__init__.py,sha256=z7di6Rcufbv_haAVfAam3Q_EZaBWjv-DWpidsqLlBmc,401
2
- prefab/__main__.py,sha256=aAgt1WXa44k1nJqsiSD3uAfNeGpwtjWqMUYCHN5_Qrw,2759
3
- prefab/compare.py,sha256=AfLJ69DTqvt0mkMqNCTdn2KWKoclt_0htGVXvarEhkY,2709
4
- prefab/device.py,sha256=qIPQNt3nqIpeZasBWHNFxdHC6EBt1_fgWdmeUVWDPlE,50109
5
- prefab/geometry.py,sha256=pXsVeu4Ycnq60bG6WqFNoUWnyiNStIaYQgbMIrEyndM,9614
6
- prefab/models.py,sha256=UMzYZzKouroxlwkXCMKIYozmQCMhNhvt8kQrZmwmZB4,3671
7
- prefab/read.py,sha256=HNuovvQx0Vg0X-z5-gUlL-T1M1RQf81Bmy0wDAdqO28,10782
8
- prefab/shapes.py,sha256=Hc6dc2Y5Wmb2mZAxhdjBNEJF7C7tF2o460HNvcqcQdo,24856
9
- prefab-1.0.4.dist-info/METADATA,sha256=DI5SMrZH42zLQnaJlzzKkU5BU9KEpAJsSayAvIxIAk0,34796
10
- prefab-1.0.4.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
11
- prefab-1.0.4.dist-info/licenses/LICENSE,sha256=IMF9i4xIpgCADf0U-V1cuf9HBmqWQd3qtI3FSuyW4zE,26526
12
- prefab-1.0.4.dist-info/RECORD,,
File without changes