diff --git a/masque/builder/tools.py b/masque/builder/tools.py index c17a7b9..923e4a3 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -1,9 +1,19 @@ """ Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides) -Concrete tools may implement native planning/rendering for `L`, `S`, or `U` routes. -Any unimplemented planning method falls back to the corresponding `trace*()` method, -and `Pather` may further synthesize some routes from simpler primitives when needed. +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 @@ -25,8 +35,11 @@ from ..error import BuildError @dataclass(frozen=True, slots=True) class RenderStep: """ - Representation of a single saved operation, used by deferred `Pather` - instances and passed to `Tool.render()` when `Pather.render()` is called. + 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. @@ -37,10 +50,13 @@ class RenderStep: """ tool: 'Tool | None' - """ The current tool. May be `None` if `opcode='P'` """ + """ 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""" @@ -101,7 +117,9 @@ class RenderStep: def measure_tool_plan(tree: ILibrary, port_names: tuple[str, str]) -> tuple[Port, Any]: """ - Extracts a Port and returns the tree (as data) for tool planning fallbacks. + 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]] @@ -113,6 +131,13 @@ def measure_tool_plan(tree: ILibrary, port_names: tuple[str, str]) -> tuple[Port 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, @@ -172,7 +197,7 @@ class Tool: """ 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 ouput port). + `jog` is positive when moving left of the direction of travel (from input to output port). Used by `Pather`. @@ -236,7 +261,7 @@ class Tool: Returns: The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0. - Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering. + Any tool-specific data, to be stored in `RenderStep.data`, for use during rendering. Raises: BuildError if an impossible or unsupported geometry is requested. @@ -284,7 +309,7 @@ class Tool: Returns: The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0. - Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering. + Any tool-specific data, to be stored in `RenderStep.data`, for use during rendering. Raises: BuildError if an impossible or unsupported geometry is requested. @@ -312,8 +337,9 @@ class Tool: **kwargs, ) -> Library: """ - Create a wire or waveguide that travels exactly `jog` distance along the axis - perpendicular to its input port (i.e. a U-bend). + 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. @@ -328,6 +354,7 @@ class Tool: 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 @@ -351,8 +378,9 @@ class Tool: **kwargs, ) -> tuple[Port, Any]: """ - Plan a wire or waveguide that travels exactly `jog` distance along the axis - perpendicular to its input port (i.e. a U-bend). + 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 @@ -374,7 +402,7 @@ class Tool: Returns: The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0. - Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering. + Any tool-specific data, to be stored in `RenderStep.data`, for use during rendering. Raises: BuildError if an impossible or unsupported geometry is requested. @@ -404,6 +432,11 @@ class Tool: 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. @@ -464,15 +497,18 @@ abstract_tuple_t = tuple[Abstract, str, str] @dataclass class SimpleTool(Tool, metaclass=ABCMeta): """ - A simple tool which relies on a single pre-rendered `bend` pattern, a function - for generating straight paths, and a table of pre-rendered `transitions` for converting - from non-native ptypes. + 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(length: float), in_port_name, out_port_name` """ + """ `(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` """ + """ `(clockwise_bend_abstract, in_port_name, out_port_name)` for L turns. """ default_out_ptype: str """ Default value for out_ptype """ @@ -482,7 +518,7 @@ class SimpleTool(Tool, metaclass=ABCMeta): @dataclass(frozen=True, slots=True) class LData: - """ Data for planL """ + """ Deferred render data returned by `planL()`. """ straight_length: float straight_kwargs: dict[str, Any] ccw: SupportsBool | None @@ -608,43 +644,114 @@ class SimpleTool(Tool, metaclass=ABCMeta): @dataclass class AutoTool(Tool, metaclass=ABCMeta): """ - A simple tool which relies on a single pre-rendered `bend` pattern, a function - for generating straight paths, and a table of pre-rendered `transitions` for converting - from non-native ptypes. + 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 """ + """ + 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 an s-bend generator """ + """ + 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. `jog` (only argument) is assumed to be left (ccw) relative to travel - and may be negative for a jog in the opposite direction. Won't be called if jog=0. + 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 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: @@ -656,10 +763,22 @@ class AutoTool(Tool, metaclass=ABCMeta): @dataclass(frozen=True, slots=True) class Transition: - """ Description of a pre-rendered 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: @@ -674,7 +793,7 @@ class AutoTool(Tool, metaclass=ABCMeta): @dataclass(frozen=True, slots=True) class LPlan: - """ Template for an L-path configuration """ + """ Candidate L-route configuration before final straight length is known. """ straight: 'AutoTool.Straight' bend: 'AutoTool.Bend | None' in_trans: 'AutoTool.Transition | None' @@ -687,7 +806,7 @@ class AutoTool(Tool, metaclass=ABCMeta): @dataclass(frozen=True, slots=True) class LData: - """ Data for planL """ + """ Deferred render data returned by `planL()`. """ straight_length: float straight: 'AutoTool.Straight' straight_kwargs: dict[str, Any] @@ -758,7 +877,7 @@ class AutoTool(Tool, metaclass=ABCMeta): @dataclass(frozen=True, slots=True) class SData: - """ Data for planS """ + """ Deferred render data for native-S routes returned by `planS()`. """ straight_length: float straight: 'AutoTool.Straight' gen_kwargs: dict[str, Any] @@ -770,7 +889,7 @@ class AutoTool(Tool, metaclass=ABCMeta): @dataclass(frozen=True, slots=True) class UData: - """ Data for planU or planS (double-L) """ + """ Deferred render data for `planU()` or double-L `planS()` routes. """ ldata0: 'AutoTool.LData' ldata1: 'AutoTool.LData' straight2: 'AutoTool.Straight' @@ -834,21 +953,27 @@ class AutoTool(Tool, metaclass=ABCMeta): raise BuildError(f"Failed to find a valid double-L configuration for {length=}, {jog=}") straights: list[Straight] - """ List of straight-generators to choose from, in order of priority """ + """ Straight generators to choose from, in priority order. """ bends: list[Bend] - """ List of bends to choose from, in order of priority """ + """ L-bend primitives to choose from, in priority order. """ sbends: list[SBend] - """ List of S-bend generators to choose from, in order of priority """ + """ Native S-bend generators to choose from, in priority order. """ transitions: dict[tuple[str, str], Transition] - """ `{(external_ptype, internal_ptype): Transition, ...}` """ + """ Mapping from `(external_ptype, internal_ptype)` to transition primitive. """ default_out_ptype: str - """ Default value for out_ptype """ + """ 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()) @@ -928,7 +1053,7 @@ class AutoTool(Tool, metaclass=ABCMeta): straight_kwargs: dict[str, Any], ) -> ILibrary: """ - Render an L step into a preexisting tree + Render an L step into an existing tree. """ pat = tree.top_pattern() if data.in_transition: @@ -1061,7 +1186,7 @@ class AutoTool(Tool, metaclass=ABCMeta): gen_kwargs: dict[str, Any], ) -> ILibrary: """ - Render an L step into a preexisting tree + Render a native-S step into an existing tree. """ pat = tree.top_pattern() if data.in_transition: @@ -1207,19 +1332,21 @@ class AutoTool(Tool, metaclass=ABCMeta): @dataclass class PathTool(Tool, metaclass=ABCMeta): """ - A tool which draws `Path` geometry elements. + Tool that renders routes directly as `Pattern.path()` geometry. - If `planL` / `render` are used, the `Path` elements can cover >2 vertices; - with `path` only individual rectangles will be drawn. + `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 on """ + """ Layer to draw generated path geometry on. """ width: float - """ `Path` width """ + """ Width of generated path geometry. """ ptype: str = 'unk' - """ ptype for any ports in patterns generated by this tool """ + """ Port type for generated input and output ports. """ #@dataclass(frozen=True, slots=True) #class LData: