[examples] expand port_pather tutorial

This commit is contained in:
Jan Petykiewicz 2026-02-14 17:06:29 -08:00
commit 737d41d592

View file

@ -1,30 +1,16 @@
"""
PortPather tutorial: Using .at() syntax for fluent port manipulation
PortPather tutorial: Using .at() syntax
"""
from numpy import pi
from masque import Pather, Library, Pattern, Port
from masque.builder.tools import AutoTool
from masque import RenderPather, Pattern, Port, R90
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
from pather import map_layer, prepare_tools
)
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)
@ -35,51 +21,150 @@ def main() -> None:
rpather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3))
#
# Routing with .at()
# Routing with .at() chaining
#
# 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:
# Route VCC: 6um South, then West to x=0.
# (Note: since the port points North into the pad, path() moves South by default)
(rpather.at('VCC')
.path(ccw=False, length=6_000)
.path_to(ccw=None, x=0)
.path(ccw=False, length=6_000) # Move South, turn West (Clockwise)
.path_to(ccw=None, x=0) # Continue West to x=0
)
rpather.at('GND').path(0, 5_000).path_to(None, x=rpather['VCC'].x)
# Route GND: 5um South, then West to match VCC's x-coordinate.
rpather.at('GND').path(ccw=False, length=5_000).path_to(ccw=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:
#
# Tool management and manual plugging
#
# We can use .retool() to change the tool for specific ports.
# We can also use .plug() directly on a PortPather.
# Manually add a via to GND and switch to M1_tool for subsequent segments
(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:
# We can also pass multiple ports to .at(), and then use .mpath() on them.
# Here we bundle them, turn South, and retool both to M1 (VCC gets an auto-via).
(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)
.mpath(ccw=True, xmax=-10_000, spacing=5_000) # Move West to -10k, turn South
.retool(M1_tool) # Retools both GND and VCC
.mpath(ccw=True, emax=50_000, spacing=1_200) # Turn East, moves 50um extension
.mpath(ccw=False, emin=1_000, spacing=1_200) # U-turn back South
.mpath(ccw=False, emin=2_000, spacing=4_500) # U-turn back West
)
# Now we can finish up the equivalent to the other tutorials..
# Retool VCC back to M2 and move both to x=-28k
rpather.at('VCC').retool(M2_tool)
rpather.at(['GND', 'VCC']).mpath(None, xmin=-28_000)
rpather.at(['GND', 'VCC']).mpath(ccw=None, xmin=-28_000)
# Final segments to -50k
rpather.at('VCC').path_to(ccw=None, x=-50_000, out_ptype='m1wire')
with rpather.at('GND').toolctx(M2_tool):
rpather.at('GND').path_to(ccw=None, x=-40_000)
rpather.at('GND').path_to(ccw=None, x=-50_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
# Branching with save_copy and into_copy
#
library['PortPather_Tutorial'] = pather.pattern
# .save_copy(new_name) creates a port copy and keeps the original selected.
# .into_copy(new_name) creates a port copy and selects the new one.
# Create a tap on GND
(rpather.at('GND')
.path(ccw=None, length=5_000) # Move GND further West
.save_copy('GND_TAP') # Mark this location for a later branch
.pathS(length=10_000, jog=-10_000) # Continue GND with an S-bend
)
# Branch VCC and follow the new branch
(rpather.at('VCC')
.path(ccw=None, length=5_000)
.into_copy('VCC_BRANCH') # We are now manipulating 'VCC_BRANCH'
.path(ccw=True, length=5_000) # VCC_BRANCH turns South
)
# The original 'VCC' port remains at x=-55k, y=VCC.y
#
# Port set management: add, drop, rename, delete
#
# Route the GND_TAP we saved earlier.
(rpather.at('GND_TAP')
.retool(M1_tool)
.path(ccw=True, length=10_000) # Turn South
.rename_to('GND_FEED') # Give it a more descriptive name
.retool(M1_tool) # Re-apply tool to the new name
)
# We can manage the active set of ports in a PortPather
pp = rpather.at(['VCC_BRANCH', 'GND_FEED'])
pp.add_port('GND') # Now tracking 3 ports
pp.drop_port('VCC_BRANCH') # Now tracking 2 ports: GND_FEED, GND
pp.path_each(ccw=None, length=5_000) # Move both 5um forward (length > transition size)
# We can also delete ports from the pather entirely
rpather.at('VCC').delete() # VCC is gone (we have VCC_BRANCH instead)
#
# Advanced Connections: path_into and path_from
#
# path_into routes FROM the selected port TO a target port.
# path_from routes TO the selected port FROM a source port.
# Create a destination component
dest_ports = {
'in_A': Port((0, 0), rotation=R90, ptype='m2wire'),
'in_B': Port((5_000, 0), rotation=R90, ptype='m2wire')
}
library['dest'] = Pattern(ports=dest_ports)
# Place dest so that its ports are to the West and South of our current wires.
# Rotating by pi/2 makes the ports face West (pointing East).
rpather.place('dest', offset=(-100_000, -100_000), rotation=R90, port_map={'in_A': 'DEST_A', 'in_B': 'DEST_B'})
# Connect GND_FEED to DEST_A
# Since GND_FEED is moving South and DEST_A faces West, a single bend will suffice.
rpather.at('GND_FEED').path_into('DEST_A')
# Connect VCC_BRANCH to DEST_B using path_from
rpather.at('DEST_B').path_from('VCC_BRANCH')
#
# Direct Port Transformations and Metadata
#
(rpather.at('GND')
.set_ptype('m1wire') # Change metadata
.translate((1000, 0)) # Shift the port 1um East
.rotate(R90 / 2) # Rotate it 45 degrees
.set_rotation(R90) # Force it to face West
)
# Demonstrate .plugged() to acknowledge a manual connection
# (Normally used when you place components so their ports perfectly overlap)
rpather.add_port_pair(offset=(0, 0), names=('TMP1', 'TMP2'))
rpather.at('TMP1').plugged('TMP2') # Removes both ports
#
# Rendering and Saving
#
# Since we used RenderPather, we must call .render() to generate the geometry.
rpather.render()
library['PortPather_Tutorial'] = rpather.pattern
library.map_layers(map_layer)
writefile(library, 'port_pather.gds', **GDS_OPTS)
print("Tutorial complete. Output written to port_pather.gds")
if __name__ == '__main__':