""" 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()