Compare commits

..

9 Commits

Author SHA1 Message Date
jan 9ed1716c07 misc cleanup 1 month ago
jan dbbc3ceea9 speed up polygon comparisons 1 month ago
jan 68d4023843 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
1 month ago
jan ec9a51f217 multiple bugfixes for ordering 1 month ago
jan 26db4c687a Add pattern.sort() and file.utils.preflight_check() 1 month ago
jan 32be3105a9 fix circular import 1 month ago
jan b1def6feff Create an ordering for everything
In order to make layouts more reproducible
1 month ago
jan 98d2231e94 add plugged() for manually-aligned ports 1 month ago
jan 60c2a57f6a cache base64encode calls since it's actually fairly slow 1 month ago

@ -430,7 +430,6 @@ class Pather(Builder):
**kwargs,
)
def path_into(
self,
portspec_src: str,
@ -465,7 +464,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 +510,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,12 +1,13 @@
"""
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
@ -21,53 +22,67 @@ 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 sort the pattern contents.
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()) for nn, pp in lib.items()
(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())
msg = 'Dangling refs in found: ' + pformat(dangling)
if allow_dangling_refs is None:
logger.warning(msg)
else:
raise LibraryError(msg)
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 = defaultdict(set)
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)
raise PatternError('Non-numeric layers found:' + pformat(named_layers))
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()
logger.info(f'Preflight pruned {len(pruned)} empty patterns')
logger.debug('Pruned: ' + pformat(pruned))
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

@ -24,6 +24,7 @@ import copy
from pprint import pformat
from collections import defaultdict
from abc import ABCMeta, abstractmethod
from functools import lru_cache
import numpy
from numpy.typing import ArrayLike, NDArray
@ -351,9 +352,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
ii = 0
suffixed_name = sanitized_name
while suffixed_name in self or suffixed_name == '':
suffix = base64.b64encode(struct.pack('>Q', ii), altchars=b'$?').decode('ASCII')
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
suffixed_name = b64suffix(ii)
ii += 1
if len(suffixed_name) > max_length:
@ -1231,3 +1230,10 @@ class AbstractView(Mapping[str, Abstract]):
def __len__(self) -> int:
return self.library.__len__()
@lru_cache(maxsize=8_000)
def b64suffix(ii: int) -> str:
"""Turn an integer into a base64-equivalent suffix."""
suffix = base64.b64encode(struct.pack('>Q', ii), altchars=b'$?').decode('ASCII')
return '$' + suffix[:-1].lstrip('A')

@ -247,6 +247,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
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))
@ -297,24 +300,34 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
return True
def sort(self) -> Self:
def sort(self, sort_elements: bool = True) -> Self:
"""
Sort the element dicts (shapes, labels, refs) and their contained lists.
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
"""
self.refs = dict(sorted(
(tgt, sorted(rrs)) for tgt, rrs in self.refs.items()
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 = dict(sorted(
(layer, sorted(lls)) for layer, lls in self.labels.items(),
key=lambda kk, vv: layer2key(ll),
self.labels = defaultdict(list, sorted(
((layer, maybe_sort(lls)) for layer, lls in self.labels.items()),
key=lambda tt: layer2key(tt[0]),
))
self.shapes = dict(sorted(
(layer, sorted(sss)) for layer, sss in self.shapes.items(),
key=lambda kk, vv: layer2key(ll),
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()))
@ -1217,7 +1230,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

@ -5,6 +5,7 @@ import logging
import functools
from collections import Counter
from abc import ABCMeta, abstractmethod
from itertools import chain
import numpy
from numpy import pi
@ -134,7 +135,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
def __eq__(self, other: Any) -> bool:
return (
type(self) == type(other)
type(self) is type(other)
and self.ptype == other.ptype
and numpy.array_equal(self.offset, other.offset)
and self.rotation == other.rotation
@ -269,6 +270,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],

@ -280,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 type(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
@ -295,7 +295,7 @@ class Grid(Repetition):
return True
def __le__(self, other: Repetition) -> bool:
if type(self) != type(other):
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:
@ -353,12 +353,12 @@ class Arbitrary(Repetition):
return (f'<Arbitrary {len(self.displacements)}pts >')
def __eq__(self, other: Any) -> bool:
if not type(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) != type(other):
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:

@ -191,7 +191,7 @@ class Arc(Shape):
def __eq__(self, other: Any) -> bool:
return (
type(self) != type(other)
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)
@ -202,8 +202,10 @@ class Arc(Shape):
)
def __lt__(self, other: Shape) -> bool:
if type(self) != type(other):
return repr(type(self)) < repr(type(other))
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

@ -72,7 +72,7 @@ class Circle(Shape):
def __eq__(self, other: Any) -> bool:
return (
type(self) != type(other)
type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and self.radius == other.radius
and self.repetition == other.repetition
@ -80,8 +80,10 @@ class Circle(Shape):
)
def __lt__(self, other: Shape) -> bool:
if type(self) != type(other):
return repr(type(self)) < repr(type(other))
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

@ -121,7 +121,7 @@ class Ellipse(Shape):
def __eq__(self, other: Any) -> bool:
return (
type(self) != type(other)
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
@ -130,8 +130,10 @@ class Ellipse(Shape):
)
def __lt__(self, other: Shape) -> bool:
if type(self) != type(other):
return repr(type(self)) < repr(type(other))
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)

@ -209,7 +209,7 @@ class Path(Shape):
def __eq__(self, other: Any) -> bool:
return (
type(self) != type(other)
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
@ -220,8 +220,10 @@ class Path(Shape):
)
def __lt__(self, other: Shape) -> bool:
if type(self) != type(other):
return repr(type(self)) < repr(type(other))
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

@ -117,7 +117,7 @@ class Polygon(Shape):
def __eq__(self, other: Any) -> bool:
return (
type(self) != type(other)
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
@ -125,11 +125,19 @@ class Polygon(Shape):
)
def __lt__(self, other: Shape) -> bool:
if type(self) != type(other):
return repr(type(self)) < repr(type(other))
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):
return tuple(tuple(xy) for xy in self.vertices) < tuple(tuple(xy) for xy in 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:

@ -100,7 +100,7 @@ class Text(RotatableImpl, Shape):
def __eq__(self, other: Any) -> bool:
return (
type(self) != type(other)
type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and self.string == other.string
and self.height == other.height
@ -111,8 +111,10 @@ class Text(RotatableImpl, Shape):
)
def __lt__(self, other: Shape) -> bool:
if type(self) != type(other):
return repr(type(self)) < repr(type(other))
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

Loading…
Cancel
Save