Source code for qnngds.layout

"""Extend phidl's Layout classes:

Modifications:
    - `Port`: add layer information
    - `CellArray`: add ports
    - `Layer`: add outline/keepout info
    - `CrossSection`: allow "hidden" sections
"""

# can be removed in python 3.14, see https://peps.python.org/pep-0749/
from __future__ import annotations

from phidl import Port as phPort
from phidl import Device as phDevice
from phidl import Layer as phLayer
from phidl import LayerSet as phLayerSet
from phidl import CrossSection as phCrossSection
from phidl import Path
from phidl.device_layout import CellArray as phCellArray
from phidl.device_layout import _parse_move
from phidl.device_layout import _rotate_points
import copy

from numpy.typing import ArrayLike
from collections.abc import Sequence

from qnngds.typing import LayerSpecs, LayerSpec

import numpy as np


[docs]class Port(phPort): """Port object used to snap objects together. Extends phidl.Port to add layer information"""
[docs] def __init__( self, name: str = None, midpoint: ArrayLike = (0, 0), width: float = 1, orientation: float = 0, layer: LayerSpec = (1, 0), parent=None, ) -> None: """Constructor for Port. Parameters: name (str): name of port midpoint (np.ArrayLike): midpoint of port location width (float): width of float orientation (float): rotation of port layer (LayerSpec): GDS layer specification parent: """ super().__init__( name=name, midpoint=midpoint, width=width, orientation=orientation, parent=parent, ) self.layer = layer
def __repr__(self) -> str: """Augment phidl.Port __repr__ to add layer info""" return super().__repr__()[:-1] + f", layer {self.layer})" def _copy(self, new_uid: bool = True) -> Port: """Copies a port Parameters: new_uid (bool): if True (default), use a new uid for the port Returns: (Port): Copied port """ new_port = Port( name=self.name, midpoint=self.midpoint, width=self.width, orientation=self.orientation, layer=self.layer, parent=self.parent, ) new_port.info = copy.deepcopy(self.info) if not new_uid: new_port.uid = self.uid Port._next_uid -= 1 return new_port
[docs]class Device(phDevice): """The basic object that holds polygons, labels, and ports in PHIDL. Augmented with methods for layer-assigned ports."""
[docs] def add_port( self, name: str = None, midpoint: ArrayLike = (0, 0), width: float = 1, orientation: float = 0, layer: LayerSpec = None, port: Port | None = None, ) -> Port: """Adds a Port to the Device. Parameters: name (str): name of port midpoint (tuple[float,float]): midpoint of port location width (float): width of float orientation (float): rotation of port layer (LayerSpec): GDS layer specification port (Port | None): a Port if the added Port is a copy of an existing Port. Notes: Can be called to copy an existing port like ``add_port(port = existing_port)`` or to create a new port ``add_port(myname, mymidpoint, mywidth, myorientation, mylayer)``. Can also be called to copy an existing port with a new name like ``add_port(port = existing_port, name = new_name)`` Returns: (Port): created port """ if port is not None: if not (isinstance(port, phPort) or isinstance(port, Port)): raise ValueError( f"Argument `port` must be a Port or phidl.Port for copying, got {type(port)}" ) if not isinstance(port, Port): p = Port( name=port.name, midpoint=port.midpoint, width=port.width, orientation=port.orientation, layer=layer, parent=self, ) else: p = port._copy(new_uid=True) p.parent = self else: p = Port( name=name, midpoint=midpoint, width=width, orientation=orientation, layer=layer, parent=self, ) if name is not None: p.name = name if layer is not None: p.layer = layer if p.name in self.ports: raise ValueError( '[DEVICE] add_port() error: Port name "%s" already exists in this Device (name "%s", uid %s)' % (p.name, self.name, self.uid) ) self.ports[p.name] = p return p
[docs] def add_ports(self, ports: Sequence[Port]) -> None: """Add multiple Ports to Device Parameters: ports (Sequence[Port]): multiple Port objects to be added """ if isinstance(ports, dict): for name, port in ports.items(): self.add_port(name=name, port=port) else: for port in ports: self.add_port(name=port.name, port=port)
[docs] def add_array( self, device: Device, columns: int = 2, rows: int = 2, spacing: tuple[float, float] = (100, 100), alias: str | None = None, ) -> DeviceArray: """Creates a DeviceArray reference. Parameters: device (Device): the referenced Device. columns (int): number of columns in the array. rows (int): number of rows in the array. spacing (Arraylike): (column spacing, row spacing) alias (str | None): Alias of the referenced Device. Returns: (DeviceArray): array containing references to the input Device. """ if not isinstance(device, phDevice): raise TypeError( """add_array() was passed something that was not a Device object. """ ) if np.size(spacing) != 2: raise ValueError( """add_array() The spacing argument must have exactly 2 elements, e.g. (150,80) """ ) a = DeviceArray( device=device, columns=int(round(columns)), rows=int(round(rows)), spacing=spacing, ) a.owner = self self.add(a) # Add DeviceReference (CellReference) to Device (Cell) if alias is not None: self.aliases[alias] = a return a # Return the CellArray
[docs]class DeviceArray(phCellArray): """Augmentation of gdspy/phidl's CellArray class, autogenerates a port dictionary for all instances"""
[docs] def __init__( self, *args, **kwargs, ) -> None: """Calls PHIDL constructor for CellArray, then generates ports for each reference in array""" super().__init__(*args, **kwargs) device = args[0] if len(args) >= 1 else kwargs["device"] self.columns = args[1] if len(args) >= 2 else kwargs["columns"] self.rows = args[2] if len(args) >= 3 else kwargs["rows"] spacing = args[3] if len(args) >= 4 else kwargs["spacing"] # generate ports self.ports = np.empty((self.rows, self.columns), dtype=object) for row in range(self.rows): dy = row * spacing[1] for column in range(self.columns): dx = column * spacing[0] self.ports[row, column] = {} for name, port in device.ports.items(): self.ports[row, column][name] = Port( name=port.name, midpoint=np.array(port.midpoint) + np.array((dx, dy)), width=port.width, orientation=port.orientation, layer=port.layer, parent=self, # not sure what exactly this should be )
[docs] def rotate(self, angle=45, center=(0, 0)) -> None: """Rotate underlying CellArray and update ports Parameters: angle (float): rotation angle center (Arraylike): coordinates about which to perform rotation """ super().rotate(angle, center) for row in range(self.rows): for column in range(self.columns): for port in self.ports[row, column].values(): port.midpoint = _rotate_points(port.midpoint, angle, center) port.orientation = np.mod(port.orientation + angle, 360)
[docs] def move(self, origin=(0, 0), destination=None, axis=None) -> None: """Translate underlying CellArray and update ports Parameters: origin (tuple): starting location destination (Arraylike | None): destination axis """ dx, dy = _parse_move(origin, destination, axis) super().move(origin, destination, axis) for row in range(self.rows): for column in range(self.columns): for port in self.ports[row, column].values(): port.midpoint = np.array(port.midpoint) + np.array((dx, dy))
[docs]class Layer(phLayer): """Augment PHIDL Layer with outline and keepout information"""
[docs] def __init__( self, gds_layer: int = 0, gds_datatype: int = 0, name: str = "unnamed", description: str | bool = None, inverted: bool = False, color: str | tuple | None = None, alpha: int | float = 0.6, dither: str | None = None, keepout: LayerSpecs | Sequence[Layer] | None = None, outline: int | float = 0, ) -> None: """Constructor for qnngds.Layer Parameters: keepout (LayerSpecs | Layers | None): if not None, defines one or more Layers for which the current layer defines keepout regions. outline (int | float): if non-zero, makes layer positive tone, written with a linewidth of outline. """ super().__init__( gds_layer=gds_layer, gds_datatype=gds_datatype, name=name, description=description, inverted=inverted, color=color, alpha=alpha, dither=dither, ) self.keepout = keepout self.outline = outline self.tuple = (gds_layer, gds_datatype)
def __eq__(self, other: Layer | phLayer): """Check for equality between two Layer instances or a Layer instance and phLayer instance Parameters: self (Layer): the current layer other (phLayer | Layer): the layer to compare with Returns: True if the layers have the same GDS (layer,type) tuple, False otherwise. """ if isinstance(other, Layer): return self.tuple == other.tuple if isinstance(other, phLayer): return self.tuple == (other.gds_layer, other.gds_datatype) return False def __hash__(self): """Hash function for layer class for use in sets/dictionaries Returns: hash(self.tuple) """ return hash(self.tuple)
[docs]class LayerSet(phLayerSet): """Augment PHIDL LayerSet to use Layers with outline and keepout information"""
[docs] def add_layer(self, layer: Layer) -> None: """Add a layer to the LayerSet Parameters: layer (Layer): layer to add """ if layer.name in self._layers: raise ValueError( f"Tried to add layer with name {layer.name}, but it already exists in LayerSet" ) self._layers[layer.name] = layer
def __iter__(self): """Iter method, looping over LayerSet will return an iterator of the layer names""" for name in self._layers: yield name
[docs]class CrossSection(phCrossSection): """Augment PHIDL CrossSection to allow for hidden layers and radius specification"""
[docs] def __init__(self, radius: int | float = 0) -> None: """Constructor for CrossSection Parameters: radius (float | int): nominal radius used when autogenerating paths. NB explicitly providing a radius (e.g. when manually creating paths) will override this setting """ super().__init__() self.radius = radius
[docs] def add( self, width: float | int = 1, offset: float | int = 0, layer: int | tuple[int, int] = 0, ports: tuple[int | str | None] = (None, None), name: str | None = None, hidden: bool = False, min_radius: float | int = 0, ) -> CrossSection: """Calls phidl.CrossSection.add() method, and also updates hidden variable Parameters: width (float | int | callable ): Width of the segment offset (float | int | callable ): Offset of the segment (positive values = right hand side) layer (int | tuple[int, int]): The polygon layer to put the segment on ports (array-like[2] of str | int | None): If not None, specifies the names for the ports at the ends of the cross-sectional element. name (str | int | None): Name of the cross-sectional element for later access hidden (bool): if True, does not add polygon during extrusion Returns: (CrossSection): updated self """ super().add(width=width, offset=offset, layer=layer, ports=ports, name=name) self.sections[-1]["hidden"] = hidden return self
[docs] def extrude(self, path: Path, simplify=None) -> Device: """Calls phidl.CrossSection.extrude() method and removes any polygons corresponding to hidden layers Parameters: path (Path): path to extrude along simplify (float | None): simplify ratio Returns: (Device): extruded cross section """ hidden_layers = {} for n, section in enumerate(self.sections): if section["hidden"]: hidden_layers[n] = section["layer"] section["layer"] = (-1, 0) D = super().extrude(path, simplify) D.remove_layers(layers=(-1,)) for n, section in enumerate(self.sections): if section["hidden"]: section["layer"] = hidden_layers[n] return D
[docs]def to_qg_device(device: phDevice, layer: LayerSpec) -> Device: """Converts a ``phidl.Device`` to a ``qnngds.Device`` Args: device (phidl.Device): device to convert layer (LayerSpec): layer to use for all ports Returns: (qnngds.Device): Device with layer-assigned ports. """ D = Device(device.name) d_i = D << device for name, port in d_i.ports.items(): D.add_port( name=name, port=port, layer=layer, ) return D