more wip -- most central stuff is first pass done
This commit is contained in:
parent
df1acd7c87
commit
6565b8baa3
@ -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 .shapes import Shape
|
||||||
from .label import Label
|
from .label import Label
|
||||||
from .ref import Ref
|
from .ref import Ref
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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))
|
||||||
|
@ -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),
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
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 .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]
|
||||||
|
Loading…
Reference in New Issue
Block a user