From 557c6c98dcaedddae949afb91a58e9e370d26849 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 22 Jan 2023 16:59:32 -0800 Subject: [PATCH] more wip -- most central stuff is first pass done --- examples/pcgen.py | 298 ---------------------- masque/__init__.py | 2 +- masque/builder/__init__.py | 2 +- masque/builder/{devices.py => builder.py} | 159 +++++++++--- masque/builder/port_utils.py | 103 +++++--- masque/error.py | 20 +- masque/file/dxf.py | 19 +- masque/file/gdsii.py | 59 +++-- masque/file/oasis.py | 59 ++--- masque/library.py | 199 ++++++++------- masque/pattern.py | 1 + masque/ports.py | 136 ++++------ 12 files changed, 432 insertions(+), 625 deletions(-) delete mode 100644 examples/pcgen.py rename masque/builder/{devices.py => builder.py} (75%) diff --git a/examples/pcgen.py b/examples/pcgen.py deleted file mode 100644 index 3c25ed6..0000000 --- a/examples/pcgen.py +++ /dev/null @@ -1,298 +0,0 @@ -""" -Routines for creating normalized 2D lattices and common photonic crystal - cavity designs. -""" - -from typing import Sequence, Tuple - -import numpy # type: ignore - - -def triangular_lattice(dims: Tuple[int, int], - asymmetric: bool = False, - origin: str = 'center', - ) -> numpy.ndarray: - """ - Return an ndarray of `[[x0, y0], [x1, y1], ...]` denoting lattice sites for - a triangular lattice in 2D. - - Args: - dims: Number of lattice sites in the [x, y] directions. - asymmetric: If true, each row will contain the same number of - x-coord lattice sites. If false, every other row will be - one site shorter (to make the structure symmetric). - origin: If 'corner', the least-(x,y) lattice site is placed at (0, 0) - If 'center', the center of the lattice (not necessarily a - lattice site) is placed at (0, 0). - - Returns: - `[[x0, y0], [x1, 1], ...]` denoting lattice sites. - """ - sx, sy = numpy.meshgrid(numpy.arange(dims[0], dtype=float), - numpy.arange(dims[1], dtype=float), indexing='ij') - - sx[sy % 2 == 1] += 0.5 - sy *= numpy.sqrt(3) / 2 - - if not asymmetric: - which = sx != sx.max() - sx = sx[which] - sy = sy[which] - - xy = numpy.column_stack((sx.flat, sy.flat)) - - if origin == 'center': - xy -= (xy.max(axis=0) - xy.min(axis=0)) / 2 - elif origin == 'corner': - pass - else: - raise Exception(f'Invalid value for `origin`: {origin}') - - return xy[xy[:, 0].argsort(), :] - - -def square_lattice(dims: Tuple[int, int]) -> numpy.ndarray: - """ - Return an ndarray of `[[x0, y0], [x1, y1], ...]` denoting lattice sites for - a square lattice in 2D. The lattice will be centered around (0, 0). - - Args: - dims: Number of lattice sites in the [x, y] directions. - - Returns: - `[[x0, y0], [x1, 1], ...]` denoting lattice sites. - """ - xs, ys = numpy.meshgrid(range(dims[0]), range(dims[1]), 'xy') - xs -= dims[0]/2 - ys -= dims[1]/2 - xy = numpy.vstack((xs.flatten(), ys.flatten())).T - return xy[xy[:, 0].argsort(), ] - - -# ### Photonic crystal functions ### - - -def nanobeam_holes(a_defect: float, - num_defect_holes: int, - num_mirror_holes: int - ) -> numpy.ndarray: - """ - Returns a list of `[[x0, r0], [x1, r1], ...]` of nanobeam hole positions and radii. - Creates a region in which the lattice constant and radius are progressively - (linearly) altered over num_defect_holes holes until they reach the value - specified by a_defect, then symmetrically returned to a lattice constant and - radius of 1, which is repeated num_mirror_holes times on each side. - - Args: - a_defect: Minimum lattice constant for the defect, as a fraction of the - mirror lattice constant (ie., for no defect, a_defect = 1). - num_defect_holes: How many holes form the defect (per-side) - num_mirror_holes: How many holes form the mirror (per-side) - - Returns: - Ndarray `[[x0, r0], [x1, r1], ...]` of nanobeam hole positions and radii. - """ - a_values = numpy.linspace(a_defect, 1, num_defect_holes, endpoint=False) - xs = a_values.cumsum() - (a_values[0] / 2) # Later mirroring makes center distance 2x as long - mirror_xs = numpy.arange(1, num_mirror_holes + 1, dtype=float) + xs[-1] - mirror_rs = numpy.ones_like(mirror_xs) - return numpy.vstack((numpy.hstack((-mirror_xs[::-1], -xs[::-1], xs, mirror_xs)), - numpy.hstack((mirror_rs[::-1], a_values[::-1], a_values, mirror_rs)))).T - - -def waveguide(length: int, num_mirror: int) -> numpy.ndarray: - """ - Line defect waveguide in a triangular lattice. - - Args: - length: waveguide length (number of holes in x direction) - num_mirror: Mirror length (number of holes per side; total size is - `2 * n + 1` holes. - - Returns: - `[[x0, y0], [x1, y1], ...]` for all the holes - """ - p = triangular_lattice([length, 2 * num_mirror + 1]) - p_wg = p[p[:, 1] != 0, :] - return p_wg - - -def wgbend(num_mirror: int) -> numpy.ndarray: - """ - Line defect waveguide bend in a triangular lattice. - - Args: - num_mirror: Mirror length (number of holes per side; total size is - approximately `2 * n + 1` - - Returns: - `[[x0, y0], [x1, y1], ...]` for all the holes - """ - p = triangular_lattice([2 * num_mirror, 2 * num_mirror + 1]) - left_horiz = (p[:, 1] == 0) & (p[:, 0] <= 0) - p = p[~left_horiz, :] - - right_diag = numpy.isclose(p[:, 1], p[:, 0] * numpy.sqrt(3)) & (p[:, 0] >= 0) - p = p[~right_diag, :] - return p - - -def y_splitter(num_mirror: int) -> numpy.ndarray: - """ - Line defect waveguide y-splitter in a triangular lattice. - - Args: - num_mirror: Mirror length (number of holes per side; total size is - approximately `2 * n + 1` holes. - - Returns: - `[[x0, y0], [x1, y1], ...]` for all the holes - """ - p = triangular_lattice([2 * num_mirror, 2 * num_mirror + 1]) - left_horiz = (p[:, 1] == 0) & (p[:, 0] <= 0) - p = p[~left_horiz, :] - - right_diag_up = numpy.isclose(p[:, 1], p[:, 0] * numpy.sqrt(3)) & (p[:, 0] >= 0) - p = p[~right_diag_up, :] - - right_diag_dn = numpy.isclose(p[:, 1], -p[:, 0] * numpy.sqrt(3)) & (p[:, 0] >= 0) - p = p[~right_diag_dn, :] - return p - - -def ln_defect(mirror_dims: Tuple[int, int], - defect_length: int, - ) -> numpy.ndarray: - """ - N-hole defect in a triangular lattice. - - Args: - mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes - is 2 * n + 1 in each direction. - defect_length: Length of defect. Should be an odd number. - - Returns: - `[[x0, y0], [x1, y1], ...]` for all the holes - """ - if defect_length % 2 != 1: - raise Exception('defect_length must be odd!') - p = triangular_lattice([2 * d + 1 for d in mirror_dims]) - half_length = numpy.floor(defect_length / 2) - hole_nums = numpy.arange(-half_length, half_length + 1) - holes_to_keep = numpy.in1d(p[:, 0], hole_nums, invert=True) - return p[numpy.logical_or(holes_to_keep, p[:, 1] != 0), ] - - -def ln_shift_defect(mirror_dims: Tuple[int, int], - defect_length: int, - shifts_a: Sequence[float] = (0.15, 0, 0.075), - shifts_r: Sequence[float] = (1, 1, 1) - ) -> numpy.ndarray: - """ - N-hole defect with shifted holes (intended to give the mode a gaussian profile - in real- and k-space so as to improve both Q and confinement). Holes along the - defect line are shifted and altered according to the shifts_* parameters. - - Args: - mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes - is `2 * n + 1` in each direction. - defect_length: Length of defect. Should be an odd number. - shifts_a: Percentage of a to shift (1st, 2nd, 3rd,...) holes along the defect line - shifts_r: Factor to multiply the radius by. Should match length of shifts_a - - Returns: - `[[x0, y0, r0], [x1, y1, r1], ...]` for all the holes - """ - if not hasattr(shifts_a, "__len__") and shifts_a is not None: - shifts_a = [shifts_a] - if not hasattr(shifts_r, "__len__") and shifts_r is not None: - shifts_r = [shifts_r] - - xy = ln_defect(mirror_dims, defect_length) - - # Add column for radius - xyr = numpy.hstack((xy, numpy.ones((xy.shape[0], 1)))) - - # Shift holes - # Expand shifts as necessary - n_shifted = max(len(shifts_a), len(shifts_r)) - - tmp_a = numpy.array(shifts_a) - shifts_a = numpy.ones((n_shifted, )) - shifts_a[:len(tmp_a)] = tmp_a - - tmp_r = numpy.array(shifts_r) - shifts_r = numpy.ones((n_shifted, )) - shifts_r[:len(tmp_r)] = tmp_r - - x_removed = numpy.floor(defect_length / 2) - - for ind in range(n_shifted): - for sign in (-1, 1): - x_val = sign * (x_removed + ind + 1) - which = numpy.logical_and(xyr[:, 0] == x_val, xyr[:, 1] == 0) - xyr[which, ] = (x_val + numpy.sign(x_val) * shifts_a[ind], 0, shifts_r[ind]) - - return xyr - - -def r6_defect(mirror_dims: Tuple[int, int]) -> numpy.ndarray: - """ - R6 defect in a triangular lattice. - - Args: - mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes - is 2 * n + 1 in each direction. - - Returns: - `[[x0, y0], [x1, y1], ...]` specifying hole centers. - """ - xy = triangular_lattice([2 * d + 1 for d in mirror_dims]) - - rem_holes_plus = numpy.array([[1, 0], - [0.5, +numpy.sqrt(3)/2], - [0.5, -numpy.sqrt(3)/2]]) - rem_holes = numpy.vstack((rem_holes_plus, -rem_holes_plus)) - - for rem_xy in rem_holes: - xy = xy[(xy != rem_xy).any(axis=1), ] - - return xy - - -def l3_shift_perturbed_defect( - mirror_dims: Tuple[int, int], - perturbed_radius: float = 1.1, - shifts_a: Sequence[float] = (), - shifts_r: Sequence[float] = () - ) -> numpy.ndarray: - """ - 3-hole defect with perturbed hole sizes intended to form an upwards-directed - beam. Can also include shifted holes along the defect line, intended - to give the mode a more gaussian profile to improve Q. - - Args: - mirror_dims: [x, y] mirror lengths (number of holes). Total number of holes - is 2 * n + 1 in each direction. - perturbed_radius: Amount to perturb the radius of the holes used for beam-forming - shifts_a: Percentage of a to shift (1st, 2nd, 3rd,...) holes along the defect line - shifts_r: Factor to multiply the radius by. Should match length of shifts_a - - Returns: - `[[x0, y0, r0], [x1, y1, r1], ...]` for all the holes - """ - xyr = ln_shift_defect(mirror_dims, 3, shifts_a, shifts_r) - - abs_x, abs_y = (numpy.fabs(xyr[:, i]) for i in (0, 1)) - - # Sorted unique xs and ys - # Ignore row y=0 because it might have shifted holes - xs = numpy.unique(abs_x[abs_x != 0]) - ys = numpy.unique(abs_y) - - # which holes should be perturbed? (xs[[3, 7]], ys[1]) and (xs[[2, 6]], ys[2]) - perturbed_holes = ((xs[a], ys[b]) for a, b in ((3, 1), (7, 1), (2, 2), (6, 2))) - for row in xyr: - if numpy.fabs(row) in perturbed_holes: - row[2] = perturbed_radius - return xyr diff --git a/masque/__init__.py b/masque/__init__.py index 2437ee0..9139cf1 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -27,7 +27,7 @@ """ -from .error import PatternError +from .error import MasqueError, PatternError, LibraryError, BuildError from .shapes import Shape from .label import Label from .ref import Ref diff --git a/masque/builder/__init__.py b/masque/builder/__init__.py index b5668d4..a922870 100644 --- a/masque/builder/__init__.py +++ b/masque/builder/__init__.py @@ -1,3 +1,3 @@ -from .devices import Builder, PortsRef +from .builder import Builder, PortsRef from .utils import ell from .tools import Tool diff --git a/masque/builder/devices.py b/masque/builder/builder.py similarity index 75% rename from masque/builder/devices.py rename to masque/builder/builder.py index b3956bc..39c8849 100644 --- a/masque/builder/devices.py +++ b/masque/builder/builder.py @@ -1,5 +1,5 @@ from typing import Dict, Iterable, List, Tuple, Union, TypeVar, Any, Iterator, Optional, Sequence -from typing import overload, KeysView, ValuesView, MutableMapping +from typing import overload, KeysView, ValuesView, MutableMapping, Mapping import copy import warnings import traceback @@ -15,8 +15,7 @@ from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirro from ..pattern import Pattern from ..ref import Ref from ..library import MutableLibrary -from ..utils import AutoSlots -from ..error import DeviceError +from ..error import PortError, BuildError from ..ports import PortList, Port from .tools import Tool from .utils import ell @@ -150,8 +149,9 @@ class Builder(PortList): def __init__( self, library: MutableLibrary, - pattern: Optional[Pattern] = None, *, + pattern: Optional[Pattern] = None, + ports: Optional[Mapping[str, Port]] = None, tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None, ) -> None: """ @@ -161,16 +161,15 @@ class Builder(PortList): pi (attached devices will be placed to the right). """ self.library = library - self.pattern = pattern or Pattern() + if pattern is not None: + self.pattern = pattern + else: + self.pattern = Pattern() - ## TODO add_port_pair function to add ports at location with rotation - #if ports is None: - # self.ports = { - # 'A': Port([0, 0], rotation=0), - # 'B': Port([0, 0], rotation=pi), - # } - #else: - # self.ports = copy.deepcopy(ports) + if ports is not None: + if self.pattern.ports: + raise BuildError('Ports supplied for pattern with pre-existing ports!') + self.pattern.ports.update(copy.deepcopy(ports)) if tools is None: self.tools = {} @@ -181,19 +180,109 @@ class Builder(PortList): self._dead = False - def as_interface( - self, + @classmethod + def interface( + cls, + source: Union[PortList, Mapping[str, Port]], + *, + library: Optional[MutableLibrary] = None, + tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None, in_prefix: str = 'in_', out_prefix: str = '', - port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None + port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None, ) -> 'Builder': - new = self.pattern.as_interface( - library=self.library, - in_prefix=in_prefix, - out_prefix=out_prefix, - port_map=port_map, - ) - new.tools = self.tools + """ + Begin building a new device based on all or some of the ports in the + source device. Do not include the source device; instead use it + to define ports (the "interface") for the new device. + + The ports specified by `port_map` (default: all ports) are copied to + new device, and additional (input) ports are created facing in the + opposite directions. The specified `in_prefix` and `out_prefix` are + prepended to the port names to differentiate them. + + By default, the flipped ports are given an 'in_' prefix and unflipped + ports keep their original names, enabling intuitive construction of + a device that will "plug into" the current device; the 'in_*' ports + are used for plugging the devices together while the original port + names are used for building the new device. + + Another use-case could be to build the new device using the 'in_' + ports, creating a new device which could be used in place of the + current device. + + Args: + source: A collection of ports (e.g. Pattern, Builder, or dict) + from which to create the interface. + library: Used for buildin functions; if not passed and the source + library: Library from which existing patterns should be referenced, + and to which new ones should be added. If not provided, + the source's library will be used (if available). + tools: Tool objects are used to dynamically generate new single-use + patterns (e.g wires or waveguides) while building. If not provided, + the source's tools will be reused (if available). + in_prefix: Prepended to port names for newly-created ports with + reversed directions compared to the current device. + out_prefix: Prepended to port names for ports which are directly + copied from the current device. + port_map: Specification for ports to copy into the new device: + - If `None`, all ports are copied. + - If a sequence, only the listed ports are copied + - If a mapping, the listed ports (keys) are copied and + renamed (to the values). + + Returns: + The new builder, with an empty pattern and 2x as many ports as + listed in port_map. + + Raises: + `PortError` if `port_map` contains port names not present in the + current device. + `PortError` if applying the prefixes results in duplicate port + names. + """ + from ..pattern import Pattern + + if library is None: + if hasattr(source, 'library') and isinstance(source, MutableLibrary): + library = source.library + else: + raise BuildError('No library provided (and not present in `source.library`') + + if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict): + tools = source.tools + + if isinstance(source, PortList): + orig_ports = source.ports + elif isinstance(source, dict): + orig_ports = source + else: + raise BuildError(f'Unable to get ports from {type(source)}: {source}') + + if port_map: + if isinstance(port_map, dict): + missing_inkeys = set(port_map.keys()) - set(orig_ports.keys()) + mapped_ports = {port_map[k]: v for k, v in orig_ports.items() if k in port_map} + else: + port_set = set(port_map) + missing_inkeys = port_set - set(orig_ports.keys()) + mapped_ports = {k: v for k, v in orig_ports.items() if k in port_set} + + if missing_inkeys: + raise PortError(f'`port_map` keys not present in source: {missing_inkeys}') + else: + mapped_ports = orig_ports + + ports_in = {f'{in_prefix}{name}': port.deepcopy().rotate(pi) + for name, port in mapped_ports.items()} + ports_out = {f'{out_prefix}{name}': port.deepcopy() + for name, port in mapped_ports.items()} + + duplicates = set(ports_out.keys()) & set(ports_in.keys()) + if duplicates: + raise PortError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}') + + new = Builder(library=library, ports={**ports_in, **ports_out}, tools=tools) return new def plug( @@ -252,11 +341,11 @@ class Builder(PortList): self Raises: - `DeviceError` if any ports specified in `map_in` or `map_out` do not + `PortError` if any ports specified in `map_in` or `map_out` do not exist in `self.ports` or `other_names`. - `DeviceError` if there are any duplicate names after `map_in` and `map_out` + `PortError` if there are any duplicate names after `map_in` and `map_out` are applied. - `DeviceError` if the specified port mapping is not achieveable (the ports + `PortError` if the specified port mapping is not achieveable (the ports do not line up) """ if self._dead: @@ -334,9 +423,9 @@ class Builder(PortList): self Raises: - `DeviceError` if any ports specified in `map_in` or `map_out` do not + `PortError` if any ports specified in `map_in` or `map_out` do not exist in `self.ports` or `library[name].ports`. - `DeviceError` if there are any duplicate names after `map_in` and `map_out` + `PortError` if there are any duplicate names after `map_in` and `map_out` are applied. """ if self._dead: @@ -491,19 +580,19 @@ class Builder(PortList): port = self.pattern[portspec] x, y = port.offset if port.rotation is None: - raise DeviceError(f'Port {portspec} has no rotation and cannot be used for path_to()') + raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()') if not numpy.isclose(port.rotation % (pi / 2), 0): - raise DeviceError('path_to was asked to route from non-manhattan port') + raise BuildError('path_to was asked to route from non-manhattan port') is_horizontal = numpy.isclose(port.rotation % pi, 0) if is_horizontal: if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x): - raise DeviceError(f'path_to routing to behind source port: x={x:g} to {position:g}') + raise BuildError(f'path_to routing to behind source port: x={x:g} to {position:g}') length = numpy.abs(position - x) else: if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y): - raise DeviceError(f'path_to routing to behind source port: y={y:g} to {position:g}') + raise BuildError(f'path_to routing to behind source port: y={y:g} to {position:g}') length = numpy.abs(position - y) return self.path(portspec, ccw, length, tool_port_names=tool_port_names, base_name=base_name, **kwargs) @@ -534,9 +623,9 @@ class Builder(PortList): bound = kwargs[bt] if not bound_types: - raise DeviceError('No bound type specified for mpath') + raise BuildError('No bound type specified for mpath') elif len(bound_types) > 1: - raise DeviceError(f'Too many bound types specified for mpath: {bound_types}') + raise BuildError(f'Too many bound types specified for mpath: {bound_types}') bound_type = tuple(bound_types)[0] if isinstance(portspec, str): @@ -550,7 +639,7 @@ class Builder(PortList): port_name = tuple(portspec)[0] return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names) else: - bld = Pattern(ports=ports).as_interface(self.library, tools=self.tools) # TODO: maybe Builder static as_interface-like should optionally take ports instead? Maybe constructor could do it? + bld = Builder.interface(source=ports, library=self.library, tools=self.tools) # TODO: maybe Builder static as_interface-like should optionally take ports instead? Maybe constructor could do it? for port_name, length in extensions.items(): bld.path(port_name, ccw, length, tool_port_names=tool_port_names) name = self.library.get_name(base_name) diff --git a/masque/builder/port_utils.py b/masque/builder/port_utils.py index 3995de3..0f7928b 100644 --- a/masque/builder/port_utils.py +++ b/masque/builder/port_utils.py @@ -14,6 +14,8 @@ from ..pattern import Pattern from ..label import Label from ..utils import rotation_matrix_2d, layer_t from ..ports import Port +from ..error import PatternError +from ..library import Library, WrapROLibrary logger = logging.getLogger(__name__) @@ -48,9 +50,9 @@ def dev2pat(pattern: Pattern, layer: layer_t) -> Pattern: def pat2dev( - pattern: Pattern, + library: Mapping[str, Pattern], + top: str, layers: Sequence[layer_t], - library: Optional[Mapping[str, Pattern]] = None, max_depth: int = 999_999, skip_subcells: bool = True, ) -> Pattern: @@ -75,48 +77,89 @@ def pat2dev( Returns: The updated `pattern`. Port labels are not removed. """ - ports = {} # Note: could do a list here, if they're not unique + if not isinstance(library, Library): + library = WrapROLibrary(library) + + ports = {} annotated_cells = set() def find_ports_each(pat, hierarchy, transform, memo) -> Pattern: - if len(hierarchy) > max_depth - 1: + if len(hierarchy) > max_depth: + if max_depth >= 999_999: + logger.warning(f'pat2dev reached max depth ({max_depth})') return pat if skip_subcells and any(parent in annotated_cells for parent in hierarchy): return pat - labels = [ll for ll in pat.labels if ll.layer in layers] - - if len(labels) == 0: - return pat + cell_name = hierarchy[-1] + pat2dev_flat(pat, cell_name) if skip_subcells: - annotated_cells.add(pat) + annotated_cells.add(cell_name) mirr_factor = numpy.array((1, -1)) ** transform[3] rot_matrix = rotation_matrix_2d(transform[2]) - for label in labels: - name, property_string = label.string.split(':') - properties = property_string.split(' ') - ptype = properties[0] - angle_deg = float(properties[1]) if len(ptype) else 0 - - xy_global = transform[:2] + rot_matrix @ (label.offset * mirr_factor) - angle = numpy.deg2rad(angle_deg) * mirr_factor[0] * mirr_factor[1] + transform[2] - - if name in ports: - logger.info(f'Duplicate port {name} in pattern {pattern.name}') # TODO DFS should include name? - - ports[name] = Port(offset=xy_global, rotation=angle, ptype=ptype) + for name, port in pat.ports.items(): + port.offset = transform[:2] + rot_matrix @ (port.offset * mirr_factor) + port.rotation = port.rotation * mirr_factor[0] * mirr_factor[1] + transform[2] + ports[name] = port return pat - # TODO TODO TODO - if skip_subcells and ports := find_ports_each(pattern, ...): - # TODO Could do this with just the `pattern` itself - pass - else - # TODO need `name` and `library` here - ports = library.dfs(name, visit_before=find_ports_each, transform=True) #TODO: don't check Library if there are ports in top level - pattern.check_ports(other_ports=ports) + # update `ports` + library.dfs(top=top, visit_before=find_ports_each, transform=True) + + pattern = library[top] + pattern.check_ports(other_names=ports.keys()) pattern.ports.update(ports) return pattern + + +def pat2dev_flat( + pattern: Pattern, + layers: Sequence[layer_t], + cell_name: Optional[str] = None, + ) -> Pattern: + """ + Examine `pattern` for labels specifying port info, and use that info + to fill out its `ports` attribute. + + Labels are assumed to be placed at the port locations, and have the format + 'name:ptype angle_deg' + + The pattern is assumed to be flat (have no `refs`) and have no pre-existing ports. + + Args: + pattern: Pattern object to scan for labels. + layers: Search for labels on all the given layers. + cell_name: optional, used for warning message only + + Returns: + The updated `pattern`. Port labels are not removed. + """ + labels = [ll for ll in pattern.labels if ll.layer in layers] + if not labels: + return pattern + + pstr = cell_name if cell_name is not None else repr(pattern) + if pattern.ports: + raise PatternError('Pattern "{pstr}" has pre-existing ports!') + + local_ports = {} + for label in labels: + name, property_string = label.string.split(':') + properties = property_string.split(' ') + ptype = properties[0] + angle_deg = float(properties[1]) if len(ptype) else 0 + + xy = label.offset + angle = numpy.deg2rad(angle_deg) + + if name in local_ports: + logger.warning(f'Duplicate port "{name}" in pattern "{pstr}"') + + local_ports[name] = Port(offset=xy, rotation=angle, ptype=ptype) + + pattern.ports.update(local_ports) + return pattern + diff --git a/masque/error.py b/masque/error.py index 3cbd0f7..3bac722 100644 --- a/masque/error.py +++ b/masque/error.py @@ -19,22 +19,14 @@ class LibraryError(MasqueError): pass -class DeviceLibraryError(MasqueError): - """ - Exception raised by DeviceLibrary classes - """ - pass - - -class DeviceError(MasqueError): - """ - Exception raised by Device and Port objects - """ - pass - - class BuildError(MasqueError): """ Exception raised by builder-related functions """ pass + +class PortError(MasqueError): + """ + Exception raised by builder-related functions + """ + pass diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 311c5e5..4c30124 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -17,6 +17,7 @@ from .. import Pattern, Ref, PatternError, Label, Shape from ..shapes import Polygon, Path from ..repetition import Grid from ..utils import rotation_matrix_2d, layer_t +from .gdsii import check_valid_names logger = logging.getLogger(__name__) @@ -33,7 +34,6 @@ def write( library: Mapping[str, Pattern], stream: io.TextIOBase, *, - modify_originals: bool = False, dxf_version='AC1024', ) -> None: """ @@ -49,8 +49,8 @@ def write( tuple: (1, 2) -> '1.2' str: '1.2' -> '1.2' (no change) - It is often a good idea to run `pattern.dedup()` prior to calling this function, - especially if calling `.polygonize()` will result in very many vertices. + DXF does not support shape repetition (only block repeptition). Please call + library.wrap_repeated_shapes() before writing to file. If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` prior to calling this function. @@ -64,20 +64,13 @@ def write( library: A {name: Pattern} mapping of patterns. Only `top_name` and patterns referenced by it are written. stream: Stream object to write to. - modify_original: If `True`, the original pattern is modified as part of the writing - process. Otherwise, a copy is made. - Default `False`. disambiguate_func: Function which takes a list of patterns and alters them to make their names valid and unique. Default is `disambiguate_pattern_names`. WARNING: No additional error checking is performed on the results. """ #TODO consider supporting DXF arcs? - #TODO name checking - bad_keys = check_valid_names(library.keys()) - - if not modify_originals: - library = library.deepcopy() + check_valid_names(library.keys()) pattern = library[top_name] @@ -329,6 +322,10 @@ def _shapes_to_elements( # Add `LWPolyline`s for each shape. # Could set do paths with width setting, but need to consider endcaps. for shape in shapes: + if shape.repetition is not None: + raise PatternError('Shape repetitions are not supported by DXF.' + ' Please call library.wrap_repeated_shapes() before writing to file.') + attribs = {'layer': _mlayer2dxf(shape.layer)} for polygon in shape.to_polygons(): xy_open = polygon.vertices + polygon.offset diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 0ac11ff..c54cc88 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -29,6 +29,8 @@ import struct import logging import pathlib import gzip +import string +from pprint import pformat import numpy from numpy.typing import NDArray, ArrayLike @@ -36,7 +38,7 @@ import klamath from klamath import records from .utils import is_gzipped -from .. import Pattern, Ref, PatternError, Label, Shape +from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape from ..shapes import Polygon, Path from ..repetition import Grid from ..utils import layer_t, normalize_mirror, annotations_t @@ -64,8 +66,6 @@ def write( meters_per_unit: float, logical_units_per_unit: float = 1, library_name: str = 'masque-klamath', - *, - modify_originals: bool = False, ) -> None: """ Convert a library to a GDSII stream, mapping data as follows: @@ -82,8 +82,8 @@ def write( datatype is chosen to be `shape.layer[1]` if available, otherwise `0` - It is often a good idea to run `pattern.dedup()` prior to calling this function, - especially if calling `.polygonize()` will result in very many vertices. + GDS does not support shape repetition (only cell repeptition). Please call + library.wrap_repeated_shapes() before writing to file. If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` prior to calling this function. @@ -97,26 +97,17 @@ def write( Default `1`. library_name: Library name written into the GDSII file. Default 'masque-klamath'. - modify_originals: If `True`, the original pattern is modified as part of the writing - process. Otherwise, a copy is made. - Default `False`. """ - # TODO check name errors - bad_keys = check_valid_names(library.keys()) + check_valid_names(library.keys()) # TODO check all hierarchy present - if not modify_originals: - library = copy.deepcopy(library) #TODO figure out best approach e.g. if lazy - if not isinstance(library, MutableLibrary): if isinstance(library, dict): library = WrapLibrary(library) else: library = WrapLibrary(dict(library)) - library.wrap_repeated_shapes() - # Create library header = klamath.library.FileHeader( name=library_name.encode('ASCII'), @@ -440,6 +431,10 @@ def _shapes_to_elements( elements: List[klamath.elements.Element] = [] # Add a Boundary element for each shape, and Path elements if necessary for shape in shapes: + if shape.repetition is not None: + raise PatternError('Shape repetitions are not supported by GDS.' + ' Please call library.wrap_repeated_shapes() before writing to file.') + layer, data_type = _mlayer2gds(shape.layer) properties = _annotations_to_properties(shape.annotations, 128) if isinstance(shape, Path) and not polygonize_paths: @@ -652,3 +647,37 @@ def load_libraryfile( else: stream = io.BufferedReader(base_stream) return load_library(stream, full_load=full_load) + + +def check_valid_names( + names: Iterable[str], + max_length: int = 32, + ) -> None: + """ + Check all provided names to see if they're valid GDSII cell names. + + Args: + names: Collection of names to check + max_length: Max allowed length + + """ + allowed_chars = set(string.ascii_letters + string.digits + '_?$') + + bad_chars = [ + name for name in names + if not set(name).issubset(allowed_chars) + ] + + bad_lengths = [ + name for name in names + if len(name) > max_length + ] + + if bad_chars: + logger.error('Names contain invalid characters:\n' + pformat(bad_chars)) + + if bad_lengths: + logger.error(f'Names too long (>{max_length}:\n' + pformat(bad_chars)) + + if bad_chars or bad_lengths: + raise LibraryError('Library contains invalid names, see log above') diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 84d0f79..4785f55 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -20,6 +20,8 @@ import struct import logging import pathlib import gzip +import string +from pprint import pformat import numpy from numpy.typing import ArrayLike, NDArray @@ -28,7 +30,7 @@ import fatamorgana.records as fatrec from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference from .utils import is_gzipped -from .. import Pattern, Ref, PatternError, Label, Shape +from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape from ..library import WrapLibrary, MutableLibrary from ..shapes import Polygon, Path, Circle from ..repetition import Grid, Arbitrary, Repetition @@ -95,9 +97,7 @@ def build( Returns: `fatamorgana.OasisLayout` """ - - # TODO check names - bad_keys = check_valid_names(library.keys()) + check_valid_names(library.keys()) # TODO check all hierarchy present @@ -110,9 +110,6 @@ def build( if layer_map is None: layer_map = {} - if disambiguate_func is None: - disambiguate_func = disambiguate_pattern_names - if annotations is None: annotations = {} @@ -616,32 +613,6 @@ def _labels_to_texts( return texts -def disambiguate_pattern_names( - names: Iterable[str], - ) -> List[str]: - new_names = [] - for name in names: - sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', name) - - i = 0 - suffixed_name = sanitized_name - while suffixed_name in new_names or suffixed_name == '': - suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII') - - suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A') - i += 1 - - if sanitized_name == '': - logger.warning(f'Empty pattern name saved as "{suffixed_name}"') - - if len(suffixed_name) == 0: - # Should never happen since zero-length names are replaced - raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{name}"') - - new_names.append(suffixed_name) - return new_names - - def repetition_fata2masq( rep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None], ) -> Optional[Repetition]: @@ -734,3 +705,25 @@ def properties_to_annotations( properties = [fatrec.Property(key, vals, is_standard=False) for key, vals in annotations.items()] return properties + + +def check_valid_names( + names: Iterable[str], + ) -> None: + """ + Check all provided names to see if they're valid GDSII cell names. + + Args: + names: Collection of names to check + max_length: Max allowed length + + """ + allowed_chars = set(string.ascii_letters + string.digits + string.punctuation + ' ') + + bad_chars = [ + name for name in names + if not set(name).issubset(allowed_chars) + ] + + if bad_chars: + raise LibraryError('Names contain invalid characters:\n' + pformat(bad_chars)) diff --git a/masque/library.py b/masque/library.py index 5fc83e2..b20ba72 100644 --- a/masque/library.py +++ b/masque/library.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, NDArray[numpy.float64]], 'Pattern'] +visitor_function_t = Callable[..., 'Pattern'] L = TypeVar('L', bound='Library') ML = TypeVar('ML', bound='MutableLibrary') LL = TypeVar('LL', bound='LazyLibrary') @@ -250,6 +250,109 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta): toplevel = list(names - not_toplevel) return toplevel + def dfs( + self: L, + top: str, + visit_before: Optional[visitor_function_t] = None, + visit_after: Optional[visitor_function_t] = None, + *, + hierarchy: Tuple[str, ...] = (), + transform: Union[ArrayLike, bool, None] = False, + memo: Optional[Dict] = None, + ) -> L: + """ + Convenience function. + Performs a depth-first traversal of a pattern and its referenced patterns. + At each pattern in the tree, the following sequence is called: + ``` + hierarchy += (top,) + current_pattern = visit_before(current_pattern, **vist_args) + for sp in current_pattern.refs] + self.dfs(sp.target, visit_before, visit_after, + hierarchy, updated_transform, memo) + current_pattern = visit_after(current_pattern, **visit_args) + ``` + where `visit_args` are + `hierarchy`: (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern, current_pattern) + tuple of all parent-and-higher pattern names + `transform`: numpy.ndarray containing cumulative + [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)] + for the instance being visited + `memo`: Arbitrary dict (not altered except by `visit_before()` and `visit_after()`) + + Args: + top: Name of the pattern to start at (root node of the tree). + visit_before: Function to call before traversing refs. + Should accept a `Pattern` and `**visit_args`, and return the (possibly modified) + pattern. Default `None` (not called). + visit_after: Function to call after traversing refs. + Should accept a `Pattern` and `**visit_args`, and return the (possibly modified) + pattern. Default `None` (not called). + transform: Initial value for `visit_args['transform']`. + Can be `False`, in which case the transform is not calculated. + `True` or `None` is interpreted as `[0, 0, 0, 0]`. + memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict). + hierarchy: Tuple of patterns specifying the hierarchy above the current pattern. + Appended to the start of the generated `visit_args['hierarchy']`. + Default is an empty tuple. + + Returns: + self + """ + if memo is None: + memo = {} + + if transform is None or transform is True: + transform = numpy.zeros(4) + elif transform is not False: + transform = numpy.array(transform) + + if top in hierarchy: + raise LibraryError('.dfs() called on pattern with circular reference') + + hierarchy += (top,) + + pat = self[top] + if visit_before is not None: + pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) + + for ref in pat.refs: + if transform is not False: + sign = numpy.ones(2) + if transform[3]: + sign[1] = -1 + xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign) + mirror_x, angle = normalize_mirror(ref.mirrored) + angle += ref.rotation + sp_transform = transform + (xy[0], xy[1], angle, mirror_x) + sp_transform[3] %= 2 + else: + sp_transform = False + + if ref.target is None: + continue + + self.dfs( + top=ref.target, + visit_before=visit_before, + visit_after=visit_after, + transform=sp_transform, + memo=memo, + hierarchy=hierarchy, + ) + + if visit_after is not None: + pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) + + if self[top] is not pat: + if isinstance(self, MutableLibrary): + self._set(top, pat) + else: + raise LibraryError('visit_* functions returned a new `Pattern` object' + ' but the library is immutable') + + return self + VVV = TypeVar('VVV') @@ -308,100 +411,6 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta): return self - #TODO maybe also in immutable case? - def dfs( - self: ML, - top: str, - visit_before: Optional[visitor_function_t] = None, - visit_after: Optional[visitor_function_t] = None, - transform: Union[ArrayLike, bool, None] = False, - memo: Optional[Dict] = None, - hierarchy: Tuple[str, ...] = (), - ) -> ML: - """ - Convenience function. - Performs a depth-first traversal of a pattern and its referenced patterns. - At each pattern in the tree, the following sequence is called: - ``` - current_pattern = visit_before(current_pattern, **vist_args) - for sp in current_pattern.refs] - self.dfs(sp.target, visit_before, visit_after, updated_transform, - memo, (current_pattern,) + hierarchy) - current_pattern = visit_after(current_pattern, **visit_args) - ``` - where `visit_args` are - `hierarchy`: (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern) - tuple of all parent-and-higher patterns - `transform`: numpy.ndarray containing cumulative - [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)] - for the instance being visited - `memo`: Arbitrary dict (not altered except by `visit_before()` and `visit_after()`) - - Args: - top: Name of the pattern to start at (root node of the tree). - visit_before: Function to call before traversing refs. - Should accept a `Pattern` and `**visit_args`, and return the (possibly modified) - pattern. Default `None` (not called). - visit_after: Function to call after traversing refs. - Should accept a `Pattern` and `**visit_args`, and return the (possibly modified) - pattern. Default `None` (not called). - transform: Initial value for `visit_args['transform']`. - Can be `False`, in which case the transform is not calculated. - `True` or `None` is interpreted as `[0, 0, 0, 0]`. - memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict). - hierarchy: Tuple of patterns specifying the hierarchy above the current pattern. - Appended to the start of the generated `visit_args['hierarchy']`. - Default is an empty tuple. - - Returns: - self - """ - if memo is None: - memo = {} - - if transform is None or transform is True: - transform = numpy.zeros(4) - elif transform is not False: - transform = numpy.array(transform) - - if top in hierarchy: - raise PatternError('.dfs() called on pattern with circular reference') - - pat = self[top] - if visit_before is not None: - pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore - - for ref in pat.refs: - if transform is not False: - sign = numpy.ones(2) - if transform[3]: - sign[1] = -1 - xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign) - mirror_x, angle = normalize_mirror(ref.mirrored) - angle += ref.rotation - sp_transform = transform + (xy[0], xy[1], angle, mirror_x) - sp_transform[3] %= 2 - else: - sp_transform = False - - if ref.target is None: - continue - - self.dfs( - top=ref.target, - visit_before=visit_before, - visit_after=visit_after, - transform=sp_transform, - memo=memo, - hierarchy=hierarchy + (top,), - ) - - if visit_after is not None: - pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore - - self._set(top, pat) - return self - def dedup( self: ML, norm_value: int = int(1e6), diff --git a/masque/pattern.py b/masque/pattern.py index 3d72122..1e5f238 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -626,3 +626,4 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots): pyplot.xlabel('x') pyplot.ylabel('y') pyplot.show() + diff --git a/masque/ports.py b/masque/ports.py index 0e0f377..9b8361a 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -13,7 +13,7 @@ from numpy.typing import ArrayLike, NDArray from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable from .utils import AutoSlots, rotate_offsets_around -from .error import DeviceError +from .error import PortError from .library import MutableLibrary from .builder import Tool @@ -74,7 +74,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, met self._rotation = None else: if not numpy.size(val) == 1: - raise DeviceError('Rotation must be a scalar') + raise PortError('Rotation must be a scalar') self._rotation = val % (2 * pi) def get_bounds(self): @@ -171,7 +171,7 @@ class PortList(metaclass=ABCMeta): if not overwrite: duplicates = (set(self.ports.keys()) - set(mapping.keys())) & set(mapping.values()) if duplicates: - raise DeviceError(f'Unrenamed ports would be overwritten: {duplicates}') + raise PortError(f'Unrenamed ports would be overwritten: {duplicates}') renamed = {mapping[k]: self.ports.pop(k) for k in mapping.keys()} if None in renamed: @@ -180,6 +180,36 @@ class PortList(metaclass=ABCMeta): self.ports.update(renamed) # type: ignore return self + def add_port_pair( + self: PL, + offset: ArrayLike, + rotation: float = 0.0, + names: Tuple[str, str] = ('A', 'B'), + ptype: str = 'unk', + ) -> PL: + """ + Add a pair of ports with opposing directions at the specified location. + + Args: + offset: Location at which to add the ports + rotation: Orientation of the first port. Radians, counterclockwise. + Default 0. + names: Names for the two ports. Default 'A' and 'B' + ptype: Sets the port type for both ports. + + Returns: + self + """ + + + new_ports = { + names[0]: Port(offset, rotation=rotation, ptype=ptype), + names[1]: Port(offset, rotation=rotation + pi, ptype=ptype), + } + self.check_ports(names) + self.ports.update(new_ports) + return self + def check_ports( self: PL, other_names: Iterable[str], @@ -203,9 +233,9 @@ class PortList(metaclass=ABCMeta): self Raises: - `DeviceError` if any ports specified in `map_in` or `map_out` do not + `PortError` if any ports specified in `map_in` or `map_out` do not exist in `self.ports` or `other_names`. - `DeviceError` if there are any duplicate names after `map_in` and `map_out` + `PortError` if there are any duplicate names after `map_in` and `map_out` are applied. """ if map_in is None: @@ -218,15 +248,15 @@ class PortList(metaclass=ABCMeta): missing_inkeys = set(map_in.keys()) - set(self.ports.keys()) if missing_inkeys: - raise DeviceError(f'`map_in` keys not present in device: {missing_inkeys}') + raise PortError(f'`map_in` keys not present in device: {missing_inkeys}') missing_invals = set(map_in.values()) - other if missing_invals: - raise DeviceError(f'`map_in` values not present in other device: {missing_invals}') + raise PortError(f'`map_in` values not present in other device: {missing_invals}') missing_outkeys = set(map_out.keys()) - other if missing_outkeys: - raise DeviceError(f'`map_out` keys not present in other device: {missing_outkeys}') + raise PortError(f'`map_out` keys not present in other device: {missing_outkeys}') orig_remaining = set(self.ports.keys()) - set(map_in.keys()) other_remaining = other - set(map_out.keys()) - set(map_in.values()) @@ -235,98 +265,20 @@ class PortList(metaclass=ABCMeta): conflicts_final = orig_remaining & (other_remaining | mapped_vals) if conflicts_final: - raise DeviceError(f'Device ports conflict with existing ports: {conflicts_final}') + raise PortError(f'Device ports conflict with existing ports: {conflicts_final}') conflicts_partial = other_remaining & mapped_vals if conflicts_partial: - raise DeviceError(f'`map_out` targets conflict with non-mapped outputs: {conflicts_partial}') + raise PortError(f'`map_out` targets conflict with non-mapped outputs: {conflicts_partial}') map_out_counts = Counter(map_out.values()) map_out_counts[None] = 0 conflicts_out = {k for k, v in map_out_counts.items() if v > 1} if conflicts_out: - raise DeviceError(f'Duplicate targets in `map_out`: {conflicts_out}') + raise PortError(f'Duplicate targets in `map_out`: {conflicts_out}') return self - def as_interface( - self, - library: MutableLibrary, - *, - tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None, - in_prefix: str = 'in_', - out_prefix: str = '', - port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None, - ) -> 'Builder': - """ - Begin building a new device based on all or some of the ports in the - current device. Do not include the current device; instead use it - to define ports (the "interface") for the new device. - - The ports specified by `port_map` (default: all ports) are copied to - new device, and additional (input) ports are created facing in the - opposite directions. The specified `in_prefix` and `out_prefix` are - prepended to the port names to differentiate them. - - By default, the flipped ports are given an 'in_' prefix and unflipped - ports keep their original names, enabling intuitive construction of - a device that will "plug into" the current device; the 'in_*' ports - are used for plugging the devices together while the original port - names are used for building the new device. - - Another use-case could be to build the new device using the 'in_' - ports, creating a new device which could be used in place of the - current device. - - Args: - in_prefix: Prepended to port names for newly-created ports with - reversed directions compared to the current device. - out_prefix: Prepended to port names for ports which are directly - copied from the current device. - port_map: Specification for ports to copy into the new device: - - If `None`, all ports are copied. - - If a sequence, only the listed ports are copied - - If a mapping, the listed ports (keys) are copied and - renamed (to the values). - - Returns: - The new device, with an empty pattern and 2x as many ports as - listed in port_map. - - Raises: - `DeviceError` if `port_map` contains port names not present in the - current device. - `DeviceError` if applying the prefixes results in duplicate port - names. - """ - from .pattern import Pattern - - if port_map: - if isinstance(port_map, dict): - missing_inkeys = set(port_map.keys()) - set(self.ports.keys()) - orig_ports = {port_map[k]: v for k, v in self.ports.items() if k in port_map} - else: - port_set = set(port_map) - missing_inkeys = port_set - set(self.ports.keys()) - orig_ports = {k: v for k, v in self.ports.items() if k in port_set} - - if missing_inkeys: - raise DeviceError(f'`port_map` keys not present in device: {missing_inkeys}') - else: - orig_ports = self.ports - - ports_in = {f'{in_prefix}{name}': port.deepcopy().rotate(pi) - for name, port in orig_ports.items()} - ports_out = {f'{out_prefix}{name}': port.deepcopy() - for name, port in orig_ports.items()} - - duplicates = set(ports_out.keys()) & set(ports_in.keys()) - if duplicates: - raise DeviceError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}') - - new = Builder(library=library, pattern=Pattern(ports={**ports_in, **ports_out}), tools=tools) - return new - def find_transform( self: PL, other: PL2, @@ -394,7 +346,7 @@ class PortList(metaclass=ABCMeta): rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi) if not has_rot.any(): if set_rotation is None: - DeviceError('Must provide set_rotation if rotation is indeterminate') + PortError('Must provide set_rotation if rotation is indeterminate') rotations[:] = set_rotation else: rotations[~has_rot] = rotations[has_rot][0] @@ -404,7 +356,7 @@ class PortList(metaclass=ABCMeta): msg = f'Port orientations do not match:\n' for nn, (k, v) in enumerate(map_in.items()): msg += f'{k} | {rot_deg[nn]:g} | {v}\n' - raise DeviceError(msg) + raise PortError(msg) pivot = o_offsets[0].copy() rotate_offsets_around(o_offsets, pivot, rotations[0]) @@ -413,6 +365,6 @@ class PortList(metaclass=ABCMeta): msg = f'Port translations do not match:\n' for nn, (k, v) in enumerate(map_in.items()): msg += f'{k} | {translations[nn]} | {v}\n' - raise DeviceError(msg) + raise PortError(msg) return translations[0], rotations[0], o_offsets[0]