Compare commits

..

No commits in common. "6db4bb96dbf7c63d1d8b761e4abeac56485a46b1" and "b33c6325694ef0737e236071e5ea53d09bd7cc4d" have entirely different histories.

18 changed files with 31 additions and 771 deletions

View File

@ -430,49 +430,17 @@ class Pather(Builder):
**kwargs, **kwargs,
) )
def path_into( def path_into(
self, self,
portspec_src: str, portspec_src: str,
portspec_dst: str, portspec_dst: str,
*, *,
tool_port_names: tuple[str, str] = ('A', 'B'), tool_port_names: tuple[str, str] = ('A', 'B'),
out_ptype: str | None = None, out_ptype: str = 'unk',
plug_destination: bool = True, plug_destination: bool = True,
**kwargs, **kwargs,
) -> Self: ) -> 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: if self._dead:
logger.error('Skipping path_into() since device is dead') logger.error('Skipping path_into() since device is dead')
return self return self
@ -480,18 +448,15 @@ class Pather(Builder):
port_src = self.pattern[portspec_src] port_src = self.pattern[portspec_src]
port_dst = self.pattern[portspec_dst] port_dst = self.pattern[portspec_dst]
if out_ptype is None:
out_ptype = port_dst.ptype
if port_src.rotation is None: if port_src.rotation is None:
raise PortError(f'Port {portspec_src} has no rotation and cannot be used for path_into()') raise PortError(f'Port {portspec_src} has no rotation and cannot be used for path_into()')
if port_dst.rotation is None: if port_dst.rotation is None:
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()') 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): if not numpy.isclose(port_src.rotation % (pi / 2), 0):
raise BuildError('path_into was asked to route from non-manhattan port') raise BuildError('path_to was asked to route from non-manhattan port')
if not numpy.isclose(port_dst.rotation % (pi / 2), 0): if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
raise BuildError('path_into was asked to route to non-manhattan port') raise BuildError('path_to was asked to route to non-manhattan port')
src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0) src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0)
dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0) dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0)
@ -500,7 +465,7 @@ class Pather(Builder):
angle = (port_dst.rotation - port_src.rotation) % (2 * pi) 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: def get_jog(ccw: SupportsBool, length: float) -> float:
tool = self.tools.get(portspec_src, self.tools[None]) tool = self.tools.get(portspec_src, self.tools[None])
@ -546,7 +511,7 @@ class Pather(Builder):
self.path_to(portspec_src, not ccw2, y=yd - jog, **src_args) self.path_to(portspec_src, not ccw2, y=yd - jog, **src_args)
self.path_to(portspec_src, ccw2, x=xd, **dst_args) self.path_to(portspec_src, ccw2, x=xd, **dst_args)
elif numpy.isclose(angle, 0): elif numpy.isclose(angle, 0):
raise BuildError('Don\'t know how to route a U-bend at this time!') raise BuildError(f'Don\'t know how to route a U-bend at this time!')
else: else:
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}') raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
@ -679,7 +644,7 @@ class Pather(Builder):
self.library[name] = bld.pattern 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_'? return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
# TODO def bus_join()? # TODO def path_join() and def bus_join()?
def flatten(self) -> Self: def flatten(self) -> Self:
""" """

View File

@ -294,7 +294,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
ipat, iport_theirs, _iport_ours = data.in_transition ipat, iport_theirs, _iport_ours = data.in_transition
pat.plug(ipat, {port_names[1]: iport_theirs}) pat.plug(ipat, {port_names[1]: iport_theirs})
if not numpy.isclose(data.straight_length, 0): if not numpy.isclose(data.straight_length, 0):
straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length, **kwargs)} straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length)}
pat.plug(straight, {port_names[1]: sport_in}) pat.plug(straight, {port_names[1]: sport_in})
if data.ccw is not None: if data.ccw is not None:
bend, bport_in, bport_out = self.bend bend, bport_in, bport_out = self.bend

View File

@ -144,7 +144,7 @@ def writefile(
with tmpfile(path) as base_stream: with tmpfile(path) as base_stream:
streams: tuple[Any, ...] = (base_stream,) streams: tuple[Any, ...] = (base_stream,)
if path.suffix == '.gz': if path.suffix == '.gz':
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6)) stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
streams = (stream,) + streams streams = (stream,) + streams
else: else:
stream = base_stream stream = base_stream

View File

@ -1,92 +1,21 @@
""" """
Helper functions for file reading and writing Helper functions for file reading and writing
""" """
from typing import IO, Iterator, Mapping from typing import IO, Iterator
import re import re
import pathlib import pathlib
import logging import logging
import tempfile import tempfile
import shutil import shutil
from collections import defaultdict
from contextlib import contextmanager from contextlib import contextmanager
from pprint import pformat
from itertools import chain
from .. import Pattern, PatternError, Library, LibraryError from .. import Pattern, PatternError
from ..shapes import Polygon, Path from ..shapes import Polygon, Path
logger = logging.getLogger(__name__) 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: def mangle_name(name: str) -> str:
""" """
Sanitize a name. Sanitize a name.

View File

@ -1,17 +1,15 @@
from typing import Self, Any from typing import Self
import copy import copy
import functools
import numpy import numpy
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
from .repetition import Repetition from .repetition import Repetition
from .utils import rotation_matrix_2d, annotations_t, annotations_eq, annotations_lt, rep2key from .utils import rotation_matrix_2d, annotations_t
from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded
from .traits import AnnotatableImpl from .traits import AnnotatableImpl
@functools.total_ordering
class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotable, Copyable): class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotable, Copyable):
""" """
A text annotation with a position (but no size; it is not drawn) A text annotation with a position (but no size; it is not drawn)
@ -66,23 +64,6 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
new._offset = self._offset.copy() new._offset = self._offset.copy()
return new 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: def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
""" """
Rotate the label around a point. Rotate the label around a point.

View File

@ -5,7 +5,6 @@
from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping
import copy import copy
import logging import logging
import functools
from itertools import chain from itertools import chain
from collections import defaultdict from collections import defaultdict
@ -18,8 +17,7 @@ from .ref import Ref
from .abstract import Abstract from .abstract import Abstract
from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES
from .label import Label from .label import Label
from .utils import rotation_matrix_2d, annotations_t, layer_t, annotations_eq, annotations_lt, layer2key from .utils import rotation_matrix_2d, annotations_t, layer_t
from .utils import ports_eq, ports_lt
from .error import PatternError, PortError from .error import PatternError, PortError
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
from .ports import Port, PortList from .ports import Port, PortList
@ -28,7 +26,6 @@ from .ports import Port, PortList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@functools.total_ordering
class Pattern(PortList, AnnotatableImpl, Mirrorable): class Pattern(PortList, AnnotatableImpl, Mirrorable):
""" """
2D layout consisting of some set of shapes, labels, and references to other 2D layout consisting of some set of shapes, labels, and references to other
@ -195,146 +192,6 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
# ) # )
# return new # 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: def append(self, other_pattern: 'Pattern') -> Self:
""" """
Appends all shapes, labels and refs from other_pattern to self's shapes, Appends all shapes, labels and refs from other_pattern to self's shapes,
@ -579,8 +436,6 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
corners = (rotation_matrix_2d(ref.rotation) @ ubounds.T).T corners = (rotation_matrix_2d(ref.rotation) @ ubounds.T).T
bounds = numpy.vstack((numpy.min(corners, axis=0), bounds = numpy.vstack((numpy.min(corners, axis=0),
numpy.max(corners, axis=0))) * ref.scale + [ref.offset] numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
if ref.repetition is not None:
bounds += ref.repetition.get_bounds()
else: else:
# Non-manhattan rotation, have to figure out bounds by rotating the pattern # Non-manhattan rotation, have to figure out bounds by rotating the pattern
@ -1232,7 +1087,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
ports specified by `map_out`. ports specified by `map_out`.
Examples: Examples:
======list, === =========
- `my_pat.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})` - `my_pat.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
instantiates `subdevice` into `my_pat`, plugging ports 'A' and 'B' instantiates `subdevice` into `my_pat`, plugging ports 'A' and 'B'
of `my_pat` into ports 'C' and 'B' of `subdevice`. The connected ports of `my_pat` into ports 'C' and 'B' of `subdevice`. The connected ports
@ -1308,7 +1163,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
map_out[vi] = None map_out[vi] = None
if isinstance(other, Pattern): if isinstance(other, Pattern):
assert append, 'Got a name (not an abstract) but was asked to reference (not append)' assert append
self.place( self.place(
other, other,

View File

@ -1,11 +1,9 @@
from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn, Any from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn
import warnings import warnings
import traceback import traceback
import logging import logging
import functools
from collections import Counter from collections import Counter
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from itertools import chain
import numpy import numpy
from numpy import pi from numpy import pi
@ -19,7 +17,6 @@ from .error import PortError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@functools.total_ordering
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable): class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
""" """
A point at which a `Device` can be snapped to another `Device`. A point at which a `Device` can be snapped to another `Device`.
@ -89,9 +86,6 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
def y(self, val: float) -> None: def y(self, val: float) -> None:
self.offset[1] = val self.offset[1] = val
def copy(self) -> Self:
return self.deepcopy()
def get_bounds(self): def get_bounds(self):
return numpy.vstack((self.offset, self.offset)) return numpy.vstack((self.offset, self.offset))
@ -123,27 +117,6 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
rot = str(numpy.rad2deg(self.rotation)) rot = str(numpy.rad2deg(self.rotation))
return f'<{self.offset}, {rot}, [{self.ptype}]>' 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): class PortList(metaclass=ABCMeta):
__slots__ = () # Allow subclasses to use __slots__ __slots__ = () # Allow subclasses to use __slots__
@ -273,75 +246,6 @@ class PortList(metaclass=ABCMeta):
self.ports.update(new_ports) self.ports.update(new_ports)
return self 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( def check_ports(
self, self,
other_names: Iterable[str], other_names: Iterable[str],

View File

@ -2,15 +2,14 @@
Ref provides basic support for nesting Pattern objects within each other. Ref provides basic support for nesting Pattern objects within each other.
It carries offset, rotation, mirroring, and scaling data for each individual instance. It carries offset, rotation, mirroring, and scaling data for each individual instance.
""" """
from typing import Mapping, TYPE_CHECKING, Self, Any from typing import Mapping, TYPE_CHECKING, Self
import copy import copy
import functools
import numpy import numpy
from numpy import pi from numpy import pi
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key from .utils import annotations_t, rotation_matrix_2d
from .repetition import Repetition from .repetition import Repetition
from .traits import ( from .traits import (
PositionableImpl, RotatableImpl, ScalableImpl, PositionableImpl, RotatableImpl, ScalableImpl,
@ -22,7 +21,6 @@ if TYPE_CHECKING:
from . import Pattern from . import Pattern
@functools.total_ordering
class Ref( class Ref(
PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable, PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
@ -101,29 +99,6 @@ class Ref(
#new.annotations = copy.deepcopy(self.annotations, memo) #new.annotations = copy.deepcopy(self.annotations, memo)
return new 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( def as_pattern(
self, self,
pattern: 'Pattern', pattern: 'Pattern',

View File

@ -2,9 +2,8 @@
Repetitions provide support for efficiently representing multiple identical Repetitions provide support for efficiently representing multiple identical
instances of an object . instances of an object .
""" """
from typing import Any, Type, Self, TypeVar, cast from typing import Any, Type, Self, TypeVar
import copy import copy
import functools
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import numpy import numpy
@ -18,7 +17,6 @@ from .utils import rotation_matrix_2d
GG = TypeVar('GG', bound='Grid') GG = TypeVar('GG', bound='Grid')
@functools.total_ordering
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=ABCMeta): class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=ABCMeta):
""" """
Interface common to all objects which specify repetitions Interface common to all objects which specify repetitions
@ -33,14 +31,6 @@ class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=A
""" """
pass pass
@abstractmethod
def __le__(self, other: 'Repetition') -> bool:
pass
@abstractmethod
def __eq__(self, other: Any) -> bool:
pass
class Grid(Repetition): class Grid(Repetition):
""" """
@ -280,7 +270,7 @@ class Grid(Repetition):
return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv})>') return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv})>')
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
if type(other) is not type(self): if not isinstance(other, type(self)):
return False return False
if self.a_count != other.a_count or self.b_count != other.b_count: if self.a_count != other.a_count or self.b_count != other.b_count:
return False return False
@ -294,24 +284,6 @@ class Grid(Repetition):
return False return False
return True 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): class Arbitrary(Repetition):
""" """
@ -353,23 +325,10 @@ class Arbitrary(Repetition):
return (f'<Arbitrary {len(self.displacements)}pts >') return (f'<Arbitrary {len(self.displacements)}pts >')
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
if not type(other) is not type(self): if not isinstance(other, type(self)):
return False return False
return numpy.array_equal(self.displacements, other.displacements) 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: def rotate(self, rotation: float) -> Self:
""" """
Rotate dispacements (around (0, 0)) Rotate dispacements (around (0, 0))

View File

@ -1,6 +1,5 @@
from typing import Any, cast from typing import Any
import copy import copy
import functools
import numpy import numpy
from numpy import pi from numpy import pi
@ -9,10 +8,9 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, annotations_t
@functools.total_ordering
class Arc(Shape): class Arc(Shape):
""" """
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
@ -189,38 +187,6 @@ class Arc(Shape):
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
return new 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( def to_polygons(
self, self,
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES, num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,

View File

@ -1,6 +1,4 @@
from typing import Any, cast
import copy import copy
import functools
import numpy import numpy
from numpy import pi from numpy import pi
@ -9,10 +7,9 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, annotations_t
@functools.total_ordering
class Circle(Shape): class Circle(Shape):
""" """
A circle, which has a position and radius. A circle, which has a position and radius.
@ -70,29 +67,6 @@ class Circle(Shape):
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
return new 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( def to_polygons(
self, self,
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES, num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,

View File

@ -1,7 +1,6 @@
from typing import Any, Self, cast from typing import Any, Self
import copy import copy
import math import math
import functools
import numpy import numpy
from numpy import pi from numpy import pi
@ -10,10 +9,9 @@ from numpy.typing import ArrayLike, NDArray
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, rotation_matrix_2d, annotations_t
@functools.total_ordering
class Ellipse(Shape): class Ellipse(Shape):
""" """
An ellipse, which has a position, two radii, and a rotation. An ellipse, which has a position, two radii, and a rotation.
@ -119,32 +117,6 @@ class Ellipse(Shape):
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
return new 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( def to_polygons(
self, self,
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES, num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,

View File

@ -1,6 +1,5 @@
from typing import Sequence, Any, cast from typing import Sequence, Any, cast
import copy import copy
import functools
from enum import Enum from enum import Enum
import numpy import numpy
@ -10,11 +9,10 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, normalized_shape_tuple, Polygon, Circle from . import Shape, normalized_shape_tuple, Polygon, Circle
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, rotation_matrix_2d
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
@functools.total_ordering
class PathCap(Enum): class PathCap(Enum):
Flush = 0 # Path ends at final vertices Flush = 0 # Path ends at final vertices
Circle = 1 # Path extends past final vertices with a semicircle of radius width/2 Circle = 1 # Path extends past final vertices with a semicircle of radius width/2
@ -22,11 +20,7 @@ class PathCap(Enum):
SquareCustom = 4 # Path extends past final vertices with a rectangle of length SquareCustom = 4 # Path extends past final vertices with a rectangle of length
# # defined by path.cap_extensions # # defined by path.cap_extensions
def __lt__(self, other: Any) -> bool:
return self.value == other.value
@functools.total_ordering
class Path(Shape): class Path(Shape):
""" """
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
@ -207,40 +201,6 @@ class Path(Shape):
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
return new 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 @staticmethod
def travel( def travel(
travel_pairs: Sequence[tuple[float, float]], travel_pairs: Sequence[tuple[float, float]],

View File

@ -1,6 +1,5 @@
from typing import Sequence, Any, cast from typing import Sequence, Any, cast
import copy import copy
import functools
import numpy import numpy
from numpy import pi from numpy import pi
@ -9,11 +8,10 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, normalized_shape_tuple from . import Shape, normalized_shape_tuple
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, rotation_matrix_2d
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
@functools.total_ordering
class Polygon(Shape): class Polygon(Shape):
""" """
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
@ -115,35 +113,6 @@ class Polygon(Shape):
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
return new 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 @staticmethod
def square( def square(
side_length: float, side_length: float,

View File

@ -42,14 +42,6 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
# #
# Methods (abstract) # Methods (abstract)
# #
@abstractmethod
def __eq__(self, other: Any) -> bool:
pass
@abstractmethod
def __lt__(self, other: 'Shape') -> bool:
pass
@abstractmethod @abstractmethod
def to_polygons( def to_polygons(
self, self,

View File

@ -1,6 +1,5 @@
from typing import Self, Any, cast from typing import Self
import copy import copy
import functools
import numpy import numpy
from numpy import pi, nan from numpy import pi, nan
@ -10,14 +9,13 @@ from . import Shape, Polygon, normalized_shape_tuple
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..traits import RotatableImpl from ..traits import RotatableImpl
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, get_bit, annotations_t
# Loaded on use: # Loaded on use:
# from freetype import Face # from freetype import Face
# from matplotlib.path import Path # from matplotlib.path import Path
@functools.total_ordering
class Text(RotatableImpl, Shape): class Text(RotatableImpl, Shape):
""" """
Text (to be printed e.g. as a set of polygons). Text (to be printed e.g. as a set of polygons).
@ -98,38 +96,6 @@ class Text(RotatableImpl, Shape):
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
return new 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( def to_polygons(
self, self,
num_vertices: int | None = None, # unused num_vertices: int | None = None, # unused

View File

@ -12,7 +12,6 @@ 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, rotate_offsets_around 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 from . import ports2data

View File

@ -1,106 +0,0 @@
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