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,
|
**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 = 'unk',
|
out_ptype: str | None = None,
|
||||||
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
|
||||||
@ -448,15 +480,18 @@ 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_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):
|
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)
|
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)
|
||||||
@ -465,7 +500,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])
|
||||||
@ -511,7 +546,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(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:
|
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}')
|
||||||
|
|
||||||
@ -644,7 +679,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 path_join() and def bus_join()?
|
# TODO def bus_join()?
|
||||||
|
|
||||||
def flatten(self) -> Self:
|
def flatten(self) -> Self:
|
||||||
"""
|
"""
|
||||||
|
@ -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)}
|
straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length, **kwargs)}
|
||||||
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
|
||||||
|
@ -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'))
|
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6))
|
||||||
streams = (stream,) + streams
|
streams = (stream,) + streams
|
||||||
else:
|
else:
|
||||||
stream = base_stream
|
stream = base_stream
|
||||||
|
@ -1,21 +1,92 @@
|
|||||||
"""
|
"""
|
||||||
Helper functions for file reading and writing
|
Helper functions for file reading and writing
|
||||||
"""
|
"""
|
||||||
from typing import IO, Iterator
|
from typing import IO, Iterator, Mapping
|
||||||
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
|
from .. import Pattern, PatternError, Library, LibraryError
|
||||||
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.
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
from typing import Self
|
from typing import Self, Any
|
||||||
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
|
from .utils import rotation_matrix_2d, annotations_t, annotations_eq, annotations_lt, rep2key
|
||||||
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)
|
||||||
@ -64,6 +66,23 @@ 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.
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
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
|
||||||
|
|
||||||
@ -17,7 +18,8 @@ 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
|
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 .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
|
||||||
@ -26,6 +28,7 @@ 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
|
||||||
@ -192,6 +195,146 @@ 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,
|
||||||
@ -436,6 +579,8 @@ 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
|
||||||
@ -1087,7 +1232,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
|
||||||
@ -1163,7 +1308,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
map_out[vi] = None
|
map_out[vi] = None
|
||||||
|
|
||||||
if isinstance(other, Pattern):
|
if isinstance(other, Pattern):
|
||||||
assert append
|
assert append, 'Got a name (not an abstract) but was asked to reference (not append)'
|
||||||
|
|
||||||
self.place(
|
self.place(
|
||||||
other,
|
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 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
|
||||||
@ -17,6 +19,7 @@ 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`.
|
||||||
@ -86,6 +89,9 @@ 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))
|
||||||
|
|
||||||
@ -117,6 +123,27 @@ 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__
|
||||||
@ -246,6 +273,75 @@ 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],
|
||||||
|
@ -2,14 +2,15 @@
|
|||||||
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
|
from typing import Mapping, TYPE_CHECKING, Self, Any
|
||||||
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
|
from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key
|
||||||
from .repetition import Repetition
|
from .repetition import Repetition
|
||||||
from .traits import (
|
from .traits import (
|
||||||
PositionableImpl, RotatableImpl, ScalableImpl,
|
PositionableImpl, RotatableImpl, ScalableImpl,
|
||||||
@ -21,6 +22,7 @@ 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,
|
||||||
@ -99,6 +101,29 @@ 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',
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
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
|
from typing import Any, Type, Self, TypeVar, cast
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -17,6 +18,7 @@ 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
|
||||||
@ -31,6 +33,14 @@ 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):
|
||||||
"""
|
"""
|
||||||
@ -270,7 +280,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 not isinstance(other, type(self)):
|
if type(other) is not 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
|
||||||
@ -284,6 +294,24 @@ 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):
|
||||||
"""
|
"""
|
||||||
@ -325,10 +353,23 @@ 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 isinstance(other, type(self)):
|
if not type(other) is not 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))
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from typing import Any
|
from typing import Any, cast
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
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 . 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
|
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
@ -187,6 +189,38 @@ 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,
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
from typing import Any, cast
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
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 . 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
|
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
|
||||||
|
|
||||||
|
@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.
|
||||||
@ -67,6 +70,29 @@ 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,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from typing import Any, Self
|
from typing import Any, Self, cast
|
||||||
import copy
|
import copy
|
||||||
import math
|
import math
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
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 . 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
|
from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
|
||||||
|
|
||||||
|
@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.
|
||||||
@ -117,6 +119,32 @@ 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,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@ -9,10 +10,11 @@ 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
|
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
|
||||||
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
|
||||||
@ -20,7 +22,11 @@ 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,
|
||||||
@ -201,6 +207,40 @@ 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]],
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@ -8,10 +9,11 @@ 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
|
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
|
||||||
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
|
||||||
@ -113,6 +115,35 @@ 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,
|
||||||
|
@ -42,6 +42,14 @@ 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,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from typing import Self
|
from typing import Self, Any, cast
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi, nan
|
from numpy import pi, nan
|
||||||
@ -9,13 +10,14 @@ 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
|
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
|
||||||
# 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).
|
||||||
@ -96,6 +98,38 @@ 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
|
||||||
|
@ -12,6 +12,7 @@ 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
|
||||||
|
|
||||||
|
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