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, | ||||
|             ) | ||||
| 
 | ||||
| 
 | ||||
|     def path_into( | ||||
|             self, | ||||
|             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, 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}') | ||||
| 
 | ||||
|  | ||||
| @ -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. | ||||
|  | ||||
| @ -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. | ||||
|  | ||||
| @ -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, | ||||
| @ -1089,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 | ||||
|  | ||||
| @ -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 traceback | ||||
| import logging | ||||
| import functools | ||||
| from collections import Counter | ||||
| from abc import ABCMeta, abstractmethod | ||||
| from itertools import chain | ||||
| @ -18,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`. | ||||
| @ -121,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__ | ||||
|  | ||||
| @ -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', | ||||
|  | ||||
| @ -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)) | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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]], | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user