Create an ordering for everything
In order to make layouts more reproducible Also add pattern.sort() and file.utils.preflight_check() optionally don't sort elements elements aren't re-ordered that often, sorting them is slow, and the sort criteria are arbitrary, so we might want to only sort stuff by name
This commit is contained in:
parent
94aa853a49
commit
6db4bb96db
@ -430,7 +430,6 @@ class Pather(Builder):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def path_into(
|
def path_into(
|
||||||
self,
|
self,
|
||||||
portspec_src: str,
|
portspec_src: str,
|
||||||
@ -547,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}')
|
||||||
|
|
||||||
|
@ -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,
|
||||||
@ -1089,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
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
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
|
from itertools import chain
|
||||||
@ -18,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`.
|
||||||
@ -121,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__
|
||||||
|
@ -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