Compare commits
9 Commits
b33c632569
...
6db4bb96db
Author | SHA1 | Date | |
---|---|---|---|
6db4bb96db | |||
94aa853a49 | |||
bb054b9eee | |||
5fb736eb74 | |||
4334d0d50b | |||
31863c9799 | |||
30982d742b | |||
447d4ba35b | |||
70a51ed8ef |
@ -430,17 +430,49 @@ class Pather(Builder):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def path_into(
|
||||
self,
|
||||
portspec_src: str,
|
||||
portspec_dst: str,
|
||||
*,
|
||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||
out_ptype: str = 'unk',
|
||||
out_ptype: str | None = None,
|
||||
plug_destination: bool = True,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Create a "wire"/"waveguide" and traveling between the ports `portspec_src` and
|
||||
`portspec_dst`, and `plug` it into both (or just the source port).
|
||||
|
||||
Only unambiguous scenarios are allowed:
|
||||
- Straight connector between facing ports
|
||||
- Single 90 degree bend
|
||||
- Jog between facing ports
|
||||
(jog is done as late as possible, i.e. only 2 L-shaped segments are used)
|
||||
|
||||
By default, the destination's `pytpe` will be used as the `out_ptype` for the
|
||||
wire, and the `portspec_dst` will be plugged (i.e. removed).
|
||||
|
||||
Args:
|
||||
portspec_src: The name of the starting port into which the wire will be plugged.
|
||||
portspec_dst: The name of the destination port.
|
||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
||||
that you will need to change these. The first port is the input (to be
|
||||
connected to `portspec`).
|
||||
out_ptype: Passed to the pathing tool in order to specify the desired port type
|
||||
to be generated at the destination end. If `None` (default), the destination
|
||||
port's `ptype` will be used.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
PortError if either port does not have a specified rotation.
|
||||
BuildError if and invalid port config is encountered:
|
||||
- Non-manhattan ports
|
||||
- U-bend
|
||||
- Destination too close to (or behind) source
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping path_into() since device is dead')
|
||||
return self
|
||||
@ -448,15 +480,18 @@ class Pather(Builder):
|
||||
port_src = self.pattern[portspec_src]
|
||||
port_dst = self.pattern[portspec_dst]
|
||||
|
||||
if out_ptype is None:
|
||||
out_ptype = port_dst.ptype
|
||||
|
||||
if port_src.rotation is None:
|
||||
raise PortError(f'Port {portspec_src} has no rotation and cannot be used for path_into()')
|
||||
if port_dst.rotation is None:
|
||||
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()')
|
||||
|
||||
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
|
||||
raise BuildError('path_to was asked to route from non-manhattan port')
|
||||
raise BuildError('path_into was asked to route from non-manhattan port')
|
||||
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
|
||||
raise BuildError('path_to was asked to route to non-manhattan port')
|
||||
raise BuildError('path_into was asked to route to non-manhattan port')
|
||||
|
||||
src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0)
|
||||
dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0)
|
||||
@ -465,7 +500,7 @@ class Pather(Builder):
|
||||
|
||||
angle = (port_dst.rotation - port_src.rotation) % (2 * pi)
|
||||
|
||||
src_ne = port_src.rotation % (2 * pi) > (3 * pi /4) # path from src will go north or east
|
||||
src_ne = port_src.rotation % (2 * pi) > (3 * pi / 4) # path from src will go north or east
|
||||
|
||||
def get_jog(ccw: SupportsBool, length: float) -> float:
|
||||
tool = self.tools.get(portspec_src, self.tools[None])
|
||||
@ -511,7 +546,7 @@ class Pather(Builder):
|
||||
self.path_to(portspec_src, not ccw2, y=yd - jog, **src_args)
|
||||
self.path_to(portspec_src, ccw2, x=xd, **dst_args)
|
||||
elif numpy.isclose(angle, 0):
|
||||
raise BuildError(f'Don\'t know how to route a U-bend at this time!')
|
||||
raise BuildError('Don\'t know how to route a U-bend at this time!')
|
||||
else:
|
||||
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
|
||||
|
||||
@ -644,7 +679,7 @@ class Pather(Builder):
|
||||
self.library[name] = bld.pattern
|
||||
return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
|
||||
|
||||
# TODO def path_join() and def bus_join()?
|
||||
# TODO def bus_join()?
|
||||
|
||||
def flatten(self) -> Self:
|
||||
"""
|
||||
|
@ -294,7 +294,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
||||
ipat, iport_theirs, _iport_ours = data.in_transition
|
||||
pat.plug(ipat, {port_names[1]: iport_theirs})
|
||||
if not numpy.isclose(data.straight_length, 0):
|
||||
straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length)}
|
||||
straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length, **kwargs)}
|
||||
pat.plug(straight, {port_names[1]: sport_in})
|
||||
if data.ccw is not None:
|
||||
bend, bport_in, bport_out = self.bend
|
||||
|
@ -144,7 +144,7 @@ def writefile(
|
||||
with tmpfile(path) as base_stream:
|
||||
streams: tuple[Any, ...] = (base_stream,)
|
||||
if path.suffix == '.gz':
|
||||
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
||||
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6))
|
||||
streams = (stream,) + streams
|
||||
else:
|
||||
stream = base_stream
|
||||
|
@ -1,21 +1,92 @@
|
||||
"""
|
||||
Helper functions for file reading and writing
|
||||
"""
|
||||
from typing import IO, Iterator
|
||||
from typing import IO, Iterator, Mapping
|
||||
import re
|
||||
import pathlib
|
||||
import logging
|
||||
import tempfile
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from pprint import pformat
|
||||
from itertools import chain
|
||||
|
||||
from .. import Pattern, PatternError
|
||||
from .. import Pattern, PatternError, Library, LibraryError
|
||||
from ..shapes import Polygon, Path
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def preflight(
|
||||
lib: Library,
|
||||
sort: bool = True,
|
||||
sort_elements: bool = False,
|
||||
allow_dangling_refs: bool | None = None,
|
||||
allow_named_layers: bool = True,
|
||||
prune_empty_patterns: bool = False,
|
||||
wrap_repeated_shapes: bool = False,
|
||||
) -> Library:
|
||||
"""
|
||||
Run a standard set of useful operations and checks, usually done immediately prior
|
||||
to writing to a file (or immediately after reading).
|
||||
|
||||
Args:
|
||||
sort: Whether to sort the patterns based on their names, and optionaly sort the pattern contents.
|
||||
Default True. Useful for reproducible builds.
|
||||
sort_elements: Whether to sort the pattern contents. Requires sort=True to run.
|
||||
allow_dangling_refs: If `None` (default), warns about any refs to patterns that are not
|
||||
in the provided library. If `True`, no check is performed; if `False`, a `LibraryError`
|
||||
is raised instead.
|
||||
allow_named_layers: If `False`, raises a `PatternError` if any layer is referred to by
|
||||
a string instead of a number (or tuple).
|
||||
prune_empty_patterns: Runs `Library.prune_empty()`, recursively deleting any empty patterns.
|
||||
wrap_repeated_shapes: Runs `Library.wrap_repeated_shapes()`, turning repeated shapes into
|
||||
repeated refs containing non-repeated shapes.
|
||||
|
||||
Returns:
|
||||
`lib` or an equivalent sorted library
|
||||
"""
|
||||
if sort:
|
||||
lib = Library(dict(sorted(
|
||||
(nn, pp.sort(sort_elements=sort_elements)) for nn, pp in lib.items()
|
||||
)))
|
||||
|
||||
if not allow_dangling_refs:
|
||||
refs = lib.referenced_patterns()
|
||||
dangling = refs - set(lib.keys())
|
||||
if dangling:
|
||||
msg = 'Dangling refs found: ' + pformat(dangling)
|
||||
if allow_dangling_refs is None:
|
||||
logger.warning(msg)
|
||||
else:
|
||||
raise LibraryError(msg)
|
||||
|
||||
if not allow_named_layers:
|
||||
named_layers: Mapping[str, set] = defaultdict(set)
|
||||
for name, pat in lib.items():
|
||||
for layer in chain(pat.shapes.keys(), pat.labels.keys()):
|
||||
if isinstance(layer, str):
|
||||
named_layers[name].add(layer)
|
||||
named_layers = dict(named_layers)
|
||||
if named_layers:
|
||||
raise PatternError('Non-numeric layers found:' + pformat(named_layers))
|
||||
|
||||
if prune_empty_patterns:
|
||||
pruned = lib.prune_empty()
|
||||
if pruned:
|
||||
logger.info(f'Preflight pruned {len(pruned)} empty patterns')
|
||||
logger.debug('Pruned: ' + pformat(pruned))
|
||||
else:
|
||||
logger.debug('Preflight found no empty patterns')
|
||||
|
||||
if wrap_repeated_shapes:
|
||||
lib.wrap_repeated_shapes()
|
||||
|
||||
return lib
|
||||
|
||||
|
||||
def mangle_name(name: str) -> str:
|
||||
"""
|
||||
Sanitize a name.
|
||||
|
@ -1,15 +1,17 @@
|
||||
from typing import Self
|
||||
from typing import Self, Any
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from .repetition import Repetition
|
||||
from .utils import rotation_matrix_2d, annotations_t
|
||||
from .utils import rotation_matrix_2d, annotations_t, annotations_eq, annotations_lt, rep2key
|
||||
from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded
|
||||
from .traits import AnnotatableImpl
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotable, Copyable):
|
||||
"""
|
||||
A text annotation with a position (but no size; it is not drawn)
|
||||
@ -64,6 +66,23 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
||||
new._offset = self._offset.copy()
|
||||
return new
|
||||
|
||||
def __lt__(self, other: 'Label') -> bool:
|
||||
if self.string != other.string:
|
||||
return self.string < other.string
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
self.string == other.string
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
||||
"""
|
||||
Rotate the label around a point.
|
||||
|
@ -5,6 +5,7 @@
|
||||
from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping
|
||||
import copy
|
||||
import logging
|
||||
import functools
|
||||
from itertools import chain
|
||||
from collections import defaultdict
|
||||
|
||||
@ -17,7 +18,8 @@ from .ref import Ref
|
||||
from .abstract import Abstract
|
||||
from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES
|
||||
from .label import Label
|
||||
from .utils import rotation_matrix_2d, annotations_t, layer_t
|
||||
from .utils import rotation_matrix_2d, annotations_t, layer_t, annotations_eq, annotations_lt, layer2key
|
||||
from .utils import ports_eq, ports_lt
|
||||
from .error import PatternError, PortError
|
||||
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
|
||||
from .ports import Port, PortList
|
||||
@ -26,6 +28,7 @@ from .ports import Port, PortList
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
"""
|
||||
2D layout consisting of some set of shapes, labels, and references to other
|
||||
@ -192,6 +195,146 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
# )
|
||||
# return new
|
||||
|
||||
def __lt__(self, other: 'Pattern') -> bool:
|
||||
self_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
|
||||
other_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
|
||||
self_tgtkeys = tuple(sorted((target is None, target) for target in self_nonempty_targets))
|
||||
other_tgtkeys = tuple(sorted((target is None, target) for target in other_nonempty_targets))
|
||||
|
||||
if self_tgtkeys != other_tgtkeys:
|
||||
return self_tgtkeys < other_tgtkeys
|
||||
|
||||
for _, target in self_tgtkeys:
|
||||
refs_ours = tuple(sorted(self.refs[target]))
|
||||
refs_theirs = tuple(sorted(other.refs[target]))
|
||||
if refs_ours != refs_theirs:
|
||||
return refs_ours < refs_theirs
|
||||
|
||||
self_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
|
||||
other_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
|
||||
self_layerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_layers))
|
||||
other_layerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_layers))
|
||||
|
||||
if self_layerkeys != other_layerkeys:
|
||||
return self_layerkeys < other_layerkeys
|
||||
|
||||
for _, _, layer in self_layerkeys:
|
||||
shapes_ours = tuple(sorted(self.shapes[layer]))
|
||||
shapes_theirs = tuple(sorted(self.shapes[layer]))
|
||||
if shapes_ours != shapes_theirs:
|
||||
return shapes_ours < shapes_theirs
|
||||
|
||||
self_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
|
||||
other_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
|
||||
self_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_txtlayers))
|
||||
other_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_txtlayers))
|
||||
|
||||
if self_txtlayerkeys != other_txtlayerkeys:
|
||||
return self_txtlayerkeys < other_txtlayerkeys
|
||||
|
||||
for _, _, layer in self_layerkeys:
|
||||
labels_ours = tuple(sorted(self.labels[layer]))
|
||||
labels_theirs = tuple(sorted(self.labels[layer]))
|
||||
if labels_ours != labels_theirs:
|
||||
return labels_ours < labels_theirs
|
||||
|
||||
if not annotations_eq(self.annotations, other.annotations):
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
if not ports_eq(self.ports, other.ports):
|
||||
return ports_lt(self.ports, other.ports)
|
||||
|
||||
return False
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if type(self) is not type(other):
|
||||
return False
|
||||
|
||||
self_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
|
||||
other_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
|
||||
self_tgtkeys = tuple(sorted((target is None, target) for target in self_nonempty_targets))
|
||||
other_tgtkeys = tuple(sorted((target is None, target) for target in other_nonempty_targets))
|
||||
|
||||
if self_tgtkeys != other_tgtkeys:
|
||||
return False
|
||||
|
||||
for _, target in self_tgtkeys:
|
||||
refs_ours = tuple(sorted(self.refs[target]))
|
||||
refs_theirs = tuple(sorted(other.refs[target]))
|
||||
if refs_ours != refs_theirs:
|
||||
return False
|
||||
|
||||
self_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
|
||||
other_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
|
||||
self_layerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_layers))
|
||||
other_layerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_layers))
|
||||
|
||||
if self_layerkeys != other_layerkeys:
|
||||
return False
|
||||
|
||||
for _, _, layer in self_layerkeys:
|
||||
shapes_ours = tuple(sorted(self.shapes[layer]))
|
||||
shapes_theirs = tuple(sorted(self.shapes[layer]))
|
||||
if shapes_ours != shapes_theirs:
|
||||
return False
|
||||
|
||||
self_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
|
||||
other_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
|
||||
self_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_txtlayers))
|
||||
other_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_txtlayers))
|
||||
|
||||
if self_txtlayerkeys != other_txtlayerkeys:
|
||||
return False
|
||||
|
||||
for _, _, layer in self_layerkeys:
|
||||
labels_ours = tuple(sorted(self.labels[layer]))
|
||||
labels_theirs = tuple(sorted(self.labels[layer]))
|
||||
if labels_ours != labels_theirs:
|
||||
return False
|
||||
|
||||
if not annotations_eq(self.annotations, other.annotations):
|
||||
return False
|
||||
|
||||
if not ports_eq(self.ports, other.ports):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def sort(self, sort_elements: bool = True) -> Self:
|
||||
"""
|
||||
Sort the element dicts (shapes, labels, refs) and (optionally) their contained lists.
|
||||
This is primarily useful for making builds more reproducible.
|
||||
|
||||
Args:
|
||||
sort_elements: Whether to sort all the shapes/labels/refs within each layer/target.
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
if sort_elements:
|
||||
def maybe_sort(xx):
|
||||
return sorted(xx)
|
||||
else:
|
||||
def maybe_sort(xx):
|
||||
return xx
|
||||
|
||||
self.refs = defaultdict(list, sorted(
|
||||
(tgt, maybe_sort(rrs)) for tgt, rrs in self.refs.items()
|
||||
))
|
||||
self.labels = defaultdict(list, sorted(
|
||||
((layer, maybe_sort(lls)) for layer, lls in self.labels.items()),
|
||||
key=lambda tt: layer2key(tt[0]),
|
||||
))
|
||||
self.shapes = defaultdict(list, sorted(
|
||||
((layer, maybe_sort(sss)) for layer, sss in self.shapes.items()),
|
||||
key=lambda tt: layer2key(tt[0]),
|
||||
))
|
||||
|
||||
self.ports = dict(sorted(self.ports.items()))
|
||||
self.annotations = dict(sorted(self.annotations.items()))
|
||||
|
||||
return self
|
||||
|
||||
def append(self, other_pattern: 'Pattern') -> Self:
|
||||
"""
|
||||
Appends all shapes, labels and refs from other_pattern to self's shapes,
|
||||
@ -436,6 +579,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
corners = (rotation_matrix_2d(ref.rotation) @ ubounds.T).T
|
||||
bounds = numpy.vstack((numpy.min(corners, axis=0),
|
||||
numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
|
||||
if ref.repetition is not None:
|
||||
bounds += ref.repetition.get_bounds()
|
||||
|
||||
else:
|
||||
# Non-manhattan rotation, have to figure out bounds by rotating the pattern
|
||||
@ -1087,7 +1232,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
ports specified by `map_out`.
|
||||
|
||||
Examples:
|
||||
=========
|
||||
======list, ===
|
||||
- `my_pat.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
||||
instantiates `subdevice` into `my_pat`, plugging ports 'A' and 'B'
|
||||
of `my_pat` into ports 'C' and 'B' of `subdevice`. The connected ports
|
||||
@ -1163,7 +1308,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
map_out[vi] = None
|
||||
|
||||
if isinstance(other, Pattern):
|
||||
assert append
|
||||
assert append, 'Got a name (not an abstract) but was asked to reference (not append)'
|
||||
|
||||
self.place(
|
||||
other,
|
||||
|
@ -1,9 +1,11 @@
|
||||
from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn
|
||||
from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn, Any
|
||||
import warnings
|
||||
import traceback
|
||||
import logging
|
||||
import functools
|
||||
from collections import Counter
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from itertools import chain
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -17,6 +19,7 @@ from .error import PortError
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
"""
|
||||
A point at which a `Device` can be snapped to another `Device`.
|
||||
@ -86,6 +89,9 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
def y(self, val: float) -> None:
|
||||
self.offset[1] = val
|
||||
|
||||
def copy(self) -> Self:
|
||||
return self.deepcopy()
|
||||
|
||||
def get_bounds(self):
|
||||
return numpy.vstack((self.offset, self.offset))
|
||||
|
||||
@ -117,6 +123,27 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
rot = str(numpy.rad2deg(self.rotation))
|
||||
return f'<{self.offset}, {rot}, [{self.ptype}]>'
|
||||
|
||||
def __lt__(self, other: 'Port') -> bool:
|
||||
if self.ptype != other.ptype:
|
||||
return self.ptype < other.ptype
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.rotation != other.rotation:
|
||||
if self.rotation is None:
|
||||
return True
|
||||
if other.rotation is None:
|
||||
return False
|
||||
return self.rotation < other.rotation
|
||||
return False
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and self.ptype == other.ptype
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and self.rotation == other.rotation
|
||||
)
|
||||
|
||||
|
||||
class PortList(metaclass=ABCMeta):
|
||||
__slots__ = () # Allow subclasses to use __slots__
|
||||
@ -246,6 +273,75 @@ class PortList(metaclass=ABCMeta):
|
||||
self.ports.update(new_ports)
|
||||
return self
|
||||
|
||||
def plugged(
|
||||
self,
|
||||
connections: dict[str, str],
|
||||
) -> Self:
|
||||
"""
|
||||
Verify that the ports specified by `connections` are coincident and have opposing
|
||||
rotations, then remove the ports.
|
||||
|
||||
This is used when ports have been "manually" aligned as part of some other routing,
|
||||
but for whatever reason were not eliminated via `plug()`.
|
||||
|
||||
Args:
|
||||
connections: Pairs of ports which "plug" each other (same offset, opposing directions)
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
`PortError` if the ports are not properly aligned.
|
||||
"""
|
||||
a_names, b_names = list(zip(*connections.items()))
|
||||
a_ports = [self.ports[pp] for pp in a_names]
|
||||
b_ports = [self.ports[pp] for pp in b_names]
|
||||
|
||||
a_types = [pp.ptype for pp in a_ports]
|
||||
b_types = [pp.ptype for pp in b_ports]
|
||||
type_conflicts = numpy.array([at != bt and at != 'unk' and bt != 'unk'
|
||||
for at, bt in zip(a_types, b_types)])
|
||||
|
||||
if type_conflicts.any():
|
||||
msg = 'Ports have conflicting types:\n'
|
||||
for nn, (k, v) in enumerate(connections.items()):
|
||||
if type_conflicts[nn]:
|
||||
msg += f'{k} | {a_types[nn]}:{b_types[nn]} | {v}\n'
|
||||
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
|
||||
a_offsets = numpy.array([pp.offset for pp in a_ports])
|
||||
b_offsets = numpy.array([pp.offset for pp in b_ports])
|
||||
a_rotations = numpy.array([pp.rotation if pp.rotation is not None else 0 for pp in a_ports])
|
||||
b_rotations = numpy.array([pp.rotation if pp.rotation is not None else 0 for pp in b_ports])
|
||||
a_has_rot = numpy.array([pp.rotation is not None for pp in a_ports], dtype=bool)
|
||||
b_has_rot = numpy.array([pp.rotation is not None for pp in b_ports], dtype=bool)
|
||||
has_rot = a_has_rot & b_has_rot
|
||||
|
||||
if has_rot.any():
|
||||
rotations = numpy.mod(a_rotations - b_rotations - pi, 2 * pi)
|
||||
rotations[~has_rot] = rotations[has_rot][0]
|
||||
|
||||
if not numpy.allclose(rotations, 0):
|
||||
rot_deg = numpy.rad2deg(rotations)
|
||||
msg = 'Port orientations do not match:\n'
|
||||
for nn, (k, v) in enumerate(connections.items()):
|
||||
if not numpy.isclose(rot_deg[nn], 0):
|
||||
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
|
||||
raise PortError(msg)
|
||||
|
||||
translations = a_offsets - b_offsets
|
||||
if not numpy.allclose(translations, 0):
|
||||
msg = 'Port translations do not match:\n'
|
||||
for nn, (k, v) in enumerate(connections.items()):
|
||||
if not numpy.allclose(translations[nn], 0):
|
||||
msg += f'{k} | {translations[nn]} | {v}\n'
|
||||
raise PortError(msg)
|
||||
|
||||
for pp in chain(a_names, b_names):
|
||||
del self.ports[pp]
|
||||
return self
|
||||
|
||||
def check_ports(
|
||||
self,
|
||||
other_names: Iterable[str],
|
||||
|
@ -2,14 +2,15 @@
|
||||
Ref provides basic support for nesting Pattern objects within each other.
|
||||
It carries offset, rotation, mirroring, and scaling data for each individual instance.
|
||||
"""
|
||||
from typing import Mapping, TYPE_CHECKING, Self
|
||||
from typing import Mapping, TYPE_CHECKING, Self, Any
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from .utils import annotations_t, rotation_matrix_2d
|
||||
from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key
|
||||
from .repetition import Repetition
|
||||
from .traits import (
|
||||
PositionableImpl, RotatableImpl, ScalableImpl,
|
||||
@ -21,6 +22,7 @@ if TYPE_CHECKING:
|
||||
from . import Pattern
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Ref(
|
||||
PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
||||
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
|
||||
@ -99,6 +101,29 @@ class Ref(
|
||||
#new.annotations = copy.deepcopy(self.annotations, memo)
|
||||
return new
|
||||
|
||||
def __lt__(self, other: 'Ref') -> bool:
|
||||
if (self.offset != other.offset).any():
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.mirrored != other.mirrored:
|
||||
return self.mirrored < other.mirrored
|
||||
if self.rotation != other.rotation:
|
||||
return self.rotation < other.rotation
|
||||
if self.scale != other.scale:
|
||||
return self.scale < other.scale
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
numpy.array_equal(self.offset, other.offset)
|
||||
and self.mirrored == other.mirrored
|
||||
and self.rotation == other.rotation
|
||||
and self.scale == other.scale
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def as_pattern(
|
||||
self,
|
||||
pattern: 'Pattern',
|
||||
|
@ -2,8 +2,9 @@
|
||||
Repetitions provide support for efficiently representing multiple identical
|
||||
instances of an object .
|
||||
"""
|
||||
from typing import Any, Type, Self, TypeVar
|
||||
from typing import Any, Type, Self, TypeVar, cast
|
||||
import copy
|
||||
import functools
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
@ -17,6 +18,7 @@ from .utils import rotation_matrix_2d
|
||||
GG = TypeVar('GG', bound='Grid')
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=ABCMeta):
|
||||
"""
|
||||
Interface common to all objects which specify repetitions
|
||||
@ -31,6 +33,14 @@ class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=A
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __le__(self, other: 'Repetition') -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
class Grid(Repetition):
|
||||
"""
|
||||
@ -270,7 +280,7 @@ class Grid(Repetition):
|
||||
return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv})>')
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, type(self)):
|
||||
if type(other) is not type(self):
|
||||
return False
|
||||
if self.a_count != other.a_count or self.b_count != other.b_count:
|
||||
return False
|
||||
@ -284,6 +294,24 @@ class Grid(Repetition):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __le__(self, other: Repetition) -> bool:
|
||||
if type(self) is not type(other):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
other = cast(Grid, other)
|
||||
if self.a_count != other.a_count:
|
||||
return self.a_count < other.a_count
|
||||
if self.b_count != other.b_count:
|
||||
return self.b_count < other.b_count
|
||||
if not numpy.array_equal(self.a_vector, other.a_vector):
|
||||
return tuple(self.a_vector) < tuple(other.a_vector)
|
||||
if self.b_vector is None:
|
||||
return other.b_vector is not None
|
||||
if other.b_vector is None:
|
||||
return False
|
||||
if not numpy.array_equal(self.b_vector, other.b_vector):
|
||||
return tuple(self.a_vector) < tuple(other.a_vector)
|
||||
return False
|
||||
|
||||
|
||||
class Arbitrary(Repetition):
|
||||
"""
|
||||
@ -325,10 +353,23 @@ class Arbitrary(Repetition):
|
||||
return (f'<Arbitrary {len(self.displacements)}pts >')
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, type(self)):
|
||||
if not type(other) is not type(self):
|
||||
return False
|
||||
return numpy.array_equal(self.displacements, other.displacements)
|
||||
|
||||
def __le__(self, other: Repetition) -> bool:
|
||||
if type(self) is not type(other):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
other = cast(Arbitrary, other)
|
||||
if self.displacements.size != other.displacements.size:
|
||||
return self.displacements.size < other.displacements.size
|
||||
|
||||
neq = (self.displacements != other.displacements)
|
||||
if neq.any():
|
||||
return self.displacements[neq][0] < other.displacements[neq][0]
|
||||
|
||||
return False
|
||||
|
||||
def rotate(self, rotation: float) -> Self:
|
||||
"""
|
||||
Rotate dispacements (around (0, 0))
|
||||
|
@ -1,5 +1,6 @@
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -8,9 +9,10 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, annotations_t
|
||||
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Arc(Shape):
|
||||
"""
|
||||
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
|
||||
@ -187,6 +189,38 @@ class Arc(Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and numpy.array_equal(self.radii, other.radii)
|
||||
and numpy.array_equal(self.angles, other.angles)
|
||||
and self.width == other.width
|
||||
and self.rotation == other.rotation
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Arc, other)
|
||||
if self.width != other.width:
|
||||
return self.width < other.width
|
||||
if not numpy.array_equal(self.radii, other.radii):
|
||||
return tuple(self.radii) < tuple(other.radii)
|
||||
if not numpy.array_equal(self.angles, other.angles):
|
||||
return tuple(self.angles) < tuple(other.angles)
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.rotation != other.rotation:
|
||||
return self.rotation < other.rotation
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
||||
|
@ -1,4 +1,6 @@
|
||||
from typing import Any, cast
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -7,9 +9,10 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, annotations_t
|
||||
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Circle(Shape):
|
||||
"""
|
||||
A circle, which has a position and radius.
|
||||
@ -67,6 +70,29 @@ class Circle(Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and self.radius == other.radius
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Circle, other)
|
||||
if not self.radius == other.radius:
|
||||
return self.radius < other.radius
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
||||
|
@ -1,6 +1,7 @@
|
||||
from typing import Any, Self
|
||||
from typing import Any, Self, cast
|
||||
import copy
|
||||
import math
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -9,9 +10,10 @@ from numpy.typing import ArrayLike, NDArray
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_t
|
||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Ellipse(Shape):
|
||||
"""
|
||||
An ellipse, which has a position, two radii, and a rotation.
|
||||
@ -117,6 +119,32 @@ class Ellipse(Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and numpy.array_equal(self.radii, other.radii)
|
||||
and self.rotation == other.rotation
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Ellipse, other)
|
||||
if not numpy.array_equal(self.radii, other.radii):
|
||||
return tuple(self.radii) < tuple(other.radii)
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.rotation != other.rotation:
|
||||
return self.rotation < other.rotation
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
|
||||
|
@ -1,5 +1,6 @@
|
||||
from typing import Sequence, Any, cast
|
||||
import copy
|
||||
import functools
|
||||
from enum import Enum
|
||||
|
||||
import numpy
|
||||
@ -9,10 +10,11 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, normalized_shape_tuple, Polygon, Circle
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, rotation_matrix_2d
|
||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
|
||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class PathCap(Enum):
|
||||
Flush = 0 # Path ends at final vertices
|
||||
Circle = 1 # Path extends past final vertices with a semicircle of radius width/2
|
||||
@ -20,7 +22,11 @@ class PathCap(Enum):
|
||||
SquareCustom = 4 # Path extends past final vertices with a rectangle of length
|
||||
# # defined by path.cap_extensions
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
return self.value == other.value
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Path(Shape):
|
||||
"""
|
||||
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
|
||||
@ -201,6 +207,40 @@ class Path(Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and numpy.array_equal(self.vertices, other.vertices)
|
||||
and self.width == other.width
|
||||
and self.cap == other.cap
|
||||
and numpy.array_equal(self.cap_extensions, other.cap_extensions) # type: ignore
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Path, other)
|
||||
if self.width != other.width:
|
||||
return self.width < other.width
|
||||
if self.cap != other.cap:
|
||||
return self.cap < other.cap
|
||||
if not numpy.array_equal(self.cap_extensions, other.cap_extensions): # type: ignore
|
||||
if other.cap_extensions is None:
|
||||
return False
|
||||
if self.cap_extensions is None:
|
||||
return True
|
||||
return tuple(self.cap_extensions) < tuple(other.cap_extensions)
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
@staticmethod
|
||||
def travel(
|
||||
travel_pairs: Sequence[tuple[float, float]],
|
||||
|
@ -1,5 +1,6 @@
|
||||
from typing import Sequence, Any, cast
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
@ -8,10 +9,11 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, normalized_shape_tuple
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, rotation_matrix_2d
|
||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
|
||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Polygon(Shape):
|
||||
"""
|
||||
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
|
||||
@ -113,6 +115,35 @@ class Polygon(Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and numpy.array_equal(self.vertices, other.vertices)
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Polygon, other)
|
||||
if not numpy.array_equal(self.vertices, other.vertices):
|
||||
min_len = min(self.vertices.shape[0], other.vertices.shape[0])
|
||||
eq_mask = self.vertices[:min_len] != other.vertices[:min_len]
|
||||
eq_lt = self.vertices[:min_len] < other.vertices[:min_len]
|
||||
eq_lt_masked = eq_lt[eq_mask]
|
||||
if eq_lt_masked.size > 0:
|
||||
return eq_lt_masked.flat[0]
|
||||
return self.vertices.shape[0] < other.vertices.shape[0]
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
@staticmethod
|
||||
def square(
|
||||
side_length: float,
|
||||
|
@ -42,6 +42,14 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
#
|
||||
# Methods (abstract)
|
||||
#
|
||||
@abstractmethod
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __lt__(self, other: 'Shape') -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def to_polygons(
|
||||
self,
|
||||
|
@ -1,5 +1,6 @@
|
||||
from typing import Self
|
||||
from typing import Self, Any, cast
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import numpy
|
||||
from numpy import pi, nan
|
||||
@ -9,13 +10,14 @@ from . import Shape, Polygon, normalized_shape_tuple
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..traits import RotatableImpl
|
||||
from ..utils import is_scalar, get_bit, annotations_t
|
||||
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
|
||||
# Loaded on use:
|
||||
# from freetype import Face
|
||||
# from matplotlib.path import Path
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Text(RotatableImpl, Shape):
|
||||
"""
|
||||
Text (to be printed e.g. as a set of polygons).
|
||||
@ -96,6 +98,38 @@ class Text(RotatableImpl, Shape):
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and self.string == other.string
|
||||
and self.height == other.height
|
||||
and self.font_path == other.font_path
|
||||
and self.rotation == other.rotation
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
)
|
||||
|
||||
def __lt__(self, other: Shape) -> bool:
|
||||
if type(self) is not type(other):
|
||||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast(Text, other)
|
||||
if not self.height == other.height:
|
||||
return self.height < other.height
|
||||
if not self.string == other.string:
|
||||
return self.string < other.string
|
||||
if not self.font_path == other.font_path:
|
||||
return self.font_path < other.font_path
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
return tuple(self.offset) < tuple(other.offset)
|
||||
if self.rotation != other.rotation:
|
||||
return self.rotation < other.rotation
|
||||
if self.repetition != other.repetition:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
num_vertices: int | None = None, # unused
|
||||
|
@ -12,6 +12,7 @@ from .vertices import (
|
||||
remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points
|
||||
)
|
||||
from .transform import rotation_matrix_2d, normalize_mirror, rotate_offsets_around
|
||||
from .comparisons import annotation2key, annotations_lt, annotations_eq, layer2key, ports_lt, ports_eq, rep2key
|
||||
|
||||
from . import ports2data
|
||||
|
||||
|
106
masque/utils/comparisons.py
Normal file
106
masque/utils/comparisons.py
Normal file
@ -0,0 +1,106 @@
|
||||
from typing import Any
|
||||
|
||||
from .types import annotations_t, layer_t
|
||||
from ..ports import Port
|
||||
from ..repetition import Repetition
|
||||
|
||||
|
||||
def annotation2key(aaa: int | float | str) -> tuple[bool, Any]:
|
||||
return (isinstance(aaa, str), aaa)
|
||||
|
||||
|
||||
def annotations_lt(aa: annotations_t, bb: annotations_t) -> bool:
|
||||
if aa is None:
|
||||
return bb is not None
|
||||
elif bb is None:
|
||||
return False
|
||||
|
||||
if len(aa) != len(bb):
|
||||
return len(aa) < len(bb)
|
||||
|
||||
keys_a = tuple(sorted(aa.keys()))
|
||||
keys_b = tuple(sorted(bb.keys()))
|
||||
if keys_a != keys_b:
|
||||
return keys_a < keys_b
|
||||
|
||||
for key in keys_a:
|
||||
va = aa[key]
|
||||
vb = bb[key]
|
||||
if len(va) != len(vb):
|
||||
return len(va) < len(vb)
|
||||
|
||||
for aaa, bbb in zip(va, vb):
|
||||
if aaa != bbb:
|
||||
return annotation2key(aaa) < annotation2key(bbb)
|
||||
return False
|
||||
|
||||
|
||||
def annotations_eq(aa: annotations_t, bb: annotations_t) -> bool:
|
||||
if aa is None:
|
||||
return bb is None
|
||||
elif bb is None:
|
||||
return False
|
||||
|
||||
if len(aa) != len(bb):
|
||||
return False
|
||||
|
||||
keys_a = tuple(sorted(aa.keys()))
|
||||
keys_b = tuple(sorted(bb.keys()))
|
||||
if keys_a != keys_b:
|
||||
return keys_a < keys_b
|
||||
|
||||
for key in keys_a:
|
||||
va = aa[key]
|
||||
vb = bb[key]
|
||||
if len(va) != len(vb):
|
||||
return False
|
||||
|
||||
for aaa, bbb in zip(va, vb):
|
||||
if aaa != bbb:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def layer2key(layer: layer_t) -> tuple[bool, bool, Any]:
|
||||
is_int = isinstance(layer, int)
|
||||
is_str = isinstance(layer, str)
|
||||
layer_tup = (layer) if (is_str or is_int) else layer
|
||||
tup = (
|
||||
is_str,
|
||||
not is_int,
|
||||
layer_tup,
|
||||
)
|
||||
return tup
|
||||
|
||||
|
||||
def rep2key(repetition: Repetition | None) -> tuple[bool, Repetition | None]:
|
||||
return (repetition is None, repetition)
|
||||
|
||||
|
||||
def ports_eq(aa: dict[str, Port], bb: dict[str, Port]) -> bool:
|
||||
if len(aa) != len(bb):
|
||||
return False
|
||||
|
||||
keys = sorted(aa.keys())
|
||||
if keys != sorted(bb.keys()):
|
||||
return False
|
||||
|
||||
return all(aa[kk] == bb[kk] for kk in keys)
|
||||
|
||||
|
||||
def ports_lt(aa: dict[str, Port], bb: dict[str, Port]) -> bool:
|
||||
if len(aa) != len(bb):
|
||||
return len(aa) < len(bb)
|
||||
|
||||
aa_keys = tuple(sorted(aa.keys()))
|
||||
bb_keys = tuple(sorted(bb.keys()))
|
||||
if aa_keys != bb_keys:
|
||||
return aa_keys < bb_keys
|
||||
|
||||
for key in aa_keys:
|
||||
pa = aa[key]
|
||||
pb = bb[key]
|
||||
if pa != pb:
|
||||
return pa < pb
|
||||
return False
|
Loading…
Reference in New Issue
Block a user