2023-10-07 01:54:16 -07:00
|
|
|
"""
|
2026-03-08 00:18:47 -08:00
|
|
|
Unified Pattern assembly and routing (`Pather`)
|
2023-10-07 01:54:16 -07:00
|
|
|
"""
|
2026-03-08 00:18:47 -08:00
|
|
|
from typing import Self, Literal, Any, overload
|
|
|
|
|
from collections.abc import Iterator, Iterable, Mapping, MutableMapping, Sequence
|
2023-04-07 23:20:09 -07:00
|
|
|
import copy
|
|
|
|
|
import logging
|
2026-03-08 00:18:47 -08:00
|
|
|
from collections import defaultdict
|
|
|
|
|
from functools import wraps
|
2023-10-07 01:50:22 -07:00
|
|
|
from pprint import pformat
|
2026-03-08 00:18:47 -08:00
|
|
|
from itertools import chain
|
|
|
|
|
from contextlib import contextmanager
|
2023-04-07 23:20:09 -07:00
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
import numpy
|
2026-02-16 13:43:54 -08:00
|
|
|
from numpy import pi
|
2026-03-08 00:18:47 -08:00
|
|
|
from numpy.typing import ArrayLike
|
2026-02-16 13:43:54 -08:00
|
|
|
|
2023-04-07 23:20:09 -07:00
|
|
|
from ..pattern import Pattern
|
2026-03-08 00:18:47 -08:00
|
|
|
from ..library import ILibrary, TreeView
|
|
|
|
|
from ..error import BuildError, PortError
|
2023-04-07 23:20:09 -07:00
|
|
|
from ..ports import PortList, Port
|
2026-03-08 00:18:47 -08:00
|
|
|
from ..abstract import Abstract
|
2025-11-11 20:30:45 -08:00
|
|
|
from ..utils import SupportsBool
|
2026-03-08 00:18:47 -08:00
|
|
|
from .tools import Tool, RenderStep
|
|
|
|
|
from .utils import ell
|
|
|
|
|
from .logging import logged_op, PatherLogger
|
2023-04-07 23:20:09 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
class Pather(PortList):
|
2023-04-07 23:20:09 -07:00
|
|
|
"""
|
2026-03-08 00:18:47 -08:00
|
|
|
A `Pather` is a helper object used for snapping together multiple
|
|
|
|
|
lower-level patterns at their `Port`s, and for routing single-use
|
|
|
|
|
patterns (e.g. wires or waveguides) between them.
|
2023-04-07 23:20:09 -07:00
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
The `Pather` holds context in the form of a `Library`, its underlying
|
|
|
|
|
pattern, and a set of `Tool`s for generating routing segments.
|
2023-04-07 23:20:09 -07:00
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
Routing operations (`trace`, `jog`, `uturn`, etc.) are by default
|
|
|
|
|
deferred: they record the intended path but do not immediately generate
|
|
|
|
|
geometry. `render()` must be called to generate the final layout.
|
|
|
|
|
Alternatively, setting `auto_render=True` in the constructor will
|
|
|
|
|
cause geometry to be generated incrementally after each routing step.
|
2023-04-07 23:20:09 -07:00
|
|
|
|
2023-10-07 01:54:16 -07:00
|
|
|
Examples: Creating a Pather
|
2023-04-07 23:20:09 -07:00
|
|
|
===========================
|
2026-03-08 00:18:47 -08:00
|
|
|
- `Pather(library, tools=my_tool)` makes an empty pattern with no ports.
|
|
|
|
|
The default routing tool for all ports is set to `my_tool`.
|
2023-04-07 23:20:09 -07:00
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
- `Pather(library, name='mypat')` makes an empty pattern and adds it to
|
|
|
|
|
`library` under the name `'mypat'`.
|
2023-10-07 01:54:16 -07:00
|
|
|
|
|
|
|
|
Examples: Adding to a pattern
|
|
|
|
|
=============================
|
2026-03-08 00:18:47 -08:00
|
|
|
- `pather.plug(subdevice, {'A': 'C'})` instantiates `subdevice` and
|
|
|
|
|
connects port 'A' of the current pattern to port 'C' of `subdevice`.
|
2026-03-07 00:33:18 -08:00
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
- `pather.trace('my_port', ccw=True, length=100)` plans a 100-unit bend
|
|
|
|
|
starting at 'my_port'. If `auto_render=True`, geometry is added
|
|
|
|
|
immediately. Otherwise, call `pather.render()` later.
|
2023-04-07 23:20:09 -07:00
|
|
|
"""
|
2026-03-08 00:18:47 -08:00
|
|
|
__slots__ = (
|
|
|
|
|
'pattern', 'library', 'tools', 'paths',
|
2026-03-08 10:12:43 -07:00
|
|
|
'_dead', '_logger', '_auto_render', '_auto_render_append'
|
2026-03-08 00:18:47 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
pattern: Pattern
|
|
|
|
|
""" Layout of this device """
|
2023-04-07 23:20:09 -07:00
|
|
|
|
|
|
|
|
library: ILibrary
|
2026-03-08 00:18:47 -08:00
|
|
|
""" Library from which patterns should be referenced """
|
2023-04-07 23:20:09 -07:00
|
|
|
|
|
|
|
|
tools: dict[str | None, Tool]
|
|
|
|
|
"""
|
2026-03-08 00:18:47 -08:00
|
|
|
Tool objects used to dynamically generate new routing segments.
|
|
|
|
|
A key of `None` indicates the default `Tool`.
|
2023-04-07 23:20:09 -07:00
|
|
|
"""
|
|
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
paths: defaultdict[str, list[RenderStep]]
|
|
|
|
|
""" Per-port list of planned operations, to be used by `render()` """
|
|
|
|
|
|
|
|
|
|
_dead: bool
|
|
|
|
|
""" If True, geometry generation is skipped (for debugging) """
|
|
|
|
|
|
|
|
|
|
_logger: PatherLogger
|
|
|
|
|
""" Handles diagnostic logging of operations """
|
|
|
|
|
|
|
|
|
|
_auto_render: bool
|
|
|
|
|
""" If True, routing operations call render() immediately """
|
|
|
|
|
|
|
|
|
|
PROBE_LENGTH: float = 1e6
|
|
|
|
|
""" Large length used when probing tools for their lateral displacement """
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def ports(self) -> dict[str, Port]:
|
|
|
|
|
return self.pattern.ports
|
|
|
|
|
|
|
|
|
|
@ports.setter
|
|
|
|
|
def ports(self, value: dict[str, Port]) -> None:
|
|
|
|
|
self.pattern.ports = value
|
|
|
|
|
|
2023-04-07 23:20:09 -07:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
library: ILibrary,
|
|
|
|
|
*,
|
|
|
|
|
pattern: Pattern | None = None,
|
|
|
|
|
ports: str | Mapping[str, Port] | None = None,
|
|
|
|
|
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
|
|
|
|
name: str | None = None,
|
2026-03-08 00:18:47 -08:00
|
|
|
debug: bool = False,
|
|
|
|
|
auto_render: bool = False,
|
2026-03-08 10:12:43 -07:00
|
|
|
auto_render_append: bool = True,
|
2023-04-07 23:20:09 -07:00
|
|
|
) -> None:
|
|
|
|
|
"""
|
2023-10-07 01:54:16 -07:00
|
|
|
Args:
|
2026-03-08 00:18:47 -08:00
|
|
|
library: The library for pattern references and generated segments.
|
|
|
|
|
pattern: The pattern to modify. If `None`, a new one is created.
|
|
|
|
|
ports: Initial set of ports. May be a string (name in `library`)
|
|
|
|
|
or a port mapping.
|
|
|
|
|
tools: Tool(s) to use for routing segments.
|
2023-10-07 01:54:16 -07:00
|
|
|
name: If specified, `library[name]` is set to `self.pattern`.
|
2026-03-08 00:18:47 -08:00
|
|
|
debug: If True, enables detailed logging.
|
|
|
|
|
auto_render: If True, enables immediate rendering of routing steps.
|
2026-03-08 10:12:43 -07:00
|
|
|
auto_render_append: If `auto_render` is True, determines whether
|
|
|
|
|
to append geometry or add a reference.
|
2023-04-07 23:20:09 -07:00
|
|
|
"""
|
|
|
|
|
self._dead = False
|
2026-03-08 00:18:47 -08:00
|
|
|
self._logger = PatherLogger(debug=debug)
|
|
|
|
|
self._auto_render = auto_render
|
2026-03-08 10:12:43 -07:00
|
|
|
self._auto_render_append = auto_render_append
|
2023-04-07 23:20:09 -07:00
|
|
|
self.library = library
|
2026-03-08 00:18:47 -08:00
|
|
|
self.pattern = pattern if pattern is not None else Pattern()
|
|
|
|
|
self.paths = defaultdict(list)
|
2023-04-07 23:20:09 -07:00
|
|
|
|
|
|
|
|
if ports is not None:
|
|
|
|
|
if self.pattern.ports:
|
|
|
|
|
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
|
|
|
|
if isinstance(ports, str):
|
|
|
|
|
ports = library.abstract(ports).ports
|
|
|
|
|
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
|
|
|
|
|
2023-04-14 22:30:42 -07:00
|
|
|
if tools is None:
|
|
|
|
|
self.tools = {}
|
|
|
|
|
elif isinstance(tools, Tool):
|
|
|
|
|
self.tools = {None: tools}
|
|
|
|
|
else:
|
|
|
|
|
self.tools = dict(tools)
|
|
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
if name is not None:
|
|
|
|
|
library[name] = self.pattern
|
|
|
|
|
|
|
|
|
|
def __del__(self) -> None:
|
|
|
|
|
if any(self.paths.values()):
|
|
|
|
|
logger.warning(f'Pather {self} had unrendered paths', stack_info=True)
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# Core Pattern Operations (Immediate)
|
|
|
|
|
#
|
|
|
|
|
def _record_break(self, names: Iterable[str | None]) -> None:
|
|
|
|
|
""" Record a batch-breaking step for the specified ports. """
|
|
|
|
|
if not self._dead:
|
|
|
|
|
for n in names:
|
|
|
|
|
if n is not None and n in self.paths:
|
|
|
|
|
port = self.ports[n]
|
|
|
|
|
self.paths[n].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
|
|
|
|
|
|
|
|
|
@logged_op(lambda args: list(args['map_in'].keys()))
|
|
|
|
|
def plug(
|
|
|
|
|
self,
|
|
|
|
|
other: Abstract | str | Pattern | TreeView,
|
|
|
|
|
map_in: dict[str, str],
|
|
|
|
|
map_out: dict[str, str | None] | None = None,
|
|
|
|
|
**kwargs,
|
|
|
|
|
) -> Self:
|
|
|
|
|
if not self._dead:
|
|
|
|
|
other_res = self.library.resolve(other, append=kwargs.get('append', False))
|
|
|
|
|
other_ports = other_res.ports
|
|
|
|
|
affected = set(map_in.keys())
|
|
|
|
|
plugged = set(map_in.values())
|
|
|
|
|
for name in other_ports:
|
|
|
|
|
if name not in plugged:
|
|
|
|
|
new_name = (map_out or {}).get(name, name)
|
|
|
|
|
if new_name is not None:
|
|
|
|
|
affected.add(new_name)
|
|
|
|
|
self._record_break(affected)
|
|
|
|
|
|
|
|
|
|
# Resolve into Abstract or Pattern
|
|
|
|
|
other = self.library.resolve(other, append=kwargs.get('append', False))
|
|
|
|
|
|
|
|
|
|
self.pattern.plug(other=other, map_in=map_in, map_out=map_out, skip_geometry=self._dead, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@logged_op()
|
|
|
|
|
def place(
|
|
|
|
|
self,
|
|
|
|
|
other: Abstract | str | Pattern | TreeView,
|
|
|
|
|
port_map: dict[str, str | None] | None = None,
|
|
|
|
|
**kwargs,
|
|
|
|
|
) -> Self:
|
|
|
|
|
if not self._dead:
|
|
|
|
|
other_res = self.library.resolve(other, append=kwargs.get('append', False))
|
|
|
|
|
other_ports = other_res.ports
|
|
|
|
|
affected = set()
|
|
|
|
|
for name in other_ports:
|
|
|
|
|
new_name = (port_map or {}).get(name, name)
|
|
|
|
|
if new_name is not None:
|
|
|
|
|
affected.add(new_name)
|
|
|
|
|
self._record_break(affected)
|
|
|
|
|
|
|
|
|
|
# Resolve into Abstract or Pattern
|
|
|
|
|
other = self.library.resolve(other, append=kwargs.get('append', False))
|
|
|
|
|
|
|
|
|
|
self.pattern.place(other=other, port_map=port_map, skip_geometry=self._dead, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@logged_op(lambda args: list(args['connections'].keys()))
|
|
|
|
|
def plugged(self, connections: dict[str, str]) -> Self:
|
|
|
|
|
self._record_break(chain(connections.keys(), connections.values()))
|
|
|
|
|
self.pattern.plugged(connections)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@logged_op(lambda args: list(args['mapping'].keys()))
|
|
|
|
|
def rename_ports(self, mapping: dict[str, str | None], overwrite: bool = False) -> Self:
|
|
|
|
|
self.pattern.rename_ports(mapping, overwrite)
|
|
|
|
|
renamed: dict[str, list[RenderStep]] = {vv: self.paths.pop(kk) for kk, vv in mapping.items() if kk in self.paths and vv is not None}
|
|
|
|
|
self.paths.update(renamed)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def set_dead(self) -> Self:
|
|
|
|
|
self._dead = True
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# Pattern Wrappers
|
|
|
|
|
#
|
|
|
|
|
@wraps(Pattern.label)
|
|
|
|
|
def label(self, *args, **kwargs) -> Self:
|
|
|
|
|
self.pattern.label(*args, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@wraps(Pattern.ref)
|
|
|
|
|
def ref(self, *args, **kwargs) -> Self:
|
|
|
|
|
self.pattern.ref(*args, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@wraps(Pattern.polygon)
|
|
|
|
|
def polygon(self, *args, **kwargs) -> Self:
|
|
|
|
|
self.pattern.polygon(*args, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@wraps(Pattern.rect)
|
|
|
|
|
def rect(self, *args, **kwargs) -> Self:
|
|
|
|
|
self.pattern.rect(*args, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@wraps(Pattern.path)
|
|
|
|
|
def path(self, *args, **kwargs) -> Self:
|
|
|
|
|
self.pattern.path(*args, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@logged_op(lambda args: list(args['self'].ports.keys()))
|
|
|
|
|
def translate(self, offset: ArrayLike) -> Self:
|
|
|
|
|
offset_arr = numpy.asarray(offset)
|
|
|
|
|
self.pattern.translate_elements(offset_arr)
|
|
|
|
|
for steps in self.paths.values():
|
|
|
|
|
for i, step in enumerate(steps):
|
|
|
|
|
steps[i] = step.transformed(offset_arr, 0, numpy.zeros(2))
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@logged_op(lambda args: list(args['self'].ports.keys()))
|
|
|
|
|
def rotate_around(self, pivot: ArrayLike, angle: float) -> Self:
|
|
|
|
|
pivot_arr = numpy.asarray(pivot)
|
|
|
|
|
self.pattern.rotate_around(pivot_arr, angle)
|
|
|
|
|
for steps in self.paths.values():
|
|
|
|
|
for i, step in enumerate(steps):
|
|
|
|
|
steps[i] = step.transformed(numpy.zeros(2), angle, pivot_arr)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@logged_op(lambda args: list(args['self'].ports.keys()))
|
|
|
|
|
def mirror(self, axis: int = 0) -> Self:
|
|
|
|
|
self.pattern.mirror(axis)
|
|
|
|
|
for steps in self.paths.values():
|
|
|
|
|
for i, step in enumerate(steps):
|
|
|
|
|
steps[i] = step.mirrored(axis)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@logged_op(lambda args: args['name'])
|
|
|
|
|
def mkport(self, name: str, value: Port) -> Self:
|
|
|
|
|
super().mkport(name, value)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# Routing Logic (Deferred / Incremental)
|
|
|
|
|
#
|
|
|
|
|
def _apply_step(
|
|
|
|
|
self,
|
|
|
|
|
opcode: Literal['L', 'S', 'U'],
|
|
|
|
|
portspec: str,
|
|
|
|
|
out_port: Port,
|
|
|
|
|
data: Any,
|
|
|
|
|
tool: Tool,
|
|
|
|
|
plug_into: str | None = None,
|
|
|
|
|
) -> None:
|
|
|
|
|
""" Common logic for applying a planned step to a port. """
|
|
|
|
|
port = self.pattern[portspec]
|
|
|
|
|
port_rot = port.rotation
|
|
|
|
|
assert port_rot is not None
|
|
|
|
|
|
|
|
|
|
out_port.rotate_around((0, 0), pi + port_rot)
|
|
|
|
|
out_port.translate(port.offset)
|
|
|
|
|
|
|
|
|
|
if not self._dead:
|
|
|
|
|
step = RenderStep(opcode, tool, port.copy(), out_port.copy(), data)
|
|
|
|
|
self.paths[portspec].append(step)
|
|
|
|
|
|
|
|
|
|
self.pattern.ports[portspec] = out_port.copy()
|
|
|
|
|
|
|
|
|
|
if plug_into is not None:
|
|
|
|
|
self.plugged({portspec: plug_into})
|
|
|
|
|
|
|
|
|
|
if self._auto_render:
|
2026-03-08 10:12:43 -07:00
|
|
|
self.render(append=self._auto_render_append)
|
2026-03-08 00:18:47 -08:00
|
|
|
|
|
|
|
|
def _get_tool_R(self, tool: Tool, ccw: SupportsBool, in_ptype: str | None, **kwargs) -> float:
|
|
|
|
|
""" Probe a tool to find the lateral displacement (radius) of its bend. """
|
|
|
|
|
kwargs_no_out = kwargs | {'out_ptype': None}
|
|
|
|
|
probe_len = kwargs.get('probe_length', self.PROBE_LENGTH)
|
|
|
|
|
try:
|
|
|
|
|
out_port, _ = tool.planL(ccw, probe_len, in_ptype=in_ptype, **kwargs_no_out)
|
|
|
|
|
return abs(out_port.y)
|
|
|
|
|
except (BuildError, NotImplementedError):
|
|
|
|
|
# Fallback for tools without planL: use traceL and measure the result
|
|
|
|
|
port_names = ('A', 'B')
|
|
|
|
|
tree = tool.traceL(ccw, probe_len, in_ptype=in_ptype, port_names=port_names, **kwargs_no_out)
|
|
|
|
|
pat = tree.top_pattern()
|
|
|
|
|
(_, R), _ = pat[port_names[0]].measure_travel(pat[port_names[1]])
|
|
|
|
|
return abs(R)
|
|
|
|
|
|
|
|
|
|
def _apply_dead_fallback(
|
|
|
|
|
self,
|
|
|
|
|
portspec: str,
|
|
|
|
|
length: float,
|
|
|
|
|
jog: float,
|
|
|
|
|
ccw: SupportsBool | None,
|
|
|
|
|
in_ptype: str,
|
|
|
|
|
plug_into: str | None = None,
|
2023-04-07 23:20:09 -07:00
|
|
|
*,
|
2026-03-08 00:18:47 -08:00
|
|
|
out_rot: float | None = None,
|
|
|
|
|
) -> None:
|
|
|
|
|
if out_rot is None:
|
|
|
|
|
if ccw is None:
|
|
|
|
|
out_rot = pi
|
|
|
|
|
elif bool(ccw):
|
|
|
|
|
out_rot = -pi / 2
|
|
|
|
|
else:
|
|
|
|
|
out_rot = pi / 2
|
|
|
|
|
logger.warning(f"Tool planning failed for dead pather. Using dummy extension for {portspec}.")
|
|
|
|
|
port = self.pattern[portspec]
|
|
|
|
|
port_rot = port.rotation
|
|
|
|
|
assert port_rot is not None
|
|
|
|
|
out_port = Port((length, jog), rotation=out_rot, ptype=in_ptype)
|
|
|
|
|
out_port.rotate_around((0, 0), pi + port_rot)
|
|
|
|
|
out_port.translate(port.offset)
|
|
|
|
|
self.pattern.ports[portspec] = out_port
|
|
|
|
|
if plug_into is not None:
|
|
|
|
|
self.plugged({portspec: plug_into})
|
2023-10-07 01:45:52 -07:00
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
@logged_op(lambda args: args['portspec'])
|
|
|
|
|
def _traceL(self, portspec: str, ccw: SupportsBool | None, length: float, *, plug_into: str | None = None, **kwargs: Any) -> Self:
|
|
|
|
|
tool = self.tools.get(portspec, self.tools.get(None))
|
|
|
|
|
if tool is None:
|
|
|
|
|
raise BuildError(f'No tool assigned for port {portspec}')
|
|
|
|
|
in_ptype = self.pattern[portspec].ptype
|
|
|
|
|
try:
|
|
|
|
|
out_port, data = tool.planL(ccw, length, in_ptype=in_ptype, **kwargs)
|
|
|
|
|
except (BuildError, NotImplementedError):
|
|
|
|
|
if not self._dead:
|
|
|
|
|
raise
|
|
|
|
|
self._apply_dead_fallback(portspec, length, 0, ccw, in_ptype, plug_into)
|
|
|
|
|
return self
|
|
|
|
|
if out_port is not None:
|
|
|
|
|
self._apply_step('L', portspec, out_port, data, tool, plug_into)
|
|
|
|
|
return self
|
2023-10-07 01:45:52 -07:00
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
@logged_op(lambda args: args['portspec'])
|
|
|
|
|
def _traceS(self, portspec: str, length: float, jog: float, *, plug_into: str | None = None, **kwargs: Any) -> Self:
|
|
|
|
|
tool = self.tools.get(portspec, self.tools.get(None))
|
|
|
|
|
if tool is None:
|
|
|
|
|
raise BuildError(f'No tool assigned for port {portspec}')
|
|
|
|
|
in_ptype = self.pattern[portspec].ptype
|
|
|
|
|
try:
|
|
|
|
|
out_port, data = tool.planS(length, jog, in_ptype=in_ptype, **kwargs)
|
|
|
|
|
except (BuildError, NotImplementedError):
|
|
|
|
|
# Try S-bend fallback (two L-bends)
|
|
|
|
|
ccw0 = jog > 0
|
|
|
|
|
try:
|
|
|
|
|
R1 = self._get_tool_R(tool, ccw0, in_ptype, **kwargs)
|
|
|
|
|
R2 = self._get_tool_R(tool, not ccw0, in_ptype, **kwargs)
|
|
|
|
|
L1, L2 = length - R2, abs(jog) - R1
|
|
|
|
|
except (BuildError, NotImplementedError):
|
|
|
|
|
if not self._dead:
|
|
|
|
|
raise
|
|
|
|
|
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=pi)
|
|
|
|
|
return self
|
2023-04-07 23:20:09 -07:00
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
if L1 < 0 or L2 < 0:
|
|
|
|
|
if not self._dead:
|
|
|
|
|
raise BuildError(f"Jog {jog} or length {length} too small for double-L fallback") from None
|
|
|
|
|
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=pi)
|
|
|
|
|
return self
|
|
|
|
|
|
2026-03-31 09:28:48 -07:00
|
|
|
try:
|
|
|
|
|
out_port0, data0 = tool.planL(ccw0, L1, in_ptype=in_ptype, **(kwargs | {'out_ptype': None}))
|
|
|
|
|
out_port1, data1 = tool.planL(not ccw0, L2, in_ptype=out_port0.ptype, **kwargs)
|
|
|
|
|
except (BuildError, NotImplementedError):
|
|
|
|
|
if not self._dead:
|
|
|
|
|
raise
|
|
|
|
|
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=pi)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
self._apply_step('L', portspec, out_port0, data0, tool)
|
|
|
|
|
self._apply_step('L', portspec, out_port1, data1, tool, plug_into)
|
2026-03-08 00:18:47 -08:00
|
|
|
return self
|
|
|
|
|
if out_port is not None:
|
|
|
|
|
self._apply_step('S', portspec, out_port, data, tool, plug_into)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@logged_op(lambda args: args['portspec'])
|
|
|
|
|
def _traceU(self, portspec: str, jog: float, *, length: float = 0, plug_into: str | None = None, **kwargs: Any) -> Self:
|
|
|
|
|
tool = self.tools.get(portspec, self.tools.get(None))
|
|
|
|
|
if tool is None:
|
|
|
|
|
raise BuildError(f'No tool assigned for port {portspec}')
|
|
|
|
|
in_ptype = self.pattern[portspec].ptype
|
|
|
|
|
try:
|
|
|
|
|
out_port, data = tool.planU(jog, length=length, in_ptype=in_ptype, **kwargs)
|
|
|
|
|
except (BuildError, NotImplementedError):
|
|
|
|
|
# Try U-turn fallback (two L-bends)
|
|
|
|
|
ccw = jog > 0
|
|
|
|
|
try:
|
|
|
|
|
R = self._get_tool_R(tool, ccw, in_ptype, **kwargs)
|
|
|
|
|
L1, L2 = length + R, abs(jog) - R
|
2026-03-31 09:28:48 -07:00
|
|
|
out_port0, data0 = tool.planL(ccw, L1, in_ptype=in_ptype, **(kwargs | {'out_ptype': None}))
|
|
|
|
|
out_port1, data1 = tool.planL(ccw, L2, in_ptype=out_port0.ptype, **kwargs)
|
2026-03-08 00:18:47 -08:00
|
|
|
except (BuildError, NotImplementedError):
|
|
|
|
|
if not self._dead:
|
|
|
|
|
raise
|
|
|
|
|
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=0)
|
|
|
|
|
return self
|
|
|
|
|
else:
|
2026-03-31 09:28:48 -07:00
|
|
|
self._apply_step('L', portspec, out_port0, data0, tool)
|
|
|
|
|
self._apply_step('L', portspec, out_port1, data1, tool, plug_into)
|
2026-03-08 00:18:47 -08:00
|
|
|
return self
|
|
|
|
|
if out_port is not None:
|
|
|
|
|
self._apply_step('U', portspec, out_port, data, tool, plug_into)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# High-level Routing Methods
|
|
|
|
|
#
|
|
|
|
|
def trace(
|
|
|
|
|
self,
|
|
|
|
|
portspec: str | Sequence[str],
|
|
|
|
|
ccw: SupportsBool | None,
|
|
|
|
|
length: float | None = None,
|
|
|
|
|
*,
|
|
|
|
|
spacing: float | ArrayLike | None = None,
|
|
|
|
|
**bounds: Any,
|
|
|
|
|
) -> Self:
|
|
|
|
|
with self._logger.log_operation(self, 'trace', portspec, ccw=ccw, length=length, spacing=spacing, **bounds):
|
|
|
|
|
if isinstance(portspec, str):
|
|
|
|
|
portspec = [portspec]
|
|
|
|
|
if length is not None:
|
|
|
|
|
if len(portspec) > 1:
|
|
|
|
|
raise BuildError('length only allowed with a single port')
|
|
|
|
|
return self._traceL(portspec[0], ccw, length, **bounds)
|
|
|
|
|
if 'each' in bounds:
|
|
|
|
|
each = bounds.pop('each')
|
|
|
|
|
for p in portspec:
|
|
|
|
|
self._traceL(p, ccw, each, **bounds)
|
|
|
|
|
return self
|
|
|
|
|
# Bundle routing
|
|
|
|
|
bt_keys = {'emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'}
|
|
|
|
|
bt = next((k for k in bounds if k in bt_keys), None)
|
|
|
|
|
if not bt:
|
|
|
|
|
raise BuildError('No bound type specified for trace()')
|
|
|
|
|
bval = bounds.pop(bt)
|
|
|
|
|
set_rot = bounds.pop('set_rotation', None)
|
|
|
|
|
exts = ell(self.pattern[tuple(portspec)], ccw, spacing=spacing, bound=bval, bound_type=bt, set_rotation=set_rot)
|
|
|
|
|
for p, length_val in exts.items():
|
|
|
|
|
self._traceL(p, ccw, length_val, **bounds)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def trace_to(
|
|
|
|
|
self,
|
|
|
|
|
portspec: str | Sequence[str],
|
|
|
|
|
ccw: SupportsBool | None,
|
|
|
|
|
*,
|
|
|
|
|
spacing: float | ArrayLike | None = None,
|
|
|
|
|
**bounds: Any,
|
|
|
|
|
) -> Self:
|
|
|
|
|
with self._logger.log_operation(self, 'trace_to', portspec, ccw=ccw, spacing=spacing, **bounds):
|
|
|
|
|
if isinstance(portspec, str):
|
|
|
|
|
portspec = [portspec]
|
|
|
|
|
pos_keys = {'p', 'x', 'y', 'pos', 'position'}
|
|
|
|
|
pb = {k: bounds[k] for k in bounds if k in pos_keys}
|
|
|
|
|
if pb:
|
|
|
|
|
if len(portspec) > 1:
|
|
|
|
|
raise BuildError('Position bounds only allowed with a single port')
|
|
|
|
|
k, v = next(iter(pb.items()))
|
|
|
|
|
port = self.pattern[portspec[0]]
|
|
|
|
|
assert port.rotation is not None
|
|
|
|
|
is_horiz = numpy.isclose(port.rotation % pi, 0)
|
|
|
|
|
if is_horiz:
|
|
|
|
|
if k == 'y':
|
|
|
|
|
raise BuildError('Port is horizontal')
|
|
|
|
|
target = Port((v, port.offset[1]), rotation=None)
|
|
|
|
|
else:
|
|
|
|
|
if k == 'x':
|
|
|
|
|
raise BuildError('Port is vertical')
|
|
|
|
|
target = Port((port.offset[0], v), rotation=None)
|
|
|
|
|
(travel, jog), _ = port.measure_travel(target)
|
|
|
|
|
other_bounds = {bk: bv for bk, bv in bounds.items() if bk not in pos_keys and bk != 'length'}
|
|
|
|
|
return self._traceL(portspec[0], ccw, -travel, **other_bounds)
|
|
|
|
|
return self.trace(portspec, ccw, spacing=spacing, **bounds)
|
|
|
|
|
|
|
|
|
|
def straight(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
|
|
|
|
|
return self.trace_to(portspec, None, length=length, **bounds)
|
|
|
|
|
|
|
|
|
|
def bend(self, portspec: str | Sequence[str], ccw: SupportsBool, length: float | None = None, **bounds) -> Self:
|
|
|
|
|
return self.trace_to(portspec, ccw, length=length, **bounds)
|
|
|
|
|
|
|
|
|
|
def ccw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
|
|
|
|
|
return self.bend(portspec, True, length, **bounds)
|
|
|
|
|
|
|
|
|
|
def cw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
|
|
|
|
|
return self.bend(portspec, False, length, **bounds)
|
|
|
|
|
|
|
|
|
|
def jog(self, portspec: str | Sequence[str], offset: float, length: float | None = None, **bounds: Any) -> Self:
|
|
|
|
|
with self._logger.log_operation(self, 'jog', portspec, offset=offset, length=length, **bounds):
|
|
|
|
|
if isinstance(portspec, str):
|
|
|
|
|
portspec = [portspec]
|
|
|
|
|
for p in portspec:
|
|
|
|
|
self._traceS(p, length, offset, **bounds)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def uturn(self, portspec: str | Sequence[str], offset: float, length: float | None = None, **bounds: Any) -> Self:
|
|
|
|
|
with self._logger.log_operation(self, 'uturn', portspec, offset=offset, length=length, **bounds):
|
|
|
|
|
if isinstance(portspec, str):
|
|
|
|
|
portspec = [portspec]
|
|
|
|
|
for p in portspec:
|
|
|
|
|
self._traceU(p, offset, length=length if length else 0, **bounds)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def trace_into(
|
|
|
|
|
self,
|
|
|
|
|
portspec_src: str,
|
|
|
|
|
portspec_dst: str,
|
|
|
|
|
*,
|
|
|
|
|
out_ptype: str | None = None,
|
|
|
|
|
plug_destination: bool = True,
|
|
|
|
|
thru: str | None = None,
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> Self:
|
|
|
|
|
with self._logger.log_operation(
|
|
|
|
|
self,
|
|
|
|
|
'trace_into',
|
|
|
|
|
[portspec_src, portspec_dst],
|
|
|
|
|
out_ptype=out_ptype,
|
|
|
|
|
plug_destination=plug_destination,
|
|
|
|
|
thru=thru,
|
|
|
|
|
**kwargs,
|
|
|
|
|
):
|
|
|
|
|
if self._dead:
|
|
|
|
|
return self
|
|
|
|
|
port_src, port_dst = self.pattern[portspec_src], self.pattern[portspec_dst]
|
|
|
|
|
if out_ptype is None:
|
|
|
|
|
out_ptype = port_dst.ptype
|
|
|
|
|
if port_src.rotation is None or port_dst.rotation is None:
|
|
|
|
|
raise PortError('Ports must have rotation')
|
|
|
|
|
src_horiz = numpy.isclose(port_src.rotation % pi, 0)
|
|
|
|
|
dst_horiz = numpy.isclose(port_dst.rotation % pi, 0)
|
|
|
|
|
xd, yd = port_dst.offset
|
|
|
|
|
angle = (port_dst.rotation - port_src.rotation) % (2 * pi)
|
|
|
|
|
dst_args = {**kwargs, 'out_ptype': out_ptype}
|
|
|
|
|
if plug_destination:
|
|
|
|
|
dst_args['plug_into'] = portspec_dst
|
|
|
|
|
if src_horiz and not dst_horiz:
|
|
|
|
|
self.trace_to(portspec_src, angle > pi, x=xd, **kwargs)
|
|
|
|
|
self.trace_to(portspec_src, None, y=yd, **dst_args)
|
|
|
|
|
elif dst_horiz and not src_horiz:
|
|
|
|
|
self.trace_to(portspec_src, angle > pi, y=yd, **kwargs)
|
|
|
|
|
self.trace_to(portspec_src, None, x=xd, **dst_args)
|
|
|
|
|
elif numpy.isclose(angle, pi):
|
|
|
|
|
(travel, jog), _ = port_src.measure_travel(port_dst)
|
|
|
|
|
if numpy.isclose(jog, 0):
|
|
|
|
|
self.trace_to(
|
|
|
|
|
portspec_src,
|
|
|
|
|
None,
|
|
|
|
|
x=xd if src_horiz else None,
|
|
|
|
|
y=yd if not src_horiz else None,
|
|
|
|
|
**dst_args,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
self.jog(portspec_src, -jog, -travel, **dst_args)
|
|
|
|
|
elif numpy.isclose(angle, 0):
|
|
|
|
|
(travel, jog), _ = port_src.measure_travel(port_dst)
|
|
|
|
|
self.uturn(portspec_src, -jog, length=-travel, **dst_args)
|
|
|
|
|
else:
|
|
|
|
|
raise BuildError(f"Cannot route relative angle {angle}")
|
|
|
|
|
if thru:
|
|
|
|
|
self.rename_ports({thru: portspec_src})
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# Rendering
|
|
|
|
|
#
|
|
|
|
|
def render(self, append: bool = True) -> Self:
|
|
|
|
|
""" Generate geometry for all planned paths. """
|
|
|
|
|
with self._logger.log_operation(self, 'render', None, append=append):
|
|
|
|
|
tool_port_names = ('A', 'B')
|
|
|
|
|
pat = Pattern()
|
|
|
|
|
|
|
|
|
|
def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None:
|
|
|
|
|
assert batch[0].tool is not None
|
|
|
|
|
tree = batch[0].tool.render(batch, port_names=tool_port_names)
|
|
|
|
|
name = self.library << tree
|
|
|
|
|
if portspec in pat.ports:
|
|
|
|
|
del pat.ports[portspec]
|
|
|
|
|
pat.ports[portspec] = batch[0].start_port.copy()
|
|
|
|
|
if append:
|
|
|
|
|
pat.plug(self.library[name], {portspec: tool_port_names[0]}, append=True)
|
|
|
|
|
del self.library[name]
|
|
|
|
|
else:
|
|
|
|
|
pat.plug(self.library.abstract(name), {portspec: tool_port_names[0]}, append=False)
|
|
|
|
|
if portspec not in pat.ports and tool_port_names[1] in pat.ports:
|
|
|
|
|
pat.rename_ports({tool_port_names[1]: portspec}, overwrite=True)
|
|
|
|
|
|
|
|
|
|
for portspec, steps in self.paths.items():
|
|
|
|
|
if not steps:
|
|
|
|
|
continue
|
|
|
|
|
batch: list[RenderStep] = []
|
|
|
|
|
for step in steps:
|
|
|
|
|
appendable = step.opcode in ('L', 'S', 'U')
|
|
|
|
|
same_tool = batch and step.tool == batch[0].tool
|
|
|
|
|
if batch and (not appendable or not same_tool or not batch[-1].is_continuous_with(step)):
|
|
|
|
|
render_batch(portspec, batch, append)
|
|
|
|
|
batch = []
|
|
|
|
|
if appendable:
|
|
|
|
|
batch.append(step)
|
|
|
|
|
elif step.opcode == 'P' and portspec in pat.ports:
|
|
|
|
|
del pat.ports[portspec]
|
|
|
|
|
if batch:
|
|
|
|
|
render_batch(portspec, batch, append)
|
|
|
|
|
|
|
|
|
|
self.paths.clear()
|
|
|
|
|
pat.ports.clear()
|
|
|
|
|
self.pattern.append(pat)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# Utilities
|
|
|
|
|
#
|
2023-04-07 23:20:09 -07:00
|
|
|
@classmethod
|
|
|
|
|
def interface(
|
2026-03-08 00:18:47 -08:00
|
|
|
cls,
|
2023-04-07 23:20:09 -07:00
|
|
|
source: PortList | Mapping[str, Port] | str,
|
|
|
|
|
*,
|
|
|
|
|
library: ILibrary | None = None,
|
|
|
|
|
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
|
|
|
|
in_prefix: str = 'in_',
|
|
|
|
|
out_prefix: str = '',
|
|
|
|
|
port_map: dict[str, str] | Sequence[str] | None = None,
|
|
|
|
|
name: str | None = None,
|
2026-03-08 00:18:47 -08:00
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> Self:
|
2023-04-07 23:20:09 -07:00
|
|
|
if library is None:
|
|
|
|
|
if hasattr(source, 'library') and isinstance(source.library, ILibrary):
|
|
|
|
|
library = source.library
|
|
|
|
|
else:
|
2026-03-08 00:18:47 -08:00
|
|
|
raise BuildError('No library provided')
|
2023-04-07 23:20:09 -07:00
|
|
|
if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict):
|
|
|
|
|
tools = source.tools
|
2023-10-07 01:45:52 -07:00
|
|
|
if isinstance(source, str):
|
|
|
|
|
source = library.abstract(source).ports
|
|
|
|
|
pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map)
|
2026-03-08 00:18:47 -08:00
|
|
|
return cls(library=library, pattern=pat, name=name, tools=tools, **kwargs)
|
2023-04-07 23:20:09 -07:00
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
def retool(self, tool: Tool, keys: str | Sequence[str | None] | None = None) -> Self:
|
|
|
|
|
if keys is None or isinstance(keys, str):
|
|
|
|
|
self.tools[keys] = tool
|
2026-03-06 22:58:32 -08:00
|
|
|
else:
|
2026-03-08 00:18:47 -08:00
|
|
|
for k in keys:
|
|
|
|
|
self.tools[k] = tool
|
2026-03-06 22:58:32 -08:00
|
|
|
return self
|
|
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
@contextmanager
|
|
|
|
|
def toolctx(self, tool: Tool, keys: str | Sequence[str | None] | None = None) -> Iterator[Self]:
|
|
|
|
|
if keys is None or isinstance(keys, str):
|
|
|
|
|
keys = [keys]
|
|
|
|
|
saved = {k: self.tools.get(k) for k in keys}
|
2026-02-16 13:43:54 -08:00
|
|
|
try:
|
2026-03-08 00:18:47 -08:00
|
|
|
yield self.retool(tool, keys)
|
|
|
|
|
finally:
|
|
|
|
|
for k, t in saved.items():
|
|
|
|
|
if t is None:
|
|
|
|
|
self.tools.pop(k, None)
|
|
|
|
|
else:
|
|
|
|
|
self.tools[k] = t
|
2026-02-16 13:43:54 -08:00
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
def flatten(self) -> Self:
|
|
|
|
|
self.pattern.flatten(self.library)
|
2023-04-07 23:20:09 -07:00
|
|
|
return self
|
|
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
def at(self, portspec: str | Iterable[str]) -> 'PortPather':
|
|
|
|
|
return PortPather(portspec, self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PortPather:
|
|
|
|
|
""" Port state manager for fluent pathing. """
|
|
|
|
|
def __init__(self, ports: str | Iterable[str], pather: Pather) -> None:
|
|
|
|
|
self.ports = [ports] if isinstance(ports, str) else list(ports)
|
|
|
|
|
self.pather = pather
|
|
|
|
|
|
|
|
|
|
def retool(self, tool: Tool) -> Self:
|
|
|
|
|
self.pather.retool(tool, self.ports)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
|
def toolctx(self, tool: Tool) -> Iterator[Self]:
|
|
|
|
|
with self.pather.toolctx(tool, keys=self.ports):
|
|
|
|
|
yield self
|
|
|
|
|
|
|
|
|
|
def trace(self, ccw: SupportsBool | None, length: float | None = None, **kw: Any) -> Self:
|
|
|
|
|
self.pather.trace(self.ports, ccw, length, **kw)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def trace_to(self, ccw: SupportsBool | None, **kw: Any) -> Self:
|
|
|
|
|
self.pather.trace_to(self.ports, ccw, **kw)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def straight(self, length: float | None = None, **kw: Any) -> Self:
|
|
|
|
|
return self.trace_to(None, length=length, **kw)
|
|
|
|
|
|
|
|
|
|
def bend(self, ccw: SupportsBool, length: float | None = None, **kw: Any) -> Self:
|
|
|
|
|
return self.trace_to(ccw, length=length, **kw)
|
|
|
|
|
|
|
|
|
|
def ccw(self, length: float | None = None, **kw: Any) -> Self:
|
|
|
|
|
return self.bend(True, length, **kw)
|
|
|
|
|
|
|
|
|
|
def cw(self, length: float | None = None, **kw: Any) -> Self:
|
|
|
|
|
return self.bend(False, length, **kw)
|
|
|
|
|
|
|
|
|
|
def jog(self, offset: float, length: float | None = None, **kw: Any) -> Self:
|
|
|
|
|
self.pather.jog(self.ports, offset, length, **kw)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def uturn(self, offset: float, length: float | None = None, **kw: Any) -> Self:
|
|
|
|
|
self.pather.uturn(self.ports, offset, length, **kw)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def trace_into(self, target_port: str, **kwargs) -> Self:
|
|
|
|
|
if len(self.ports) > 1:
|
|
|
|
|
raise BuildError(f'Unable use implicit trace_into() with {len(self.ports)} (>1) ports.')
|
|
|
|
|
self.pather.trace_into(self.ports[0], target_port, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def plug(self, other: Abstract | str, other_port: str, **kwargs) -> Self:
|
|
|
|
|
if len(self.ports) > 1:
|
|
|
|
|
raise BuildError(f'Unable use implicit plug() with {len(self.ports)} ports.'
|
|
|
|
|
'Use the pather or pattern directly to plug multiple ports.')
|
|
|
|
|
self.pather.plug(other, {self.ports[0]: other_port}, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def plugged(self, other_port: str | Mapping[str, str]) -> Self:
|
|
|
|
|
if isinstance(other_port, Mapping):
|
|
|
|
|
self.pather.plugged(dict(other_port))
|
|
|
|
|
elif len(self.ports) > 1:
|
|
|
|
|
raise BuildError(f'Unable use implicit plugged() with {len(self.ports)} (>1) ports.')
|
|
|
|
|
else:
|
|
|
|
|
self.pather.plugged({self.ports[0]: other_port})
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# Delegate to port
|
|
|
|
|
#
|
|
|
|
|
def set_ptype(self, ptype: str) -> Self:
|
|
|
|
|
for port in self.ports:
|
|
|
|
|
self.pather.pattern[port].set_ptype(ptype)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def translate(self, *args, **kwargs) -> Self:
|
|
|
|
|
for port in self.ports:
|
|
|
|
|
self.pather.pattern[port].translate(*args, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def mirror(self, *args, **kwargs) -> Self:
|
|
|
|
|
for port in self.ports:
|
|
|
|
|
self.pather.pattern[port].mirror(*args, **kwargs)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def rotate(self, rotation: float) -> Self:
|
|
|
|
|
for port in self.ports:
|
|
|
|
|
self.pather.pattern[port].rotate(rotation)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def set_rotation(self, rotation: float | None) -> Self:
|
|
|
|
|
for port in self.ports:
|
|
|
|
|
self.pather.pattern[port].set_rotation(rotation)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def rename(self, name: str | Mapping[str, str | None]) -> Self:
|
|
|
|
|
""" Rename active ports. """
|
|
|
|
|
name_map: dict[str, str | None]
|
|
|
|
|
if isinstance(name, str):
|
|
|
|
|
if len(self.ports) > 1:
|
|
|
|
|
raise BuildError('Use a mapping to rename >1 port')
|
|
|
|
|
name_map = {self.ports[0]: name}
|
|
|
|
|
else:
|
|
|
|
|
name_map = dict(name)
|
|
|
|
|
self.pather.rename_ports(name_map)
|
|
|
|
|
self.ports = [mm for mm in [name_map.get(pp, pp) for pp in self.ports] if mm is not None]
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def select(self, ports: str | Iterable[str]) -> Self:
|
|
|
|
|
""" Add ports to the selection. """
|
|
|
|
|
if isinstance(ports, str):
|
|
|
|
|
ports = [ports]
|
|
|
|
|
for port in ports:
|
|
|
|
|
if port not in self.ports:
|
|
|
|
|
self.ports.append(port)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def deselect(self, ports: str | Iterable[str]) -> Self:
|
|
|
|
|
""" Remove ports from the selection. """
|
|
|
|
|
if isinstance(ports, str):
|
|
|
|
|
ports = [ports]
|
|
|
|
|
ports_set = set(ports)
|
|
|
|
|
self.ports = [pp for pp in self.ports if pp not in ports_set]
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def mark(self, name: str | Mapping[str, str]) -> Self:
|
|
|
|
|
""" Bookmark current port(s). """
|
|
|
|
|
name_map: Mapping[str, str] = {self.ports[0]: name} if isinstance(name, str) else name
|
|
|
|
|
if isinstance(name, str) and len(self.ports) > 1:
|
|
|
|
|
raise BuildError('Use a mapping to mark >1 port')
|
|
|
|
|
for src, dst in name_map.items():
|
|
|
|
|
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def fork(self, name: str | Mapping[str, str]) -> Self:
|
|
|
|
|
""" Split and follow new name. """
|
|
|
|
|
name_map: Mapping[str, str] = {self.ports[0]: name} if isinstance(name, str) else name
|
|
|
|
|
if isinstance(name, str) and len(self.ports) > 1:
|
|
|
|
|
raise BuildError('Use a mapping to fork >1 port')
|
|
|
|
|
for src, dst in name_map.items():
|
|
|
|
|
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
|
|
|
|
|
self.ports = [(dst if pp == src else pp) for pp in self.ports]
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def drop(self) -> Self:
|
|
|
|
|
""" Remove selected ports from the pattern and the PortPather. """
|
|
|
|
|
for pp in self.ports:
|
|
|
|
|
del self.pather.pattern.ports[pp]
|
|
|
|
|
self.ports = []
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@overload
|
|
|
|
|
def delete(self, name: None) -> None: ...
|
|
|
|
|
|
|
|
|
|
@overload
|
|
|
|
|
def delete(self, name: str) -> Self: ...
|
|
|
|
|
|
|
|
|
|
def delete(self, name: str | None = None) -> Self | None:
|
|
|
|
|
if name is None:
|
|
|
|
|
self.drop()
|
|
|
|
|
return None
|
|
|
|
|
del self.pather.pattern.ports[name]
|
|
|
|
|
self.ports = [pp for pp in self.ports if pp != name]
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Builder(Pather):
|
|
|
|
|
"""
|
|
|
|
|
Backward-compatible wrapper for Pather with auto_render=True.
|
|
|
|
|
"""
|
|
|
|
|
def __init__(
|
2025-11-17 22:12:24 -08:00
|
|
|
self,
|
2026-03-08 00:18:47 -08:00
|
|
|
library: ILibrary,
|
2025-11-17 22:12:24 -08:00
|
|
|
*,
|
2026-03-08 00:18:47 -08:00
|
|
|
pattern: Pattern | None = None,
|
|
|
|
|
ports: str | Mapping[str, Port] | None = None,
|
|
|
|
|
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
|
|
|
|
name: str | None = None,
|
|
|
|
|
debug: bool = False,
|
|
|
|
|
) -> None:
|
|
|
|
|
super().__init__(
|
|
|
|
|
library=library,
|
|
|
|
|
pattern=pattern,
|
|
|
|
|
ports=ports,
|
|
|
|
|
tools=tools,
|
|
|
|
|
name=name,
|
|
|
|
|
debug=debug,
|
|
|
|
|
auto_render=True,
|
|
|
|
|
)
|
2025-11-17 22:12:24 -08:00
|
|
|
|
|
|
|
|
|
2026-03-08 00:18:47 -08:00
|
|
|
class RenderPather(Pather):
|
|
|
|
|
"""
|
|
|
|
|
Backward-compatible wrapper for Pather with auto_render=False.
|
|
|
|
|
"""
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
library: ILibrary,
|
|
|
|
|
*,
|
|
|
|
|
pattern: Pattern | None = None,
|
|
|
|
|
ports: str | Mapping[str, Port] | None = None,
|
|
|
|
|
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
|
|
|
|
name: str | None = None,
|
|
|
|
|
debug: bool = False,
|
|
|
|
|
) -> None:
|
|
|
|
|
super().__init__(
|
|
|
|
|
library=library,
|
|
|
|
|
pattern=pattern,
|
|
|
|
|
ports=ports,
|
|
|
|
|
tools=tools,
|
|
|
|
|
name=name,
|
|
|
|
|
debug=debug,
|
|
|
|
|
auto_render=False,
|
|
|
|
|
)
|