The geometry-first idea
Traditional parametric-layout tools (stretch-handle PCells) assume shapes are rectangles — pull an edge and that equals a parameter W. CMOS is all boxes, so it works. But photonics, flexible electronics, MEMS and superconducting circuits are not boxes: graded tapers, arc bends, free topologies — there is no "pull that edge = a parameter" mapping.
klink takes another path: lightweight semantic annotation rather than a complete constraint system. You mark intent at the geometric positions you are sure of; routing algorithms fill in between the markers; if it's wrong, add more marks and re-run. It is an iterative, convergent process, not a one-shot black box. Two kinds of marker:
Connectivity intent (net endpoint)
A klink_Port PCell carrying net + orientation + width. It answers "a wire on some net leaves here, in this direction, at this width".
Routing constraint
A klink_Anchor PCell whose kind is waypoint_region / bend_region / corridor. It answers "the wire should pass here / bend here / take this channel".
These two are the input to the routing backends: mark Ports + Anchors, then call one routing.* tool and the wiring geometry is generated. This boundary keeps klink's mechanism layer process-agnostic — all process facts (layers, sizes, spacing) are passed in explicitly at call time.
Port: connectivity intent
A Port marks a net endpoint. It is a klink_Port PCell whose parameters are the single source of truth (center / orientation / width / target_layer / port_type / net) — the triangle orientation marker and text label are generated by the PCell and never drift from the parameters. Routing backends read Port markers from layer 999/99 by default.
port.mark cell="NET1" name="A" center_um=[0,0] orientation="E" width_um=2 net="sig"
port.mark cell="NET1" name="B" center_um=[80,20] orientation="W" width_um=2 net="sig"
port.list cell="NET1"
Naming aligns with gdsfactory conventions (directional E0/N0/W0/S0, typed o1/e1). A triangle/rectangle you draw in the GUI can be snapped into a standard Port too — via direction inference + snapping to the nearest real edge (edge tolerance, 45° grid fallback). A Port is identified by an immutable name and can be updated / transformed (batch by GUI selection) / unmarked; duplicate/empty names have port.repair_names as a fallback. Full tools on MCP Reference · Ports & anchors.
Anchor: routing constraint
An Anchor expresses "how the wire should go". Read from layer 999/1 by default. Three kinds:
| kind | Geometry & semantics |
|---|---|
waypoint_region | A rectangular must-pass region — the wire has to cross it. |
bend_region | A triangle whose incircle is the bend search area — the wire makes its arc/corner transition here. |
corridor | A directional channel defined by a polyline: the wire must "enter → run along → exit", not simply cross. A plain corridor is required; labeling choice_group=BUS makes it an optional load-balancing candidate channel for routing.global_channel_cell. |
anchor.mark cell="NET1" kind="waypoint_region" center_um=[40,40] radius_um=6 net="sig"
Anchors share Ports' identity model: id/name for locating, label for display, net for connectivity — three separate axes, with anchor.repair_ids as a duplicate/empty-id fallback. The split "thin plugin does create/edit, thick client does the understanding" is deliberate: how to interpret a hand-drawn marker is evolving workflow logic that should not be frozen into the KLayout plugin.
obstacle_layers you pass to routing tools — your own design's keepout layers. klink ships no default keepout layer (900/0 is a reserved keepout layer that structdevice uses internally as scratch).Mark → route → verify
The minimal geometry-first loop:
port.mark cell="NET1" name="A" center_um=[0,0] orientation="E" width_um=2 net="sig"
port.mark cell="NET1" name="B" center_um=[80,20] orientation="W" width_um=2 net="sig"
anchor.mark cell="NET1" kind="waypoint_region" center_um=[40,40] radius_um=6 net="sig"
routing.tapered_hybrid_cell cell="NET1" angle_mode="manhattan" obstacle_layers=["900/0"]
# check the result: ok=true, obstacle_hit_count=0, no sibling overlap
Routing backends divide by topology/quality (tapered hybrid main, tapered polygon continuous taper, steiner multi-terminal, damped strong clearance, global channel global-decision, multilayer escape cross-layer, gdsfactory strategies). See MCP Reference · Routing backends. The many backends reject "one universal router": they share one Port/Anchor planner, and the output differences are "form of expression", not "quality tiers".
A few principles run through the routing layer:
- Rasterization-free geometric routing. A visibility graph + Dijkstra yields clean straight segments (not staircase artifacts), with native 0/45/90° angle modes; no dependency on
klayout.db. - Port launch contract. A router may not treat a Port as an arbitrary point — it must launch along
orientationfrom the center before entering global search, adding a small perpendicular dogleg if that would fold back, rather than silently rewriting the Port's direction. - Results must self-justify.
ok/obstacle_hit_count/ sibling overlap /route_countare structural quality gates; zero matches is not success. A parameter the chosen backend can't honor is an error that names the backends which do — never silently ignored. - Honest about limits. global channel states it is "not a full rip-up/reroute congestion router"; multilayer escape does not model via enclosure; gdsfactory's astar is fragile, so klink re-verifies and errors on a wall-crossing route.
The user-project model
klink and klink_plugin are the mechanism layer (installed packages, never edited). Your process, devices, sizes, layer numbers, DRC/LVS rules live in your project and are passed explicitly into the mechanism.
your-project/
pdk.py your process — the only home for process facts
custom_devices/ build scripts / device generators the agent writes
specs/ .klink specs, net tables, interaction intent
out/ generated GDS / results (never commit GDS)
AGENTS.md agent rules (CLAUDE.md points to it)
mcp.example.json
.gitignore blocks *.gds/*.oas. Point recipe code at such files at run time (open PDKs are fine to depend on, still not committed).Create a project
Scaffold with the CLI bundled in the wheel, then open it with your agent.
klink init my-chip
cd my-chip
Describe what you build; the agent identifies the domain, scaffolds pdk.py + a first custom_devices/ script from the matching recipe, and passes your process explicitly into klink. There is no hard-coded default project — a fresh project must name its domain and geometry tier first.
After you upgrade klink, run klink update mychip (or just klink update from inside the project folder) to refresh the starters under example_template/; your own pdk.py, custom_devices/, .klink/, out/, and specs/ are never overwritten:
python -m pip install -U klayout-klink
klink update mychip
Pick a recipe
A recipe is the reference implementation for one domain. The geometry tier says whether it needs your confidential geometry.
| Domain | Geometry tier | Public release status |
|---|---|---|
| EBL nanodevice | Self-contained | Runs offline, params inside the example. |
| Neural electrode harness | Self-contained | Needs live KLayout; public example uses Port/Anchor PCells + tapered-hybrid router. |
| Silicon photonics | Open or your own | Public features use the open gf.gpdk (needs gdsfactory); swap your PDK to route yours. |
| Digital P&R → LVS | Self-contained or your own | The fit-device demo runs on synthetic exemplars; swap in your device geometry. Verilog→gates needs an external yosys; the flow returns the exact fix if missing. |
A new domain is just a pdk.py shaped for it + a custom_devices/ script that imports your process and calls klink explicitly. Copy the closest catalog entry and adapt — you never edit klink to add a domain.
Step-by-step tutorials
These are 7 step-by-step walkthroughs, all runnable directly from example_template/ as starters (the two digital-family ones need a live KLayout session): a tutorial and its demo are two views of the same thing. Each tutorial corresponds to one demo script and breaks it into observable stages: every step is actually drawn in a live KLayout session, screenshotted, and paired with the klink calls that produced it, then verified with a structured report -- not a screenshot. The tutorial ends with the same one-line command you can copy and run. Read them in order; each one builds on concepts from the one before it. For what each demo does, its requirements, and measured numbers, see Examples.
Hall bar device
The full geometry-first loop end to end: draw the mesa/contacts/pads, mark Port + Anchor intent, route the fanout with one call, verify with the routing report. A fully offline, self-contained device; needs live KLayout.
Read the tutorial → EBL · writefieldEBL wraparound nanodevice
Hand-drawn routing under writefield constraints: keepout boundaries, pre-planned crossing windows (corridor Anchors), stitch-patch compensation, and marking Port/Anchor after the fact instead of before.
Read the tutorial → Batch RPC · parametricNeural electrode array harness
Parametrized by --elec-rows; 394 geometry objects written with batched shape.insert_many calls. An almost 8x scale gap between bond pads and electrodes, closed with a corridor Anchor that forces convergence before routing all 24 nets in one pass.
gdsfactory MZI takeover and drag-to-reroute
One import_gf_component call takes over an ordinary gdsfactory thermo-optic MZI script; after dragging any component in the GUI, the same photonics.reroute call only re-routes -- it never rebuilds a device. Needs gdsfactory.
Custom device: fit → PCell → P&R → LVS
Fit a parametric edge model from a few exemplar drawings at different sizes, register a real PCell, place 173 instances from a netlist, run FlexDR detailed routing, and finish with live LVS match=True. Starter, needs a live KLayout session.
Read the tutorial → structdevice · padframeA probe-card-first padframe
The probe card / pad ring exists first and the circuit must meet it: harvest the pad ring, assign net→pad, place half-in/half-out, split the power grid by region, and finish with both the card and --no-card bare-stub modes each passing live LVS match=True. Starter, needs a live KLayout session.
Passive-device geometry templates
Four parametric geometry templates -- IDC capacitor, square spiral inductor, SAW IDT filter, BAW/FBAR plan view -- each with a default-parameter and a variant-parameter live screenshot. A passives/-category starter, runs offline, --live pushes to KLayout.
Offline device tutorial
Starting from the fully offline EBL wraparound or Hall bar is safest — they show "the mechanism layer holds no process facts": the example carries sizes, layer numbers and writefield rules, then calls klink.domains.nanodevice.
klink init nano-demo
cd nano-demo
python example_template/nanodevice/ebl_wraparound.py
python example_template/nanodevice/hallbar.py
From a repo checkout you can also run the public gallery directly:
python -m examples_klink.public.demos.nanodevice.ebl_wraparound
python -m examples_klink.public.demos.nanodevice.hallbar
After copying into your project, edit params first, don't touch klink/; once stable, tidy it into custom_devices/<device>.py. Per-demo detail on Examples.
Silicon-photonics example (gdsfactory bridge)
If you already have gdsfactory code, the public checkout has two related examples: low-level features (features/24_gdsfactory_route_ports.py, 30_gdsfactory_routing_zoo.py) and the full demo demos/gf_mzi_module.py — taking an ordinary gdsfactory MZI script into klink's reroute loop.
Install is always one command: pip install klayout-klink installs all of klink. This example additionally needs gdsfactory in the same interpreter (klink is an RPC client; the geometry is built client-side by gdsfactory) — klink bundles no third-party libs and names the exact pip install when one is missing. [photonics] is not a "per-domain install", just a convenience that also pulls a tested gdsfactory:
pip install "klayout-klink[photonics]" # = klink + a tested gdsfactory
klink init my-chip && cd my-chip
python example_template/photonics/gf_mzi_module.py --port <session-port>
# from a repo checkout you can also run: python -m examples_klink.public.demos.photonics.gf_mzi_module --port <session-port>
After takeover, drag a component in the GUI and one photonics.reroute re-routes optics and metal together — see Workflows · 8082 klive display and MCP Reference · Photonics. From a plain shell, re-route with the --reroute flag, which re-routes from the dragged positions without rebuilding:
python example_template/photonics/gf_mzi_module.py --port <session-port> --reroute
For your own PDK, layers, stub rules and cross-section come from your script or pdk.py.
P&R to LVS
The public release digital path is either self-contained or your-own-geometry. The fit-device demo runs device fit, placement, routing and live LVS on synthetic exemplars. The whole digital family (fit-device, padframe, chat-to-netlist, multilayer) are starters under example_template/digital/, needing a running KLayout session:
python example_template/digital/fit_device_pnr_lvs.py --port <session-port>
# from a repo clone you can also run: python -m examples_klink.public.demos.digital.fit_device_pnr_lvs --port <session-port>
The device-agnostic custom-device flow is the structdevice.* family: give a device-level netlist → build_from_netlist auto floorplan/route/draw/LVS; or the SEND-driven declare_nets → connect_nets → lvs_check → spec_write. Any LVS mismatch undoes every mutation.
Verification habits
klink tutorials do not treat screenshots as proof of done. Decide with:
layout.info,cell.tree,layer.listfor structure.shape.query,instance.query,selection.getfor geometry.- the routing report's
ok, overlap, obstacle hit, route count. - live LVS
match=True.
A route/layout is "done" only when live KLayout LVS returns match=True; marker counts and "looks routed" do not count. Screenshots can show a human the result but never replace structured verification.