[builder] major Pather/Planner/Tool rework
This commit is contained in:
parent
0f39ce8816
commit
22e645e527
28 changed files with 6036 additions and 2467 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue