wip
This commit is contained in:
parent
f7902fa517
commit
d9ae8dd6e3
@ -14,8 +14,9 @@ from numpy.typing import ArrayLike, NDArray
|
||||
from ..pattern import Pattern
|
||||
from ..subpattern import SubPattern
|
||||
from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
|
||||
from ..utils import AutoSlots, rotation_matrix_2d
|
||||
from ..utils import AutoSlots
|
||||
from ..error import DeviceError
|
||||
from ..ports import PortList, Port
|
||||
from .tools import Tool
|
||||
from .utils import ell
|
||||
|
||||
@ -23,382 +24,10 @@ from .utils import ell
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
P = TypeVar('P', bound='Port')
|
||||
PL = TypeVar('PL', bound='PortList')
|
||||
PL2 = TypeVar('PL2', bound='PortList')
|
||||
D = TypeVar('D', bound='Device')
|
||||
DR = TypeVar('DR', bound='DeviceRef')
|
||||
|
||||
|
||||
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, metaclass=AutoSlots):
|
||||
"""
|
||||
A point at which a `Device` can be snapped to another `Device`.
|
||||
|
||||
Each port has an `offset` ((x, y) position) and may also have a
|
||||
`rotation` (orientation) and a `ptype` (port type).
|
||||
|
||||
The `rotation` is an angle, in radians, measured counterclockwise
|
||||
from the +x axis, pointing inwards into the device which owns the port.
|
||||
The rotation may be set to `None`, indicating that any orientation is
|
||||
allowed (e.g. for a DC electrical port). It is stored modulo 2pi.
|
||||
|
||||
The `ptype` is an arbitrary string, default of `unk` (unknown).
|
||||
"""
|
||||
__slots__ = ('ptype', '_rotation')
|
||||
|
||||
_rotation: Optional[float]
|
||||
""" radians counterclockwise from +x, pointing into device body.
|
||||
Can be `None` to signify undirected port """
|
||||
|
||||
ptype: str
|
||||
""" Port types must match to be plugged together if both are non-zero """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
offset: ArrayLike,
|
||||
rotation: Optional[float],
|
||||
ptype: str = 'unk',
|
||||
) -> None:
|
||||
self.offset = offset
|
||||
self.rotation = rotation
|
||||
self.ptype = ptype
|
||||
|
||||
@property
|
||||
def rotation(self) -> Optional[float]:
|
||||
""" Rotation, radians counterclockwise, pointing into device body. Can be None. """
|
||||
return self._rotation
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, val: float) -> None:
|
||||
if val is None:
|
||||
self._rotation = None
|
||||
else:
|
||||
if not numpy.size(val) == 1:
|
||||
raise DeviceError('Rotation must be a scalar')
|
||||
self._rotation = val % (2 * pi)
|
||||
|
||||
def get_bounds(self):
|
||||
return numpy.vstack((self.offset, self.offset))
|
||||
|
||||
def set_ptype(self: P, ptype: str) -> P:
|
||||
""" Chainable setter for `ptype` """
|
||||
self.ptype = ptype
|
||||
return self
|
||||
|
||||
def mirror(self: P, axis: int) -> P:
|
||||
self.offset[1 - axis] *= -1
|
||||
if self.rotation is not None:
|
||||
self.rotation *= -1
|
||||
self.rotation += axis * pi
|
||||
return self
|
||||
|
||||
def rotate(self: P, rotation: float) -> P:
|
||||
if self.rotation is not None:
|
||||
self.rotation += rotation
|
||||
return self
|
||||
|
||||
def set_rotation(self: P, rotation: Optional[float]) -> P:
|
||||
self.rotation = rotation
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.rotation is None:
|
||||
rot = 'any'
|
||||
else:
|
||||
rot = str(numpy.rad2deg(self.rotation))
|
||||
return f'<{self.offset}, {rot}, [{self.ptype}]>'
|
||||
|
||||
|
||||
class PortList(Copyable, Mirrorable, metaclass=ABCMeta):
|
||||
__slots__ = ('ports',)
|
||||
|
||||
ports: Dict[str, Port]
|
||||
""" Uniquely-named ports which can be used to snap to other Device instances"""
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: str) -> Port:
|
||||
pass
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: Union[List[str], Tuple[str, ...], KeysView[str], ValuesView[str]]) -> Dict[str, Port]:
|
||||
pass
|
||||
|
||||
def __getitem__(self, key: Union[str, Iterable[str]]) -> Union[Port, Dict[str, Port]]:
|
||||
"""
|
||||
For convenience, ports can be read out using square brackets:
|
||||
- `device['A'] == Port((0, 0), 0)`
|
||||
- `device[['A', 'B']] == {'A': Port((0, 0), 0),
|
||||
'B': Port((0, 0), pi)}`
|
||||
"""
|
||||
if isinstance(key, str):
|
||||
return self.ports[key]
|
||||
else:
|
||||
return {k: self.ports[k] for k in key}
|
||||
|
||||
def rename_ports(
|
||||
self: PL,
|
||||
mapping: Dict[str, Optional[str]],
|
||||
overwrite: bool = False,
|
||||
) -> PL:
|
||||
"""
|
||||
Renames ports as specified by `mapping`.
|
||||
Ports can be explicitly deleted by mapping them to `None`.
|
||||
|
||||
Args:
|
||||
mapping: Dict of `{'old_name': 'new_name'}` pairs. Names can be mapped
|
||||
to `None` to perform an explicit deletion. `'new_name'` can also
|
||||
overwrite an existing non-renamed port to implicitly delete it if
|
||||
`overwrite` is set to `True`.
|
||||
overwrite: Allows implicit deletion of ports if set to `True`; see `mapping`.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
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}')
|
||||
|
||||
renamed = {mapping[k]: self.ports.pop(k) for k in mapping.keys()}
|
||||
if None in renamed:
|
||||
del renamed[None]
|
||||
|
||||
self.ports.update(renamed) # type: ignore
|
||||
return self
|
||||
|
||||
def check_ports(
|
||||
self: PL,
|
||||
other_names: Iterable[str],
|
||||
map_in: Optional[Dict[str, str]] = None,
|
||||
map_out: Optional[Dict[str, Optional[str]]] = None,
|
||||
) -> PL:
|
||||
"""
|
||||
Given the provided port mappings, check that:
|
||||
- All of the ports specified in the mappings exist
|
||||
- There are no duplicate port names after all the mappings are performed
|
||||
|
||||
Args:
|
||||
other_names: List of port names being considered for inclusion into
|
||||
`self.ports` (before mapping)
|
||||
map_in: Dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||
port connections between the two devices.
|
||||
map_out: Dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||
new names for unconnected `other_names` ports.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
`DeviceError` 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`
|
||||
are applied.
|
||||
"""
|
||||
if map_in is None:
|
||||
map_in = {}
|
||||
|
||||
if map_out is None:
|
||||
map_out = {}
|
||||
|
||||
other = set(other_names)
|
||||
|
||||
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}')
|
||||
|
||||
missing_invals = set(map_in.values()) - other
|
||||
if missing_invals:
|
||||
raise DeviceError(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}')
|
||||
|
||||
orig_remaining = set(self.ports.keys()) - set(map_in.keys())
|
||||
other_remaining = other - set(map_out.keys()) - set(map_in.values())
|
||||
mapped_vals = set(map_out.values())
|
||||
mapped_vals.discard(None)
|
||||
|
||||
conflicts_final = orig_remaining & (other_remaining | mapped_vals)
|
||||
if conflicts_final:
|
||||
raise DeviceError(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}')
|
||||
|
||||
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}')
|
||||
|
||||
return self
|
||||
|
||||
def as_interface(
|
||||
self,
|
||||
in_prefix: str = 'in_',
|
||||
out_prefix: str = '',
|
||||
port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None
|
||||
) -> 'Device':
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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 = Device(ports={**ports_in, **ports_out})
|
||||
return new
|
||||
|
||||
def find_transform(
|
||||
self: PL,
|
||||
other: PL2,
|
||||
map_in: Dict[str, str],
|
||||
*,
|
||||
mirrored: Tuple[bool, bool] = (False, False),
|
||||
set_rotation: Optional[bool] = None,
|
||||
) -> Tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
|
||||
"""
|
||||
Given a device `other` and a mapping `map_in` specifying port connections,
|
||||
find the transform which will correctly align the specified ports.
|
||||
|
||||
Args:
|
||||
other: a device
|
||||
map_in: Dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||
port connections between the two devices.
|
||||
mirrored: Mirrors `other` across the x or y axes prior to
|
||||
connecting any ports.
|
||||
set_rotation: If the necessary rotation cannot be determined from
|
||||
the ports being connected (i.e. all pairs have at least one
|
||||
port with `rotation=None`), `set_rotation` must be provided
|
||||
to indicate how much `other` should be rotated. Otherwise,
|
||||
`set_rotation` must remain `None`.
|
||||
|
||||
Returns:
|
||||
- The (x, y) translation (performed last)
|
||||
- The rotation (radians, counterclockwise)
|
||||
- The (x, y) pivot point for the rotation
|
||||
|
||||
The rotation should be performed before the translation.
|
||||
"""
|
||||
s_ports = self[map_in.keys()]
|
||||
o_ports = other[map_in.values()]
|
||||
|
||||
s_offsets = numpy.array([p.offset for p in s_ports.values()])
|
||||
o_offsets = numpy.array([p.offset for p in o_ports.values()])
|
||||
s_types = [p.ptype for p in s_ports.values()]
|
||||
o_types = [p.ptype for p in o_ports.values()]
|
||||
|
||||
s_rotations = numpy.array([p.rotation if p.rotation is not None else 0 for p in s_ports.values()])
|
||||
o_rotations = numpy.array([p.rotation if p.rotation is not None else 0 for p in o_ports.values()])
|
||||
s_has_rot = numpy.array([p.rotation is not None for p in s_ports.values()], dtype=bool)
|
||||
o_has_rot = numpy.array([p.rotation is not None for p in o_ports.values()], dtype=bool)
|
||||
has_rot = s_has_rot & o_has_rot
|
||||
|
||||
if mirrored[0]:
|
||||
o_offsets[:, 1] *= -1
|
||||
o_rotations *= -1
|
||||
if mirrored[1]:
|
||||
o_offsets[:, 0] *= -1
|
||||
o_rotations *= -1
|
||||
o_rotations += pi
|
||||
|
||||
type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk'
|
||||
for st, ot in zip(s_types, o_types)])
|
||||
if type_conflicts.any():
|
||||
ports = numpy.where(type_conflicts)
|
||||
msg = 'Ports have conflicting types:\n'
|
||||
for nn, (k, v) in enumerate(map_in.items()):
|
||||
if type_conflicts[nn]:
|
||||
msg += f'{k} | {s_types[nn]}:{o_types[nn]} | {v}\n'
|
||||
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
|
||||
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')
|
||||
rotations[:] = set_rotation
|
||||
else:
|
||||
rotations[~has_rot] = rotations[has_rot][0]
|
||||
|
||||
if not numpy.allclose(rotations[:1], rotations):
|
||||
rot_deg = numpy.rad2deg(rotations)
|
||||
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)
|
||||
|
||||
pivot = o_offsets[0].copy()
|
||||
rotate_offsets_around(o_offsets, pivot, rotations[0])
|
||||
translations = s_offsets - o_offsets
|
||||
if not numpy.allclose(translations[:1], translations):
|
||||
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)
|
||||
|
||||
return translations[0], rotations[0], o_offsets[0]
|
||||
|
||||
|
||||
class DeviceRef(PortList):
|
||||
__slots__ = ('name',)
|
||||
|
||||
@ -908,12 +537,3 @@ class Device(PortList):
|
||||
# TODO def path_join() and def bus_join()?
|
||||
|
||||
|
||||
def rotate_offsets_around(
|
||||
offsets: NDArray[numpy.float64],
|
||||
pivot: NDArray[numpy.float64],
|
||||
angle: float,
|
||||
) -> NDArray[numpy.float64]:
|
||||
offsets -= pivot
|
||||
offsets[:] = (rotation_matrix_2d(angle) @ offsets.T).T
|
||||
offsets += pivot
|
||||
return offsets
|
||||
|
@ -11,7 +11,7 @@ Note that OASIS references follow the same convention as `masque`,
|
||||
Scaling, rotation, and mirroring apply to individual instances, not grid
|
||||
vectors or offsets.
|
||||
"""
|
||||
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Mapping, Optional
|
||||
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Mapping, Optional, cast
|
||||
import re
|
||||
import io
|
||||
import copy
|
||||
@ -482,7 +482,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]:
|
||||
|
||||
def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> SubPattern:
|
||||
"""
|
||||
Helper function to create a SubPattern from a placment. Sets subpat.target to the placemen name.
|
||||
Helper function to create a SubPattern from a placment. Sets subpat.target to the placement name.
|
||||
"""
|
||||
assert(not isinstance(placement.repetition, fatamorgana.ReuseRepetition))
|
||||
xy = numpy.array((placement.x, placement.y))
|
||||
@ -551,7 +551,7 @@ def _shapes_to_elements(
|
||||
circle = fatrec.Circle(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
radius=radius,
|
||||
radius=cast(int, radius),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
properties=properties,
|
||||
@ -568,8 +568,8 @@ def _shapes_to_elements(
|
||||
path = fatrec.Path(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
point_list=deltas,
|
||||
half_width=half_width,
|
||||
point_list=cast(Sequence[Sequence[int]], deltas),
|
||||
half_width=cast(int, half_width),
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
extension_start=extension_start, # TODO implement multiple cap types?
|
||||
@ -587,7 +587,7 @@ def _shapes_to_elements(
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
point_list=points,
|
||||
point_list=cast(List[List[int]], points),
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
@ -674,16 +674,16 @@ def repetition_masq2fata(
|
||||
a_count = rint_cast(rep.a_count)
|
||||
b_count = rint_cast(rep.b_count) if rep.b_count is not None else None
|
||||
frep = fatamorgana.GridRepetition(
|
||||
a_vector=a_vector,
|
||||
b_vector=b_vector,
|
||||
a_count=a_count,
|
||||
b_count=b_count,
|
||||
a_vector=cast(List[int], a_vector),
|
||||
b_vector=cast(Optional[List[int]], b_vector),
|
||||
a_count=cast(int, a_count),
|
||||
b_count=cast(Optional[int], b_count),
|
||||
)
|
||||
offset = (0, 0)
|
||||
elif isinstance(rep, Arbitrary):
|
||||
diffs = numpy.diff(rep.displacements, axis=0)
|
||||
diff_ints = rint_cast(diffs)
|
||||
frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1])
|
||||
frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore
|
||||
offset = rep.displacements[0, :]
|
||||
else:
|
||||
assert(rep is None)
|
||||
|
@ -18,19 +18,19 @@ from .shapes import Shape, Polygon
|
||||
from .label import Label
|
||||
from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t
|
||||
from .error import PatternError
|
||||
from .traits import AnnotatableImpl, Scalable, Mirrorable
|
||||
from .traits import Rotatable, Positionable
|
||||
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable
|
||||
from .ports import Port, PortList
|
||||
|
||||
|
||||
P = TypeVar('P', bound='Pattern')
|
||||
|
||||
|
||||
class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
"""
|
||||
2D layout consisting of some set of shapes, labels, and references to other Pattern objects
|
||||
(via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions.
|
||||
"""
|
||||
__slots__ = ('shapes', 'labels', 'subpatterns')
|
||||
__slots__ = ('shapes', 'labels', 'subpatterns', 'ports')
|
||||
|
||||
shapes: List[Shape]
|
||||
""" List of all shapes in this Pattern.
|
||||
@ -46,6 +46,9 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
(i.e. multiple instances of the same object).
|
||||
"""
|
||||
|
||||
ports: Dict[str, Port]
|
||||
""" Uniquely-named ports which can be used to snap to other Pattern instances"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@ -53,6 +56,7 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
labels: Sequence[Label] = (),
|
||||
subpatterns: Sequence[SubPattern] = (),
|
||||
annotations: Optional[annotations_t] = None,
|
||||
ports: Optional[Mapping[str, Port]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Basic init; arguments get assigned to member variables.
|
||||
@ -62,6 +66,8 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
shapes: Initial shapes in the Pattern
|
||||
labels: Initial labels in the Pattern
|
||||
subpatterns: Initial subpatterns in the Pattern
|
||||
annotations: Initial annotations for the pattern
|
||||
ports: Any ports in the pattern
|
||||
"""
|
||||
if isinstance(shapes, list):
|
||||
self.shapes = shapes
|
||||
@ -78,14 +84,25 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
else:
|
||||
self.subpatterns = list(subpatterns)
|
||||
|
||||
if ports is not None:
|
||||
ports = dict(copy.deepcopy(ports))
|
||||
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<Pattern: sh{len(self.shapes)} sp{len(self.subpatterns)} la{len(self.labels)} ['
|
||||
for name, port in self.ports.items():
|
||||
s += f'\n\t{name}: {port}'
|
||||
s += ']>'
|
||||
return s
|
||||
|
||||
def __copy__(self) -> 'Pattern':
|
||||
return Pattern(
|
||||
shapes=copy.deepcopy(self.shapes),
|
||||
labels=copy.deepcopy(self.labels),
|
||||
subpatterns=[copy.copy(sp) for sp in self.subpatterns],
|
||||
annotations=copy.deepcopy(self.annotations),
|
||||
ports=copy.deepcopy(self.ports),
|
||||
)
|
||||
|
||||
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Pattern':
|
||||
@ -95,10 +112,11 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
labels=copy.deepcopy(self.labels, memo),
|
||||
subpatterns=copy.deepcopy(self.subpatterns, memo),
|
||||
annotations=copy.deepcopy(self.annotations, memo),
|
||||
ports=copy.deepcopy(self.ports),
|
||||
)
|
||||
return new
|
||||
|
||||
def append(self: P, other_pattern: P) -> P:
|
||||
def append(self: P, other_pattern: Pattern) -> P:
|
||||
"""
|
||||
Appends all shapes, labels and subpatterns from other_pattern to self's shapes,
|
||||
labels, and supbatterns.
|
||||
@ -112,6 +130,8 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
self.subpatterns += other_pattern.subpatterns
|
||||
self.shapes += other_pattern.shapes
|
||||
self.labels += other_pattern.labels
|
||||
self.annotations += other_pattern.annotations
|
||||
self.ports += other_pattern.ports
|
||||
return self
|
||||
|
||||
def subset(
|
||||
@ -119,31 +139,55 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
shapes: Optional[Callable[[Shape], bool]] = None,
|
||||
labels: Optional[Callable[[Label], bool]] = None,
|
||||
subpatterns: Optional[Callable[[SubPattern], bool]] = None,
|
||||
annotations: Optional[Callable[[annotation_t], bool]] = None,
|
||||
ports: Optional[Callable[[str], bool]] = None,
|
||||
default_keep: bool = False
|
||||
) -> 'Pattern':
|
||||
"""
|
||||
Returns a Pattern containing only the entities (e.g. shapes) for which the
|
||||
given entity_func returns True.
|
||||
Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied.
|
||||
Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied, just referenced.
|
||||
|
||||
Args:
|
||||
shapes: Given a shape, returns a boolean denoting whether the shape is a member
|
||||
of the subset. Default always returns False.
|
||||
labels: Given a label, returns a boolean denoting whether the label is a member
|
||||
of the subset. Default always returns False.
|
||||
subpatterns: Given a subpattern, returns a boolean denoting if it is a member
|
||||
of the subset. Default always returns False.
|
||||
shapes: Given a shape, returns a boolean denoting whether the shape is a member of the subset.
|
||||
labels: Given a label, returns a boolean denoting whether the label is a member of the subset.
|
||||
subpatterns: Given a subpattern, returns a boolean denoting if it is a member of the subset.
|
||||
annotations: Given an annotation, returns a boolean denoting if it is a member of the subset.
|
||||
ports: Given a port, returns a boolean denoting if it is a member of the subset.
|
||||
default_keep: If `True`, keeps all elements of a given type if no function is supplied.
|
||||
Default `False` (discards all elements).
|
||||
|
||||
Returns:
|
||||
A Pattern containing all the shapes and subpatterns for which the parameter
|
||||
functions return True
|
||||
"""
|
||||
pat = Pattern()
|
||||
|
||||
if shapes is not None:
|
||||
pat.shapes = [s for s in self.shapes if shapes(s)]
|
||||
elif default_keep:
|
||||
pat.shapes = copy.copy(self.shapes)
|
||||
|
||||
if labels is not None:
|
||||
pat.labels = [s for s in self.labels if labels(s)]
|
||||
elif default_keep:
|
||||
pat.labels = copy.copy(self.labels)
|
||||
|
||||
if subpatterns is not None:
|
||||
pat.subpatterns = [s for s in self.subpatterns if subpatterns(s)]
|
||||
elif default_keep:
|
||||
pat.subpatterns = copy.copy(self.subpatterns)
|
||||
|
||||
if annotations is not None:
|
||||
pat.annotations = [s for s in self.annotations if annotations(s)]
|
||||
elif default_keep:
|
||||
pat.annotations = copy.copy(self.annotations)
|
||||
|
||||
if ports is not None:
|
||||
pat.ports = {k: v for k, v in self.ports.items() if ports(k)}
|
||||
elif default_keep:
|
||||
pat.ports = copy.copy(self.ports)
|
||||
|
||||
return pat
|
||||
|
||||
def polygonize(
|
||||
@ -281,8 +325,8 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain(self.shapes, self.subpatterns, self.labels):
|
||||
entry.translate(offset)
|
||||
for entry in chain(self.shapes, self.subpatterns, self.labels, self.ports):
|
||||
cast(Positionable, entry).translate(offset)
|
||||
return self
|
||||
|
||||
def scale_elements(self: P, c: float) -> P:
|
||||
@ -295,9 +339,8 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
entry: Scalable
|
||||
for entry in chain(self.shapes, self.subpatterns):
|
||||
entry.scale_by(c)
|
||||
cast(Scalable, entry).scale_by(c)
|
||||
return self
|
||||
|
||||
def scale_by(self: P, c: float) -> P:
|
||||
@ -311,16 +354,23 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
entry: Scalable
|
||||
for entry in chain(self.shapes, self.subpatterns):
|
||||
entry.offset *= c
|
||||
entry.scale_by(c)
|
||||
if entry.repetition:
|
||||
entry.repetition.scale_by(c)
|
||||
cast(Positionable, entry).offset *= c
|
||||
cast(Scalable, entry).scale_by(c)
|
||||
|
||||
rep = cast(Repeatable, entry).repetition
|
||||
if rep:
|
||||
rep.scale_by(c)
|
||||
|
||||
for label in self.labels:
|
||||
label.offset *= c
|
||||
if label.repetition:
|
||||
label.repetition.scale_by(c)
|
||||
cast(Positionable, label).offset *= c
|
||||
|
||||
rep = cast(Repeatable, label).repetition
|
||||
if rep:
|
||||
rep.scale_by(c)
|
||||
|
||||
for port in self.ports.values():
|
||||
port.offset *= c
|
||||
return self
|
||||
|
||||
def rotate_around(self: P, pivot: ArrayLike, rotation: float) -> P:
|
||||
@ -351,8 +401,9 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain(self.shapes, self.subpatterns, self.labels):
|
||||
entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset)
|
||||
for entry in chain(self.shapes, self.subpatterns, self.labels, self.ports):
|
||||
old_offset = cast(Positionable, entry).offset
|
||||
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
|
||||
return self
|
||||
|
||||
def rotate_elements(self: P, rotation: float) -> P:
|
||||
@ -380,8 +431,8 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain(self.shapes, self.subpatterns, self.labels):
|
||||
entry.offset[axis - 1] *= -1
|
||||
for entry in chain(self.shapes, self.subpatterns, self.labels, self.ports):
|
||||
cast(Positionable, entry).offset[axis - 1] *= -1
|
||||
return self
|
||||
|
||||
def mirror_elements(self: P, axis: int) -> P:
|
||||
@ -566,6 +617,3 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||
pyplot.xlabel('x')
|
||||
pyplot.ylabel('y')
|
||||
pyplot.show()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (f'<Pattern: sh{len(self.shapes)} sp{len(self.subpatterns)} la{len(self.labels)}>')
|
||||
|
@ -39,10 +39,10 @@ class Grid(Repetition, metaclass=AutoSlots):
|
||||
|
||||
Note that the offsets in either the 2D or 1D grids do not have to be axis-aligned.
|
||||
"""
|
||||
__slots__ = ('_a_vector',
|
||||
'_b_vector',
|
||||
'_a_count',
|
||||
'_b_count')
|
||||
__slots__ = (
|
||||
'_a_vector','_b_vector',
|
||||
'_a_count', '_b_count',
|
||||
)
|
||||
|
||||
_a_vector: NDArray[numpy.float64]
|
||||
""" Vector `[x, y]` specifying the first lattice vector of the grid.
|
||||
|
@ -21,8 +21,12 @@ class Arc(Shape, metaclass=AutoSlots):
|
||||
The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
|
||||
The start and stop angle are measured counterclockwise from the first (x) radius.
|
||||
"""
|
||||
__slots__ = ('_radii', '_angles', '_width', '_rotation',
|
||||
'poly_num_points', 'poly_max_arclen')
|
||||
__slots__ = (
|
||||
'_radii', '_angles', '_width', '_rotation',
|
||||
'poly_num_points', 'poly_max_arclen',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_radii: NDArray[numpy.float64]
|
||||
""" Two radii for defining an ellipse """
|
||||
|
@ -15,7 +15,11 @@ class Circle(Shape, metaclass=AutoSlots):
|
||||
"""
|
||||
A circle, which has a position and radius.
|
||||
"""
|
||||
__slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen')
|
||||
__slots__ = (
|
||||
'_radius', 'poly_num_points', 'poly_max_arclen',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_radius: float
|
||||
""" Circle radius """
|
||||
|
@ -17,8 +17,12 @@ class Ellipse(Shape, metaclass=AutoSlots):
|
||||
An ellipse, which has a position, two radii, and a rotation.
|
||||
The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
|
||||
"""
|
||||
__slots__ = ('_radii', '_rotation',
|
||||
'poly_num_points', 'poly_max_arclen')
|
||||
__slots__ = (
|
||||
'_radii', '_rotation',
|
||||
'poly_num_points', 'poly_max_arclen',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_radii: NDArray[numpy.float64]
|
||||
""" Ellipse radii """
|
||||
|
@ -28,7 +28,11 @@ class Path(Shape, metaclass=AutoSlots):
|
||||
|
||||
A normalized_form(...) is available, but can be quite slow with lots of vertices.
|
||||
"""
|
||||
__slots__ = ('_vertices', '_width', '_cap', '_cap_extensions')
|
||||
__slots__ = (
|
||||
'_vertices', '_width', '_cap', '_cap_extensions',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
)
|
||||
_vertices: NDArray[numpy.float64]
|
||||
_width: float
|
||||
_cap: PathCap
|
||||
|
@ -19,7 +19,11 @@ class Polygon(Shape, metaclass=AutoSlots):
|
||||
|
||||
A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
|
||||
"""
|
||||
__slots__ = ('_vertices',)
|
||||
__slots__ = (
|
||||
'_vertices',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_vertices: NDArray[numpy.float64]
|
||||
""" Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """
|
||||
|
@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
# Type definitions
|
||||
normalized_shape_tuple = Tuple[
|
||||
Tuple,
|
||||
Tuple[NDArray[numpy.float64], float, float, bool, float],
|
||||
Tuple[NDArray[numpy.float64], float, float, bool],
|
||||
Callable[[], 'Shape'],
|
||||
]
|
||||
|
||||
|
@ -22,7 +22,11 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
|
||||
Text (to be printed e.g. as a set of polygons).
|
||||
This is distinct from non-printed Label objects.
|
||||
"""
|
||||
__slots__ = ('_string', '_height', '_mirrored', 'font_path')
|
||||
__slots__ = (
|
||||
'_string', '_height', '_mirrored', 'font_path',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations', '_rotation',
|
||||
)
|
||||
|
||||
_string: str
|
||||
_height: float
|
||||
|
@ -30,10 +30,10 @@ class Positionable(metaclass=ABCMeta):
|
||||
"""
|
||||
pass
|
||||
|
||||
# @offset.setter
|
||||
# @abstractmethod
|
||||
# def offset(self, val: ArrayLike):
|
||||
# pass
|
||||
@offset.setter
|
||||
@abstractmethod
|
||||
def offset(self, val: ArrayLike):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_offset(self: T, offset: ArrayLike) -> T:
|
||||
|
@ -11,6 +11,6 @@ from .bitwise import get_bit, set_bit
|
||||
from .vertices import (
|
||||
remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points
|
||||
)
|
||||
from .transform import rotation_matrix_2d, normalize_mirror
|
||||
from .transform import rotation_matrix_2d, normalize_mirror, rotate_offsets_around
|
||||
|
||||
#from . import pack2d
|
||||
|
@ -38,3 +38,17 @@ def normalize_mirror(mirrored: Sequence[bool]) -> Tuple[bool, float]:
|
||||
mirror_x = (mirrored_x != mirrored_y) # XOR
|
||||
angle = numpy.pi if mirrored_y else 0
|
||||
return mirror_x, angle
|
||||
|
||||
|
||||
def rotate_offsets_around(
|
||||
offsets: NDArray[numpy.float64],
|
||||
pivot: NDArray[numpy.float64],
|
||||
angle: float,
|
||||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Rotates offsets around a pivot point.
|
||||
"""
|
||||
offsets -= pivot
|
||||
offsets[:] = (rotation_matrix_2d(angle) @ offsets.T).T
|
||||
offsets += pivot
|
||||
return offsets
|
||||
|
@ -77,7 +77,7 @@ def poly_contains_points(
|
||||
vertices = numpy.array(vertices, copy=False)
|
||||
|
||||
if points.size == 0:
|
||||
return numpy.zeros(0)
|
||||
return numpy.zeros(0, dtype=numpy.int8)
|
||||
|
||||
min_bounds = numpy.min(vertices, axis=0)[None, :]
|
||||
max_bounds = numpy.max(vertices, axis=0)[None, :]
|
||||
|
Loading…
Reference in New Issue
Block a user