Compare commits

...

10 Commits

9 changed files with 89 additions and 22 deletions

View File

@ -233,5 +233,3 @@ my_pattern.ref(_make_my_subpattern(), offset=..., ...)
* Tests tests tests * Tests tests tests
* check renderpather * check renderpather
* pather and renderpather examples * pather and renderpather examples
* context manager for retool
* allow a specific mismatch when connecting ports

View File

@ -265,6 +265,12 @@ def main() -> None:
# when using pather.retool(). # when using pather.retool().
pather.path_to('VCC', None, -50_000, out_ptype='m1wire') pather.path_to('VCC', None, -50_000, out_ptype='m1wire')
# Now extend GND out to x=-50_000, using M2 for a portion of the path.
# We can use `pather.toolctx()` to temporarily retool, instead of calling `retool()` twice.
with pather.toolctx(M2_tool, keys=['GND']):
pather.path_to('GND', None, -40_000)
pather.path_to('GND', None, -50_000)
# Save the pather's pattern into our library # Save the pather's pattern into our library
library['Pather_and_BasicTool'] = pather.pattern library['Pather_and_BasicTool'] = pather.pattern

View File

@ -2,7 +2,7 @@
Simplified Pattern assembly (`Builder`) Simplified Pattern assembly (`Builder`)
""" """
from typing import Self from typing import Self
from collections.abc import Sequence, Mapping from collections.abc import Iterable, Sequence, Mapping
import copy import copy
import logging import logging
from functools import wraps from functools import wraps
@ -226,6 +226,7 @@ class Builder(PortList):
inherit_name: bool = True, inherit_name: bool = True,
set_rotation: bool | None = None, set_rotation: bool | None = None,
append: bool = False, append: bool = False,
ok_connections: Iterable[tuple[str, str]] = (),
) -> Self: ) -> Self:
""" """
Wrapper around `Pattern.plug` which allows a string for `other`. Wrapper around `Pattern.plug` which allows a string for `other`.
@ -260,6 +261,11 @@ class Builder(PortList):
append: If `True`, `other` is appended instead of being referenced. append: If `True`, `other` is appended instead of being referenced.
Note that this does not flatten `other`, so its refs will still Note that this does not flatten `other`, so its refs will still
be refs (now inside `self`). be refs (now inside `self`).
ok_connections: Set of "allowed" ptype combinations. Identical
ptypes are always allowed to connect, as is `'unk'` with
any other ptypte. Non-allowed ptype connections will emit a
warning. Order is ignored, i.e. `(a, b)` is equivalent to
`(b, a)`.
Returns: Returns:
self self
@ -293,6 +299,7 @@ class Builder(PortList):
inherit_name=inherit_name, inherit_name=inherit_name,
set_rotation=set_rotation, set_rotation=set_rotation,
append=append, append=append,
ok_connections=ok_connections,
) )
return self return self

View File

@ -2,9 +2,10 @@
Manual wire/waveguide routing (`Pather`) Manual wire/waveguide routing (`Pather`)
""" """
from typing import Self from typing import Self
from collections.abc import Sequence, MutableMapping, Mapping from collections.abc import Sequence, MutableMapping, Mapping, Iterator
import copy import copy
import logging import logging
from contextlib import contextmanager
from pprint import pformat from pprint import pformat
import numpy import numpy
@ -281,6 +282,37 @@ class Pather(Builder):
self.tools[key] = tool self.tools[key] = tool
return self return self
@contextmanager
def toolctx(
self,
tool: Tool,
keys: str | Sequence[str | None] | None = None,
) -> Iterator[Self]:
"""
Context manager for temporarily `retool`-ing and reverting the `retool`
upon exiting the context.
Args:
tool: The new `Tool` to use for the given ports.
keys: Which ports the tool should apply to. `None` indicates the default tool,
used when there is no matching entry in `self.tools` for the port in question.
Returns:
self
"""
if keys is None or isinstance(keys, str):
keys = [keys]
saved_tools = {kk: self.tools.get(kk, None) for kk in keys} # If not in self.tools, save `None`
try:
yield self.retool(tool=tool, keys=keys)
finally:
for kk, tt in saved_tools.items():
if tt is None:
# delete if present
self.tools.pop(kk, None)
else:
self.tools[kk] = tt
def path( def path(
self, self,
portspec: str, portspec: str,

View File

@ -542,7 +542,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
Return: Return:
Topologically sorted list of pattern names. Topologically sorted list of pattern names.
""" """
return list(TopologicalSorter(self.child_graph()).static_order()) return cast(list[str], list(TopologicalSorter(self.child_graph()).static_order()))
def find_refs_local( def find_refs_local(
self, self,
@ -1068,20 +1068,22 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
Returns: Returns:
A set containing the names of all deleted patterns A set containing the names of all deleted patterns
""" """
parent_graph = self.parent_graph()
empty = {name for name, pat in self.items() if pat.is_empty()}
trimmed = set() trimmed = set()
while empty := {name for name, pat in self.items() if pat.is_empty()}: while empty:
parents = set()
for name in empty: for name in empty:
del self[name] del self[name]
for parent in parent_graph[name]:
for pat in self.values(): del self[parent].refs[name]
for name in empty: parents |= parent_graph[name]
# Second pass to skip looking at refs in empty patterns
if name in pat.refs:
del pat.refs[name]
trimmed |= empty trimmed |= empty
if not repeat: if not repeat:
break break
empty = {parent for parent in parents if self[parent].is_empty()}
return trimmed return trimmed
def delete( def delete(

View File

@ -1225,6 +1225,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
inherit_name: bool = True, inherit_name: bool = True,
set_rotation: bool | None = None, set_rotation: bool | None = None,
append: bool = False, append: bool = False,
ok_connections: Iterable[tuple[str, str]] = (),
) -> Self: ) -> Self:
""" """
Instantiate or append a pattern into the current pattern, connecting Instantiate or append a pattern into the current pattern, connecting
@ -1270,6 +1271,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
append: If `True`, `other` is appended instead of being referenced. append: If `True`, `other` is appended instead of being referenced.
Note that this does not flatten `other`, so its refs will still Note that this does not flatten `other`, so its refs will still
be refs (now inside `self`). be refs (now inside `self`).
ok_connections: Set of "allowed" ptype combinations. Identical
ptypes are always allowed to connect, as is `'unk'` with
any other ptypte. Non-allowed ptype connections will emit a
warning. Order is ignored, i.e. `(a, b)` is equivalent to
`(b, a)`.
Returns: Returns:
self self
@ -1300,6 +1306,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
map_in, map_in,
mirrored=mirrored, mirrored=mirrored,
set_rotation=set_rotation, set_rotation=set_rotation,
ok_connections=ok_connections,
) )
# get rid of plugged ports # get rid of plugged ports

View File

@ -419,6 +419,7 @@ class PortList(metaclass=ABCMeta):
*, *,
mirrored: bool = False, mirrored: bool = False,
set_rotation: bool | None = None, set_rotation: bool | None = None,
ok_connections: Iterable[tuple[str, str]] = (),
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]: ) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
""" """
Given a device `other` and a mapping `map_in` specifying port connections, Given a device `other` and a mapping `map_in` specifying port connections,
@ -435,6 +436,11 @@ class PortList(metaclass=ABCMeta):
port with `rotation=None`), `set_rotation` must be provided port with `rotation=None`), `set_rotation` must be provided
to indicate how much `other` should be rotated. Otherwise, to indicate how much `other` should be rotated. Otherwise,
`set_rotation` must remain `None`. `set_rotation` must remain `None`.
ok_connections: Set of "allowed" ptype combinations. Identical
ptypes are always allowed to connect, as is `'unk'` with
any other ptypte. Non-allowed ptype connections will emit a
warning. Order is ignored, i.e. `(a, b)` is equivalent to
`(b, a)`.
Returns: Returns:
- The (x, y) translation (performed last) - The (x, y) translation (performed last)
@ -451,6 +457,7 @@ class PortList(metaclass=ABCMeta):
map_in=map_in, map_in=map_in,
mirrored=mirrored, mirrored=mirrored,
set_rotation=set_rotation, set_rotation=set_rotation,
ok_connections=ok_connections,
) )
@staticmethod @staticmethod
@ -461,13 +468,14 @@ class PortList(metaclass=ABCMeta):
*, *,
mirrored: bool = False, mirrored: bool = False,
set_rotation: bool | None = None, set_rotation: bool | None = None,
ok_connections: Iterable[tuple[str, str]] = (),
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]: ) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
""" """
Given two sets of ports (s_ports and o_ports) and a mapping `map_in` Given two sets of ports (s_ports and o_ports) and a mapping `map_in`
specifying port connections, find the transform which will correctly specifying port connections, find the transform which will correctly
align the specified o_ports onto their respective s_ports. align the specified o_ports onto their respective s_ports.
Args:t Args:
s_ports: A list of stationary ports s_ports: A list of stationary ports
o_ports: A list of ports which are to be moved/mirrored. o_ports: A list of ports which are to be moved/mirrored.
map_in: dict of `{'s_port': 'o_port'}` mappings, specifying map_in: dict of `{'s_port': 'o_port'}` mappings, specifying
@ -479,6 +487,11 @@ class PortList(metaclass=ABCMeta):
port with `rotation=None`), `set_rotation` must be provided port with `rotation=None`), `set_rotation` must be provided
to indicate how much `o_ports` should be rotated. Otherwise, to indicate how much `o_ports` should be rotated. Otherwise,
`set_rotation` must remain `None`. `set_rotation` must remain `None`.
ok_connections: Set of "allowed" ptype combinations. Identical
ptypes are always allowed to connect, as is `'unk'` with
any other ptypte. Non-allowed ptype connections will emit a
warning. Order is ignored, i.e. `(a, b)` is equivalent to
`(b, a)`.
Returns: Returns:
- The (x, y) translation (performed last) - The (x, y) translation (performed last)
@ -502,7 +515,8 @@ class PortList(metaclass=ABCMeta):
o_offsets[:, 1] *= -1 o_offsets[:, 1] *= -1
o_rotations *= -1 o_rotations *= -1
type_conflicts = numpy.array([st != ot and 'unk' not in (st, ot) ok_pairs = {tuple(sorted(pair)) for pair in ok_connections if pair[0] != pair[1]}
type_conflicts = numpy.array([(st != ot) and ('unk' not in (st, ot)) and (tuple(sorted((st, ot))) not in ok_pairs)
for st, ot in zip(s_types, o_types, strict=True)]) for st, ot in zip(s_types, o_types, strict=True)])
if type_conflicts.any(): if type_conflicts.any():
msg = 'Ports have conflicting types:\n' msg = 'Ports have conflicting types:\n'
@ -523,8 +537,8 @@ class PortList(metaclass=ABCMeta):
if not numpy.allclose(rotations[:1], rotations): if not numpy.allclose(rotations[:1], rotations):
rot_deg = numpy.rad2deg(rotations) rot_deg = numpy.rad2deg(rotations)
msg = 'Port orientations do not match:\n' msg = 'Port orientations do not match:\n'
for nn, (k, v) in enumerate(map_in.items()): for nn, (kk, vv) in enumerate(map_in.items()):
msg += f'{k} | {rot_deg[nn]:g} | {v}\n' msg += f'{kk} | {rot_deg[nn]:g} | {vv}\n'
raise PortError(msg) raise PortError(msg)
pivot = o_offsets[0].copy() pivot = o_offsets[0].copy()
@ -532,8 +546,8 @@ class PortList(metaclass=ABCMeta):
translations = s_offsets - o_offsets translations = s_offsets - o_offsets
if not numpy.allclose(translations[:1], translations): if not numpy.allclose(translations[:1], translations):
msg = 'Port translations do not match:\n' msg = 'Port translations do not match:\n'
for nn, (k, v) in enumerate(map_in.items()): for nn, (kk, vv) in enumerate(map_in.items()):
msg += f'{k} | {translations[nn]} | {v}\n' msg += f'{kk} | {translations[nn]} | {vv}\n'
raise PortError(msg) raise PortError(msg)
return translations[0], rotations[0], o_offsets[0] return translations[0], rotations[0], o_offsets[0]

View File

@ -233,7 +233,7 @@ class Arc(Shape):
r0, r1 = self.radii r0, r1 = self.radii
# Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation) # Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
a_ranges = self._angles_to_parameters() a_ranges = cast(tuple[tuple[float, float], tuple[float, float]], self._angles_to_parameters())
# Approximate perimeter via numerical integration # Approximate perimeter via numerical integration

View File

@ -20,7 +20,7 @@ class Polygon(Shape):
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
implicitly-closed boundary, and an offset. implicitly-closed boundary, and an offset.
Note that the setter for `Polygon.vertices` may creates a copy of the Note that the setter for `Polygon.vertices` creates a copy of the
passed vertex coordinates. passed vertex coordinates.
A `normalized_form(...)` is available, but can be quite slow with lots of vertices. A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
@ -379,8 +379,9 @@ class Polygon(Shape):
def normalized_form(self, norm_value: float) -> normalized_shape_tuple: def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
# Note: this function is going to be pretty slow for many-vertexed polygons, relative to # Note: this function is going to be pretty slow for many-vertexed polygons, relative to
# other shapes # other shapes
offset = self.vertices.mean(axis=0) + self.offset meanv = self.vertices.mean(axis=0)
zeroed_vertices = self.vertices - offset zeroed_vertices = self.vertices - meanv
offset = meanv + self.offset
scale = zeroed_vertices.std() scale = zeroed_vertices.std()
normed_vertices = zeroed_vertices / scale normed_vertices = zeroed_vertices / scale