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
* check renderpather
* 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().
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
library['Pather_and_BasicTool'] = pather.pattern

View File

@ -2,7 +2,7 @@
Simplified Pattern assembly (`Builder`)
"""
from typing import Self
from collections.abc import Sequence, Mapping
from collections.abc import Iterable, Sequence, Mapping
import copy
import logging
from functools import wraps
@ -226,6 +226,7 @@ class Builder(PortList):
inherit_name: bool = True,
set_rotation: bool | None = None,
append: bool = False,
ok_connections: Iterable[tuple[str, str]] = (),
) -> Self:
"""
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.
Note that this does not flatten `other`, so its refs will still
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:
self
@ -293,6 +299,7 @@ class Builder(PortList):
inherit_name=inherit_name,
set_rotation=set_rotation,
append=append,
ok_connections=ok_connections,
)
return self

View File

@ -2,9 +2,10 @@
Manual wire/waveguide routing (`Pather`)
"""
from typing import Self
from collections.abc import Sequence, MutableMapping, Mapping
from collections.abc import Sequence, MutableMapping, Mapping, Iterator
import copy
import logging
from contextlib import contextmanager
from pprint import pformat
import numpy
@ -281,6 +282,37 @@ class Pather(Builder):
self.tools[key] = tool
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(
self,
portspec: str,

View File

@ -542,7 +542,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
Return:
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(
self,
@ -1068,20 +1068,22 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
Returns:
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()
while empty := {name for name, pat in self.items() if pat.is_empty()}:
while empty:
parents = set()
for name in empty:
del self[name]
for pat in self.values():
for name in empty:
# Second pass to skip looking at refs in empty patterns
if name in pat.refs:
del pat.refs[name]
for parent in parent_graph[name]:
del self[parent].refs[name]
parents |= parent_graph[name]
trimmed |= empty
if not repeat:
break
empty = {parent for parent in parents if self[parent].is_empty()}
return trimmed
def delete(

View File

@ -1225,6 +1225,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
inherit_name: bool = True,
set_rotation: bool | None = None,
append: bool = False,
ok_connections: Iterable[tuple[str, str]] = (),
) -> Self:
"""
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.
Note that this does not flatten `other`, so its refs will still
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:
self
@ -1300,6 +1306,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
map_in,
mirrored=mirrored,
set_rotation=set_rotation,
ok_connections=ok_connections,
)
# get rid of plugged ports

View File

@ -419,6 +419,7 @@ class PortList(metaclass=ABCMeta):
*,
mirrored: bool = False,
set_rotation: bool | None = None,
ok_connections: Iterable[tuple[str, str]] = (),
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
"""
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
to indicate how much `other` should be rotated. Otherwise,
`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:
- The (x, y) translation (performed last)
@ -451,6 +457,7 @@ class PortList(metaclass=ABCMeta):
map_in=map_in,
mirrored=mirrored,
set_rotation=set_rotation,
ok_connections=ok_connections,
)
@staticmethod
@ -461,13 +468,14 @@ class PortList(metaclass=ABCMeta):
*,
mirrored: bool = False,
set_rotation: bool | None = None,
ok_connections: Iterable[tuple[str, str]] = (),
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
"""
Given two sets of ports (s_ports and o_ports) and a mapping `map_in`
specifying port connections, find the transform which will correctly
align the specified o_ports onto their respective s_ports.
Args:t
Args:
s_ports: A list of stationary ports
o_ports: A list of ports which are to be moved/mirrored.
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
to indicate how much `o_ports` should be rotated. Otherwise,
`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:
- The (x, y) translation (performed last)
@ -502,7 +515,8 @@ class PortList(metaclass=ABCMeta):
o_offsets[:, 1] *= -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)])
if type_conflicts.any():
msg = 'Ports have conflicting types:\n'
@ -523,8 +537,8 @@ class PortList(metaclass=ABCMeta):
if not numpy.allclose(rotations[:1], rotations):
rot_deg = numpy.rad2deg(rotations)
msg = 'Port orientations do not match:\n'
for nn, (k, v) in enumerate(map_in.items()):
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
for nn, (kk, vv) in enumerate(map_in.items()):
msg += f'{kk} | {rot_deg[nn]:g} | {vv}\n'
raise PortError(msg)
pivot = o_offsets[0].copy()
@ -532,8 +546,8 @@ class PortList(metaclass=ABCMeta):
translations = s_offsets - o_offsets
if not numpy.allclose(translations[:1], translations):
msg = 'Port translations do not match:\n'
for nn, (k, v) in enumerate(map_in.items()):
msg += f'{k} | {translations[nn]} | {v}\n'
for nn, (kk, vv) in enumerate(map_in.items()):
msg += f'{kk} | {translations[nn]} | {vv}\n'
raise PortError(msg)
return translations[0], rotations[0], o_offsets[0]

View File

@ -233,7 +233,7 @@ class Arc(Shape):
r0, r1 = self.radii
# 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

View File

@ -20,7 +20,7 @@ class Polygon(Shape):
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
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.
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:
# Note: this function is going to be pretty slow for many-vertexed polygons, relative to
# other shapes
offset = self.vertices.mean(axis=0) + self.offset
zeroed_vertices = self.vertices - offset
meanv = self.vertices.mean(axis=0)
zeroed_vertices = self.vertices - meanv
offset = meanv + self.offset
scale = zeroed_vertices.std()
normed_vertices = zeroed_vertices / scale