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 ..pattern import Pattern
from ..subpattern import SubPattern from ..subpattern import SubPattern
from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
from ..utils import AutoSlots, rotation_matrix_2d from ..utils import AutoSlots
from ..error import DeviceError from ..error import DeviceError
from ..ports import PortList, Port
from .tools import Tool from .tools import Tool
from .utils import ell from .utils import ell
@ -23,382 +24,10 @@ from .utils import ell
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
P = TypeVar('P', bound='Port')
PL = TypeVar('PL', bound='PortList')
PL2 = TypeVar('PL2', bound='PortList')
D = TypeVar('D', bound='Device') D = TypeVar('D', bound='Device')
DR = TypeVar('DR', bound='DeviceRef') 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): class DeviceRef(PortList):
__slots__ = ('name',) __slots__ = ('name',)
@ -908,12 +537,3 @@ class Device(PortList):
# TODO def path_join() and def bus_join()? # 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 Scaling, rotation, and mirroring apply to individual instances, not grid
vectors or offsets. 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 re
import io import io
import copy 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: 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)) assert(not isinstance(placement.repetition, fatamorgana.ReuseRepetition))
xy = numpy.array((placement.x, placement.y)) xy = numpy.array((placement.x, placement.y))
@ -551,7 +551,7 @@ def _shapes_to_elements(
circle = fatrec.Circle( circle = fatrec.Circle(
layer=layer, layer=layer,
datatype=datatype, datatype=datatype,
radius=radius, radius=cast(int, radius),
x=offset[0], x=offset[0],
y=offset[1], y=offset[1],
properties=properties, properties=properties,
@ -568,8 +568,8 @@ def _shapes_to_elements(
path = fatrec.Path( path = fatrec.Path(
layer=layer, layer=layer,
datatype=datatype, datatype=datatype,
point_list=deltas, point_list=cast(Sequence[Sequence[int]], deltas),
half_width=half_width, half_width=cast(int, half_width),
x=xy[0], x=xy[0],
y=xy[1], y=xy[1],
extension_start=extension_start, # TODO implement multiple cap types? extension_start=extension_start, # TODO implement multiple cap types?
@ -587,7 +587,7 @@ def _shapes_to_elements(
datatype=datatype, datatype=datatype,
x=xy[0], x=xy[0],
y=xy[1], y=xy[1],
point_list=points, point_list=cast(List[List[int]], points),
properties=properties, properties=properties,
repetition=repetition, repetition=repetition,
)) ))
@ -674,16 +674,16 @@ def repetition_masq2fata(
a_count = rint_cast(rep.a_count) a_count = rint_cast(rep.a_count)
b_count = rint_cast(rep.b_count) if rep.b_count is not None else None b_count = rint_cast(rep.b_count) if rep.b_count is not None else None
frep = fatamorgana.GridRepetition( frep = fatamorgana.GridRepetition(
a_vector=a_vector, a_vector=cast(List[int], a_vector),
b_vector=b_vector, b_vector=cast(Optional[List[int]], b_vector),
a_count=a_count, a_count=cast(int, a_count),
b_count=b_count, b_count=cast(Optional[int], b_count),
) )
offset = (0, 0) offset = (0, 0)
elif isinstance(rep, Arbitrary): elif isinstance(rep, Arbitrary):
diffs = numpy.diff(rep.displacements, axis=0) diffs = numpy.diff(rep.displacements, axis=0)
diff_ints = rint_cast(diffs) 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, :] offset = rep.displacements[0, :]
else: else:
assert(rep is None) assert(rep is None)

View File

@ -18,19 +18,19 @@ from .shapes import Shape, Polygon
from .label import Label from .label import Label
from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t
from .error import PatternError from .error import PatternError
from .traits import AnnotatableImpl, Scalable, Mirrorable from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable
from .traits import Rotatable, Positionable from .ports import Port, PortList
P = TypeVar('P', bound='Pattern') 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 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. (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] shapes: List[Shape]
""" List of all shapes in this Pattern. """ 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). (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__( def __init__(
self, self,
*, *,
@ -53,6 +56,7 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
labels: Sequence[Label] = (), labels: Sequence[Label] = (),
subpatterns: Sequence[SubPattern] = (), subpatterns: Sequence[SubPattern] = (),
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
ports: Optional[Mapping[str, Port]] = None
) -> None: ) -> None:
""" """
Basic init; arguments get assigned to member variables. Basic init; arguments get assigned to member variables.
@ -62,6 +66,8 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
shapes: Initial shapes in the Pattern shapes: Initial shapes in the Pattern
labels: Initial labels in the Pattern labels: Initial labels in the Pattern
subpatterns: Initial subpatterns 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): if isinstance(shapes, list):
self.shapes = shapes self.shapes = shapes
@ -78,14 +84,25 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
else: else:
self.subpatterns = list(subpatterns) self.subpatterns = list(subpatterns)
if ports is not None:
ports = dict(copy.deepcopy(ports))
self.annotations = annotations if annotations is not None else {} 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': def __copy__(self) -> 'Pattern':
return Pattern( return Pattern(
shapes=copy.deepcopy(self.shapes), shapes=copy.deepcopy(self.shapes),
labels=copy.deepcopy(self.labels), labels=copy.deepcopy(self.labels),
subpatterns=[copy.copy(sp) for sp in self.subpatterns], subpatterns=[copy.copy(sp) for sp in self.subpatterns],
annotations=copy.deepcopy(self.annotations), annotations=copy.deepcopy(self.annotations),
ports=copy.deepcopy(self.ports),
) )
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Pattern': def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Pattern':
@ -95,10 +112,11 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
labels=copy.deepcopy(self.labels, memo), labels=copy.deepcopy(self.labels, memo),
subpatterns=copy.deepcopy(self.subpatterns, memo), subpatterns=copy.deepcopy(self.subpatterns, memo),
annotations=copy.deepcopy(self.annotations, memo), annotations=copy.deepcopy(self.annotations, memo),
ports=copy.deepcopy(self.ports),
) )
return new 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, Appends all shapes, labels and subpatterns from other_pattern to self's shapes,
labels, and supbatterns. labels, and supbatterns.
@ -112,6 +130,8 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
self.subpatterns += other_pattern.subpatterns self.subpatterns += other_pattern.subpatterns
self.shapes += other_pattern.shapes self.shapes += other_pattern.shapes
self.labels += other_pattern.labels self.labels += other_pattern.labels
self.annotations += other_pattern.annotations
self.ports += other_pattern.ports
return self return self
def subset( def subset(
@ -119,31 +139,55 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
shapes: Optional[Callable[[Shape], bool]] = None, shapes: Optional[Callable[[Shape], bool]] = None,
labels: Optional[Callable[[Label], bool]] = None, labels: Optional[Callable[[Label], bool]] = None,
subpatterns: Optional[Callable[[SubPattern], 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': ) -> 'Pattern':
""" """
Returns a Pattern containing only the entities (e.g. shapes) for which the Returns a Pattern containing only the entities (e.g. shapes) for which the
given entity_func returns True. 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: Args:
shapes: Given a shape, returns a boolean denoting whether the shape is a member shapes: Given a shape, returns a boolean denoting whether the shape is a member of the subset.
of the subset. Default always returns False. labels: Given a label, returns a boolean denoting whether the label is a member of the subset.
labels: Given a label, returns a boolean denoting whether the label is a member subpatterns: Given a subpattern, returns a boolean denoting if it is a member of the subset.
of the subset. Default always returns False. annotations: Given an annotation, returns a boolean denoting if it is a member of the subset.
subpatterns: Given a subpattern, returns a boolean denoting if it is a member ports: Given a port, returns a boolean denoting if it is a member of the subset.
of the subset. Default always returns False. default_keep: If `True`, keeps all elements of a given type if no function is supplied.
Default `False` (discards all elements).
Returns: Returns:
A Pattern containing all the shapes and subpatterns for which the parameter A Pattern containing all the shapes and subpatterns for which the parameter
functions return True functions return True
""" """
pat = Pattern() pat = Pattern()
if shapes is not None: if shapes is not None:
pat.shapes = [s for s in self.shapes if shapes(s)] 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: if labels is not None:
pat.labels = [s for s in self.labels if labels(s)] 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: if subpatterns is not None:
pat.subpatterns = [s for s in self.subpatterns if subpatterns(s)] 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 return pat
def polygonize( def polygonize(
@ -281,8 +325,8 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns: Returns:
self self
""" """
for entry in chain(self.shapes, self.subpatterns, self.labels): for entry in chain(self.shapes, self.subpatterns, self.labels, self.ports):
entry.translate(offset) cast(Positionable, entry).translate(offset)
return self return self
def scale_elements(self: P, c: float) -> P: def scale_elements(self: P, c: float) -> P:
@ -295,9 +339,8 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns: Returns:
self self
""" """
entry: Scalable
for entry in chain(self.shapes, self.subpatterns): for entry in chain(self.shapes, self.subpatterns):
entry.scale_by(c) cast(Scalable, entry).scale_by(c)
return self return self
def scale_by(self: P, c: float) -> P: def scale_by(self: P, c: float) -> P:
@ -311,16 +354,23 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns: Returns:
self self
""" """
entry: Scalable
for entry in chain(self.shapes, self.subpatterns): for entry in chain(self.shapes, self.subpatterns):
entry.offset *= c cast(Positionable, entry).offset *= c
entry.scale_by(c) cast(Scalable, entry).scale_by(c)
if entry.repetition:
entry.repetition.scale_by(c) rep = cast(Repeatable, entry).repetition
if rep:
rep.scale_by(c)
for label in self.labels: for label in self.labels:
label.offset *= c cast(Positionable, label).offset *= c
if label.repetition:
label.repetition.scale_by(c) rep = cast(Repeatable, label).repetition
if rep:
rep.scale_by(c)
for port in self.ports.values():
port.offset *= c
return self return self
def rotate_around(self: P, pivot: ArrayLike, rotation: float) -> P: def rotate_around(self: P, pivot: ArrayLike, rotation: float) -> P:
@ -351,8 +401,9 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns: Returns:
self self
""" """
for entry in chain(self.shapes, self.subpatterns, self.labels): for entry in chain(self.shapes, self.subpatterns, self.labels, self.ports):
entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset) old_offset = cast(Positionable, entry).offset
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
return self return self
def rotate_elements(self: P, rotation: float) -> P: def rotate_elements(self: P, rotation: float) -> P:
@ -380,8 +431,8 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns: Returns:
self self
""" """
for entry in chain(self.shapes, self.subpatterns, self.labels): for entry in chain(self.shapes, self.subpatterns, self.labels, self.ports):
entry.offset[axis - 1] *= -1 cast(Positionable, entry).offset[axis - 1] *= -1
return self return self
def mirror_elements(self: P, axis: int) -> P: def mirror_elements(self: P, axis: int) -> P:
@ -566,6 +617,3 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
pyplot.xlabel('x') pyplot.xlabel('x')
pyplot.ylabel('y') pyplot.ylabel('y')
pyplot.show() 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. Note that the offsets in either the 2D or 1D grids do not have to be axis-aligned.
""" """
__slots__ = ('_a_vector', __slots__ = (
'_b_vector', '_a_vector','_b_vector',
'_a_count', '_a_count', '_b_count',
'_b_count') )
_a_vector: NDArray[numpy.float64] _a_vector: NDArray[numpy.float64]
""" Vector `[x, y]` specifying the first lattice vector of the grid. """ 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 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. The start and stop angle are measured counterclockwise from the first (x) radius.
""" """
__slots__ = ('_radii', '_angles', '_width', '_rotation', __slots__ = (
'poly_num_points', 'poly_max_arclen') '_radii', '_angles', '_width', '_rotation',
'poly_num_points', 'poly_max_arclen',
# Inherited
'_offset', '_layer', '_repetition', '_annotations',
)
_radii: NDArray[numpy.float64] _radii: NDArray[numpy.float64]
""" Two radii for defining an ellipse """ """ 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. 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 _radius: float
""" Circle radius """ """ Circle radius """

View File

@ -17,8 +17,12 @@ class Ellipse(Shape, metaclass=AutoSlots):
An ellipse, which has a position, two radii, and a rotation. 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. The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
""" """
__slots__ = ('_radii', '_rotation', __slots__ = (
'poly_num_points', 'poly_max_arclen') '_radii', '_rotation',
'poly_num_points', 'poly_max_arclen',
# Inherited
'_offset', '_layer', '_repetition', '_annotations',
)
_radii: NDArray[numpy.float64] _radii: NDArray[numpy.float64]
""" Ellipse radii """ """ 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. 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] _vertices: NDArray[numpy.float64]
_width: float _width: float
_cap: PathCap _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. 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] _vertices: NDArray[numpy.float64]
""" Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """ """ Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """

View File

@ -17,7 +17,7 @@ if TYPE_CHECKING:
# Type definitions # Type definitions
normalized_shape_tuple = Tuple[ normalized_shape_tuple = Tuple[
Tuple, Tuple,
Tuple[NDArray[numpy.float64], float, float, bool, float], Tuple[NDArray[numpy.float64], float, float, bool],
Callable[[], 'Shape'], 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). Text (to be printed e.g. as a set of polygons).
This is distinct from non-printed Label objects. 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 _string: str
_height: float _height: float

View File

@ -30,10 +30,10 @@ class Positionable(metaclass=ABCMeta):
""" """
pass pass
# @offset.setter @offset.setter
# @abstractmethod @abstractmethod
# def offset(self, val: ArrayLike): def offset(self, val: ArrayLike):
# pass pass
@abstractmethod @abstractmethod
def set_offset(self: T, offset: ArrayLike) -> T: def set_offset(self: T, offset: ArrayLike) -> T:

View File

@ -11,6 +11,6 @@ from .bitwise import get_bit, set_bit
from .vertices import ( from .vertices import (
remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points 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 #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 mirror_x = (mirrored_x != mirrored_y) # XOR
angle = numpy.pi if mirrored_y else 0 angle = numpy.pi if mirrored_y else 0
return mirror_x, angle 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) vertices = numpy.array(vertices, copy=False)
if points.size == 0: if points.size == 0:
return numpy.zeros(0) return numpy.zeros(0, dtype=numpy.int8)
min_bounds = numpy.min(vertices, axis=0)[None, :] min_bounds = numpy.min(vertices, axis=0)[None, :]
max_bounds = numpy.max(vertices, axis=0)[None, :] max_bounds = numpy.max(vertices, axis=0)[None, :]