[builder] major Pather/Planner/Tool rework

This commit is contained in:
Jan Petykiewicz 2026-07-08 23:58:24 -07:00
commit 22e645e527
28 changed files with 6036 additions and 2467 deletions

View file

@ -26,8 +26,8 @@ Contents
* Explore alternate ways of specifying a pattern for `.plug()` and `.place()`
- [pather](pather.py)
* Use `Pather` to route individual wires and wire bundles
* Use `AutoTool` to generate paths
* Use `AutoTool` to automatically transition between path types
* Define a custom `Tool` that exposes primitive routing offers
* Use primitive offers to automatically transition between path types
- [renderpather](renderpather.py)
* Use `Pather(auto_render=False)` and `PathTool` to build a layout similar to the one in [pather](pather.py),
but using `Path` shapes instead of `Polygon`s.

View file

@ -1,10 +1,18 @@
"""
Manual wire routing tutorial: Pather and AutoTool
Manual wire routing tutorial: Pather and primitive offers
"""
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any, Literal
import numpy
from numpy import pi
from masque import Pather, Library, Pattern, Port, layer_t
from masque.builder.tools import AutoTool, Tool
from masque.abstract import Abstract
from masque.builder import BendOffer, RenderStep, StraightOffer, Tool
from masque.error import BuildError
from masque.file.gdsii import writefile
from masque.library import ILibrary, SINGLE_USE_PREFIX
from basic_shapes import GDS_OPTS
@ -111,6 +119,259 @@ def map_layer(layer: layer_t) -> layer_t:
return layer
@dataclass(frozen=True, slots=True)
class WireStraightData:
length: float
out_transition: 'WireTransitionSpec | None' = None
@dataclass(frozen=True, slots=True)
class WireBendData:
straight_length: float
ccw: bool
@dataclass(frozen=True, slots=True)
class WireTransitionSpec:
abstract: Abstract
in_port_name: str
out_port_name: str
@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 WireTransitionData:
spec: WireTransitionSpec
@dataclass
class PrimitiveWireTool(Tool):
"""
Minimal routing tool that exposes local routing primitives directly.
The high-level `Pather` methods below still decide how to compose straights,
bends, and ptype transitions. This tool only describes which one-step
primitives it can draw and how selected primitives should be rendered.
"""
layer: layer_t
width: float
ptype: str
bend: Abstract
transitions: Sequence[WireTransitionSpec]
def _straight_pattern(self, length: float) -> Pattern:
return make_straight_wire(layer=self.layer, width=self.width, ptype=self.ptype, length=length)
@staticmethod
def _transition_length(spec: WireTransitionSpec) -> float | None:
dxy, angle = spec.in_port.measure_travel(spec.out_port)
if angle is None or not numpy.isclose(angle, pi) or not numpy.isclose(dxy[1], 0):
return None
return float(dxy[0])
def _transition_offers(self, in_ptype: str | None) -> tuple[StraightOffer, ...]:
offers: list[StraightOffer] = []
for index, spec in enumerate(self.transitions):
if spec.out_port.ptype != self.ptype:
continue
if in_ptype not in (None, 'unk', spec.in_port.ptype):
continue
length = self._transition_length(spec)
if length is None:
continue
def endpoint_planner(
parameter: float,
*,
spec: WireTransitionSpec = spec,
length: float = length,
) -> Port:
_ = parameter
return Port((length, 0), rotation=pi, ptype=spec.out_port.ptype)
def commit_planner(
parameter: float,
*,
spec: WireTransitionSpec = spec,
) -> WireTransitionData:
_ = parameter
return WireTransitionData(spec)
offers.append(StraightOffer(
in_ptype = spec.in_port.ptype,
out_ptype = spec.out_port.ptype,
priority_bias = index * 1e7,
length_domain = (length, length),
endpoint_planner = endpoint_planner,
commit_planner = commit_planner,
))
return tuple(offers)
def _out_transition_offers(self, out_ptype: str | None) -> tuple[StraightOffer, ...]:
if out_ptype in ('unk', self.ptype):
return ()
offers: list[StraightOffer] = []
for index, spec in enumerate(self.transitions):
if spec.in_port.ptype != self.ptype:
continue
if out_ptype is not None and spec.out_port.ptype != out_ptype:
continue
transition_length = self._transition_length(spec)
if transition_length is None:
continue
def endpoint_planner(
length: float,
*,
spec: WireTransitionSpec = spec,
transition_length: float = transition_length,
) -> Port:
straight_length = length - transition_length
if straight_length < 0:
raise BuildError(
f'Asked to draw straight path with total length {length:,g}, shorter than required transition: {transition_length:,g}'
)
return Port((length, 0), rotation=pi, ptype=spec.out_port.ptype)
def commit_planner(
length: float,
*,
spec: WireTransitionSpec = spec,
transition_length: float = transition_length,
) -> WireStraightData:
endpoint_planner(length)
return WireStraightData(length - transition_length, spec)
offers.append(StraightOffer(
in_ptype = self.ptype,
out_ptype = spec.out_port.ptype,
priority_bias = index * 1e7,
length_domain = (transition_length, numpy.inf),
endpoint_planner = endpoint_planner,
commit_planner = commit_planner,
))
return tuple(offers)
def primitive_offers(
self,
kind: Literal['straight', 'bend', 's', 'u'],
*,
in_ptype: str | None = None,
out_ptype: str | None = None, # noqa: ARG002 (Pather validates selected output ptypes)
**kwargs: Any,
) -> tuple[StraightOffer | BendOffer, ...]:
if kind == 'straight':
route_kwargs = dict(kwargs)
def endpoint_planner(length: float) -> Port:
return Port((length, 0), rotation=pi, ptype=self.ptype)
def commit_planner(length: float) -> WireStraightData:
_ = route_kwargs
return WireStraightData(length)
native_offer = StraightOffer(
in_ptype = self.ptype,
out_ptype = self.ptype,
endpoint_planner = endpoint_planner,
commit_planner = commit_planner,
)
return (*self._transition_offers(in_ptype), native_offer, *self._out_transition_offers(out_ptype))
if kind == 'bend':
ccw = bool(kwargs.pop('ccw'))
bend_forward = self.width / 2
bend_run = bend_forward if ccw else -bend_forward
bend_rotation = -pi / 2 if ccw else pi / 2
def endpoint_planner(length: float) -> Port:
straight_length = length - bend_forward
if straight_length < 0:
raise BuildError(
f'Asked to draw L-path with total length {length:,g}, shorter than required bend: {bend_forward:,g}'
)
return Port((length, bend_run), rotation=bend_rotation, ptype=self.ptype)
def commit_planner(length: float) -> WireBendData:
endpoint_planner(length)
return WireBendData(straight_length=length - bend_forward, ccw=ccw)
return (BendOffer(
in_ptype = self.ptype,
out_ptype = self.ptype,
ccw = ccw,
length_domain = (bend_forward, numpy.inf),
endpoint_planner = endpoint_planner,
commit_planner = commit_planner,
),)
if kind in ('s', 'u'):
return ()
raise BuildError(f'Unrecognized primitive offer kind {kind!r}')
def _render_straight(self, tree: ILibrary, port_names: tuple[str, str], data: WireStraightData) -> None:
if numpy.isclose(data.length, 0) and data.out_transition is None:
return
if not numpy.isclose(data.length, 0):
tree.top_pattern().plug(
self._straight_pattern(data.length),
{port_names[1]: 'input'},
append=True,
)
if data.out_transition is not None:
self._render_transition(tree, port_names, WireTransitionData(data.out_transition))
def _render_bend(self, tree: ILibrary, port_names: tuple[str, str], data: WireBendData) -> None:
self._render_straight(tree, port_names, WireStraightData(data.straight_length))
tree.top_pattern().plug(
self.bend,
{port_names[1]: 'input'},
mirrored=data.ccw,
)
@staticmethod
def _render_transition(tree: ILibrary, port_names: tuple[str, str], data: WireTransitionData) -> None:
tree.top_pattern().plug(
data.spec.abstract,
{port_names[1]: data.spec.in_port_name},
)
def render(
self,
batch: Sequence[RenderStep],
*,
port_names: tuple[str, str] = ('A', 'B'),
**kwargs: Any, # noqa: ARG002 (no per-render options in this example tool)
) -> ILibrary:
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'primitive_wire')
pat.add_port_pair(names=port_names, ptype=batch[0].start_port.ptype if batch else self.ptype)
for step in batch:
assert step.tool == self
if isinstance(step.data, WireTransitionData):
self._render_transition(tree, port_names, step.data)
elif isinstance(step.data, WireStraightData):
self._render_straight(tree, port_names, step.data)
elif isinstance(step.data, WireBendData):
self._render_bend(tree, port_names, step.data)
else:
raise BuildError(f'Unexpected primitive render data {type(step.data)}')
return tree
def prepare_tools() -> tuple[Library, Tool, Tool]:
"""
Create some basic library elements and tools for drawing M1 and M2
@ -133,70 +394,33 @@ def prepare_tools() -> tuple[Library, Tool, Tool]:
#
# Now, define two tools.
# M1_tool will route on M1, using wires with M1_WIDTH
# M2_tool will route on M2, using wires with M2_WIDTH
# Both tools are able to automatically transition from the other wire type (with a via)
# M1_tool will route on M1, using wires with M1_WIDTH.
# M2_tool will route on M2, using wires with M2_WIDTH.
#
# Note that while we use AutoTool for this tutorial, you can define your own `Tool`
# with arbitrary logic inside -- e.g. with single-use bends, complex transition rules,
# transmission line geometry, or other features.
# Unlike the reusable `AutoTool`, this tutorial tool exposes primitive offers
# directly: it tells `Pather` about native straight/bend primitives and about
# via adapters that can transition between M1 and M2 port types.
#
M1_tool = AutoTool(
# First, we need a function which takes in a length and spits out an M1 wire
straights = [
AutoTool.Straight(
ptype = 'm1wire',
fn = lambda length: make_straight_wire(layer='M1', ptype='m1wire', width=M1_WIDTH, length=length),
in_port_name = 'input', # When we get a pattern from make_straight_wire, use the port named 'input' as the input
out_port_name = 'output', # and use the port named 'output' as the output
),
],
bends = [
AutoTool.Bend(
abstract = library.abstract('m1_bend'), # When we need a bend, we'll reference the pattern we generated earlier
in_port_name = 'input',
out_port_name = 'output',
clockwise = True,
),
],
transitions = { # We can automate transitions for different (normally incompatible) port types
('m2wire', 'm1wire'): AutoTool.Transition( # For example, when we're attaching to a port with type 'm2wire'
library.abstract('v1_via'), # we can place a V1 via
'top', # using the port named 'top' as the input (i.e. the M2 side of the via)
'bottom', # and using the port named 'bottom' as the output
),
},
sbends = [],
default_out_ptype = 'm1wire', # Unless otherwise requested, we'll default to trying to stay on M1
via = library.abstract('v1_via')
via_transitions = (
WireTransitionSpec(via, 'top', 'bottom'),
WireTransitionSpec(via, 'bottom', 'top'),
)
M2_tool = AutoTool(
straights = [
# Again, we use make_straight_wire, but this time we set parameters for M2
AutoTool.Straight(
ptype = 'm2wire',
fn = lambda length: make_straight_wire(layer='M2', ptype='m2wire', width=M2_WIDTH, length=length),
in_port_name = 'input',
out_port_name = 'output',
),
],
bends = [
# and we use an M2 bend
AutoTool.Bend(
abstract = library.abstract('m2_bend'),
in_port_name = 'input',
out_port_name = 'output',
),
],
transitions = {
('m1wire', 'm2wire'): AutoTool.Transition(
library.abstract('v1_via'), # We still use the same via,
'bottom', # but the input port is now 'bottom'
'top', # and the output port is now 'top'
),
},
sbends = [],
default_out_ptype = 'm2wire', # We default to trying to stay on M2
M1_tool = PrimitiveWireTool(
layer = 'M1',
width = M1_WIDTH,
ptype = 'm1wire',
bend = library.abstract('m1_bend'),
transitions = via_transitions,
)
M2_tool = PrimitiveWireTool(
layer = 'M2',
width = M2_WIDTH,
ptype = 'm2wire',
bend = library.abstract('m2_bend'),
transitions = via_transitions,
)
return library, M1_tool, M2_tool
@ -292,7 +516,7 @@ def main() -> None:
pather.straight('GND', x=-50_000)
# Save the pather's pattern into our library
library['Pather_and_AutoTool'] = pather.pattern
library['Pather_and_PrimitiveOffers'] = pather.pattern
# Convert from text-based layers to numeric layers for GDS, and output the file
library.map_layers(map_layer)

View file

@ -2,7 +2,7 @@
Manual wire routing tutorial: deferred Pather and PathTool
"""
from masque import Pather, Library
from masque.builder.tools import PathTool
from masque.builder import PathTool
from masque.file.gdsii import writefile
from basic_shapes import GDS_OPTS