This commit is contained in:
jan 2023-01-19 22:20:16 -08:00
parent f7902fa517
commit d9ae8dd6e3
15 changed files with 149 additions and 443 deletions

View File

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

View File

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

View File

@ -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)}>')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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], ...]` """

View File

@ -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'],
]

View File

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

View File

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

View File

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

View File

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

View File

@ -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, :]