From 737d41d592a392ac8f967096bea47062c3082421 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 14 Feb 2026 17:06:29 -0800 Subject: [PATCH] [examples] expand port_pather tutorial --- examples/tutorial/port_pather.py | 161 +++++++++++++++++++++++-------- 1 file changed, 123 insertions(+), 38 deletions(-) diff --git a/examples/tutorial/port_pather.py b/examples/tutorial/port_pather.py index f6f2a76..3fad6e7 100644 --- a/examples/tutorial/port_pather.py +++ b/examples/tutorial/port_pather.py @@ -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__':