diff --git a/masque/builder/pather.py b/masque/builder/pather.py index c795e75..11e4d80 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -430,49 +430,17 @@ 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 | None = None, + out_ptype: str = 'unk', 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 @@ -480,18 +448,15 @@ 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_into was asked to route from non-manhattan port') + raise BuildError('path_to was asked to route from non-manhattan port') if not numpy.isclose(port_dst.rotation % (pi / 2), 0): - raise BuildError('path_into was asked to route to non-manhattan port') + raise BuildError('path_to was asked to route to non-manhattan port') src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0) dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0) @@ -500,7 +465,7 @@ class Pather(Builder): angle = (port_dst.rotation - port_src.rotation) % (2 * pi) - 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]) @@ -546,7 +511,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('Don\'t know how to route a U-bend at this time!') + raise BuildError(f'Don\'t know how to route a U-bend at this time!') else: raise BuildError(f'Don\'t know how to route ports with relative angle {angle}') @@ -679,7 +644,7 @@ class Pather(Builder): self.library[name] = bld.pattern return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'? - # TODO def bus_join()? + # TODO def path_join() and def bus_join()? def flatten(self) -> Self: """ diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 1b7a74f..3733ad1 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -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, **kwargs)} + straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length)} pat.plug(straight, {port_names[1]: sport_in}) if data.ccw is not None: bend, bport_in, bport_out = self.bend diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 4dce32c..cd2c6c5 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -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', compresslevel=6)) + stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb')) streams = (stream,) + streams else: stream = base_stream diff --git a/masque/file/utils.py b/masque/file/utils.py index 898bd05..9edc0f9 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -1,92 +1,21 @@ """ Helper functions for file reading and writing """ -from typing import IO, Iterator, Mapping +from typing import IO, Iterator 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, Library, LibraryError +from .. import Pattern, PatternError 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. diff --git a/masque/label.py b/masque/label.py index 7eb9068..0c250cc 100644 --- a/masque/label.py +++ b/masque/label.py @@ -1,17 +1,15 @@ -from typing import Self, Any +from typing import Self import copy -import functools import numpy from numpy.typing import ArrayLike, NDArray from .repetition import Repetition -from .utils import rotation_matrix_2d, annotations_t, annotations_eq, annotations_lt, rep2key +from .utils import rotation_matrix_2d, annotations_t from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded from .traits import 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) @@ -66,23 +64,6 @@ 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. diff --git a/masque/pattern.py b/masque/pattern.py index 614124a..0d98164 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -5,7 +5,6 @@ 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 @@ -18,8 +17,7 @@ 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, annotations_eq, annotations_lt, layer2key -from .utils import ports_eq, ports_lt +from .utils import rotation_matrix_2d, annotations_t, layer_t from .error import PatternError, PortError from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded from .ports import Port, PortList @@ -28,7 +26,6 @@ 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 @@ -195,146 +192,6 @@ 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, @@ -579,8 +436,6 @@ 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 @@ -1232,7 +1087,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 @@ -1308,7 +1163,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): map_out[vi] = None if isinstance(other, Pattern): - assert append, 'Got a name (not an abstract) but was asked to reference (not append)' + assert append self.place( other, diff --git a/masque/ports.py b/masque/ports.py index 9ee58f5..3e78759 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -1,11 +1,9 @@ -from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn, Any +from typing import Iterable, KeysView, ValuesView, overload, Self, Mapping, NoReturn import warnings import traceback import logging -import functools from collections import Counter from abc import ABCMeta, abstractmethod -from itertools import chain import numpy from numpy import pi @@ -19,7 +17,6 @@ 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`. @@ -89,9 +86,6 @@ 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)) @@ -123,27 +117,6 @@ 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__ @@ -273,75 +246,6 @@ 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], diff --git a/masque/ref.py b/masque/ref.py index 9c9c519..d0b7dd4 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -2,15 +2,14 @@ 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, Any +from typing import Mapping, TYPE_CHECKING, Self import copy -import functools import numpy from numpy import pi from numpy.typing import NDArray, ArrayLike -from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key +from .utils import annotations_t, rotation_matrix_2d from .repetition import Repetition from .traits import ( PositionableImpl, RotatableImpl, ScalableImpl, @@ -22,7 +21,6 @@ if TYPE_CHECKING: from . import Pattern -@functools.total_ordering class Ref( PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl, @@ -101,29 +99,6 @@ 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', diff --git a/masque/repetition.py b/masque/repetition.py index 5436c11..685d815 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -2,9 +2,8 @@ Repetitions provide support for efficiently representing multiple identical instances of an object . """ -from typing import Any, Type, Self, TypeVar, cast +from typing import Any, Type, Self, TypeVar import copy -import functools from abc import ABCMeta, abstractmethod import numpy @@ -18,7 +17,6 @@ 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 @@ -33,14 +31,6 @@ 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): """ @@ -280,7 +270,7 @@ class Grid(Repetition): return (f'') def __eq__(self, other: Any) -> bool: - if type(other) is not type(self): + if not isinstance(other, type(self)): return False if self.a_count != other.a_count or self.b_count != other.b_count: return False @@ -294,24 +284,6 @@ 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): """ @@ -353,23 +325,10 @@ class Arbitrary(Repetition): return (f'') def __eq__(self, other: Any) -> bool: - if not type(other) is not type(self): + if not isinstance(other, 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)) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index aa171ed..b69d9b5 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -1,6 +1,5 @@ -from typing import Any, cast +from typing import Any import copy -import functools import numpy from numpy import pi @@ -9,10 +8,9 @@ from numpy.typing import NDArray, ArrayLike from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from ..error import PatternError from ..repetition import Repetition -from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key +from ..utils import is_scalar, annotations_t -@functools.total_ordering class Arc(Shape): """ An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its @@ -189,38 +187,6 @@ class Arc(Shape): new._annotations = copy.deepcopy(self._annotations) 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, diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 628f8b8..705c2d4 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -1,6 +1,4 @@ -from typing import Any, cast import copy -import functools import numpy from numpy import pi @@ -9,10 +7,9 @@ from numpy.typing import NDArray, ArrayLike from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from ..error import PatternError from ..repetition import Repetition -from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key +from ..utils import is_scalar, annotations_t -@functools.total_ordering class Circle(Shape): """ A circle, which has a position and radius. @@ -70,29 +67,6 @@ 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, diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 9c671d6..1062061 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -1,7 +1,6 @@ -from typing import Any, Self, cast +from typing import Any, Self import copy import math -import functools import numpy from numpy import pi @@ -10,10 +9,9 @@ from numpy.typing import ArrayLike, NDArray from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from ..error import PatternError from ..repetition import Repetition -from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key +from ..utils import is_scalar, rotation_matrix_2d, annotations_t -@functools.total_ordering class Ellipse(Shape): """ An ellipse, which has a position, two radii, and a rotation. @@ -119,32 +117,6 @@ 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, diff --git a/masque/shapes/path.py b/masque/shapes/path.py index d87286a..abb4973 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -1,6 +1,5 @@ from typing import Sequence, Any, cast import copy -import functools from enum import Enum import numpy @@ -10,11 +9,10 @@ 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, annotations_lt, annotations_eq, rep2key +from ..utils import is_scalar, rotation_matrix_2d 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 @@ -22,11 +20,7 @@ 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, @@ -207,40 +201,6 @@ 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]], diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 144371b..ef1ba35 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -1,6 +1,5 @@ from typing import Sequence, Any, cast import copy -import functools import numpy from numpy import pi @@ -9,11 +8,10 @@ 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, annotations_lt, annotations_eq, rep2key +from ..utils import is_scalar, rotation_matrix_2d 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 @@ -115,35 +113,6 @@ 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, diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index a1c4bcd..4263531 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -42,14 +42,6 @@ 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, diff --git a/masque/shapes/text.py b/masque/shapes/text.py index d19d6c5..017bdc1 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -1,6 +1,5 @@ -from typing import Self, Any, cast +from typing import Self import copy -import functools import numpy from numpy import pi, nan @@ -10,14 +9,13 @@ 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, annotations_lt, annotations_eq, rep2key +from ..utils import is_scalar, get_bit, annotations_t # 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). @@ -98,38 +96,6 @@ 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 diff --git a/masque/utils/__init__.py b/masque/utils/__init__.py index 571c406..f4a7805 100644 --- a/masque/utils/__init__.py +++ b/masque/utils/__init__.py @@ -12,7 +12,6 @@ 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 diff --git a/masque/utils/comparisons.py b/masque/utils/comparisons.py deleted file mode 100644 index fe462b5..0000000 --- a/masque/utils/comparisons.py +++ /dev/null @@ -1,106 +0,0 @@ -from typing import Any - -from .types import annotations_t, layer_t -from ..ports import Port -from ..repetition import Repetition - - -def annotation2key(aaa: int | float | str) -> tuple[bool, Any]: - return (isinstance(aaa, str), aaa) - - -def annotations_lt(aa: annotations_t, bb: annotations_t) -> bool: - if aa is None: - return bb is not None - elif bb is None: - return False - - if len(aa) != len(bb): - return len(aa) < len(bb) - - keys_a = tuple(sorted(aa.keys())) - keys_b = tuple(sorted(bb.keys())) - if keys_a != keys_b: - return keys_a < keys_b - - for key in keys_a: - va = aa[key] - vb = bb[key] - if len(va) != len(vb): - return len(va) < len(vb) - - for aaa, bbb in zip(va, vb): - if aaa != bbb: - return annotation2key(aaa) < annotation2key(bbb) - return False - - -def annotations_eq(aa: annotations_t, bb: annotations_t) -> bool: - if aa is None: - return bb is None - elif bb is None: - return False - - if len(aa) != len(bb): - return False - - keys_a = tuple(sorted(aa.keys())) - keys_b = tuple(sorted(bb.keys())) - if keys_a != keys_b: - return keys_a < keys_b - - for key in keys_a: - va = aa[key] - vb = bb[key] - if len(va) != len(vb): - return False - - for aaa, bbb in zip(va, vb): - if aaa != bbb: - return False - - return True - - -def layer2key(layer: layer_t) -> tuple[bool, bool, Any]: - is_int = isinstance(layer, int) - is_str = isinstance(layer, str) - layer_tup = (layer) if (is_str or is_int) else layer - tup = ( - is_str, - not is_int, - layer_tup, - ) - return tup - - -def rep2key(repetition: Repetition | None) -> tuple[bool, Repetition | None]: - return (repetition is None, repetition) - - -def ports_eq(aa: dict[str, Port], bb: dict[str, Port]) -> bool: - if len(aa) != len(bb): - return False - - keys = sorted(aa.keys()) - if keys != sorted(bb.keys()): - return False - - return all(aa[kk] == bb[kk] for kk in keys) - - -def ports_lt(aa: dict[str, Port], bb: dict[str, Port]) -> bool: - if len(aa) != len(bb): - return len(aa) < len(bb) - - aa_keys = tuple(sorted(aa.keys())) - bb_keys = tuple(sorted(bb.keys())) - if aa_keys != bb_keys: - return aa_keys < bb_keys - - for key in aa_keys: - pa = aa[key] - pb = bb[key] - if pa != pb: - return pa < pb - return False