Compare commits

...

9 Commits

Author SHA1 Message Date
jan
6db4bb96db 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
2024-06-03 17:00:20 -07:00
jan
94aa853a49 add plugged() for manually-aligned ports 2024-06-03 16:57:07 -07:00
bb054b9eee port .copy() should deepcopy 2024-06-03 16:54:25 -07:00
5fb736eb74 add a more descriptive error message 2024-06-03 16:54:15 -07:00
4334d0d50b fix bounds calculation for arrays with manhattan rotation 2024-06-03 16:54:02 -07:00
31863c9799 reduce compression level to improve speed 2024-06-03 16:53:14 -07:00
30982d742b make sure kwargs get passed into gen_straight() 2024-06-03 16:53:03 -07:00
447d4ba35b improve path_into docs and error messages 2024-06-03 16:52:34 -07:00
70a51ed8ef path_into should use destination port's ptype by default 2024-06-03 16:26:12 -07:00
18 changed files with 771 additions and 31 deletions

View File

@ -430,17 +430,49 @@ class Pather(Builder):
**kwargs,
)
def path_into(
self,
portspec_src: str,
portspec_dst: str,
*,
tool_port_names: tuple[str, str] = ('A', 'B'),
out_ptype: str = 'unk',
out_ptype: str | None = None,
plug_destination: bool = True,
**kwargs,
) -> Self:
"""
Create a "wire"/"waveguide" and traveling between the ports `portspec_src` and
`portspec_dst`, and `plug` it into both (or just the source port).
Only unambiguous scenarios are allowed:
- Straight connector between facing ports
- Single 90 degree bend
- Jog between facing ports
(jog is done as late as possible, i.e. only 2 L-shaped segments are used)
By default, the destination's `pytpe` will be used as the `out_ptype` for the
wire, and the `portspec_dst` will be plugged (i.e. removed).
Args:
portspec_src: The name of the starting port into which the wire will be plugged.
portspec_dst: The name of the destination port.
tool_port_names: The names of the ports on the generated pattern. It is unlikely
that you will need to change these. The first port is the input (to be
connected to `portspec`).
out_ptype: Passed to the pathing tool in order to specify the desired port type
to be generated at the destination end. If `None` (default), the destination
port's `ptype` will be used.
Returns:
self
Raises:
PortError if either port does not have a specified rotation.
BuildError if and invalid port config is encountered:
- Non-manhattan ports
- U-bend
- Destination too close to (or behind) source
"""
if self._dead:
logger.error('Skipping path_into() since device is dead')
return self
@ -448,15 +480,18 @@ class Pather(Builder):
port_src = self.pattern[portspec_src]
port_dst = self.pattern[portspec_dst]
if out_ptype is None:
out_ptype = port_dst.ptype
if port_src.rotation is None:
raise PortError(f'Port {portspec_src} has no rotation and cannot be used for path_into()')
if port_dst.rotation is None:
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()')
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
raise BuildError('path_to was asked to route from non-manhattan port')
raise BuildError('path_into was asked to route from non-manhattan port')
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
raise BuildError('path_to was asked to route to non-manhattan port')
raise BuildError('path_into was asked to route to non-manhattan port')
src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0)
dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0)
@ -465,7 +500,7 @@ class Pather(Builder):
angle = (port_dst.rotation - port_src.rotation) % (2 * pi)
src_ne = port_src.rotation % (2 * pi) > (3 * pi /4) # path from src will go north or east
src_ne = port_src.rotation % (2 * pi) > (3 * pi / 4) # path from src will go north or east
def get_jog(ccw: SupportsBool, length: float) -> float:
tool = self.tools.get(portspec_src, self.tools[None])
@ -511,7 +546,7 @@ class Pather(Builder):
self.path_to(portspec_src, not ccw2, y=yd - jog, **src_args)
self.path_to(portspec_src, ccw2, x=xd, **dst_args)
elif numpy.isclose(angle, 0):
raise BuildError(f'Don\'t know how to route a U-bend at this time!')
raise BuildError('Don\'t know how to route a U-bend at this time!')
else:
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
@ -644,7 +679,7 @@ class Pather(Builder):
self.library[name] = bld.pattern
return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
# TODO def path_join() and def bus_join()?
# TODO def bus_join()?
def flatten(self) -> Self:
"""

View File

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

View File

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

View File

@ -1,21 +1,92 @@
"""
Helper functions for file reading and writing
"""
from typing import IO, Iterator
from typing import IO, Iterator, Mapping
import re
import pathlib
import logging
import tempfile
import shutil
from collections import defaultdict
from contextlib import contextmanager
from pprint import pformat
from itertools import chain
from .. import Pattern, PatternError
from .. import Pattern, PatternError, Library, LibraryError
from ..shapes import Polygon, Path
logger = logging.getLogger(__name__)
def preflight(
lib: Library,
sort: bool = True,
sort_elements: bool = False,
allow_dangling_refs: bool | None = None,
allow_named_layers: bool = True,
prune_empty_patterns: bool = False,
wrap_repeated_shapes: bool = False,
) -> Library:
"""
Run a standard set of useful operations and checks, usually done immediately prior
to writing to a file (or immediately after reading).
Args:
sort: Whether to sort the patterns based on their names, and optionaly sort the pattern contents.
Default True. Useful for reproducible builds.
sort_elements: Whether to sort the pattern contents. Requires sort=True to run.
allow_dangling_refs: If `None` (default), warns about any refs to patterns that are not
in the provided library. If `True`, no check is performed; if `False`, a `LibraryError`
is raised instead.
allow_named_layers: If `False`, raises a `PatternError` if any layer is referred to by
a string instead of a number (or tuple).
prune_empty_patterns: Runs `Library.prune_empty()`, recursively deleting any empty patterns.
wrap_repeated_shapes: Runs `Library.wrap_repeated_shapes()`, turning repeated shapes into
repeated refs containing non-repeated shapes.
Returns:
`lib` or an equivalent sorted library
"""
if sort:
lib = Library(dict(sorted(
(nn, pp.sort(sort_elements=sort_elements)) for nn, pp in lib.items()
)))
if not allow_dangling_refs:
refs = lib.referenced_patterns()
dangling = refs - set(lib.keys())
if dangling:
msg = 'Dangling refs found: ' + pformat(dangling)
if allow_dangling_refs is None:
logger.warning(msg)
else:
raise LibraryError(msg)
if not allow_named_layers:
named_layers: Mapping[str, set] = defaultdict(set)
for name, pat in lib.items():
for layer in chain(pat.shapes.keys(), pat.labels.keys()):
if isinstance(layer, str):
named_layers[name].add(layer)
named_layers = dict(named_layers)
if named_layers:
raise PatternError('Non-numeric layers found:' + pformat(named_layers))
if prune_empty_patterns:
pruned = lib.prune_empty()
if pruned:
logger.info(f'Preflight pruned {len(pruned)} empty patterns')
logger.debug('Pruned: ' + pformat(pruned))
else:
logger.debug('Preflight found no empty patterns')
if wrap_repeated_shapes:
lib.wrap_repeated_shapes()
return lib
def mangle_name(name: str) -> str:
"""
Sanitize a name.

View File

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

View File

@ -5,6 +5,7 @@
from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping
import copy
import logging
import functools
from itertools import chain
from collections import defaultdict
@ -17,7 +18,8 @@ from .ref import Ref
from .abstract import Abstract
from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES
from .label import Label
from .utils import rotation_matrix_2d, annotations_t, layer_t
from .utils import rotation_matrix_2d, annotations_t, layer_t, annotations_eq, annotations_lt, layer2key
from .utils import ports_eq, ports_lt
from .error import PatternError, PortError
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
from .ports import Port, PortList
@ -26,6 +28,7 @@ from .ports import Port, PortList
logger = logging.getLogger(__name__)
@functools.total_ordering
class Pattern(PortList, AnnotatableImpl, Mirrorable):
"""
2D layout consisting of some set of shapes, labels, and references to other
@ -192,6 +195,146 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
# )
# return new
def __lt__(self, other: 'Pattern') -> bool:
self_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
other_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
self_tgtkeys = tuple(sorted((target is None, target) for target in self_nonempty_targets))
other_tgtkeys = tuple(sorted((target is None, target) for target in other_nonempty_targets))
if self_tgtkeys != other_tgtkeys:
return self_tgtkeys < other_tgtkeys
for _, target in self_tgtkeys:
refs_ours = tuple(sorted(self.refs[target]))
refs_theirs = tuple(sorted(other.refs[target]))
if refs_ours != refs_theirs:
return refs_ours < refs_theirs
self_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
other_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
self_layerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_layers))
other_layerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_layers))
if self_layerkeys != other_layerkeys:
return self_layerkeys < other_layerkeys
for _, _, layer in self_layerkeys:
shapes_ours = tuple(sorted(self.shapes[layer]))
shapes_theirs = tuple(sorted(self.shapes[layer]))
if shapes_ours != shapes_theirs:
return shapes_ours < shapes_theirs
self_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
other_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
self_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_txtlayers))
other_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_txtlayers))
if self_txtlayerkeys != other_txtlayerkeys:
return self_txtlayerkeys < other_txtlayerkeys
for _, _, layer in self_layerkeys:
labels_ours = tuple(sorted(self.labels[layer]))
labels_theirs = tuple(sorted(self.labels[layer]))
if labels_ours != labels_theirs:
return labels_ours < labels_theirs
if not annotations_eq(self.annotations, other.annotations):
return annotations_lt(self.annotations, other.annotations)
if not ports_eq(self.ports, other.ports):
return ports_lt(self.ports, other.ports)
return False
def __eq__(self, other: Any) -> bool:
if type(self) is not type(other):
return False
self_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
other_nonempty_targets = [target for target, reflist in self.refs.items() if reflist]
self_tgtkeys = tuple(sorted((target is None, target) for target in self_nonempty_targets))
other_tgtkeys = tuple(sorted((target is None, target) for target in other_nonempty_targets))
if self_tgtkeys != other_tgtkeys:
return False
for _, target in self_tgtkeys:
refs_ours = tuple(sorted(self.refs[target]))
refs_theirs = tuple(sorted(other.refs[target]))
if refs_ours != refs_theirs:
return False
self_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
other_nonempty_layers = [ll for ll, elems in self.shapes.items() if elems]
self_layerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_layers))
other_layerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_layers))
if self_layerkeys != other_layerkeys:
return False
for _, _, layer in self_layerkeys:
shapes_ours = tuple(sorted(self.shapes[layer]))
shapes_theirs = tuple(sorted(self.shapes[layer]))
if shapes_ours != shapes_theirs:
return False
self_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
other_nonempty_txtlayers = [ll for ll, elems in self.labels.items() if elems]
self_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in self_nonempty_txtlayers))
other_txtlayerkeys = tuple(sorted(layer2key(ll) for ll in other_nonempty_txtlayers))
if self_txtlayerkeys != other_txtlayerkeys:
return False
for _, _, layer in self_layerkeys:
labels_ours = tuple(sorted(self.labels[layer]))
labels_theirs = tuple(sorted(self.labels[layer]))
if labels_ours != labels_theirs:
return False
if not annotations_eq(self.annotations, other.annotations):
return False
if not ports_eq(self.ports, other.ports):
return False
return True
def sort(self, sort_elements: bool = True) -> Self:
"""
Sort the element dicts (shapes, labels, refs) and (optionally) their contained lists.
This is primarily useful for making builds more reproducible.
Args:
sort_elements: Whether to sort all the shapes/labels/refs within each layer/target.
Returns:
self
"""
if sort_elements:
def maybe_sort(xx):
return sorted(xx)
else:
def maybe_sort(xx):
return xx
self.refs = defaultdict(list, sorted(
(tgt, maybe_sort(rrs)) for tgt, rrs in self.refs.items()
))
self.labels = defaultdict(list, sorted(
((layer, maybe_sort(lls)) for layer, lls in self.labels.items()),
key=lambda tt: layer2key(tt[0]),
))
self.shapes = defaultdict(list, sorted(
((layer, maybe_sort(sss)) for layer, sss in self.shapes.items()),
key=lambda tt: layer2key(tt[0]),
))
self.ports = dict(sorted(self.ports.items()))
self.annotations = dict(sorted(self.annotations.items()))
return self
def append(self, other_pattern: 'Pattern') -> Self:
"""
Appends all shapes, labels and refs from other_pattern to self's shapes,
@ -436,6 +579,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
corners = (rotation_matrix_2d(ref.rotation) @ ubounds.T).T
bounds = numpy.vstack((numpy.min(corners, axis=0),
numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
if ref.repetition is not None:
bounds += ref.repetition.get_bounds()
else:
# Non-manhattan rotation, have to figure out bounds by rotating the pattern
@ -1087,7 +1232,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
ports specified by `map_out`.
Examples:
=========
======list, ===
- `my_pat.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
instantiates `subdevice` into `my_pat`, plugging ports 'A' and 'B'
of `my_pat` into ports 'C' and 'B' of `subdevice`. The connected ports
@ -1163,7 +1308,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
map_out[vi] = None
if isinstance(other, Pattern):
assert append
assert append, 'Got a name (not an abstract) but was asked to reference (not append)'
self.place(
other,

View File

@ -1,9 +1,11 @@
from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn
from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn, Any
import warnings
import traceback
import logging
import functools
from collections import Counter
from abc import ABCMeta, abstractmethod
from itertools import chain
import numpy
from numpy import pi
@ -17,6 +19,7 @@ from .error import PortError
logger = logging.getLogger(__name__)
@functools.total_ordering
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
"""
A point at which a `Device` can be snapped to another `Device`.
@ -86,6 +89,9 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
def y(self, val: float) -> None:
self.offset[1] = val
def copy(self) -> Self:
return self.deepcopy()
def get_bounds(self):
return numpy.vstack((self.offset, self.offset))
@ -117,6 +123,27 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
rot = str(numpy.rad2deg(self.rotation))
return f'<{self.offset}, {rot}, [{self.ptype}]>'
def __lt__(self, other: 'Port') -> bool:
if self.ptype != other.ptype:
return self.ptype < other.ptype
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.rotation != other.rotation:
if self.rotation is None:
return True
if other.rotation is None:
return False
return self.rotation < other.rotation
return False
def __eq__(self, other: Any) -> bool:
return (
type(self) is type(other)
and self.ptype == other.ptype
and numpy.array_equal(self.offset, other.offset)
and self.rotation == other.rotation
)
class PortList(metaclass=ABCMeta):
__slots__ = () # Allow subclasses to use __slots__
@ -246,6 +273,75 @@ class PortList(metaclass=ABCMeta):
self.ports.update(new_ports)
return self
def plugged(
self,
connections: dict[str, str],
) -> Self:
"""
Verify that the ports specified by `connections` are coincident and have opposing
rotations, then remove the ports.
This is used when ports have been "manually" aligned as part of some other routing,
but for whatever reason were not eliminated via `plug()`.
Args:
connections: Pairs of ports which "plug" each other (same offset, opposing directions)
Returns:
self
Raises:
`PortError` if the ports are not properly aligned.
"""
a_names, b_names = list(zip(*connections.items()))
a_ports = [self.ports[pp] for pp in a_names]
b_ports = [self.ports[pp] for pp in b_names]
a_types = [pp.ptype for pp in a_ports]
b_types = [pp.ptype for pp in b_ports]
type_conflicts = numpy.array([at != bt and at != 'unk' and bt != 'unk'
for at, bt in zip(a_types, b_types)])
if type_conflicts.any():
msg = 'Ports have conflicting types:\n'
for nn, (k, v) in enumerate(connections.items()):
if type_conflicts[nn]:
msg += f'{k} | {a_types[nn]}:{b_types[nn]} | {v}\n'
msg = ''.join(traceback.format_stack()) + '\n' + msg
warnings.warn(msg, stacklevel=2)
a_offsets = numpy.array([pp.offset for pp in a_ports])
b_offsets = numpy.array([pp.offset for pp in b_ports])
a_rotations = numpy.array([pp.rotation if pp.rotation is not None else 0 for pp in a_ports])
b_rotations = numpy.array([pp.rotation if pp.rotation is not None else 0 for pp in b_ports])
a_has_rot = numpy.array([pp.rotation is not None for pp in a_ports], dtype=bool)
b_has_rot = numpy.array([pp.rotation is not None for pp in b_ports], dtype=bool)
has_rot = a_has_rot & b_has_rot
if has_rot.any():
rotations = numpy.mod(a_rotations - b_rotations - pi, 2 * pi)
rotations[~has_rot] = rotations[has_rot][0]
if not numpy.allclose(rotations, 0):
rot_deg = numpy.rad2deg(rotations)
msg = 'Port orientations do not match:\n'
for nn, (k, v) in enumerate(connections.items()):
if not numpy.isclose(rot_deg[nn], 0):
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
raise PortError(msg)
translations = a_offsets - b_offsets
if not numpy.allclose(translations, 0):
msg = 'Port translations do not match:\n'
for nn, (k, v) in enumerate(connections.items()):
if not numpy.allclose(translations[nn], 0):
msg += f'{k} | {translations[nn]} | {v}\n'
raise PortError(msg)
for pp in chain(a_names, b_names):
del self.ports[pp]
return self
def check_ports(
self,
other_names: Iterable[str],

View File

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

View File

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

View File

@ -1,5 +1,6 @@
from typing import Any
from typing import Any, cast
import copy
import functools
import numpy
from numpy import pi
@ -8,9 +9,10 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError
from ..repetition import Repetition
from ..utils import is_scalar, annotations_t
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
@functools.total_ordering
class Arc(Shape):
"""
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
@ -187,6 +189,38 @@ class Arc(Shape):
new._annotations = copy.deepcopy(self._annotations)
return new
def __eq__(self, other: Any) -> bool:
return (
type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and numpy.array_equal(self.radii, other.radii)
and numpy.array_equal(self.angles, other.angles)
and self.width == other.width
and self.rotation == other.rotation
and self.repetition == other.repetition
and annotations_eq(self.annotations, other.annotations)
)
def __lt__(self, other: Shape) -> bool:
if type(self) is not type(other):
if repr(type(self)) != repr(type(other)):
return repr(type(self)) < repr(type(other))
return id(type(self)) < id(type(other))
other = cast(Arc, other)
if self.width != other.width:
return self.width < other.width
if not numpy.array_equal(self.radii, other.radii):
return tuple(self.radii) < tuple(other.radii)
if not numpy.array_equal(self.angles, other.angles):
return tuple(self.angles) < tuple(other.angles)
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.rotation != other.rotation:
return self.rotation < other.rotation
if self.repetition != other.repetition:
return rep2key(self.repetition) < rep2key(other.repetition)
return annotations_lt(self.annotations, other.annotations)
def to_polygons(
self,
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,

View File

@ -1,4 +1,6 @@
from typing import Any, cast
import copy
import functools
import numpy
from numpy import pi
@ -7,9 +9,10 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError
from ..repetition import Repetition
from ..utils import is_scalar, annotations_t
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
@functools.total_ordering
class Circle(Shape):
"""
A circle, which has a position and radius.
@ -67,6 +70,29 @@ class Circle(Shape):
new._annotations = copy.deepcopy(self._annotations)
return new
def __eq__(self, other: Any) -> bool:
return (
type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and self.radius == other.radius
and self.repetition == other.repetition
and annotations_eq(self.annotations, other.annotations)
)
def __lt__(self, other: Shape) -> bool:
if type(self) is not type(other):
if repr(type(self)) != repr(type(other)):
return repr(type(self)) < repr(type(other))
return id(type(self)) < id(type(other))
other = cast(Circle, other)
if not self.radius == other.radius:
return self.radius < other.radius
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.repetition != other.repetition:
return rep2key(self.repetition) < rep2key(other.repetition)
return annotations_lt(self.annotations, other.annotations)
def to_polygons(
self,
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,

View File

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

View File

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

View File

@ -1,5 +1,6 @@
from typing import Sequence, Any, cast
import copy
import functools
import numpy
from numpy import pi
@ -8,10 +9,11 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, normalized_shape_tuple
from ..error import PatternError
from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
@functools.total_ordering
class Polygon(Shape):
"""
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
@ -113,6 +115,35 @@ class Polygon(Shape):
new._annotations = copy.deepcopy(self._annotations)
return new
def __eq__(self, other: Any) -> bool:
return (
type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and numpy.array_equal(self.vertices, other.vertices)
and self.repetition == other.repetition
and annotations_eq(self.annotations, other.annotations)
)
def __lt__(self, other: Shape) -> bool:
if type(self) is not type(other):
if repr(type(self)) != repr(type(other)):
return repr(type(self)) < repr(type(other))
return id(type(self)) < id(type(other))
other = cast(Polygon, other)
if not numpy.array_equal(self.vertices, other.vertices):
min_len = min(self.vertices.shape[0], other.vertices.shape[0])
eq_mask = self.vertices[:min_len] != other.vertices[:min_len]
eq_lt = self.vertices[:min_len] < other.vertices[:min_len]
eq_lt_masked = eq_lt[eq_mask]
if eq_lt_masked.size > 0:
return eq_lt_masked.flat[0]
return self.vertices.shape[0] < other.vertices.shape[0]
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.repetition != other.repetition:
return rep2key(self.repetition) < rep2key(other.repetition)
return annotations_lt(self.annotations, other.annotations)
@staticmethod
def square(
side_length: float,

View File

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

View File

@ -1,5 +1,6 @@
from typing import Self
from typing import Self, Any, cast
import copy
import functools
import numpy
from numpy import pi, nan
@ -9,13 +10,14 @@ from . import Shape, Polygon, normalized_shape_tuple
from ..error import PatternError
from ..repetition import Repetition
from ..traits import RotatableImpl
from ..utils import is_scalar, get_bit, annotations_t
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key
# Loaded on use:
# from freetype import Face
# from matplotlib.path import Path
@functools.total_ordering
class Text(RotatableImpl, Shape):
"""
Text (to be printed e.g. as a set of polygons).
@ -96,6 +98,38 @@ class Text(RotatableImpl, Shape):
new._annotations = copy.deepcopy(self._annotations)
return new
def __eq__(self, other: Any) -> bool:
return (
type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and self.string == other.string
and self.height == other.height
and self.font_path == other.font_path
and self.rotation == other.rotation
and self.repetition == other.repetition
and annotations_eq(self.annotations, other.annotations)
)
def __lt__(self, other: Shape) -> bool:
if type(self) is not type(other):
if repr(type(self)) != repr(type(other)):
return repr(type(self)) < repr(type(other))
return id(type(self)) < id(type(other))
other = cast(Text, other)
if not self.height == other.height:
return self.height < other.height
if not self.string == other.string:
return self.string < other.string
if not self.font_path == other.font_path:
return self.font_path < other.font_path
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.rotation != other.rotation:
return self.rotation < other.rotation
if self.repetition != other.repetition:
return rep2key(self.repetition) < rep2key(other.repetition)
return annotations_lt(self.annotations, other.annotations)
def to_polygons(
self,
num_vertices: int | None = None, # unused

View File

@ -12,6 +12,7 @@ from .vertices import (
remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points
)
from .transform import rotation_matrix_2d, normalize_mirror, rotate_offsets_around
from .comparisons import annotation2key, annotations_lt, annotations_eq, layer2key, ports_lt, ports_eq, rep2key
from . import ports2data

106
masque/utils/comparisons.py Normal file
View 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