[docs / examples] Update docs and examples

This commit is contained in:
Jan Petykiewicz 2026-04-02 12:19:51 -07:00
commit fd2698c503
7 changed files with 132 additions and 107 deletions

View file

@ -1,6 +1,12 @@
masque Tutorial masque Tutorial
=============== ===============
These examples are meant to be read roughly in order.
- Start with `basic_shapes.py` for the core `Pattern` / GDS concepts.
- Then read `devices.py` and `library.py` for hierarchical composition and libraries.
- Read the `pather*` tutorials separately when you want routing helpers.
Contents Contents
-------- --------
@ -8,11 +14,13 @@ Contents
* Draw basic geometry * Draw basic geometry
* Export to GDS * Export to GDS
- [devices](devices.py) - [devices](devices.py)
* Build hierarchical photonic-crystal example devices
* Reference other patterns * Reference other patterns
* Add ports to a pattern * Add ports to a pattern
* Snap ports together to build a circuit * Use `Builder` to snap ports together into a circuit
* Check for dangling references * Check for dangling references
- [library](library.py) - [library](library.py)
* Continue from `devices.py` using a lazy library
* Create a `LazyLibrary`, which loads / generates patterns only when they are first used * Create a `LazyLibrary`, which loads / generates patterns only when they are first used
* Explore alternate ways of specifying a pattern for `.plug()` and `.place()` * Explore alternate ways of specifying a pattern for `.plug()` and `.place()`
* Design a pattern which is meant to plug into an existing pattern (via `.interface()`) * Design a pattern which is meant to plug into an existing pattern (via `.interface()`)
@ -28,7 +36,8 @@ Contents
* Advanced port manipulation and connections * Advanced port manipulation and connections
Additionaly, [pcgen](pcgen.py) is a utility module for generating photonic crystal lattices. Additionally, [pcgen](pcgen.py) is a utility module used by `devices.py` for generating
photonic-crystal lattices; it is support code rather than a step-by-step tutorial.
Running Running
@ -40,3 +49,6 @@ cd examples/tutorial
python3 basic_shapes.py python3 basic_shapes.py
klayout -e basic_shapes.gds klayout -e basic_shapes.gds
``` ```
Some tutorials depend on outputs from earlier ones. In particular, `library.py`
expects `circuit.gds`, which is generated by `devices.py`.

View file

@ -1,3 +1,11 @@
"""
Tutorial: building hierarchical devices with `Pattern`, `Port`, and `Builder`.
This file uses photonic-crystal components as the concrete example, so some of
the geometry-generation code is domain-specific. The tutorial value is in the
Masque patterns around it: creating reusable cells, annotating ports, composing
hierarchy with references, and snapping ports together to build a larger circuit.
"""
from collections.abc import Sequence, Mapping from collections.abc import Sequence, Mapping
import numpy import numpy
@ -64,9 +72,9 @@ def perturbed_l3(
Provided sequence should have same length as `shifts_a`. Provided sequence should have same length as `shifts_a`.
xy_size: `(x, y)` number of mirror periods in each direction; total size is xy_size: `(x, y)` number of mirror periods in each direction; total size is
`2 * n + 1` holes in each direction. Default (10, 10). `2 * n + 1` holes in each direction. Default (10, 10).
perturbed_radius: radius of holes perturbed to form an upwards-driected beam perturbed_radius: radius of holes perturbed to form an upwards-directed beam
(multiplicative factor). Default 1.1. (multiplicative factor). Default 1.1.
trench width: Width of the undercut trenches. Default 1200. trench_width: Width of the undercut trenches. Default 1200.
Returns: Returns:
`Pattern` object representing the L3 design. `Pattern` object representing the L3 design.
@ -79,14 +87,15 @@ def perturbed_l3(
shifts_a=shifts_a, shifts_a=shifts_a,
shifts_r=shifts_r) shifts_r=shifts_r)
# Build L3 cavity, using references to the provided hole pattern # Build the cavity by instancing the supplied `hole` pattern many times.
# Using references keeps the pattern compact even though it contains many holes.
pat = Pattern() pat = Pattern()
pat.refs[hole] += [ pat.refs[hole] += [
Ref(scale=r, offset=(lattice_constant * x, Ref(scale=r, offset=(lattice_constant * x,
lattice_constant * y)) lattice_constant * y))
for x, y, r in xyr] for x, y, r in xyr]
# Add rectangular undercut aids # Add rectangular undercut aids based on the referenced hole extents.
min_xy, max_xy = pat.get_bounds_nonempty(hole_lib) min_xy, max_xy = pat.get_bounds_nonempty(hole_lib)
trench_dx = max_xy[0] - min_xy[0] trench_dx = max_xy[0] - min_xy[0]
@ -95,7 +104,7 @@ def perturbed_l3(
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width), Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width),
] ]
# Ports are at outer extents of the device (with y=0) # Define the interface in Masque terms: two ports at the left/right extents.
extent = lattice_constant * xy_size[0] extent = lattice_constant * xy_size[0]
pat.ports = dict( pat.ports = dict(
input=Port((-extent, 0), rotation=0, ptype='pcwg'), input=Port((-extent, 0), rotation=0, ptype='pcwg'),
@ -125,17 +134,17 @@ def waveguide(
Returns: Returns:
`Pattern` object representing the waveguide. `Pattern` object representing the waveguide.
""" """
# Generate hole locations # Generate the normalized lattice locations for the line defect.
xy = pcgen.waveguide(length=length, num_mirror=mirror_periods) xy = pcgen.waveguide(length=length, num_mirror=mirror_periods)
# Build the pattern # Build the pattern by placing repeated references to the same hole cell.
pat = Pattern() pat = Pattern()
pat.refs[hole] += [ pat.refs[hole] += [
Ref(offset=(lattice_constant * x, Ref(offset=(lattice_constant * x,
lattice_constant * y)) lattice_constant * y))
for x, y in xy] for x, y in xy]
# Ports are at outer edges, with y=0 # Publish the device interface as two ports at the outer edges.
extent = lattice_constant * length / 2 extent = lattice_constant * length / 2
pat.ports = dict( pat.ports = dict(
left=Port((-extent, 0), rotation=0, ptype='pcwg'), left=Port((-extent, 0), rotation=0, ptype='pcwg'),
@ -164,17 +173,17 @@ def bend(
`Pattern` object representing the waveguide bend. `Pattern` object representing the waveguide bend.
Ports are named 'left' (input) and 'right' (output). Ports are named 'left' (input) and 'right' (output).
""" """
# Generate hole locations # Generate the normalized lattice locations for the bend.
xy = pcgen.wgbend(num_mirror=mirror_periods) xy = pcgen.wgbend(num_mirror=mirror_periods)
# Build the pattern # Build the pattern by instancing the shared hole cell.
pat = Pattern() pat = Pattern()
pat.refs[hole] += [ pat.refs[hole] += [
Ref(offset=(lattice_constant * x, Ref(offset=(lattice_constant * x,
lattice_constant * y)) lattice_constant * y))
for x, y in xy] for x, y in xy]
# Figure out port locations. # Publish the bend interface as two ports.
extent = lattice_constant * mirror_periods extent = lattice_constant * mirror_periods
pat.ports = dict( pat.ports = dict(
left=Port((-extent, 0), rotation=0, ptype='pcwg'), left=Port((-extent, 0), rotation=0, ptype='pcwg'),
@ -203,17 +212,17 @@ def y_splitter(
`Pattern` object representing the y-splitter. `Pattern` object representing the y-splitter.
Ports are named 'in', 'top', and 'bottom'. Ports are named 'in', 'top', and 'bottom'.
""" """
# Generate hole locations # Generate the normalized lattice locations for the splitter.
xy = pcgen.y_splitter(num_mirror=mirror_periods) xy = pcgen.y_splitter(num_mirror=mirror_periods)
# Build pattern # Build the pattern by instancing the shared hole cell.
pat = Pattern() pat = Pattern()
pat.refs[hole] += [ pat.refs[hole] += [
Ref(offset=(lattice_constant * x, Ref(offset=(lattice_constant * x,
lattice_constant * y)) lattice_constant * y))
for x, y in xy] for x, y in xy]
# Determine port locations # Publish the splitter interface as one input and two outputs.
extent = lattice_constant * mirror_periods extent = lattice_constant * mirror_periods
pat.ports = { pat.ports = {
'in': Port((-extent, 0), rotation=0, ptype='pcwg'), 'in': Port((-extent, 0), rotation=0, ptype='pcwg'),
@ -227,13 +236,13 @@ def y_splitter(
def main(interactive: bool = True) -> None: def main(interactive: bool = True) -> None:
# Generate some basic hole patterns # First make a couple of reusable primitive cells.
shape_lib = { shape_lib = {
'smile': basic_shapes.smile(RADIUS), 'smile': basic_shapes.smile(RADIUS),
'hole': basic_shapes.hole(RADIUS), 'hole': basic_shapes.hole(RADIUS),
} }
# Build some devices # Then build a small library of higher-level devices from those primitives.
a = LATTICE_CONSTANT a = LATTICE_CONSTANT
devices = {} devices = {}
@ -245,22 +254,23 @@ def main(interactive: bool = True) -> None:
devices['ysplit'] = y_splitter(lattice_constant=a, hole='hole', mirror_periods=5) devices['ysplit'] = y_splitter(lattice_constant=a, hole='hole', mirror_periods=5)
devices['l3cav'] = perturbed_l3(lattice_constant=a, hole='smile', hole_lib=shape_lib, xy_size=(4, 10)) # uses smile :) devices['l3cav'] = perturbed_l3(lattice_constant=a, hole='smile', hole_lib=shape_lib, xy_size=(4, 10)) # uses smile :)
# Turn our dict of devices into a Library. # Turn the device mapping into a `Library`.
# This provides some convenience functions in the future! # That gives us convenience helpers for hierarchy inspection and abstract views.
lib = Library(devices) lib = Library(devices)
# #
# Build a circuit # Build a circuit
# #
# Create a `Builder`, and add the circuit to our library as "my_circuit". # Create a `Builder`, and register the resulting top cell as "my_circuit".
circ = Builder(library=lib, name='my_circuit') circ = Builder(library=lib, name='my_circuit')
# Start by placing a waveguide. Call its ports "in" and "signal". # Start by placing a waveguide and renaming its ports to match the circuit-level
# names we want to use while assembling the design.
circ.place('wg10', offset=(0, 0), port_map={'left': 'in', 'right': 'signal'}) circ.place('wg10', offset=(0, 0), port_map={'left': 'in', 'right': 'signal'})
# Extend the signal path by attaching the "left" port of a waveguide. # Extend the signal path by attaching another waveguide.
# Since there is only one other port ("right") on the waveguide we # Because `wg10` only has one unattached port left after the plug, Masque can
# are attaching (wg10), it automatically inherits the name "signal". # infer that it should keep the name `signal`.
circ.plug('wg10', {'signal': 'left'}) circ.plug('wg10', {'signal': 'left'})
# We could have done the following instead: # We could have done the following instead:
@ -268,8 +278,8 @@ def main(interactive: bool = True) -> None:
# lib['my_circuit'] = circ_pat # lib['my_circuit'] = circ_pat
# circ_pat.place(lib.abstract('wg10'), ...) # circ_pat.place(lib.abstract('wg10'), ...)
# circ_pat.plug(lib.abstract('wg10'), ...) # circ_pat.plug(lib.abstract('wg10'), ...)
# but `Builder` lets us omit some of the repetition of `lib.abstract(...)`, and uses similar # but `Builder` removes some repeated `lib.abstract(...)` boilerplate and keeps
# syntax to `Pather` and `RenderPather`, which add wire/waveguide routing functionality. # the assembly code focused on port-level intent.
# Attach a y-splitter to the signal path. # Attach a y-splitter to the signal path.
# Since the y-splitter has 3 ports total, we can't auto-inherit the # Since the y-splitter has 3 ports total, we can't auto-inherit the
@ -281,13 +291,10 @@ def main(interactive: bool = True) -> None:
circ.plug('wg05', {'signal1': 'left'}) circ.plug('wg05', {'signal1': 'left'})
circ.plug('wg05', {'signal2': 'left'}) circ.plug('wg05', {'signal2': 'left'})
# Add a bend to both ports. # Add a bend to both branches.
# Our bend's ports "left" and "right" refer to the original counterclockwise # Our bend primitive is defined with a specific orientation, so choosing which
# orientation. We want the bends to turn in opposite directions, so we attach # port to plug determines whether the path turns clockwise or counterclockwise.
# the "right" port to "signal1" to bend clockwise, and the "left" port # We could also mirror one instance instead of using opposite ports.
# to "signal2" to bend counterclockwise.
# We could also use `mirrored=(True, False)` to mirror one of the devices
# and then use same device port on both paths.
circ.plug('bend0', {'signal1': 'right'}) circ.plug('bend0', {'signal1': 'right'})
circ.plug('bend0', {'signal2': 'left'}) circ.plug('bend0', {'signal2': 'left'})
@ -296,29 +303,26 @@ def main(interactive: bool = True) -> None:
circ.plug('l3cav', {'signal1': 'input'}) circ.plug('l3cav', {'signal1': 'input'})
circ.plug('wg10', {'signal1': 'left'}) circ.plug('wg10', {'signal1': 'left'})
# "signal2" just gets a single of equivalent length # `signal2` gets a single waveguide of equivalent overall length.
circ.plug('wg28', {'signal2': 'left'}) circ.plug('wg28', {'signal2': 'left'})
# Now we bend both waveguides back towards each other # Now bend both branches back towards each other.
circ.plug('bend0', {'signal1': 'right'}) circ.plug('bend0', {'signal1': 'right'})
circ.plug('bend0', {'signal2': 'left'}) circ.plug('bend0', {'signal2': 'left'})
circ.plug('wg05', {'signal1': 'left'}) circ.plug('wg05', {'signal1': 'left'})
circ.plug('wg05', {'signal2': 'left'}) circ.plug('wg05', {'signal2': 'left'})
# To join the waveguides, we attach a second y-junction. # To join the branches, attach a second y-junction.
# We plug "signal1" into the "bot" port, and "signal2" into the "top" port. # This succeeds only if both chosen ports agree on the same translation and
# The remaining port gets named "signal_out". # rotation for the inserted device; otherwise Masque raises an exception.
# This operation would raise an exception if the ports did not line up
# correctly (i.e. they required different rotations or translations of the
# y-junction device).
circ.plug('ysplit', {'signal1': 'bot', 'signal2': 'top'}, {'in': 'signal_out'}) circ.plug('ysplit', {'signal1': 'bot', 'signal2': 'top'}, {'in': 'signal_out'})
# Finally, add some more waveguide to "signal_out". # Finally, add some more waveguide to "signal_out".
circ.plug('wg10', {'signal_out': 'left'}) circ.plug('wg10', {'signal_out': 'left'})
# We can also add text labels for our circuit's ports. # Bake the top-level port metadata into labels so it survives GDS export.
# They will appear at the uppermost hierarchy level, while the individual # These labels appear on the circuit cell; individual child devices keep their
# device ports will appear further down, in their respective cells. # own port labels in their own cells.
ports_to_data(circ.pattern) ports_to_data(circ.pattern)
# Check if we forgot to include any patterns... ooops! # Check if we forgot to include any patterns... ooops!
@ -330,12 +334,12 @@ def main(interactive: bool = True) -> None:
lib.add(shape_lib) lib.add(shape_lib)
assert not lib.dangling_refs() assert not lib.dangling_refs()
# We can visualize the design. Usually it's easier to just view the GDS. # We can visualize the design directly, though opening the written GDS is often easier.
if interactive: if interactive:
print('Visualizing... this step may be slow') print('Visualizing... this step may be slow')
circ.pattern.visualize(lib) circ.pattern.visualize(lib)
#Write out to GDS, only keeping patterns referenced by our circuit (including itself) # Write out only the subtree reachable from our top cell.
subtree = lib.subtree('my_circuit') # don't include wg90, which we don't use subtree = lib.subtree('my_circuit') # don't include wg90, which we don't use
check_valid_names(subtree.keys()) check_valid_names(subtree.keys())
writefile(subtree, 'circuit.gds', **GDS_OPTS) writefile(subtree, 'circuit.gds', **GDS_OPTS)

View file

@ -1,3 +1,11 @@
"""
Tutorial: using `LazyLibrary` and `Builder.interface()`.
This example assumes you have already read `devices.py` and generated the
`circuit.gds` file it writes. The goal here is not the photonic-crystal geometry
itself, but rather how Masque lets you mix lazily loaded GDS content with
python-generated devices inside one library.
"""
from typing import Any from typing import Any
from pprint import pformat from pprint import pformat
@ -12,8 +20,9 @@ from basic_shapes import GDS_OPTS
def main() -> None: def main() -> None:
# Define a `LazyLibrary`, which provides lazy evaluation for generating # A `LazyLibrary` delays work until a pattern is actually needed.
# patterns and lazy-loading of GDS contents. # That applies both to GDS cells we load from disk and to python callables
# that generate patterns on demand.
lib = LazyLibrary() lib = LazyLibrary()
# #
@ -23,9 +32,9 @@ def main() -> None:
# Scan circuit.gds and prepare to lazy-load its contents # Scan circuit.gds and prepare to lazy-load its contents
gds_lib, _properties = load_libraryfile('circuit.gds', postprocess=data_to_ports) gds_lib, _properties = load_libraryfile('circuit.gds', postprocess=data_to_ports)
# Add it into the device library by providing a way to read port info # Add those cells into our lazy library.
# This maintains the lazy evaluation from above, so no patterns # Nothing is read yet; we are only registering how to fetch and postprocess
# are actually read yet. # each pattern when it is first requested.
lib.add(gds_lib) lib.add(gds_lib)
print('Patterns loaded from GDS into library:\n' + pformat(list(lib.keys()))) print('Patterns loaded from GDS into library:\n' + pformat(list(lib.keys())))
@ -40,8 +49,8 @@ def main() -> None:
hole = 'triangle', hole = 'triangle',
) )
# Triangle-based variants. These are defined here, but they won't run until they're # Triangle-based variants. These lambdas are only recipes for building the
# retrieved from the library. # patterns; they do not execute until someone asks for the cell.
lib['tri_wg10'] = lambda: devices.waveguide(length=10, mirror_periods=5, **opts) lib['tri_wg10'] = lambda: devices.waveguide(length=10, mirror_periods=5, **opts)
lib['tri_wg05'] = lambda: devices.waveguide(length=5, mirror_periods=5, **opts) lib['tri_wg05'] = lambda: devices.waveguide(length=5, mirror_periods=5, **opts)
lib['tri_wg28'] = lambda: devices.waveguide(length=28, mirror_periods=5, **opts) lib['tri_wg28'] = lambda: devices.waveguide(length=28, mirror_periods=5, **opts)
@ -53,22 +62,22 @@ def main() -> None:
# Build a mixed waveguide with an L3 cavity in the middle # Build a mixed waveguide with an L3 cavity in the middle
# #
# Immediately start building from an instance of the L3 cavity # Start a new design by copying the ports from an existing library cell.
# This gives `circ2` the same external interface as `tri_l3cav`.
circ2 = Builder(library=lib, ports='tri_l3cav') circ2 = Builder(library=lib, ports='tri_l3cav')
# First way to get abstracts is `lib.abstract(name)` # First way to specify what we are plugging in: request an explicit abstract.
# We can use this syntax directly with `Pattern.plug()` and `Pattern.place()` as well as through `Builder`. # This works with `Pattern` methods directly as well as with `Builder`.
circ2.plug(lib.abstract('wg10'), {'input': 'right'}) circ2.plug(lib.abstract('wg10'), {'input': 'right'})
# Second way to get abstracts is to use an AbstractView # Second way: use an `AbstractView`, which behaves like a mapping of names
# This also works directly with `Pattern.plug()` / `Pattern.place()`. # to abstracts.
abstracts = lib.abstract_view() abstracts = lib.abstract_view()
circ2.plug(abstracts['wg10'], {'output': 'left'}) circ2.plug(abstracts['wg10'], {'output': 'left'})
# Third way to specify an abstract works by automatically getting # Third way: let `Builder` resolve a pattern name through its own library.
# it from the library already within the Builder object. # This shorthand is convenient, but it is specific to helpers that already
# This wouldn't work if we only had a `Pattern` (not a `Builder`). # carry a library reference.
# Just pass the pattern name!
circ2.plug('tri_wg10', {'input': 'right'}) circ2.plug('tri_wg10', {'input': 'right'})
circ2.plug('tri_wg10', {'output': 'left'}) circ2.plug('tri_wg10', {'output': 'left'})
@ -77,13 +86,15 @@ def main() -> None:
# #
# Build a device that could plug into our mixed_wg_cav and joins the two ports # Build a second device that is explicitly designed to mate with `circ2`.
# #
# We'll be designing against an existing device's interface... # `Builder.interface()` makes a new pattern whose ports mirror an existing
# design's external interface. That is useful when you want to design an
# adapter, continuation, or mating structure.
circ3 = Builder.interface(source=circ2) circ3 = Builder.interface(source=circ2)
# ... that lets us continue from where we left off. # Continue routing outward from those inherited ports.
circ3.plug('tri_bend0', {'input': 'right'}) circ3.plug('tri_bend0', {'input': 'right'})
circ3.plug('tri_bend0', {'input': 'left'}, mirrored=True) # mirror since no tri y-symmetry circ3.plug('tri_bend0', {'input': 'left'}, mirrored=True) # mirror since no tri y-symmetry
circ3.plug('tri_bend0', {'input': 'right'}) circ3.plug('tri_bend0', {'input': 'right'})

View file

@ -243,7 +243,7 @@ def main() -> None:
# If we wanted to place our via manually, we could add `pather.plug('m1_via', {'GND': 'top'})` here # 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. # 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 # Note that even though we have changed the tool used for GND, the via doesn't get placed until
# the next time we draw a path on GND (the pather.mpath() statement below). # the next time we route GND (the `pather.ccw()` call below).
pather.retool(M1_tool, keys='GND') pather.retool(M1_tool, keys='GND')
# Bundle together GND and VCC, and path the bundle forward and counterclockwise. # Bundle together GND and VCC, and path the bundle forward and counterclockwise.

View file

@ -27,14 +27,14 @@ def main() -> None:
# and remembers the selected port(s). This allows method chaining. # and remembers the selected port(s). This allows method chaining.
# Route VCC: 6um South, then West to x=0. # Route VCC: 6um South, then West to x=0.
# (Note: since the port points North into the pad, path() moves South by default) # (Note: since the port points North into the pad, trace() moves South by default)
(rpather.at('VCC') (rpather.at('VCC')
.path(ccw=False, length=6_000) # Move South, turn West (Clockwise) .trace(False, length=6_000) # Move South, turn West (Clockwise)
.path_to(ccw=None, x=0) # Continue West to x=0 .trace_to(None, x=0) # Continue West to x=0
) )
# Route GND: 5um South, then West to match VCC's x-coordinate. # 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) rpather.at('GND').trace(False, length=5_000).trace_to(None, x=rpather['VCC'].x)
# #
@ -49,45 +49,45 @@ def main() -> None:
.retool(M1_tool) # this only retools the 'GND' port .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 route them together.
# Here we bundle them, turn South, and retool both to M1 (VCC gets an auto-via). # Here we bundle them, turn South, and retool both to M1 (VCC gets an auto-via).
(rpather.at(['GND', 'VCC']) (rpather.at(['GND', 'VCC'])
.mpath(ccw=True, xmax=-10_000, spacing=5_000) # Move West to -10k, turn South .trace(True, xmax=-10_000, spacing=5_000) # Move West to -10k, turn South
.retool(M1_tool) # Retools both GND and VCC .retool(M1_tool) # Retools both GND and VCC
.mpath(ccw=True, emax=50_000, spacing=1_200) # Turn East, moves 50um extension .trace(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 .trace(False, emin=1_000, spacing=1_200) # U-turn back South
.mpath(ccw=False, emin=2_000, spacing=4_500) # U-turn back West .trace(False, emin=2_000, spacing=4_500) # U-turn back West
) )
# Retool VCC back to M2 and move both to x=-28k # Retool VCC back to M2 and move both to x=-28k
rpather.at('VCC').retool(M2_tool) rpather.at('VCC').retool(M2_tool)
rpather.at(['GND', 'VCC']).mpath(ccw=None, xmin=-28_000) rpather.at(['GND', 'VCC']).trace(None, xmin=-28_000)
# Final segments to -50k # Final segments to -50k
rpather.at('VCC').path_to(ccw=None, x=-50_000, out_ptype='m1wire') rpather.at('VCC').trace_to(None, x=-50_000, out_ptype='m1wire')
with rpather.at('GND').toolctx(M2_tool): with rpather.at('GND').toolctx(M2_tool):
rpather.at('GND').path_to(ccw=None, x=-40_000) rpather.at('GND').trace_to(None, x=-40_000)
rpather.at('GND').path_to(ccw=None, x=-50_000) rpather.at('GND').trace_to(None, x=-50_000)
# #
# Branching with save_copy and into_copy # Branching with mark and fork
# #
# .save_copy(new_name) creates a port copy and keeps the original selected. # .mark(new_name) creates a port copy and keeps the original selected.
# .into_copy(new_name) creates a port copy and selects the new one. # .fork(new_name) creates a port copy and selects the new one.
# Create a tap on GND # Create a tap on GND
(rpather.at('GND') (rpather.at('GND')
.path(ccw=None, length=5_000) # Move GND further West .trace(None, length=5_000) # Move GND further West
.save_copy('GND_TAP') # Mark this location for a later branch .mark('GND_TAP') # Mark this location for a later branch
.pathS(length=10_000, jog=-10_000) # Continue GND with an S-bend .jog(offset=-10_000, length=10_000) # Continue GND with an S-bend
) )
# Branch VCC and follow the new branch # Branch VCC and follow the new branch
(rpather.at('VCC') (rpather.at('VCC')
.path(ccw=None, length=5_000) .trace(None, length=5_000)
.into_copy('VCC_BRANCH') # We are now manipulating 'VCC_BRANCH' .fork('VCC_BRANCH') # We are now manipulating 'VCC_BRANCH'
.path(ccw=True, length=5_000) # VCC_BRANCH turns South .trace(True, length=5_000) # VCC_BRANCH turns South
) )
# The original 'VCC' port remains at x=-55k, y=VCC.y # The original 'VCC' port remains at x=-55k, y=VCC.y
@ -99,27 +99,25 @@ def main() -> None:
# Route the GND_TAP we saved earlier. # Route the GND_TAP we saved earlier.
(rpather.at('GND_TAP') (rpather.at('GND_TAP')
.retool(M1_tool) .retool(M1_tool)
.path(ccw=True, length=10_000) # Turn South .trace(True, length=10_000) # Turn South
.rename_to('GND_FEED') # Give it a more descriptive name .rename('GND_FEED') # Give it a more descriptive name
.retool(M1_tool) # Re-apply tool to the new name .retool(M1_tool) # Re-apply tool to the new name
) )
# We can manage the active set of ports in a PortPather # We can manage the active set of ports in a PortPather
pp = rpather.at(['VCC_BRANCH', 'GND_FEED']) pp = rpather.at(['VCC_BRANCH', 'GND_FEED'])
pp.add_port('GND') # Now tracking 3 ports pp.select('GND') # Now tracking 3 ports
pp.drop_port('VCC_BRANCH') # Now tracking 2 ports: GND_FEED, GND pp.deselect('VCC_BRANCH') # Now tracking 2 ports: GND_FEED, GND
pp.path_each(ccw=None, length=5_000) # Move both 5um forward (length > transition size) pp.trace(None, each=5_000) # Move both 5um forward (length > transition size)
# We can also delete ports from the pather entirely # We can also delete ports from the pather entirely
rpather.at('VCC').delete() # VCC is gone (we have VCC_BRANCH instead) rpather.at('VCC').delete() # VCC is gone (we have VCC_BRANCH instead)
# #
# Advanced Connections: path_into and path_from # Advanced Connections: trace_into
# #
# trace_into routes FROM the selected port TO a target port.
# 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 # Create a destination component
dest_ports = { dest_ports = {
@ -133,10 +131,10 @@ def main() -> None:
# Connect GND_FEED to DEST_A # Connect GND_FEED to DEST_A
# Since GND_FEED is moving South and DEST_A faces West, a single bend will suffice. # Since GND_FEED is moving South and DEST_A faces West, a single bend will suffice.
rpather.at('GND_FEED').path_into('DEST_A') rpather.at('GND_FEED').trace_into('DEST_A')
# Connect VCC_BRANCH to DEST_B using path_from # Connect VCC_BRANCH to DEST_B
rpather.at('DEST_B').path_from('VCC_BRANCH') rpather.at('VCC_BRANCH').trace_into('DEST_B')
# #

View file

@ -276,8 +276,8 @@ class Tool:
Args: Args:
length: The total distance from input to output, along the input's axis only. length: The total distance from input to output, along the input's axis only.
jog: The total offset from the input to output, along the perpendicular axis. jog: The total offset from the input to output, along the perpendicular axis.
A positive number implies a rightwards shift (i.e. clockwise bend followed A positive number implies a leftward shift (i.e. counterclockwise bend followed
by a counterclockwise bend) by a clockwise bend)
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged. in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged. out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
kwargs: Custom tool-specific parameters. kwargs: Custom tool-specific parameters.

View file

@ -46,7 +46,7 @@ def ell(
ccw: Turn direction. `True` means counterclockwise, `False` means clockwise, ccw: Turn direction. `True` means counterclockwise, `False` means clockwise,
and `None` means no bend. If `None`, spacing must remain `None` or `0` (default), and `None` means no bend. If `None`, spacing must remain `None` or `0` (default),
Otherwise, spacing must be set to a non-`None` value. Otherwise, spacing must be set to a non-`None` value.
bound_method: Method used for determining the travel distance; see diagram above. bound_type: Method used for determining the travel distance; see diagram above.
Valid values are: Valid values are:
- 'min_extension' or 'emin': - 'min_extension' or 'emin':
The total extension value for the furthest-out port (B in the diagram). The total extension value for the furthest-out port (B in the diagram).
@ -64,7 +64,7 @@ def ell(
the x- and y- axes. If specifying a position, it is projected onto the x- and y- axes. If specifying a position, it is projected onto
the extension direction. the extension direction.
bound_value: Value associated with `bound_type`, see above. bound: Value associated with `bound_type`, see above.
spacing: Distance between adjacent channels. Can be scalar, resulting in evenly spacing: Distance between adjacent channels. Can be scalar, resulting in evenly
spaced channels, or a vector with length one less than `ports`, allowing spaced channels, or a vector with length one less than `ports`, allowing
non-uniform spacing. non-uniform spacing.