more wip -- most central stuff is first pass done

This commit is contained in:
Jan Petykiewicz 2023-01-22 16:59:32 -08:00
parent df1acd7c87
commit 6565b8baa3
12 changed files with 432 additions and 625 deletions

View File

@ -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

View File

@ -27,7 +27,7 @@
""" """
from .error import PatternError from .error import MasqueError, PatternError, LibraryError, BuildError
from .shapes import Shape from .shapes import Shape
from .label import Label from .label import Label
from .ref import Ref from .ref import Ref

View File

@ -1,3 +1,3 @@
from .devices import Builder, PortsRef from .builder import Builder, PortsRef
from .utils import ell from .utils import ell
from .tools import Tool from .tools import Tool

View File

@ -1,5 +1,5 @@
from typing import Dict, Iterable, List, Tuple, Union, TypeVar, Any, Iterator, Optional, Sequence 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 copy
import warnings import warnings
import traceback import traceback
@ -15,8 +15,7 @@ from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirro
from ..pattern import Pattern from ..pattern import Pattern
from ..ref import Ref from ..ref import Ref
from ..library import MutableLibrary from ..library import MutableLibrary
from ..utils import AutoSlots from ..error import PortError, BuildError
from ..error import DeviceError
from ..ports import PortList, Port from ..ports import PortList, Port
from .tools import Tool from .tools import Tool
from .utils import ell from .utils import ell
@ -150,8 +149,9 @@ class Builder(PortList):
def __init__( def __init__(
self, self,
library: MutableLibrary, 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, tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None,
) -> None: ) -> None:
""" """
@ -161,16 +161,15 @@ class Builder(PortList):
pi (attached devices will be placed to the right). pi (attached devices will be placed to the right).
""" """
self.library = library 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 not None:
#if ports is None: if self.pattern.ports:
# self.ports = { raise BuildError('Ports supplied for pattern with pre-existing ports!')
# 'A': Port([0, 0], rotation=0), self.pattern.ports.update(copy.deepcopy(ports))
# 'B': Port([0, 0], rotation=pi),
# }
#else:
# self.ports = copy.deepcopy(ports)
if tools is None: if tools is None:
self.tools = {} self.tools = {}
@ -181,19 +180,109 @@ class Builder(PortList):
self._dead = False self._dead = False
def as_interface( @classmethod
self, 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_', in_prefix: str = 'in_',
out_prefix: str = '', out_prefix: str = '',
port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None,
) -> 'Builder': ) -> 'Builder':
new = self.pattern.as_interface( """
library=self.library, Begin building a new device based on all or some of the ports in the
in_prefix=in_prefix, source device. Do not include the source device; instead use it
out_prefix=out_prefix, to define ports (the "interface") for the new device.
port_map=port_map,
) The ports specified by `port_map` (default: all ports) are copied to
new.tools = self.tools 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 return new
def plug( def plug(
@ -252,11 +341,11 @@ class Builder(PortList):
self self
Raises: 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`. 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. 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) do not line up)
""" """
if self._dead: if self._dead:
@ -334,9 +423,9 @@ class Builder(PortList):
self self
Raises: 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`. 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. are applied.
""" """
if self._dead: if self._dead:
@ -491,19 +580,19 @@ class Builder(PortList):
port = self.pattern[portspec] port = self.pattern[portspec]
x, y = port.offset x, y = port.offset
if port.rotation is None: 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): 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) is_horizontal = numpy.isclose(port.rotation % pi, 0)
if is_horizontal: if is_horizontal:
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x): 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) length = numpy.abs(position - x)
else: else:
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y): 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) length = numpy.abs(position - y)
return self.path(portspec, ccw, length, tool_port_names=tool_port_names, base_name=base_name, **kwargs) 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] bound = kwargs[bt]
if not bound_types: 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: 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] bound_type = tuple(bound_types)[0]
if isinstance(portspec, str): if isinstance(portspec, str):
@ -550,7 +639,7 @@ class Builder(PortList):
port_name = tuple(portspec)[0] port_name = tuple(portspec)[0]
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names) return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names)
else: 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(): for port_name, length in extensions.items():
bld.path(port_name, ccw, length, tool_port_names=tool_port_names) bld.path(port_name, ccw, length, tool_port_names=tool_port_names)
name = self.library.get_name(base_name) name = self.library.get_name(base_name)

View File

@ -14,6 +14,8 @@ from ..pattern import Pattern
from ..label import Label from ..label import Label
from ..utils import rotation_matrix_2d, layer_t from ..utils import rotation_matrix_2d, layer_t
from ..ports import Port from ..ports import Port
from ..error import PatternError
from ..library import Library, WrapROLibrary
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -48,9 +50,9 @@ def dev2pat(pattern: Pattern, layer: layer_t) -> Pattern:
def pat2dev( def pat2dev(
pattern: Pattern, library: Mapping[str, Pattern],
top: str,
layers: Sequence[layer_t], layers: Sequence[layer_t],
library: Optional[Mapping[str, Pattern]] = None,
max_depth: int = 999_999, max_depth: int = 999_999,
skip_subcells: bool = True, skip_subcells: bool = True,
) -> Pattern: ) -> Pattern:
@ -75,48 +77,89 @@ def pat2dev(
Returns: Returns:
The updated `pattern`. Port labels are not removed. 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() annotated_cells = set()
def find_ports_each(pat, hierarchy, transform, memo) -> Pattern: 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 return pat
if skip_subcells and any(parent in annotated_cells for parent in hierarchy): if skip_subcells and any(parent in annotated_cells for parent in hierarchy):
return pat return pat
labels = [ll for ll in pat.labels if ll.layer in layers] cell_name = hierarchy[-1]
pat2dev_flat(pat, cell_name)
if len(labels) == 0:
return pat
if skip_subcells: if skip_subcells:
annotated_cells.add(pat) annotated_cells.add(cell_name)
mirr_factor = numpy.array((1, -1)) ** transform[3] mirr_factor = numpy.array((1, -1)) ** transform[3]
rot_matrix = rotation_matrix_2d(transform[2]) rot_matrix = rotation_matrix_2d(transform[2])
for label in labels: for name, port in pat.ports.items():
name, property_string = label.string.split(':') port.offset = transform[:2] + rot_matrix @ (port.offset * mirr_factor)
properties = property_string.split(' ') port.rotation = port.rotation * mirr_factor[0] * mirr_factor[1] + transform[2]
ptype = properties[0] ports[name] = port
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)
return pat return pat
# TODO TODO TODO # update `ports`
if skip_subcells and ports := find_ports_each(pattern, ...): library.dfs(top=top, visit_before=find_ports_each, transform=True)
# TODO Could do this with just the `pattern` itself
pass pattern = library[top]
else pattern.check_ports(other_names=ports.keys())
# 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)
pattern.ports.update(ports) pattern.ports.update(ports)
return pattern 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

View File

@ -19,22 +19,14 @@ class LibraryError(MasqueError):
pass pass
class DeviceLibraryError(MasqueError):
"""
Exception raised by DeviceLibrary classes
"""
pass
class DeviceError(MasqueError):
"""
Exception raised by Device and Port objects
"""
pass
class BuildError(MasqueError): class BuildError(MasqueError):
""" """
Exception raised by builder-related functions Exception raised by builder-related functions
""" """
pass pass
class PortError(MasqueError):
"""
Exception raised by builder-related functions
"""
pass

View File

@ -17,6 +17,7 @@ from .. import Pattern, Ref, PatternError, Label, Shape
from ..shapes import Polygon, Path from ..shapes import Polygon, Path
from ..repetition import Grid from ..repetition import Grid
from ..utils import rotation_matrix_2d, layer_t from ..utils import rotation_matrix_2d, layer_t
from .gdsii import check_valid_names
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,7 +34,6 @@ def write(
library: Mapping[str, Pattern], library: Mapping[str, Pattern],
stream: io.TextIOBase, stream: io.TextIOBase,
*, *,
modify_originals: bool = False,
dxf_version='AC1024', dxf_version='AC1024',
) -> None: ) -> None:
""" """
@ -49,8 +49,8 @@ def write(
tuple: (1, 2) -> '1.2' tuple: (1, 2) -> '1.2'
str: '1.2' -> '1.2' (no change) str: '1.2' -> '1.2' (no change)
It is often a good idea to run `pattern.dedup()` prior to calling this function, DXF does not support shape repetition (only block repeptition). Please call
especially if calling `.polygonize()` will result in very many vertices. library.wrap_repeated_shapes() before writing to file.
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
prior to calling this function. prior to calling this function.
@ -64,20 +64,13 @@ def write(
library: A {name: Pattern} mapping of patterns. Only `top_name` and patterns referenced library: A {name: Pattern} mapping of patterns. Only `top_name` and patterns referenced
by it are written. by it are written.
stream: Stream object to write to. 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 disambiguate_func: Function which takes a list of patterns and alters them
to make their names valid and unique. Default is `disambiguate_pattern_names`. to make their names valid and unique. Default is `disambiguate_pattern_names`.
WARNING: No additional error checking is performed on the results. WARNING: No additional error checking is performed on the results.
""" """
#TODO consider supporting DXF arcs? #TODO consider supporting DXF arcs?
#TODO name checking check_valid_names(library.keys())
bad_keys = check_valid_names(library.keys())
if not modify_originals:
library = library.deepcopy()
pattern = library[top_name] pattern = library[top_name]
@ -329,6 +322,10 @@ def _shapes_to_elements(
# Add `LWPolyline`s for each shape. # Add `LWPolyline`s for each shape.
# Could set do paths with width setting, but need to consider endcaps. # Could set do paths with width setting, but need to consider endcaps.
for shape in shapes: 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)} attribs = {'layer': _mlayer2dxf(shape.layer)}
for polygon in shape.to_polygons(): for polygon in shape.to_polygons():
xy_open = polygon.vertices + polygon.offset xy_open = polygon.vertices + polygon.offset

View File

@ -29,6 +29,8 @@ import struct
import logging import logging
import pathlib import pathlib
import gzip import gzip
import string
from pprint import pformat
import numpy import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
@ -36,7 +38,7 @@ import klamath
from klamath import records from klamath import records
from .utils import is_gzipped 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 ..shapes import Polygon, Path
from ..repetition import Grid from ..repetition import Grid
from ..utils import layer_t, normalize_mirror, annotations_t from ..utils import layer_t, normalize_mirror, annotations_t
@ -64,8 +66,6 @@ def write(
meters_per_unit: float, meters_per_unit: float,
logical_units_per_unit: float = 1, logical_units_per_unit: float = 1,
library_name: str = 'masque-klamath', library_name: str = 'masque-klamath',
*,
modify_originals: bool = False,
) -> None: ) -> None:
""" """
Convert a library to a GDSII stream, mapping data as follows: 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, datatype is chosen to be `shape.layer[1]` if available,
otherwise `0` otherwise `0`
It is often a good idea to run `pattern.dedup()` prior to calling this function, GDS does not support shape repetition (only cell repeptition). Please call
especially if calling `.polygonize()` will result in very many vertices. library.wrap_repeated_shapes() before writing to file.
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
prior to calling this function. prior to calling this function.
@ -97,26 +97,17 @@ def write(
Default `1`. Default `1`.
library_name: Library name written into the GDSII file. library_name: Library name written into the GDSII file.
Default 'masque-klamath'. 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 check_valid_names(library.keys())
bad_keys = check_valid_names(library.keys())
# TODO check all hierarchy present # 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 not isinstance(library, MutableLibrary):
if isinstance(library, dict): if isinstance(library, dict):
library = WrapLibrary(library) library = WrapLibrary(library)
else: else:
library = WrapLibrary(dict(library)) library = WrapLibrary(dict(library))
library.wrap_repeated_shapes()
# Create library # Create library
header = klamath.library.FileHeader( header = klamath.library.FileHeader(
name=library_name.encode('ASCII'), name=library_name.encode('ASCII'),
@ -440,6 +431,10 @@ def _shapes_to_elements(
elements: List[klamath.elements.Element] = [] elements: List[klamath.elements.Element] = []
# Add a Boundary element for each shape, and Path elements if necessary # Add a Boundary element for each shape, and Path elements if necessary
for shape in shapes: 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) layer, data_type = _mlayer2gds(shape.layer)
properties = _annotations_to_properties(shape.annotations, 128) properties = _annotations_to_properties(shape.annotations, 128)
if isinstance(shape, Path) and not polygonize_paths: if isinstance(shape, Path) and not polygonize_paths:
@ -652,3 +647,37 @@ def load_libraryfile(
else: else:
stream = io.BufferedReader(base_stream) stream = io.BufferedReader(base_stream)
return load_library(stream, full_load=full_load) 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')

View File

@ -20,6 +20,8 @@ import struct
import logging import logging
import pathlib import pathlib
import gzip import gzip
import string
from pprint import pformat
import numpy import numpy
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
@ -28,7 +30,7 @@ import fatamorgana.records as fatrec
from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference
from .utils import is_gzipped 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 ..library import WrapLibrary, MutableLibrary
from ..shapes import Polygon, Path, Circle from ..shapes import Polygon, Path, Circle
from ..repetition import Grid, Arbitrary, Repetition from ..repetition import Grid, Arbitrary, Repetition
@ -95,9 +97,7 @@ def build(
Returns: Returns:
`fatamorgana.OasisLayout` `fatamorgana.OasisLayout`
""" """
check_valid_names(library.keys())
# TODO check names
bad_keys = check_valid_names(library.keys())
# TODO check all hierarchy present # TODO check all hierarchy present
@ -110,9 +110,6 @@ def build(
if layer_map is None: if layer_map is None:
layer_map = {} layer_map = {}
if disambiguate_func is None:
disambiguate_func = disambiguate_pattern_names
if annotations is None: if annotations is None:
annotations = {} annotations = {}
@ -613,32 +610,6 @@ def _labels_to_texts(
return 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( def repetition_fata2masq(
rep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None], rep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None],
) -> Optional[Repetition]: ) -> Optional[Repetition]:
@ -731,3 +702,25 @@ def properties_to_annotations(
properties = [fatrec.Property(key, vals, is_standard=False) properties = [fatrec.Property(key, vals, is_standard=False)
for key, vals in annotations.items()] for key, vals in annotations.items()]
return properties 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))

View File

@ -28,7 +28,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) 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') L = TypeVar('L', bound='Library')
ML = TypeVar('ML', bound='MutableLibrary') ML = TypeVar('ML', bound='MutableLibrary')
LL = TypeVar('LL', bound='LazyLibrary') LL = TypeVar('LL', bound='LazyLibrary')
@ -250,6 +250,109 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
toplevel = list(names - not_toplevel) toplevel = list(names - not_toplevel)
return 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') VVV = TypeVar('VVV')
@ -308,100 +411,6 @@ class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta):
return self 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( def dedup(
self: ML, self: ML,
norm_value: int = int(1e6), norm_value: int = int(1e6),

View File

@ -626,3 +626,4 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
pyplot.xlabel('x') pyplot.xlabel('x')
pyplot.ylabel('y') pyplot.ylabel('y')
pyplot.show() pyplot.show()

View File

@ -13,7 +13,7 @@ from numpy.typing import ArrayLike, NDArray
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
from .utils import AutoSlots, rotate_offsets_around from .utils import AutoSlots, rotate_offsets_around
from .error import DeviceError from .error import PortError
from .library import MutableLibrary from .library import MutableLibrary
from .builder import Tool from .builder import Tool
@ -74,7 +74,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, met
self._rotation = None self._rotation = None
else: else:
if not numpy.size(val) == 1: 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) self._rotation = val % (2 * pi)
def get_bounds(self): def get_bounds(self):
@ -171,7 +171,7 @@ class PortList(metaclass=ABCMeta):
if not overwrite: if not overwrite:
duplicates = (set(self.ports.keys()) - set(mapping.keys())) & set(mapping.values()) duplicates = (set(self.ports.keys()) - set(mapping.keys())) & set(mapping.values())
if duplicates: 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()} renamed = {mapping[k]: self.ports.pop(k) for k in mapping.keys()}
if None in renamed: if None in renamed:
@ -180,6 +180,36 @@ class PortList(metaclass=ABCMeta):
self.ports.update(renamed) # type: ignore self.ports.update(renamed) # type: ignore
return self 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( def check_ports(
self: PL, self: PL,
other_names: Iterable[str], other_names: Iterable[str],
@ -203,9 +233,9 @@ class PortList(metaclass=ABCMeta):
self self
Raises: 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`. 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. are applied.
""" """
if map_in is None: if map_in is None:
@ -218,15 +248,15 @@ class PortList(metaclass=ABCMeta):
missing_inkeys = set(map_in.keys()) - set(self.ports.keys()) missing_inkeys = set(map_in.keys()) - set(self.ports.keys())
if missing_inkeys: 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 missing_invals = set(map_in.values()) - other
if missing_invals: 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 missing_outkeys = set(map_out.keys()) - other
if missing_outkeys: 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()) orig_remaining = set(self.ports.keys()) - set(map_in.keys())
other_remaining = other - set(map_out.keys()) - set(map_in.values()) 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) conflicts_final = orig_remaining & (other_remaining | mapped_vals)
if conflicts_final: 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 conflicts_partial = other_remaining & mapped_vals
if conflicts_partial: 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 = Counter(map_out.values())
map_out_counts[None] = 0 map_out_counts[None] = 0
conflicts_out = {k for k, v in map_out_counts.items() if v > 1} conflicts_out = {k for k, v in map_out_counts.items() if v > 1}
if conflicts_out: 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 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( def find_transform(
self: PL, self: PL,
other: PL2, other: PL2,
@ -394,7 +346,7 @@ class PortList(metaclass=ABCMeta):
rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi) rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi)
if not has_rot.any(): if not has_rot.any():
if set_rotation is None: 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 rotations[:] = set_rotation
else: else:
rotations[~has_rot] = rotations[has_rot][0] rotations[~has_rot] = rotations[has_rot][0]
@ -404,7 +356,7 @@ class PortList(metaclass=ABCMeta):
msg = f'Port orientations do not match:\n' msg = f'Port orientations do not match:\n'
for nn, (k, v) in enumerate(map_in.items()): for nn, (k, v) in enumerate(map_in.items()):
msg += f'{k} | {rot_deg[nn]:g} | {v}\n' msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
raise DeviceError(msg) raise PortError(msg)
pivot = o_offsets[0].copy() pivot = o_offsets[0].copy()
rotate_offsets_around(o_offsets, pivot, rotations[0]) rotate_offsets_around(o_offsets, pivot, rotations[0])
@ -413,6 +365,6 @@ class PortList(metaclass=ABCMeta):
msg = f'Port translations do not match:\n' msg = f'Port translations do not match:\n'
for nn, (k, v) in enumerate(map_in.items()): for nn, (k, v) in enumerate(map_in.items()):
msg += f'{k} | {translations[nn]} | {v}\n' msg += f'{k} | {translations[nn]} | {v}\n'
raise DeviceError(msg) raise PortError(msg)
return translations[0], rotations[0], o_offsets[0] return translations[0], rotations[0], o_offsets[0]