more wip -- most central stuff is first pass done
This commit is contained in:
parent
6549faddbb
commit
557c6c98dc
@ -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
|
@ -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
|
||||
|
@ -1,3 +1,3 @@
|
||||
from .devices import Builder, PortsRef
|
||||
from .builder import Builder, PortsRef
|
||||
from .utils import ell
|
||||
from .tools import Tool
|
||||
|
@ -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)
|
@ -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 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
|
||||
|
||||
# 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_global = transform[:2] + rot_matrix @ (label.offset * mirr_factor)
|
||||
angle = numpy.deg2rad(angle_deg) * mirr_factor[0] * mirr_factor[1] + transform[2]
|
||||
xy = label.offset
|
||||
angle = numpy.deg2rad(angle_deg)
|
||||
|
||||
if name in ports:
|
||||
logger.info(f'Duplicate port {name} in pattern {pattern.name}') # TODO DFS should include name?
|
||||
if name in local_ports:
|
||||
logger.warning(f'Duplicate port "{name}" in pattern "{pstr}"')
|
||||
|
||||
ports[name] = Port(offset=xy_global, rotation=angle, ptype=ptype)
|
||||
local_ports[name] = Port(offset=xy, rotation=angle, ptype=ptype)
|
||||
|
||||
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)
|
||||
pattern.ports.update(ports)
|
||||
pattern.ports.update(local_ports)
|
||||
return pattern
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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))
|
||||
|
@ -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),
|
||||
|
@ -626,3 +626,4 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
pyplot.xlabel('x')
|
||||
pyplot.ylabel('y')
|
||||
pyplot.show()
|
||||
|
||||
|
136
masque/ports.py
136
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]
|
||||
|
Loading…
Reference in New Issue
Block a user