[examples] fixup examples and add port_pather example

This commit is contained in:
Jan Petykiewicz 2026-02-14 16:07:19 -08:00
commit 37418d2137
4 changed files with 176 additions and 67 deletions

View file

@ -1,10 +1,10 @@
""" """
Manual wire routing tutorial: Pather and BasicTool Manual wire routing tutorial: Pather and AutoTool
""" """
from collections.abc import Callable from collections.abc import Callable
from numpy import pi from numpy import pi
from masque import Pather, RenderPather, Library, Pattern, Port, layer_t, map_layers from masque import Pather, RenderPather, Library, Pattern, Port, layer_t, map_layers
from masque.builder.tools import BasicTool, PathTool from masque.builder.tools import AutoTool, PathTool
from masque.file.gdsii import writefile from masque.file.gdsii import writefile
from basic_shapes import GDS_OPTS from basic_shapes import GDS_OPTS
@ -110,28 +110,24 @@ def map_layer(layer: layer_t) -> layer_t:
return layer_mapping.get(layer, layer) return layer_mapping.get(layer, layer)
# def prepare_tools() -> tuple[Library, Tool, Tool]:
# Now we can start building up our library (collection of static cells) and pathing tools. """
# Create some basic library elements and tools for drawing M1 and M2
# If any of the operations below are confusing, you can cross-reference against the `RenderPather` """
# tutorial, which handles some things more explicitly (e.g. via placement) and simplifies others
# (e.g. geometry definition).
#
def main() -> None:
# Build some patterns (static cells) using the above functions and store them in a library # Build some patterns (static cells) using the above functions and store them in a library
library = Library() library = Library()
library['pad'] = make_pad() library['pad'] = make_pad()
library['m1_bend'] = make_bend(layer='M1', ptype='m1wire', width=M1_WIDTH) 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['m2_bend'] = make_bend(layer='M2', ptype='m2wire', width=M2_WIDTH)
library['v1_via'] = make_via( library['v1_via'] = make_via(
layer_top='M2', layer_top = 'M2',
layer_via='V1', layer_via = 'V1',
layer_bot='M1', layer_bot = 'M1',
width_top=M2_WIDTH, width_top = M2_WIDTH,
width_via=V1_WIDTH, width_via = V1_WIDTH,
width_bot=M1_WIDTH, width_bot = M1_WIDTH,
ptype_bot='m1wire', ptype_bot = 'm1wire',
ptype_top='m2wire', ptype_top = 'm2wire',
) )
# #
@ -140,53 +136,79 @@ def main() -> None:
# M2_tool will route on M2, using wires with M2_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) # Both tools are able to automatically transition from the other wire type (with a via)
# #
# Note that while we use BasicTool for this tutorial, you can define your own `Tool` # 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, # with arbitrary logic inside -- e.g. with single-use bends, complex transition rules,
# transmission line geometry, or other features. # transmission line geometry, or other features.
# #
M1_tool = BasicTool( M1_tool = AutoTool(
straight = (
# First, we need a function which takes in a length and spits out an M1 wire # First, we need a function which takes in a length and spits out an M1 wire
lambda length: make_straight_wire(layer='M1', ptype='m1wire', width=M1_WIDTH, length=length), straights = [
'input', # When we get a pattern from make_straight_wire, use the port named 'input' as the input AutoTool.Straight(
'output', # and use the port named 'output' as the output 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
), ),
bend = ( ],
library.abstract('m1_bend'), # When we need a bend, we'll reference the pattern we generated earlier bends = [
'input', # To orient it clockwise, use the port named 'input' as the input AutoTool.Bend(
'output', # and 'output' as the output 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 transitions = { # We can automate transitions for different (normally incompatible) port types
'm2wire': ( # For example, when we're attaching to a port with type 'm2wire' ('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 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) '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 '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 default_out_ptype = 'm1wire', # Unless otherwise requested, we'll default to trying to stay on M1
) )
M2_tool = BasicTool( M2_tool = AutoTool(
straight = ( straights = [
# Again, we use make_straight_wire, but this time we set parameters for M2 # Again, we use make_straight_wire, but this time we set parameters for M2
lambda length: make_straight_wire(layer='M2', ptype='m2wire', width=M2_WIDTH, length=length), AutoTool.Straight(
'input', ptype = 'm2wire',
'output', fn = lambda length: make_straight_wire(layer='M2', ptype='m2wire', width=M2_WIDTH, length=length),
in_port_name = 'input',
out_port_name = 'output',
), ),
bend = ( ],
library.abstract('m2_bend'), # and we use an M2 bend bends = [
'input', # and we use an M2 bend
'output', AutoTool.Bend(
abstract = library.abstract('m2_bend'),
in_port_name = 'input',
out_port_name = 'output',
), ),
],
transitions = { transitions = {
'm1wire': ( ('m1wire', 'm2wire'): AutoTool.Transition(
library.abstract('v1_via'), # We still use the same via, library.abstract('v1_via'), # We still use the same via,
'bottom', # but the input port is now 'bottom' 'bottom', # but the input port is now 'bottom'
'top', # and the output port is now 'top' 'top', # and the output port is now 'top'
), ),
}, },
sbends = [],
default_out_ptype = 'm2wire', # We default to trying to stay on M2 default_out_ptype = 'm2wire', # We default to trying to stay on M2
) )
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 `RenderPather`
# 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. # Create a new pather which writes to `library` and uses `M2_tool` as its default tool.
@ -272,7 +294,7 @@ def main() -> None:
pather.path_to('GND', None, -50_000) pather.path_to('GND', None, -50_000)
# Save the pather's pattern into our library # Save the pather's pattern into our library
library['Pather_and_BasicTool'] = pather.pattern library['Pather_and_AutoTool'] = pather.pattern
# Convert from text-based layers to numeric layers for GDS, and output the file # Convert from text-based layers to numeric layers for GDS, and output the file
library.map_layers(map_layer) library.map_layers(map_layer)

View file

@ -2,7 +2,7 @@
Routines for creating normalized 2D lattices and common photonic crystal Routines for creating normalized 2D lattices and common photonic crystal
cavity designs. cavity designs.
""" """
from collection.abc import Sequence from collections.abc import Sequence
import numpy import numpy
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
@ -198,11 +198,11 @@ def ln_defect(
""" """
if defect_length % 2 != 1: if defect_length % 2 != 1:
raise Exception('defect_length must be odd!') raise Exception('defect_length must be odd!')
p = triangular_lattice([2 * d + 1 for d in mirror_dims]) pp = triangular_lattice([2 * dd + 1 for dd in mirror_dims])
half_length = numpy.floor(defect_length / 2) half_length = numpy.floor(defect_length / 2)
hole_nums = numpy.arange(-half_length, half_length + 1) hole_nums = numpy.arange(-half_length, half_length + 1)
holes_to_keep = numpy.in1d(p[:, 0], hole_nums, invert=True) holes_to_keep = numpy.isin(pp[:, 0], hole_nums, invert=True)
return p[numpy.logical_or(holes_to_keep, p[:, 1] != 0), ] return pp[numpy.logical_or(holes_to_keep, pp[:, 1] != 0), :]
def ln_shift_defect( def ln_shift_defect(
@ -248,7 +248,7 @@ def ln_shift_defect(
for sign in (-1, 1): for sign in (-1, 1):
x_val = sign * (x_removed + ind + 1) x_val = sign * (x_removed + ind + 1)
which = numpy.logical_and(xyr[:, 0] == x_val, xyr[:, 1] == 0) which = numpy.logical_and(xyr[:, 0] == x_val, xyr[:, 1] == 0)
xyr[which, ] = (x_val + numpy.sign(x_val) * shifts_a[ind], 0, shifts_r[ind]) xyr[which, :] = (x_val + numpy.sign(x_val) * shifts_a[ind], 0, shifts_r[ind])
return xyr return xyr
@ -309,7 +309,7 @@ def l3_shift_perturbed_defect(
# which holes should be perturbed? (xs[[3, 7]], ys[1]) and (xs[[2, 6]], ys[2]) # which holes should be perturbed? (xs[[3, 7]], ys[1]) and (xs[[2, 6]], ys[2])
perturbed_holes = ((xs[a], ys[b]) for a, b in ((3, 1), (7, 1), (2, 2), (6, 2))) perturbed_holes = ((xs[a], ys[b]) for a, b in ((3, 1), (7, 1), (2, 2), (6, 2)))
for row in xyr: for xy in perturbed_holes:
if numpy.fabs(row) in perturbed_holes: which = (numpy.fabs(xyr[:, :2]) == xy).all(axis=1)
row[2] = perturbed_radius xyr[which, 2] = perturbed_radius
return xyr return xyr

View file

@ -0,0 +1,86 @@
"""
PortPather tutorial: Using .at() syntax for fluent port manipulation
"""
from numpy import pi
from masque import Pather, Library, Pattern, Port
from masque.builder.tools import AutoTool
from masque.file.gdsii import writefile
from basic_shapes import GDS_OPTS
# Reuse helper functions and constants from the basic pather tutorial
from pather import (
M1_WIDTH, V1_WIDTH, M2_WIDTH,
make_pad, make_via, make_bend, make_straight_wire, map_layer
)
def main() -> None:
# Reuse the same patterns (pads, bends, vias) and tools as in pather.py
library, M1_tool, M2_tool = prepare_tools()
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',
)
# Create a RenderPather and place some initial pads (same as Pather tutorial)
rpather = RenderPather(library, tools=M2_tool)
rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'})
rpather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'})
rpather.pattern.label(layer='M2', string='VCC', offset=(18e3, 30e3))
rpather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3))
#
# Routing with .at()
#
# The .at(port_name) method returns a PortPather object which wraps the Pather
# and remembers the selected port(s). This allows method chaining.
# Then we can route just like in the other pather tutorials:
(rpather.at('VCC')
.path(ccw=False, length=6_000)
.path_to(ccw=None, x=0)
)
rpather.at('GND').path(0, 5_000).path_to(None, x=rpather['VCC'].x)
# We're using AutoTool so we could retool directly to M1_ptool like in the Pather
# tutorial, but let's manually plug to demonstrate what it looks like:
(rpather.at('GND')
.plug('v1_via', 'top')
.retool(M1_tool) # this only retools the 'GND' port
)
# We can also pass multiple ports to .at(), and then use .mpath() on them:
(rpather.at(['GND', 'VCC'])
.mpath(ccw=True, xmax=-10_000, spacing=5_000)
.retool(M1_tool) # this retools both ports
.mpath(ccw=True, emax=50_000, spacing=1_200) # ...causing an automatic via on VCC here
.mpath(ccw=False, emin=1_000, spacing=1_200)
.mpath(ccw=False, emin=2_000, spacing=4_500)
)
# Now we can finish up the equivalent to the other tutorials..
rpather.at('VCC').retool(M2_tool)
rpather.at(['GND', 'VCC']).mpath(None, xmin=-28_000)
rpather.at('VCC').path_to(None, x=-50_000, out_ptype='m1wire')
with pather.toolctx(M2_tool, keys=['GND']):
pather.at('GND').path_to(None, x=-40_000)
pather.at('GND').path_to(None, x=-50_000)
#
# Save result
#
library['PortPather_Tutorial'] = pather.pattern
library.map_layers(map_layer)
writefile(library, 'port_pather.gds', **GDS_OPTS)
if __name__ == '__main__':
main()

View file

@ -25,14 +25,14 @@ def main() -> None:
library = Library() library = Library()
library['pad'] = make_pad() library['pad'] = make_pad()
library['v1_via'] = make_via( library['v1_via'] = make_via(
layer_top='M2', layer_top = 'M2',
layer_via='V1', layer_via = 'V1',
layer_bot='M1', layer_bot = 'M1',
width_top=M2_WIDTH, width_top = M2_WIDTH,
width_via=V1_WIDTH, width_via = V1_WIDTH,
width_bot=M1_WIDTH, width_bot = M1_WIDTH,
ptype_bot='m1wire', ptype_bot = 'm1wire',
ptype_top='m2wire', ptype_top = 'm2wire',
) )
# `PathTool` is more limited than `BasicTool`. It only generates one type of shape # `PathTool` is more limited than `BasicTool`. It only generates one type of shape
@ -42,7 +42,7 @@ def main() -> None:
M2_ptool = PathTool(layer='M2', width=M2_WIDTH, ptype='m2wire') M2_ptool = PathTool(layer='M2', width=M2_WIDTH, ptype='m2wire')
rpather = RenderPather(tools=M2_ptool, library=library) rpather = RenderPather(tools=M2_ptool, library=library)
# As in the pather tutorial, we make soem pads and labels... # As in the pather tutorial, we make some pads and labels...
rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'}) rpather.place('pad', offset=(18_000, 30_000), port_map={'wire_port': 'VCC'})
rpather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'}) rpather.place('pad', offset=(18_000, 60_000), port_map={'wire_port': 'GND'})
rpather.pattern.label(layer='M2', string='VCC', offset=(18e3, 30e3)) rpather.pattern.label(layer='M2', string='VCC', offset=(18e3, 30e3))
@ -52,7 +52,7 @@ def main() -> None:
rpather.path('VCC', ccw=False, length=6_000) rpather.path('VCC', ccw=False, length=6_000)
rpather.path_to('VCC', ccw=None, x=0) rpather.path_to('VCC', ccw=None, x=0)
rpather.path('GND', 0, 5_000) rpather.path('GND', 0, 5_000)
rpather.path_to('GND', None, x=rpather['VCC'].offset[0]) rpather.path_to('GND', None, x=rpather['VCC'].x)
# `PathTool` doesn't know how to transition betwen metal layers, so we have to # `PathTool` doesn't know how to transition betwen metal layers, so we have to
# `plug` the via into the GND wire ourselves. # `plug` the via into the GND wire ourselves.
@ -76,13 +76,14 @@ def main() -> None:
# just ask it to transition to an 'm1wire' port at the end of the final VCC segment. # just ask it to transition to an 'm1wire' port at the end of the final VCC segment.
# Instead, we have to calculate the via size ourselves, and adjust the final position # Instead, we have to calculate the via size ourselves, and adjust the final position
# to account for it. # to account for it.
via_size = abs( v1pat = library['v1_via']
library['v1_via'].ports['top'].offset[0] via_size = abs(v1pat.ports['top'].x - v1pat.ports['bottom'].x)
- library['v1_via'].ports['bottom'].offset[0] # alternatively, via_size = v1pat.ports['top'].measure_travel(v1pat.ports['bottom'])[0][0]
) # would take into account the port orientations if we didn't already know they're along x
rpather.path_to('VCC', None, -50_000 + via_size) rpather.path_to('VCC', None, -50_000 + via_size)
rpather.plug('v1_via', {'VCC': 'top'}) rpather.plug('v1_via', {'VCC': 'top'})
# Render the path we defined
rpather.render() rpather.render()
library['RenderPather_and_PathTool'] = rpather.pattern library['RenderPather_and_PathTool'] = rpather.pattern