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.
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.