1550 lines
62 KiB
Python
1550 lines
62 KiB
Python
"""
|
|
Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides)
|
|
|
|
The `Tool` interface has two layers:
|
|
|
|
* `traceL`/`traceS`/`traceU` create concrete single-use geometry immediately.
|
|
* `planL`/`planS`/`planU` return an output `Port` plus tool-specific render
|
|
data, allowing `Pather(auto_render=False)` to defer geometry creation until
|
|
`Tool.render()` is called with a batch of `RenderStep`s.
|
|
|
|
Plans are expressed in local tool coordinates: the input port is at `(0, 0)`
|
|
with rotation `0`, `length` is measured along the input axis, and positive
|
|
`jog` is left of the direction of travel. Concrete tools may implement native
|
|
planning/rendering for L, S, and U routes; otherwise the base planning methods
|
|
fall back to the corresponding `trace*()` methods. `Pather` may also synthesize
|
|
some routes from simpler primitives when a tool does not provide a native route.
|
|
"""
|
|
from typing import Literal, Any, Self, cast
|
|
from collections.abc import Sequence, Callable, Iterator
|
|
from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method?
|
|
from dataclasses import dataclass
|
|
|
|
import numpy
|
|
from numpy.typing import NDArray
|
|
from numpy import pi
|
|
|
|
from ..utils import SupportsBool, rotation_matrix_2d, layer_t
|
|
from ..ports import Port
|
|
from ..pattern import Pattern
|
|
from ..abstract import Abstract
|
|
from ..library import ILibrary, Library, SINGLE_USE_PREFIX
|
|
from ..error import BuildError
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RenderStep:
|
|
"""
|
|
A single deferred routing operation.
|
|
|
|
`Pather(auto_render=False)` stores these records while routing and later
|
|
passes batches of compatible steps to `Tool.render()` when `Pather.render()`
|
|
is called.
|
|
"""
|
|
opcode: Literal['L', 'S', 'U', 'P']
|
|
""" What operation is being performed.
|
|
L: planL (straight, optionally with a single bend)
|
|
S: planS (s-bend)
|
|
U: planU (u-bend)
|
|
P: plug
|
|
"""
|
|
|
|
tool: 'Tool | None'
|
|
""" Tool that produced this step, or `None` for `opcode='P'`. """
|
|
|
|
start_port: Port
|
|
""" Input-side port before this step is rendered. """
|
|
|
|
end_port: Port
|
|
""" Output-side port after this step is rendered. """
|
|
|
|
data: Any
|
|
""" Arbitrary tool-specific data"""
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.opcode != 'P' and self.tool is None:
|
|
raise BuildError('Got tool=None but the opcode is not "P"')
|
|
|
|
def is_continuous_with(self, other: 'RenderStep') -> bool:
|
|
"""
|
|
Check if another RenderStep can be appended to this one.
|
|
"""
|
|
# Check continuity with tolerance
|
|
offsets_match = bool(numpy.allclose(other.start_port.offset, self.end_port.offset))
|
|
rotations_match = (other.start_port.rotation is None and self.end_port.rotation is None) or (
|
|
other.start_port.rotation is not None and self.end_port.rotation is not None and
|
|
bool(numpy.isclose(other.start_port.rotation, self.end_port.rotation))
|
|
)
|
|
return offsets_match and rotations_match
|
|
|
|
def transformed(self, translation: NDArray[numpy.float64], rotation: float, pivot: NDArray[numpy.float64]) -> 'RenderStep':
|
|
"""
|
|
Return a new RenderStep with transformed start and end ports.
|
|
"""
|
|
new_start = self.start_port.copy()
|
|
new_end = self.end_port.copy()
|
|
|
|
for pp in (new_start, new_end):
|
|
pp.rotate_around(pivot, rotation)
|
|
pp.translate(translation)
|
|
|
|
return RenderStep(
|
|
opcode = self.opcode,
|
|
tool = self.tool,
|
|
start_port = new_start,
|
|
end_port = new_end,
|
|
data = self.data,
|
|
)
|
|
|
|
def mirrored(self, axis: int) -> 'RenderStep':
|
|
"""
|
|
Return a new RenderStep with mirrored start and end ports.
|
|
"""
|
|
new_start = self.start_port.copy()
|
|
new_end = self.end_port.copy()
|
|
|
|
new_start.flip_across(axis=axis)
|
|
new_end.flip_across(axis=axis)
|
|
|
|
return RenderStep(
|
|
opcode = self.opcode,
|
|
tool = self.tool,
|
|
start_port = new_start,
|
|
end_port = new_end,
|
|
data = self.data,
|
|
)
|
|
|
|
|
|
def measure_tool_plan(tree: ILibrary, port_names: tuple[str, str]) -> tuple[Port, Any]:
|
|
"""
|
|
Measure generated geometry for the base `Tool.plan*()` fallbacks.
|
|
|
|
Returns the calculated output port and the original tree as render data.
|
|
"""
|
|
pat = tree.top_pattern()
|
|
in_p = pat[port_names[0]]
|
|
out_p = pat[port_names[1]]
|
|
(travel, jog), rot = in_p.measure_travel(out_p)
|
|
return Port((travel, jog), rotation=rot, ptype=out_p.ptype), tree
|
|
|
|
|
|
class Tool:
|
|
"""
|
|
Interface for path (e.g. wire or waveguide) generation.
|
|
|
|
Subclasses may implement immediate `trace*()` methods, deferred
|
|
`plan*()`/`render()` methods, or both. The base `plan*()` implementations
|
|
call the matching `trace*()` method and measure the resulting ports, so a
|
|
simple immediate-rendering tool can implement only `traceL`, `traceS`, or
|
|
`traceU` as needed. Tools that support deferred rendering should return
|
|
compact, tool-specific data from `plan*()` and consume it in `render()`.
|
|
"""
|
|
def traceL(
|
|
self,
|
|
ccw: SupportsBool | None,
|
|
length: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs,
|
|
) -> Library:
|
|
"""
|
|
Create a wire or waveguide that travels exactly `length` distance along the axis
|
|
of its input port.
|
|
|
|
Used by `Pather`.
|
|
|
|
The output port must be exactly `length` away along the input port's axis, but
|
|
may be placed an additional (unspecified) distance away along the perpendicular
|
|
direction. The output port should be rotated (or not) based on the value of
|
|
`ccw`.
|
|
|
|
The input and output ports should be compatible with `in_ptype` and
|
|
`out_ptype`, respectively. They should also be named `port_names[0]` and
|
|
`port_names[1]`, respectively.
|
|
|
|
Args:
|
|
ccw: If `None`, the output should be along the same axis as the input.
|
|
Otherwise, cast to bool and turn counterclockwise if True
|
|
and clockwise otherwise.
|
|
length: The total distance from input to output, along the input's axis only.
|
|
(There may be a tool-dependent offset along the other axis.)
|
|
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
|
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
|
port_names: The output pattern will have its input port named `port_names[0]` and
|
|
its output named `port_names[1]`.
|
|
kwargs: Custom tool-specific parameters.
|
|
|
|
Returns:
|
|
A pattern tree containing the requested L-shaped (or straight) wire or waveguide
|
|
|
|
Raises:
|
|
BuildError if an impossible or unsupported geometry is requested.
|
|
"""
|
|
raise NotImplementedError(f'traceL() not implemented for {type(self)}')
|
|
|
|
def traceS(
|
|
self,
|
|
length: float,
|
|
jog: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs,
|
|
) -> Library:
|
|
"""
|
|
Create a wire or waveguide that travels exactly `length` distance along the axis
|
|
of its input port, and `jog` distance on the perpendicular axis.
|
|
`jog` is positive when moving left of the direction of travel (from input to output port).
|
|
|
|
Used by `Pather`.
|
|
|
|
The output port should be rotated to face the input port (i.e. plugging the device
|
|
into a port will move that port but keep its orientation).
|
|
|
|
The input and output ports should be compatible with `in_ptype` and
|
|
`out_ptype`, respectively. They should also be named `port_names[0]` and
|
|
`port_names[1]`, respectively.
|
|
|
|
Args:
|
|
length: The total distance from input to output, along the input's axis only.
|
|
jog: The total distance from input to output, along the second axis. Positive indicates
|
|
a leftward shift when moving from input to output port.
|
|
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
|
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
|
port_names: The output pattern will have its input port named `port_names[0]` and
|
|
its output named `port_names[1]`.
|
|
kwargs: Custom tool-specific parameters.
|
|
|
|
Returns:
|
|
A pattern tree containing the requested S-shaped (or straight) wire or waveguide
|
|
|
|
Raises:
|
|
BuildError if an impossible or unsupported geometry is requested.
|
|
"""
|
|
raise NotImplementedError(f'traceS() not implemented for {type(self)}')
|
|
|
|
def planL(
|
|
self,
|
|
ccw: SupportsBool | None,
|
|
length: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
**kwargs,
|
|
) -> tuple[Port, Any]:
|
|
"""
|
|
Plan a wire or waveguide that travels exactly `length` distance along the axis
|
|
of its input port.
|
|
|
|
Used by `Pather` when `auto_render=False`.
|
|
|
|
The output port must be exactly `length` away along the input port's axis, but
|
|
may be placed an additional (unspecified) distance away along the perpendicular
|
|
direction. The output port should be rotated (or not) based on the value of
|
|
`ccw`.
|
|
|
|
The input and output ports should be compatible with `in_ptype` and
|
|
`out_ptype`, respectively.
|
|
|
|
Args:
|
|
ccw: If `None`, the output should be along the same axis as the input.
|
|
Otherwise, cast to bool and turn counterclockwise if True
|
|
and clockwise otherwise.
|
|
length: The total distance from input to output, along the input's axis only.
|
|
(There may be a tool-dependent offset along the other axis.)
|
|
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
|
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
|
kwargs: Custom tool-specific parameters.
|
|
|
|
Returns:
|
|
The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
|
|
Any tool-specific data, to be stored in `RenderStep.data`, for use during rendering.
|
|
|
|
Raises:
|
|
BuildError if an impossible or unsupported geometry is requested.
|
|
"""
|
|
# Fallback implementation using traceL
|
|
port_names = kwargs.pop('port_names', ('A', 'B'))
|
|
tree = self.traceL(
|
|
ccw,
|
|
length,
|
|
in_ptype=in_ptype,
|
|
out_ptype=out_ptype,
|
|
port_names=port_names,
|
|
**kwargs,
|
|
)
|
|
return measure_tool_plan(tree, port_names)
|
|
|
|
def planS(
|
|
self,
|
|
length: float,
|
|
jog: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
**kwargs,
|
|
) -> tuple[Port, Any]:
|
|
"""
|
|
Plan a wire or waveguide that travels exactly `length` distance along the axis
|
|
of its input port and `jog` distance along the perpendicular axis (i.e. an S-bend).
|
|
|
|
Used by `Pather` when `auto_render=False`.
|
|
|
|
The output port must have an orientation rotated by pi from the input port.
|
|
|
|
The input and output ports should be compatible with `in_ptype` and
|
|
`out_ptype`, respectively.
|
|
|
|
Args:
|
|
length: The total distance from input to output, along the input's axis only.
|
|
jog: The total offset from the input to output, along the perpendicular axis.
|
|
A positive number implies a leftward shift (i.e. counterclockwise bend followed
|
|
by a clockwise bend)
|
|
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
|
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
|
kwargs: Custom tool-specific parameters.
|
|
|
|
Returns:
|
|
The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
|
|
Any tool-specific data, to be stored in `RenderStep.data`, for use during rendering.
|
|
|
|
Raises:
|
|
BuildError if an impossible or unsupported geometry is requested.
|
|
"""
|
|
# Fallback implementation using traceS
|
|
port_names = kwargs.pop('port_names', ('A', 'B'))
|
|
tree = self.traceS(
|
|
length,
|
|
jog,
|
|
in_ptype=in_ptype,
|
|
out_ptype=out_ptype,
|
|
port_names=port_names,
|
|
**kwargs,
|
|
)
|
|
return measure_tool_plan(tree, port_names)
|
|
|
|
def traceU(
|
|
self,
|
|
jog: float,
|
|
*,
|
|
length: float = 0,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs,
|
|
) -> Library:
|
|
"""
|
|
Create a wire or waveguide whose output is displaced by `length` along
|
|
the input axis and `jog` along the perpendicular axis, while preserving
|
|
the input orientation (i.e. a U-bend or jogged U-turn).
|
|
|
|
Used by `Pather`. Tools may leave this unimplemented if they
|
|
do not support a native U-bend primitive.
|
|
|
|
The output port must have an orientation identical to the input port.
|
|
|
|
The input and output ports should be compatible with `in_ptype` and
|
|
`out_ptype`, respectively. They should also be named `port_names[0]` and
|
|
`port_names[1]`, respectively.
|
|
|
|
Args:
|
|
jog: The total offset from the input to output, along the perpendicular axis.
|
|
A positive number implies a leftwards shift (i.e. counterclockwise bend
|
|
followed by a clockwise bend)
|
|
length: The total offset from the input to output, along the input axis.
|
|
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
|
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
|
port_names: The output pattern will have its input port named `port_names[0]` and
|
|
its output named `port_names[1]`.
|
|
kwargs: Custom tool-specific parameters.
|
|
|
|
Returns:
|
|
A pattern tree containing the requested U-shaped wire or waveguide
|
|
|
|
Raises:
|
|
BuildError if an impossible or unsupported geometry is requested.
|
|
"""
|
|
raise NotImplementedError(f'traceU() not implemented for {type(self)}')
|
|
|
|
def planU(
|
|
self,
|
|
jog: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
**kwargs,
|
|
) -> tuple[Port, Any]:
|
|
"""
|
|
Plan a wire or waveguide whose output is displaced by optional `length`
|
|
along the input axis and `jog` along the perpendicular axis, while
|
|
preserving the input orientation (i.e. a U-bend or jogged U-turn).
|
|
|
|
Used by `Pather` when `auto_render=False`. This is an optional native-planning hook: tools may
|
|
implement it when they can represent a U-turn directly, otherwise they may rely
|
|
on `traceU()` or let `Pather` synthesize the route from simpler primitives.
|
|
|
|
The output port must have an orientation identical to the input port.
|
|
|
|
The input and output ports should be compatible with `in_ptype` and
|
|
`out_ptype`, respectively.
|
|
|
|
Args:
|
|
jog: The total offset from the input to output, along the perpendicular axis.
|
|
A positive number implies a leftwards shift (i.e. counterclockwise_bend
|
|
followed by a clockwise bend)
|
|
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
|
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
|
kwargs: Custom tool-specific parameters. `length` may be supplied here to
|
|
request a U-turn whose final port is displaced along both axes.
|
|
|
|
Returns:
|
|
The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
|
|
Any tool-specific data, to be stored in `RenderStep.data`, for use during rendering.
|
|
|
|
Raises:
|
|
BuildError if an impossible or unsupported geometry is requested.
|
|
"""
|
|
# Fallback implementation using traceU
|
|
kwargs = dict(kwargs)
|
|
length = kwargs.pop('length', 0)
|
|
port_names = kwargs.pop('port_names', ('A', 'B'))
|
|
tree = self.traceU(
|
|
jog,
|
|
length=length,
|
|
in_ptype=in_ptype,
|
|
out_ptype=out_ptype,
|
|
port_names=port_names,
|
|
**kwargs,
|
|
)
|
|
return measure_tool_plan(tree, port_names)
|
|
|
|
def render(
|
|
self,
|
|
batch: Sequence[RenderStep],
|
|
*,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs,
|
|
) -> ILibrary:
|
|
"""
|
|
Render the provided `batch` of `RenderStep`s into geometry, returning a tree
|
|
(a Library with a single topcell).
|
|
|
|
The base implementation is intended for steps whose plan data came from
|
|
the base fallback planners, where `RenderStep.data` is already an
|
|
`ILibrary`. Subclasses with native `plan*()` data should generally
|
|
override this method.
|
|
|
|
Args:
|
|
batch: A sequence of `RenderStep` objects containing the ports and data
|
|
provided by this tool's `planL`/`planS`/`planU` functions.
|
|
port_names: The topcell's input and output ports should be named
|
|
`port_names[0]` and `port_names[1]` respectively.
|
|
kwargs: Custom tool-specific parameters.
|
|
"""
|
|
assert not batch or batch[0].tool == self
|
|
# Fallback: render each step individually
|
|
lib, pat = Library.mktree(SINGLE_USE_PREFIX + 'batch')
|
|
pat.add_port_pair(names=port_names, ptype=batch[0].start_port.ptype if batch else 'unk')
|
|
|
|
for step in batch:
|
|
if step.opcode == 'L':
|
|
if isinstance(step.data, ILibrary):
|
|
seg_tree = step.data
|
|
else:
|
|
# extract parameters from kwargs or data
|
|
seg_tree = self.traceL(
|
|
ccw=step.data.get('ccw') if isinstance(step.data, dict) else None,
|
|
length=float(step.data.get('length', 0)) if isinstance(step.data, dict) else 0.0,
|
|
port_names=port_names,
|
|
**kwargs,
|
|
)
|
|
elif step.opcode == 'S':
|
|
if isinstance(step.data, ILibrary):
|
|
seg_tree = step.data
|
|
else:
|
|
seg_tree = self.traceS(
|
|
length=float(step.data.get('length', 0)) if isinstance(step.data, dict) else 0.0,
|
|
jog=float(step.data.get('jog', 0)) if isinstance(step.data, dict) else 0.0,
|
|
port_names=port_names,
|
|
**kwargs,
|
|
)
|
|
elif step.opcode == 'U':
|
|
if isinstance(step.data, ILibrary):
|
|
seg_tree = step.data
|
|
else:
|
|
seg_tree = self.traceU(
|
|
jog=float(step.data.get('jog', 0)) if isinstance(step.data, dict) else 0.0,
|
|
length=float(step.data.get('length', 0)) if isinstance(step.data, dict) else 0.0,
|
|
port_names=port_names,
|
|
**kwargs,
|
|
)
|
|
else:
|
|
continue
|
|
|
|
seg_name = lib << seg_tree
|
|
pat.plug(lib[seg_name], {port_names[1]: port_names[0]}, append=True)
|
|
del lib[seg_name]
|
|
|
|
return lib
|
|
|
|
|
|
abstract_tuple_t = tuple[Abstract, str, str]
|
|
|
|
|
|
@dataclass
|
|
class SimpleTool(Tool, metaclass=ABCMeta):
|
|
"""
|
|
Minimal L-route tool built from one straight generator and one bend.
|
|
|
|
`SimpleTool` supports straight segments and single-bend L routes through
|
|
`planL`/`traceL`/`render`. It does not perform automatic port-type
|
|
transitions and does not provide native S or U routes. Use `AutoTool` when
|
|
routes need multiple candidate primitives, transitions, S-bends, or U-turns.
|
|
"""
|
|
straight: tuple[Callable[[float], Pattern] | Callable[[float], Library], str, str]
|
|
""" `(create_straight, in_port_name, out_port_name)` for straight segments. """
|
|
|
|
bend: abstract_tuple_t # Assumed to be clockwise
|
|
""" `(clockwise_bend_abstract, in_port_name, out_port_name)` for L turns. """
|
|
|
|
default_out_ptype: str
|
|
""" Default value for out_ptype """
|
|
|
|
mirror_bend: bool = True
|
|
""" Whether a clockwise bend should be mirrored (vs rotated) to get a ccw bend """
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class LData:
|
|
""" Deferred render data returned by `planL()`. """
|
|
straight_length: float
|
|
straight_kwargs: dict[str, Any]
|
|
ccw: SupportsBool | None
|
|
|
|
def planL(
|
|
self,
|
|
ccw: SupportsBool | None,
|
|
length: float,
|
|
*,
|
|
in_ptype: str | None = None, # noqa: ARG002 (unused)
|
|
out_ptype: str | None = None, # noqa: ARG002 (unused)
|
|
**kwargs, # noqa: ARG002 (unused)
|
|
) -> tuple[Port, LData]:
|
|
if ccw is not None:
|
|
bend, bport_in, bport_out = self.bend
|
|
|
|
angle_in = bend.ports[bport_in].rotation
|
|
angle_out = bend.ports[bport_out].rotation
|
|
assert angle_in is not None
|
|
assert angle_out is not None
|
|
|
|
bend_dxy = rotation_matrix_2d(-angle_in) @ (
|
|
bend.ports[bport_out].offset
|
|
- bend.ports[bport_in].offset
|
|
)
|
|
|
|
bend_angle = angle_out - angle_in
|
|
|
|
if bool(ccw):
|
|
bend_dxy[1] *= -1
|
|
bend_angle *= -1
|
|
else:
|
|
bend_dxy = numpy.zeros(2)
|
|
bend_angle = pi
|
|
|
|
if ccw is not None:
|
|
out_ptype_actual = bend.ports[bport_out].ptype
|
|
else:
|
|
out_ptype_actual = self.default_out_ptype
|
|
|
|
straight_length = length - bend_dxy[0]
|
|
bend_run = bend_dxy[1]
|
|
|
|
if straight_length < 0:
|
|
raise BuildError(
|
|
f'Asked to draw L-path with total length {length:,g}, shorter than required bends ({bend_dxy[0]:,})'
|
|
)
|
|
|
|
data = self.LData(straight_length, kwargs, ccw)
|
|
out_port = Port((length, bend_run), rotation=bend_angle, ptype=out_ptype_actual)
|
|
return out_port, data
|
|
|
|
def _renderL(
|
|
self,
|
|
data: LData,
|
|
tree: ILibrary,
|
|
port_names: tuple[str, str],
|
|
straight_kwargs: dict[str, Any],
|
|
) -> ILibrary:
|
|
"""
|
|
Render an L step into a preexisting tree
|
|
"""
|
|
pat = tree.top_pattern()
|
|
gen_straight, sport_in, _sport_out = self.straight
|
|
if not numpy.isclose(data.straight_length, 0):
|
|
straight_pat_or_tree = gen_straight(data.straight_length, **(straight_kwargs | data.straight_kwargs))
|
|
pmap = {port_names[1]: sport_in}
|
|
if isinstance(straight_pat_or_tree, Pattern):
|
|
straight_pat = straight_pat_or_tree
|
|
pat.plug(straight_pat, pmap, append=True)
|
|
else:
|
|
straight_tree = straight_pat_or_tree
|
|
top = straight_tree.top()
|
|
straight_tree.flatten(top, dangling_ok=True)
|
|
pat.plug(straight_tree[top], pmap, append=True)
|
|
if data.ccw is not None:
|
|
bend, bport_in, bport_out = self.bend
|
|
mirrored = self.mirror_bend and bool(data.ccw)
|
|
inport = bport_in if (self.mirror_bend or not data.ccw) else bport_out
|
|
pat.plug(bend, {port_names[1]: inport}, mirrored=mirrored)
|
|
return tree
|
|
|
|
def traceL(
|
|
self,
|
|
ccw: SupportsBool | None,
|
|
length: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs,
|
|
) -> Library:
|
|
_out_port, data = self.planL(
|
|
ccw,
|
|
length,
|
|
in_ptype = in_ptype,
|
|
out_ptype = out_ptype,
|
|
)
|
|
|
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
|
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
|
self._renderL(data=data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
|
return tree
|
|
|
|
def render(
|
|
self,
|
|
batch: Sequence[RenderStep],
|
|
*,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs,
|
|
) -> ILibrary:
|
|
|
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
|
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
|
|
|
for step in batch:
|
|
assert step.tool == self
|
|
if step.opcode == 'L':
|
|
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
|
return tree
|
|
|
|
|
|
@dataclass
|
|
class AutoTool(Tool, metaclass=ABCMeta):
|
|
"""
|
|
A routing tool assembled from reusable path primitives.
|
|
|
|
`AutoTool` chooses among prioritized straight generators, pre-rendered bends,
|
|
optional native S-bend generators, and pre-rendered transitions to satisfy the
|
|
`Tool` planning/rendering interface used by `Pather`.
|
|
|
|
Route selection is greedy in the order supplied by `straights`, `bends`, and
|
|
`sbends`. For each route, the planner subtracts any transition and bend
|
|
overhead from the requested distance, then uses the first candidate whose
|
|
remaining straight or jog length falls within that candidate's range.
|
|
|
|
`planL` uses one straight and, if `ccw` is not `None`, one bend. `planS`
|
|
first tries a straight plus a native S-bend, then a pure native S-bend, and
|
|
falls back to a two-L route when no native S-bend candidate fits. `planU`
|
|
is implemented as a two-L route.
|
|
|
|
Transition keys are `(external_ptype, internal_ptype)`. For example, a
|
|
transition keyed by `('m2wire', 'm1wire')` is used when the route is being
|
|
attached to an external `m2wire` port but the selected primitive is `m1wire`.
|
|
Call `add_complementary_transitions()` to automatically add reversed entries
|
|
for any missing opposite directions.
|
|
|
|
Straight and S-bend generator functions may return either a `Pattern` or a
|
|
single-top `Library`. Extra keyword arguments passed to `trace*()` or
|
|
`render()` are forwarded to those generators, along with any keyword
|
|
arguments captured during `plan*()`.
|
|
"""
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Straight:
|
|
"""
|
|
Description of a straight-path generator.
|
|
|
|
`fn(length, **kwargs)` must return a path whose `in_port_name` and
|
|
`out_port_name` ports are separated by `length` along the input axis.
|
|
The planner considers this generator only when the required length is in
|
|
`length_range`, with an inclusive lower bound and exclusive upper bound.
|
|
"""
|
|
ptype: str
|
|
""" Port type produced by this straight segment. """
|
|
|
|
fn: Callable[[float], Pattern] | Callable[[float], Library]
|
|
""" Generator function called as `fn(length, **kwargs)`. """
|
|
|
|
in_port_name: str
|
|
""" Name of the input port on the generated pattern. """
|
|
|
|
out_port_name: str
|
|
""" Name of the output port on the generated pattern. """
|
|
|
|
length_range: tuple[float, float] = (0, numpy.inf)
|
|
""" Valid generated lengths, as `(inclusive_min, exclusive_max)`. """
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SBend:
|
|
"""
|
|
Description of a native S-bend generator.
|
|
|
|
`fn(jog, **kwargs)` is called with a non-negative jog magnitude and must
|
|
return a path whose output port faces back toward the input port. For a
|
|
negative requested jog, `AutoTool` mirrors the generated S-bend during
|
|
rendering.
|
|
"""
|
|
ptype: str
|
|
""" Port type produced by this S-bend. """
|
|
|
|
fn: Callable[[float], Pattern] | Callable[[float], Library]
|
|
"""
|
|
Generator function called as `fn(abs(jog), **kwargs)`. The generated
|
|
geometry is assumed to jog left, i.e. counterclockwise relative to the
|
|
direction of travel. This function is not called when the residual jog is
|
|
zero.
|
|
"""
|
|
|
|
in_port_name: str
|
|
""" Name of the input port on the generated pattern. """
|
|
|
|
out_port_name: str
|
|
""" Name of the output port on the generated pattern. """
|
|
|
|
jog_range: tuple[float, float] = (0, numpy.inf)
|
|
""" Valid residual jog magnitudes, as `(inclusive_min, exclusive_max)`. """
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Bend:
|
|
"""
|
|
Description of a pre-rendered L-bend.
|
|
|
|
`abstract` must contain `in_port_name` and `out_port_name`. The
|
|
`clockwise` flag describes the in-to-out turn direction of that stored
|
|
bend. If `mirror` is true, `AutoTool` mirrors the stored bend to realize
|
|
the opposite turn direction; otherwise it plugs the bend from the
|
|
opposite port where possible.
|
|
"""
|
|
abstract: Abstract
|
|
""" Abstract for the reusable bend pattern. """
|
|
|
|
in_port_name: str
|
|
""" Name of the bend input port. """
|
|
|
|
out_port_name: str
|
|
""" Name of the bend output port. """
|
|
|
|
clockwise: bool = True # Is in-to-out clockwise?
|
|
""" Whether the stored bend turns clockwise from input to output. """
|
|
|
|
mirror: bool = True # Should we mirror to get the other rotation?
|
|
""" Whether to mirror the stored bend to produce the opposite turn. """
|
|
|
|
@property
|
|
def in_port(self) -> Port:
|
|
return self.abstract.ports[self.in_port_name]
|
|
|
|
@property
|
|
def out_port(self) -> Port:
|
|
return self.abstract.ports[self.out_port_name]
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Transition:
|
|
"""
|
|
Description of a pre-rendered port-type transition.
|
|
|
|
`their_port_name` is the external side of the transition and
|
|
`our_port_name` is the side compatible with the selected internal
|
|
primitive. The transition table key should match that direction:
|
|
`(their_ptype, our_ptype)`.
|
|
"""
|
|
abstract: Abstract
|
|
""" Abstract for the reusable transition pattern. """
|
|
|
|
their_port_name: str
|
|
""" Name of the external-side port. """
|
|
|
|
our_port_name: str
|
|
""" Name of the internal primitive-side port. """
|
|
|
|
@property
|
|
def our_port(self) -> Port:
|
|
return self.abstract.ports[self.our_port_name]
|
|
|
|
@property
|
|
def their_port(self) -> Port:
|
|
return self.abstract.ports[self.their_port_name]
|
|
|
|
def reversed(self) -> Self:
|
|
return type(self)(self.abstract, self.our_port_name, self.their_port_name)
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class LPlan:
|
|
""" Candidate L-route configuration before final straight length is known. """
|
|
straight: 'AutoTool.Straight'
|
|
bend: 'AutoTool.Bend | None'
|
|
in_trans: 'AutoTool.Transition | None'
|
|
b_trans: 'AutoTool.Transition | None'
|
|
out_trans: 'AutoTool.Transition | None'
|
|
overhead_x: float
|
|
overhead_y: float
|
|
bend_angle: float
|
|
out_ptype: str
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class LData:
|
|
""" Deferred render data returned by `planL()`. """
|
|
straight_length: float
|
|
straight: 'AutoTool.Straight'
|
|
straight_kwargs: dict[str, Any]
|
|
ccw: SupportsBool | None
|
|
bend: 'AutoTool.Bend | None'
|
|
in_transition: 'AutoTool.Transition | None'
|
|
b_transition: 'AutoTool.Transition | None'
|
|
out_transition: 'AutoTool.Transition | None'
|
|
|
|
def _iter_l_plans(
|
|
self,
|
|
ccw: SupportsBool | None,
|
|
in_ptype: str | None,
|
|
out_ptype: str | None,
|
|
) -> Iterator[LPlan]:
|
|
"""
|
|
Iterate over all possible combinations of straights and bends that
|
|
could form an L-path.
|
|
"""
|
|
bends = cast('list[AutoTool.Bend | None]', self.bends)
|
|
if ccw is None and not bends:
|
|
bends = [None]
|
|
|
|
for straight in self.straights:
|
|
for bend in bends:
|
|
bend_dxy, bend_angle = self._bend2dxy(bend, ccw)
|
|
|
|
in_ptype_pair = ('unk' if in_ptype is None else in_ptype, straight.ptype)
|
|
in_transition = self.transitions.get(in_ptype_pair, None)
|
|
itrans_dxy = self._itransition2dxy(in_transition)
|
|
|
|
out_ptype_pair = (
|
|
'unk' if out_ptype is None else out_ptype,
|
|
straight.ptype if ccw is None else cast('AutoTool.Bend', bend).out_port.ptype
|
|
)
|
|
out_transition = self.transitions.get(out_ptype_pair, None)
|
|
otrans_dxy = self._otransition2dxy(out_transition, bend_angle)
|
|
|
|
b_transition = None
|
|
if ccw is not None:
|
|
assert bend is not None
|
|
if bend.in_port.ptype != straight.ptype:
|
|
b_transition = self.transitions.get((bend.in_port.ptype, straight.ptype), None)
|
|
btrans_dxy = self._itransition2dxy(b_transition)
|
|
|
|
overhead_x = bend_dxy[0] + itrans_dxy[0] + btrans_dxy[0] + otrans_dxy[0]
|
|
overhead_y = bend_dxy[1] + itrans_dxy[1] + btrans_dxy[1] + otrans_dxy[1]
|
|
|
|
if out_transition is not None:
|
|
out_ptype_actual = out_transition.their_port.ptype
|
|
elif ccw is not None:
|
|
assert bend is not None
|
|
out_ptype_actual = bend.out_port.ptype
|
|
else:
|
|
out_ptype_actual = straight.ptype
|
|
|
|
yield self.LPlan(
|
|
straight = straight,
|
|
bend = bend,
|
|
in_trans = in_transition,
|
|
b_trans = b_transition,
|
|
out_trans = out_transition,
|
|
overhead_x = overhead_x,
|
|
overhead_y = overhead_y,
|
|
bend_angle = bend_angle,
|
|
out_ptype = out_ptype_actual,
|
|
)
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SData:
|
|
""" Deferred render data for native-S routes returned by `planS()`. """
|
|
straight_length: float
|
|
straight: 'AutoTool.Straight'
|
|
gen_kwargs: dict[str, Any]
|
|
jog_remaining: float
|
|
sbend: 'AutoTool.SBend'
|
|
in_transition: 'AutoTool.Transition | None'
|
|
b_transition: 'AutoTool.Transition | None'
|
|
out_transition: 'AutoTool.Transition | None'
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class UData:
|
|
""" Deferred render data for `planU()` or double-L `planS()` routes. """
|
|
ldata0: 'AutoTool.LData'
|
|
ldata1: 'AutoTool.LData'
|
|
straight2: 'AutoTool.Straight'
|
|
l2_length: float
|
|
mid_transition: 'AutoTool.Transition | None'
|
|
|
|
def _solve_double_l(
|
|
self,
|
|
length: float,
|
|
jog: float,
|
|
ccw1: SupportsBool,
|
|
ccw2: SupportsBool,
|
|
in_ptype: str | None,
|
|
out_ptype: str | None,
|
|
**kwargs,
|
|
) -> tuple[Port, UData]:
|
|
"""
|
|
Solve for a path consisting of two L-bends connected by a straight segment.
|
|
Used for both U-turns (ccw1 == ccw2) and S-bends (ccw1 != ccw2).
|
|
"""
|
|
is_u = bool(ccw1) == bool(ccw2)
|
|
out_rot = 0 if is_u else pi
|
|
|
|
for plan1 in self._iter_l_plans(ccw1, in_ptype, None):
|
|
rot_mid = rotation_matrix_2d(pi + plan1.bend_angle)
|
|
mid_axis = rot_mid @ numpy.array((1.0, 0.0))
|
|
if not numpy.isclose(mid_axis[0], 0) or numpy.isclose(mid_axis[1], 0):
|
|
continue
|
|
|
|
for straight_mid in self.straights:
|
|
mid_ptype_pair = (plan1.out_ptype, straight_mid.ptype)
|
|
mid_trans = self.transitions.get(mid_ptype_pair, None)
|
|
mid_trans_dxy = self._itransition2dxy(mid_trans)
|
|
|
|
for plan2 in self._iter_l_plans(ccw2, straight_mid.ptype, out_ptype):
|
|
fixed_dxy = numpy.array((plan1.overhead_x, plan1.overhead_y))
|
|
fixed_dxy += rot_mid @ (
|
|
mid_trans_dxy
|
|
+ numpy.array((plan2.overhead_x, plan2.overhead_y))
|
|
)
|
|
|
|
l1_straight = length - fixed_dxy[0]
|
|
l2_straight = (jog - fixed_dxy[1]) / mid_axis[1]
|
|
|
|
if plan1.straight.length_range[0] <= l1_straight < plan1.straight.length_range[1] \
|
|
and straight_mid.length_range[0] <= l2_straight < straight_mid.length_range[1]:
|
|
l3_straight = 0
|
|
if plan2.straight.length_range[0] <= l3_straight < plan2.straight.length_range[1]:
|
|
ldata0 = self.LData(
|
|
l1_straight, plan1.straight, kwargs, ccw1, plan1.bend,
|
|
plan1.in_trans, plan1.b_trans, plan1.out_trans,
|
|
)
|
|
ldata1 = self.LData(
|
|
l3_straight, plan2.straight, kwargs, ccw2, plan2.bend,
|
|
plan2.in_trans, plan2.b_trans, plan2.out_trans,
|
|
)
|
|
|
|
data = self.UData(ldata0, ldata1, straight_mid, l2_straight, mid_trans)
|
|
out_port = Port((length, jog), rotation=out_rot, ptype=plan2.out_ptype)
|
|
return out_port, data
|
|
raise BuildError(f"Failed to find a valid double-L configuration for {length=}, {jog=}")
|
|
|
|
straights: list[Straight]
|
|
""" Straight generators to choose from, in priority order. """
|
|
|
|
bends: list[Bend]
|
|
""" L-bend primitives to choose from, in priority order. """
|
|
|
|
sbends: list[SBend]
|
|
""" Native S-bend generators to choose from, in priority order. """
|
|
|
|
transitions: dict[tuple[str, str], Transition]
|
|
""" Mapping from `(external_ptype, internal_ptype)` to transition primitive. """
|
|
|
|
default_out_ptype: str
|
|
""" Output port type used when a zero-length route provides no primitive ptype. """
|
|
|
|
def add_complementary_transitions(self) -> Self:
|
|
"""
|
|
Add reversed transition entries for any missing opposite directions.
|
|
|
|
Existing explicit entries are preserved. The method mutates
|
|
`self.transitions` and returns `self` for fluent construction.
|
|
"""
|
|
for iioo in list(self.transitions.keys()):
|
|
ooii = (iioo[1], iioo[0])
|
|
self.transitions.setdefault(ooii, self.transitions[iioo].reversed())
|
|
return self
|
|
|
|
@staticmethod
|
|
def _bend2dxy(bend: Bend | None, ccw: SupportsBool | None) -> tuple[NDArray[numpy.float64], float]:
|
|
if ccw is None:
|
|
return numpy.zeros(2), pi
|
|
assert bend is not None
|
|
bend_dxy, bend_angle = bend.in_port.measure_travel(bend.out_port)
|
|
assert bend_angle is not None
|
|
if bool(ccw):
|
|
bend_dxy[1] *= -1
|
|
bend_angle *= -1
|
|
return bend_dxy, bend_angle
|
|
|
|
@staticmethod
|
|
def _sbend2dxy(sbend: SBend, jog: float) -> NDArray[numpy.float64]:
|
|
if numpy.isclose(jog, 0):
|
|
return numpy.zeros(2)
|
|
|
|
sbend_pat_or_tree = sbend.fn(abs(jog))
|
|
sbpat = sbend_pat_or_tree if isinstance(sbend_pat_or_tree, Pattern) else sbend_pat_or_tree.top_pattern()
|
|
dxy, _ = sbpat[sbend.in_port_name].measure_travel(sbpat[sbend.out_port_name])
|
|
return dxy
|
|
|
|
@staticmethod
|
|
def _itransition2dxy(in_transition: Transition | None) -> NDArray[numpy.float64]:
|
|
if in_transition is None:
|
|
return numpy.zeros(2)
|
|
dxy, _ = in_transition.their_port.measure_travel(in_transition.our_port)
|
|
return dxy
|
|
|
|
@staticmethod
|
|
def _otransition2dxy(out_transition: Transition | None, bend_angle: float) -> NDArray[numpy.float64]:
|
|
if out_transition is None:
|
|
return numpy.zeros(2)
|
|
orot = out_transition.our_port.rotation
|
|
assert orot is not None
|
|
otrans_dxy = rotation_matrix_2d(pi - orot - bend_angle) @ (out_transition.their_port.offset - out_transition.our_port.offset)
|
|
return otrans_dxy
|
|
|
|
def planL(
|
|
self,
|
|
ccw: SupportsBool | None,
|
|
length: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
**kwargs,
|
|
) -> tuple[Port, LData]:
|
|
|
|
for plan in self._iter_l_plans(ccw, in_ptype, out_ptype):
|
|
straight_length = length - plan.overhead_x
|
|
if plan.straight.length_range[0] <= straight_length < plan.straight.length_range[1]:
|
|
data = self.LData(
|
|
straight_length = straight_length,
|
|
straight = plan.straight,
|
|
straight_kwargs = kwargs,
|
|
ccw = ccw,
|
|
bend = plan.bend,
|
|
in_transition = plan.in_trans,
|
|
b_transition = plan.b_trans,
|
|
out_transition = plan.out_trans,
|
|
)
|
|
out_port = Port((length, plan.overhead_y), rotation=plan.bend_angle, ptype=plan.out_ptype)
|
|
return out_port, data
|
|
|
|
raise BuildError(f'Failed to find a valid L-path configuration for {length=:,g}, {ccw=}, {in_ptype=}, {out_ptype=}')
|
|
|
|
def _renderL(
|
|
self,
|
|
data: LData,
|
|
tree: ILibrary,
|
|
port_names: tuple[str, str],
|
|
straight_kwargs: dict[str, Any],
|
|
) -> ILibrary:
|
|
"""
|
|
Render an L step into an existing tree.
|
|
"""
|
|
pat = tree.top_pattern()
|
|
if data.in_transition:
|
|
pat.plug(data.in_transition.abstract, {port_names[1]: data.in_transition.their_port_name})
|
|
if not numpy.isclose(data.straight_length, 0):
|
|
straight_pat_or_tree = data.straight.fn(data.straight_length, **(straight_kwargs | data.straight_kwargs))
|
|
pmap = {port_names[1]: data.straight.in_port_name}
|
|
if isinstance(straight_pat_or_tree, Pattern):
|
|
pat.plug(straight_pat_or_tree, pmap, append=True)
|
|
else:
|
|
straight_tree = straight_pat_or_tree
|
|
top = straight_tree.top()
|
|
straight_tree.flatten(top, dangling_ok=True)
|
|
pat.plug(straight_tree[top], pmap, append=True)
|
|
if data.b_transition:
|
|
pat.plug(data.b_transition.abstract, {port_names[1]: data.b_transition.our_port_name})
|
|
if data.ccw is not None:
|
|
bend = data.bend
|
|
assert bend is not None
|
|
mirrored = bend.mirror and (bool(data.ccw) == bend.clockwise)
|
|
inport = bend.in_port_name if (bend.mirror or bool(data.ccw) != bend.clockwise) else bend.out_port_name
|
|
pat.plug(bend.abstract, {port_names[1]: inport}, mirrored=mirrored)
|
|
if data.out_transition:
|
|
pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name})
|
|
return tree
|
|
|
|
def traceL(
|
|
self,
|
|
ccw: SupportsBool | None,
|
|
length: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs,
|
|
) -> Library:
|
|
_out_port, data = self.planL(
|
|
ccw,
|
|
length,
|
|
in_ptype = in_ptype,
|
|
out_ptype = out_ptype,
|
|
)
|
|
|
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
|
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
|
self._renderL(data=data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
|
return tree
|
|
|
|
def planS(
|
|
self,
|
|
length: float,
|
|
jog: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
**kwargs,
|
|
) -> tuple[Port, Any]:
|
|
|
|
success = False
|
|
for straight in self.straights:
|
|
for sbend in self.sbends:
|
|
out_ptype_pair = (
|
|
'unk' if out_ptype is None else out_ptype,
|
|
straight.ptype if numpy.isclose(jog, 0) else sbend.ptype
|
|
)
|
|
out_transition = self.transitions.get(out_ptype_pair, None)
|
|
otrans_dxy = self._otransition2dxy(out_transition, pi)
|
|
|
|
# Assume we'll need a straight segment with transitions, then discard them if they don't fit
|
|
# We do this before generating the s-bend because the transitions might have some dy component
|
|
in_ptype_pair = ('unk' if in_ptype is None else in_ptype, straight.ptype)
|
|
in_transition = self.transitions.get(in_ptype_pair, None)
|
|
itrans_dxy = self._itransition2dxy(in_transition)
|
|
|
|
b_transition = None
|
|
if not numpy.isclose(jog, 0) and sbend.ptype != straight.ptype:
|
|
b_transition = self.transitions.get((sbend.ptype, straight.ptype), None)
|
|
btrans_dxy = self._itransition2dxy(b_transition)
|
|
|
|
if length > itrans_dxy[0] + btrans_dxy[0] + otrans_dxy[0]:
|
|
# `if` guard to avoid unnecessary calls to `_sbend2dxy()`, which calls `sbend.fn()`
|
|
# note some S-bends may have 0 length, so we can't be more restrictive
|
|
jog_remaining = jog - itrans_dxy[1] - btrans_dxy[1] - otrans_dxy[1]
|
|
sbend_dxy = self._sbend2dxy(sbend, jog_remaining)
|
|
straight_length = length - sbend_dxy[0] - itrans_dxy[0] - btrans_dxy[0] - otrans_dxy[0]
|
|
success = straight.length_range[0] <= straight_length < straight.length_range[1]
|
|
if success:
|
|
break
|
|
|
|
# Straight didn't work, see if just the s-bend is enough
|
|
if sbend.ptype != straight.ptype:
|
|
# Need to use a different in-transition for sbend (vs straight)
|
|
in_ptype_pair = ('unk' if in_ptype is None else in_ptype, sbend.ptype)
|
|
in_transition = self.transitions.get(in_ptype_pair, None)
|
|
itrans_dxy = self._itransition2dxy(in_transition)
|
|
|
|
jog_remaining = jog - itrans_dxy[1] - otrans_dxy[1]
|
|
if sbend.jog_range[0] <= jog_remaining < sbend.jog_range[1]:
|
|
sbend_dxy = self._sbend2dxy(sbend, jog_remaining)
|
|
success = numpy.isclose(length, sbend_dxy[0] + itrans_dxy[0] + otrans_dxy[0])
|
|
if success:
|
|
b_transition = None
|
|
straight_length = 0
|
|
break
|
|
if success:
|
|
break
|
|
|
|
if not success:
|
|
ccw0 = jog > 0
|
|
return self._solve_double_l(length, jog, ccw0, not ccw0, in_ptype, out_ptype, **kwargs)
|
|
|
|
if out_transition is not None:
|
|
out_ptype_actual = out_transition.their_port.ptype
|
|
elif not numpy.isclose(jog_remaining, 0):
|
|
out_ptype_actual = sbend.ptype
|
|
elif not numpy.isclose(straight_length, 0):
|
|
out_ptype_actual = straight.ptype
|
|
else:
|
|
out_ptype_actual = self.default_out_ptype
|
|
|
|
data = self.SData(straight_length, straight, kwargs, jog_remaining, sbend, in_transition, b_transition, out_transition)
|
|
out_port = Port((length, jog), rotation=pi, ptype=out_ptype_actual)
|
|
return out_port, data
|
|
|
|
def _renderS(
|
|
self,
|
|
data: SData,
|
|
tree: ILibrary,
|
|
port_names: tuple[str, str],
|
|
gen_kwargs: dict[str, Any],
|
|
) -> ILibrary:
|
|
"""
|
|
Render a native-S step into an existing tree.
|
|
"""
|
|
pat = tree.top_pattern()
|
|
if data.in_transition:
|
|
pat.plug(data.in_transition.abstract, {port_names[1]: data.in_transition.their_port_name})
|
|
if not numpy.isclose(data.straight_length, 0):
|
|
straight_pat_or_tree = data.straight.fn(data.straight_length, **(gen_kwargs | data.gen_kwargs))
|
|
pmap = {port_names[1]: data.straight.in_port_name}
|
|
if isinstance(straight_pat_or_tree, Pattern):
|
|
straight_pat = straight_pat_or_tree
|
|
pat.plug(straight_pat, pmap, append=True)
|
|
else:
|
|
straight_tree = straight_pat_or_tree
|
|
top = straight_tree.top()
|
|
straight_tree.flatten(top, dangling_ok=True)
|
|
pat.plug(straight_tree[top], pmap, append=True)
|
|
if data.b_transition:
|
|
pat.plug(data.b_transition.abstract, {port_names[1]: data.b_transition.our_port_name})
|
|
if not numpy.isclose(data.jog_remaining, 0):
|
|
sbend_pat_or_tree = data.sbend.fn(abs(data.jog_remaining), **(gen_kwargs | data.gen_kwargs))
|
|
pmap = {port_names[1]: data.sbend.in_port_name}
|
|
if isinstance(sbend_pat_or_tree, Pattern):
|
|
pat.plug(sbend_pat_or_tree, pmap, append=True, mirrored=data.jog_remaining < 0)
|
|
else:
|
|
sbend_tree = sbend_pat_or_tree
|
|
top = sbend_tree.top()
|
|
sbend_tree.flatten(top, dangling_ok=True)
|
|
pat.plug(sbend_tree[top], pmap, append=True, mirrored=data.jog_remaining < 0)
|
|
if data.out_transition:
|
|
pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name})
|
|
return tree
|
|
|
|
def traceS(
|
|
self,
|
|
length: float,
|
|
jog: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs,
|
|
) -> Library:
|
|
_out_port, data = self.planS(
|
|
length,
|
|
jog,
|
|
in_ptype = in_ptype,
|
|
out_ptype = out_ptype,
|
|
)
|
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceS')
|
|
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
|
if isinstance(data, self.UData):
|
|
self._renderU(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
|
else:
|
|
self._renderS(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
|
return tree
|
|
|
|
def planU(
|
|
self,
|
|
jog: float,
|
|
*,
|
|
length: float = 0,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
**kwargs,
|
|
) -> tuple[Port, UData]:
|
|
ccw = jog > 0
|
|
return self._solve_double_l(length, jog, ccw, ccw, in_ptype, out_ptype, **kwargs)
|
|
|
|
def _renderU(
|
|
self,
|
|
data: UData,
|
|
tree: ILibrary,
|
|
port_names: tuple[str, str],
|
|
gen_kwargs: dict[str, Any],
|
|
) -> ILibrary:
|
|
pat = tree.top_pattern()
|
|
# 1. First L-bend
|
|
self._renderL(data.ldata0, tree, port_names, gen_kwargs)
|
|
# 2. Connecting straight
|
|
if data.mid_transition:
|
|
pat.plug(data.mid_transition.abstract, {port_names[1]: data.mid_transition.their_port_name})
|
|
if not numpy.isclose(data.l2_length, 0):
|
|
s2_pat_or_tree = data.straight2.fn(data.l2_length, **(gen_kwargs | data.ldata0.straight_kwargs))
|
|
pmap = {port_names[1]: data.straight2.in_port_name}
|
|
if isinstance(s2_pat_or_tree, Pattern):
|
|
pat.plug(s2_pat_or_tree, pmap, append=True)
|
|
else:
|
|
s2_tree = s2_pat_or_tree
|
|
top = s2_tree.top()
|
|
s2_tree.flatten(top, dangling_ok=True)
|
|
pat.plug(s2_tree[top], pmap, append=True)
|
|
# 3. Second L-bend
|
|
self._renderL(data.ldata1, tree, port_names, gen_kwargs)
|
|
return tree
|
|
|
|
def traceU(
|
|
self,
|
|
jog: float,
|
|
*,
|
|
length: float = 0,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs,
|
|
) -> Library:
|
|
_out_port, data = self.planU(
|
|
jog,
|
|
length = length,
|
|
in_ptype = in_ptype,
|
|
out_ptype = out_ptype,
|
|
**kwargs,
|
|
)
|
|
|
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceU')
|
|
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
|
self._renderU(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
|
return tree
|
|
|
|
def render(
|
|
self,
|
|
batch: Sequence[RenderStep],
|
|
*,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs,
|
|
) -> ILibrary:
|
|
|
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
|
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
|
|
|
for step in batch:
|
|
assert step.tool == self
|
|
if step.opcode == 'L':
|
|
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
|
elif step.opcode == 'S':
|
|
if isinstance(step.data, self.UData):
|
|
self._renderU(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
|
else:
|
|
self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
|
elif step.opcode == 'U':
|
|
self._renderU(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
|
return tree
|
|
|
|
|
|
@dataclass
|
|
class PathTool(Tool, metaclass=ABCMeta):
|
|
"""
|
|
Tool that renders routes directly as `Pattern.path()` geometry.
|
|
|
|
`PathTool` supports L and S routes. Immediate `traceL()` and `traceS()`
|
|
create one path element per route, while deferred `render()` combines a
|
|
compatible batch of L/S `RenderStep`s into one multi-vertex path. U routes
|
|
are left to `Pather` synthesis or to a different tool.
|
|
"""
|
|
layer: layer_t
|
|
""" Layer to draw generated path geometry on. """
|
|
|
|
width: float
|
|
""" Width of generated path geometry. """
|
|
|
|
ptype: str = 'unk'
|
|
""" Port type for generated input and output ports. """
|
|
|
|
#@dataclass(frozen=True, slots=True)
|
|
#class LData:
|
|
# dxy: NDArray[numpy.float64]
|
|
|
|
#def __init__(self, layer: layer_t, width: float, ptype: str = 'unk') -> None:
|
|
# Tool.__init__(self)
|
|
# self.layer = layer
|
|
# self.width = width
|
|
# self.ptype: str
|
|
|
|
def _check_out_ptype(self, out_ptype: str | None) -> None:
|
|
if out_ptype and out_ptype != self.ptype:
|
|
raise BuildError(f'Requested {out_ptype=} does not match path ptype {self.ptype}')
|
|
|
|
def _bend_radius(self) -> float:
|
|
return self.width / 2
|
|
|
|
def _plan_l_vertices(self, length: float, bend_run: float) -> NDArray[numpy.float64]:
|
|
vertices = [(0.0, 0.0), (length, 0.0)]
|
|
if not numpy.isclose(bend_run, 0):
|
|
vertices.append((length, bend_run))
|
|
return numpy.array(vertices, dtype=float)
|
|
|
|
def _plan_s_vertices(self, length: float, jog: float) -> NDArray[numpy.float64]:
|
|
if numpy.isclose(jog, 0):
|
|
return numpy.array([(0.0, 0.0), (length, 0.0)], dtype=float)
|
|
|
|
if length < self.width:
|
|
raise BuildError(
|
|
f'Asked to draw S-path with total length {length:,g}, shorter than required bend: {self.width:,g}'
|
|
)
|
|
|
|
# Match AutoTool's straight-then-s-bend placement so the jog happens
|
|
# width/2 before the end while still allowing smaller lateral offsets.
|
|
jog_x = length - self._bend_radius()
|
|
vertices = [
|
|
(0.0, 0.0),
|
|
(jog_x, 0.0),
|
|
(jog_x, jog),
|
|
(length, jog),
|
|
]
|
|
return numpy.array(vertices, dtype=float)
|
|
|
|
def traceL(
|
|
self,
|
|
ccw: SupportsBool | None,
|
|
length: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs, # noqa: ARG002 (unused)
|
|
) -> Library:
|
|
out_port, data = self.planL(
|
|
ccw,
|
|
length,
|
|
in_ptype=in_ptype,
|
|
out_ptype=out_ptype,
|
|
)
|
|
|
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
|
pat.path(layer=self.layer, width=self.width, vertices=self._plan_l_vertices(length, float(out_port.y)))
|
|
|
|
if ccw is None:
|
|
out_rot = pi
|
|
elif bool(ccw):
|
|
out_rot = -pi / 2
|
|
else:
|
|
out_rot = pi / 2
|
|
|
|
pat.ports = {
|
|
port_names[0]: Port((0, 0), rotation=0, ptype=self.ptype),
|
|
port_names[1]: Port(out_port.offset, rotation=out_rot, ptype=self.ptype),
|
|
}
|
|
|
|
return tree
|
|
|
|
def planL(
|
|
self,
|
|
ccw: SupportsBool | None,
|
|
length: float,
|
|
*,
|
|
in_ptype: str | None = None, # noqa: ARG002 (unused)
|
|
out_ptype: str | None = None,
|
|
**kwargs, # noqa: ARG002 (unused)
|
|
) -> tuple[Port, NDArray[numpy.float64]]:
|
|
# TODO check all the math for L-shaped bends
|
|
|
|
self._check_out_ptype(out_ptype)
|
|
|
|
if ccw is not None:
|
|
bend_dxy = numpy.array([1, -1]) * self._bend_radius()
|
|
bend_angle = pi / 2
|
|
|
|
if bool(ccw):
|
|
bend_dxy[1] *= -1
|
|
bend_angle *= -1
|
|
else:
|
|
bend_dxy = numpy.zeros(2)
|
|
bend_angle = pi
|
|
|
|
straight_length = length - bend_dxy[0]
|
|
bend_run = bend_dxy[1]
|
|
|
|
if straight_length < 0:
|
|
raise BuildError(
|
|
f'Asked to draw L-path with total length {length:,g}, shorter than required bend: {bend_dxy[0]:,g}'
|
|
)
|
|
data = numpy.array((length, bend_run))
|
|
out_port = Port(data, rotation=bend_angle, ptype=self.ptype)
|
|
return out_port, data
|
|
|
|
def traceS(
|
|
self,
|
|
length: float,
|
|
jog: float,
|
|
*,
|
|
in_ptype: str | None = None,
|
|
out_ptype: str | None = None,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs, # noqa: ARG002 (unused)
|
|
) -> Library:
|
|
out_port, _data = self.planS(
|
|
length,
|
|
jog,
|
|
in_ptype=in_ptype,
|
|
out_ptype=out_ptype,
|
|
)
|
|
|
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceS')
|
|
pat.path(layer=self.layer, width=self.width, vertices=self._plan_s_vertices(length, jog))
|
|
pat.ports = {
|
|
port_names[0]: Port((0, 0), rotation=0, ptype=self.ptype),
|
|
port_names[1]: out_port,
|
|
}
|
|
return tree
|
|
|
|
def planS(
|
|
self,
|
|
length: float,
|
|
jog: float,
|
|
*,
|
|
in_ptype: str | None = None, # noqa: ARG002 (unused)
|
|
out_ptype: str | None = None,
|
|
**kwargs, # noqa: ARG002 (unused)
|
|
) -> tuple[Port, NDArray[numpy.float64]]:
|
|
self._check_out_ptype(out_ptype)
|
|
self._plan_s_vertices(length, jog)
|
|
data = numpy.array((length, jog))
|
|
out_port = Port((length, jog), rotation=pi, ptype=self.ptype)
|
|
return out_port, data
|
|
|
|
def render(
|
|
self,
|
|
batch: Sequence[RenderStep],
|
|
*,
|
|
port_names: tuple[str, str] = ('A', 'B'),
|
|
**kwargs, # noqa: ARG002 (unused)
|
|
) -> ILibrary:
|
|
|
|
# Transform the batch so the first port is local (at 0,0) but retains its global rotation.
|
|
# This allows the path to be rendered with its original orientation, simplified by
|
|
# translation to the origin. Pather.render will handle the final placement
|
|
# (including rotation alignment) via `pat.plug`.
|
|
first_port = batch[0].start_port
|
|
translation = -first_port.offset
|
|
rotation = 0
|
|
pivot = first_port.offset
|
|
|
|
# Localize the batch for rendering
|
|
local_batch = [step.transformed(translation, rotation, pivot) for step in batch]
|
|
|
|
path_vertices = [local_batch[0].start_port.offset]
|
|
for step in local_batch:
|
|
assert step.tool == self
|
|
|
|
port_rot = step.start_port.rotation
|
|
# Masque convention: Port rotation points INTO the device.
|
|
# So the direction of travel for the path is AWAY from the port, i.e., port_rot + pi.
|
|
assert port_rot is not None
|
|
transform = rotation_matrix_2d(port_rot + pi)
|
|
delta = step.end_port.offset - step.start_port.offset
|
|
local_end = rotation_matrix_2d(-(port_rot + pi)) @ delta
|
|
if step.opcode == 'L':
|
|
local_vertices = self._plan_l_vertices(float(local_end[0]), float(local_end[1]))
|
|
elif step.opcode == 'S':
|
|
local_vertices = self._plan_s_vertices(float(local_end[0]), float(local_end[1]))
|
|
else:
|
|
raise BuildError(f'Unrecognized opcode "{step.opcode}"')
|
|
|
|
for vertex in local_vertices[1:]:
|
|
path_vertices.append(step.start_port.offset + transform @ vertex)
|
|
|
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
|
pat.path(layer=self.layer, width=self.width, vertices=path_vertices)
|
|
pat.ports = {
|
|
port_names[0]: local_batch[0].start_port.copy().rotate(pi),
|
|
port_names[1]: local_batch[-1].end_port.copy().rotate(pi),
|
|
}
|
|
return tree
|