527 lines
20 KiB
Python
527 lines
20 KiB
Python
"""
|
|
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.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
|
|
|
|
#
|
|
# Define some basic wire widths, in nanometers
|
|
# M2 is the top metal; M1 is below it and connected with vias on V1
|
|
#
|
|
M1_WIDTH = 1000
|
|
V1_WIDTH = 500
|
|
M2_WIDTH = 4000
|
|
|
|
#
|
|
# First, we can define some functions for generating our wire geometry
|
|
#
|
|
|
|
def make_pad() -> Pattern:
|
|
"""
|
|
Create a pattern with a single rectangle of M2, with a single port on the bottom
|
|
|
|
Every pad will be an instance of the same pattern, so we will only call this function once.
|
|
"""
|
|
pat = Pattern()
|
|
pat.rect(layer='M2', xctr=0, yctr=0, lx=3 * M2_WIDTH, ly=4 * M2_WIDTH)
|
|
pat.ports['wire_port'] = Port((0, -2 * M2_WIDTH), rotation=pi / 2, ptype='m2wire')
|
|
return pat
|
|
|
|
|
|
def make_via(
|
|
layer_top: layer_t,
|
|
layer_via: layer_t,
|
|
layer_bot: layer_t,
|
|
width_top: float,
|
|
width_via: float,
|
|
width_bot: float,
|
|
ptype_top: str,
|
|
ptype_bot: str,
|
|
) -> Pattern:
|
|
"""
|
|
Generate three concentric squares, on the provided layers
|
|
(`layer_top`, `layer_via`, `layer_bot`) and with the provided widths
|
|
(`width_top`, `width_via`, `width_bot`).
|
|
|
|
Two ports are added, with the provided ptypes (`ptype_top`, `ptype_bot`).
|
|
They are placed at the left edge of the top layer and right edge of the
|
|
bottom layer, respectively.
|
|
|
|
We only have one via type, so we will only call this function once.
|
|
"""
|
|
pat = Pattern()
|
|
pat.rect(layer=layer_via, xctr=0, yctr=0, lx=width_via, ly=width_via)
|
|
pat.rect(layer=layer_bot, xctr=0, yctr=0, lx=width_bot, ly=width_bot)
|
|
pat.rect(layer=layer_top, xctr=0, yctr=0, lx=width_top, ly=width_top)
|
|
pat.ports = {
|
|
'top': Port(offset=(-width_top / 2, 0), rotation=0, ptype=ptype_top),
|
|
'bottom': Port(offset=(width_bot / 2, 0), rotation=pi, ptype=ptype_bot),
|
|
}
|
|
return pat
|
|
|
|
|
|
def make_bend(layer: layer_t, width: float, ptype: str) -> Pattern:
|
|
"""
|
|
Generate a triangular wire, with ports at the left (input) and bottom (output) edges.
|
|
This is effectively a clockwise wire bend.
|
|
|
|
Every bend will be the same, so we only need to call this twice (once each for M1 and M2).
|
|
We could call it additional times for different wire widths or bend types (e.g. squares).
|
|
"""
|
|
pat = Pattern()
|
|
pat.polygon(layer=layer, vertices=[(0, -width / 2), (0, width / 2), (width, -width / 2)])
|
|
pat.ports = {
|
|
'input': Port(offset=(0, 0), rotation=0, ptype=ptype),
|
|
'output': Port(offset=(width / 2, -width / 2), rotation=pi / 2, ptype=ptype),
|
|
}
|
|
return pat
|
|
|
|
|
|
def make_straight_wire(layer: layer_t, width: float, ptype: str, length: float) -> Pattern:
|
|
"""
|
|
Generate a straight wire with ports along either end (x=0 and x=length).
|
|
|
|
Every waveguide will be single-use, so we'll need to create lots of (mostly unique)
|
|
`Pattern`s, and this function will get called very often.
|
|
"""
|
|
pat = Pattern()
|
|
pat.rect(layer=layer, xmin=0, xmax=length, yctr=0, ly=width)
|
|
pat.ports = {
|
|
'input': Port(offset=(0, 0), rotation=0, ptype=ptype),
|
|
'output': Port(offset=(length, 0), rotation=pi, ptype=ptype),
|
|
}
|
|
return pat
|
|
|
|
|
|
def map_layer(layer: layer_t) -> layer_t:
|
|
"""
|
|
Map from a strings to GDS layer numbers
|
|
"""
|
|
layer_mapping = {
|
|
'M1': (10, 0),
|
|
'M2': (20, 0),
|
|
'V1': (30, 0),
|
|
}
|
|
if isinstance(layer, str):
|
|
return layer_mapping.get(layer, layer)
|
|
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
|
|
"""
|
|
# Build some patterns (static cells) using the above functions and store them in a library
|
|
library = Library()
|
|
library['pad'] = make_pad()
|
|
library['m1_bend'] = make_bend(layer='M1', ptype='m1wire', width=M1_WIDTH)
|
|
library['m2_bend'] = make_bend(layer='M2', ptype='m2wire', width=M2_WIDTH)
|
|
library['v1_via'] = make_via(
|
|
layer_top = 'M2',
|
|
layer_via = 'V1',
|
|
layer_bot = 'M1',
|
|
width_top = M2_WIDTH,
|
|
width_via = V1_WIDTH,
|
|
width_bot = M1_WIDTH,
|
|
ptype_bot = 'm1wire',
|
|
ptype_top = 'm2wire',
|
|
)
|
|
|
|
#
|
|
# 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.
|
|
#
|
|
# 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.
|
|
#
|
|
via = library.abstract('v1_via')
|
|
via_transitions = (
|
|
WireTransitionSpec(via, 'top', 'bottom'),
|
|
WireTransitionSpec(via, 'bottom', 'top'),
|
|
)
|
|
|
|
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
|
|
|
|
|
|
#
|
|
# Now we can start building up our library (collection of static cells) and pathing tools.
|
|
#
|
|
# If any of the operations below are confusing, you can cross-reference against the deferred
|
|
# `Pather` tutorial, which handles some things more explicitly (e.g. via placement) and simplifies
|
|
# others (e.g. geometry definition).
|
|
#
|
|
def main() -> None:
|
|
library, M1_tool, M2_tool = prepare_tools()
|
|
|
|
#
|
|
# Create a new pather which writes to `library` and uses `M2_tool` as its default tool.
|
|
# Then, place some pads and start routing wires!
|
|
#
|
|
pather = Pather(library, tools=M2_tool)
|
|
|
|
# Place two pads, and define their ports as 'VCC' and 'GND'
|
|
pather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'})
|
|
pather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'})
|
|
# Add some labels to make the pads easier to distinguish
|
|
pather.pattern.label(layer='M2', string='VCC', offset=(18e3, 30e3))
|
|
pather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3))
|
|
|
|
# Path VCC forward (in this case south) and turn clockwise 90 degrees (ccw=False)
|
|
# The total distance forward (including the bend's forward component) must be 6um
|
|
pather.cw('VCC', 6_000)
|
|
|
|
# Now path VCC to x=0. This time, don't include any bend.
|
|
# Note that if we tried y=0 here, we would get an error since the VCC port is facing in the x-direction.
|
|
pather.straight('VCC', x=0)
|
|
|
|
# Path GND forward by 5um, turning clockwise 90 degrees.
|
|
pather.cw('GND', 5_000)
|
|
|
|
# This time, path GND until it matches the current x-coordinate of VCC. Don't place a bend.
|
|
pather.straight('GND', x=pather['VCC'].offset[0])
|
|
|
|
# Now, start using M1_tool for GND.
|
|
# Since we have defined an M2-to-M1 transition for Pather, we don't need to place one ourselves.
|
|
# If we wanted to place our via manually, we could add `pather.plug('m1_via', {'GND': 'top'})` here
|
|
# and achieve the same result without having to define any transitions in M1_tool.
|
|
# Note that even though we have changed the tool used for GND, the via doesn't get placed until
|
|
# the next time we route GND (the `pather.ccw()` call below).
|
|
pather.retool(M1_tool, keys='GND')
|
|
|
|
# Bundle together GND and VCC, and path the bundle forward and counterclockwise.
|
|
# Pick the distance so that the leading/outermost wire (in this case GND) ends up at x=-10_000.
|
|
# Other wires in the bundle (in this case VCC) should be spaced at 5_000 pitch (so VCC ends up at x=-5_000)
|
|
#
|
|
# Since we recently retooled GND, its path starts with a via down to M1 (included in the distance
|
|
# calculation), and its straight segment and bend will be drawn using M1 while VCC's are drawn with M2.
|
|
pather.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000)
|
|
|
|
# Now use M1_tool as the default tool for all ports/signals.
|
|
# Since VCC does not have an explicitly assigned tool, it will now transition down to M1.
|
|
pather.retool(M1_tool)
|
|
|
|
# Path the GND + VCC bundle forward and counterclockwise by 90 degrees.
|
|
# The total extension (travel distance along the forward direction) for the longest segment (in
|
|
# this case the segment being added to GND) should be exactly 50um.
|
|
# After turning, the wire pitch should be reduced only 1.2um.
|
|
pather.ccw(['GND', 'VCC'], emax=50_000, spacing=1_200)
|
|
|
|
# Make a U-turn with the bundle and expand back out to 4.5um wire pitch.
|
|
# Here, emin specifies the travel distance for the shortest segment. For the first call
|
|
# that applies to VCC, and for the second call, that applies to GND; the relative lengths of the
|
|
# segments depend on their starting positions and their ordering within the bundle.
|
|
pather.cw(['GND', 'VCC'], emin=1_000, spacing=1_200)
|
|
pather.cw(['GND', 'VCC'], emin=2_000, spacing=4_500)
|
|
|
|
# Now, set the default tool back to M2_tool. Note that GND remains on M1 since it has been
|
|
# explicitly assigned a tool.
|
|
pather.retool(M2_tool)
|
|
|
|
# Now path both ports to x=-28_000.
|
|
# With ccw=None, all ports stop at the same coordinate, and so specifying xmin= or xmax= is
|
|
# equivalent.
|
|
pather.straight(['GND', 'VCC'], xmin=-28_000)
|
|
|
|
# Further extend VCC out to x=-50_000, and specify that we would like to get an output on M1.
|
|
# This results in a via at the end of the wire (instead of having one at the start like we got
|
|
# when using pather.retool().
|
|
pather.straight('VCC', x=-50_000, out_ptype='m1wire')
|
|
|
|
# Now extend GND out to x=-50_000, using M2 for a portion of the path.
|
|
# We can use `pather.toolctx()` to temporarily retool, instead of calling `retool()` twice.
|
|
with pather.toolctx(M2_tool, keys='GND'):
|
|
pather.straight('GND', x=-40_000)
|
|
pather.straight('GND', x=-50_000)
|
|
|
|
# Save the pather's pattern into our library
|
|
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)
|
|
writefile(library, 'pather.gds', **GDS_OPTS)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|