Loading: one file, zero compile
After pip install klayout-klink, klink init scaffolds example_template/ledit_bridge/:
ledit_bridge.cpp— the single macro file. In L-Edit: Tools → Macro → Load Macro… and pick the source file itself (L-Edit compiles it in place; the macro uses only the old-version API subset — older installs compatible by design, tested on v16.x; klink ships zero Siemens/Tanner files — the SDK header comes from your own L-Edit installation).driver.py— dependency-free smoke driver:python driver.py pingshows macro version, current design, and the capability list.tcell_workflows.py— the T-Cell loop CLI (below).
Transport is an inbox/outbox JSON file pair under %LOCALAPPDATA%\klink\ledit_bridge\; the macro polls on the UI thread every 200 ms and answers atomically. A hello.json heartbeat provides liveness.
Capability surface
| Area | Commands |
|---|---|
| Design files | new_design (bootstraps a .tdb even with nothing open) · open_design · save_design |
| Layers | ensure_layer (GDS numbers stamped; new layers auto-colored: solid fill, no outline) · set_layer_style · get_layers (fill color, special-layer flags) |
| Drawing | create_cell · draw (box/polygon/wire/circle, batch) · place_instance (incl. nx×ny arrays) · clear_cell |
| Full readout | get_selection · get_cell (shapes + per-object property trees + instances + ports + labels; wires carry cap/join, torus/pie carry exact params) · list_cells (now always carries a hidden boolean, plus hidden_property when it disagrees with the flag) |
| Navigation | show_cell · set_cell_hidden · list_windows · close_window · layout_view · save_image |
| Destructive | delete_cell · rename_cell · delete_objects · close_design |
| Verification | run_drc · drc_summary · export_gds |
| Process knowledge | get_drc_rules — the design's whole rule table, machine-readable |
| T-Cells | get_tcell_params · instance_tcell (programmatic, parameterized) · set_tcell_code (write generator code back as a native T-Cell) |
Exchanging with KLayout (one-call MCP tools)
Once klink MCP is configured, an agent gets eighteen intent-level tools in the bridge_ledit domain: the five below cover exchange with KLayout; the other thirteen — navigation and view, markers over the bridge, destructive commands, DRC and the GDS return path — are grouped into the sections that follow.
ledit.status— discovery and triage: macro liveness, whether a design is open, heartbeat age. Call it first when anything misbehaves; errors name the fix. New in 0.5.3: when the macro supports list_designs/list_cells, it also reports the open designs and the active design's cell list (with T-Cell flags) — "what's in L-Edit right now" is one call; older macros behave exactly as before (capability-gated).ledit.import_selection— you select in L-Edit, the agent imports into a fresh KLayout cell: circles stay parametric (CIRCLE PCells), layers migrate by name and GDS number (conflicts append, never overwrite), non-convertibles are listed, never dropped.ledit.push_cell— flat KLayout cell back into L-Edit (layers created by name there too).ledit.import_cell_tree— import an L-Edit cell AND its whole hierarchy into KLayout: children-first rebuild, instances stay instances (never flattened to shapes), layer identity migrates by name and GDS number the same way; every target cell is recreated, so re-importing is idempotent; shapes with no convertible outline are listed innot_convertible, never silently dropped.ledit.push_cell_tree— the other direction: push a KLayout cell and every cell below it into L-Edit, same children-first order, instances rebuilt as real instances;clear=Trueby default keeps re-pushing idempotent (L-Edit's draw only appends); the whole tree goes over as one batched request; placements L-Edit can't express exactly (magnification, non-orthogonal rotation, skewed arrays) are reported inunsupported_instances, never approximated.
Navigation and view
ledit.show_cell opens/raises a layout window on a cell and makes it the visible one ("open X in L-Edit"); ledit.set_cell_hidden toggles the "Hide In Lists" flag that auto-generated T-Cell variants carry; ledit.list_windows / ledit.close_window enumerate/close layout, text, and log windows (by cell name, or by the index from ledit.list_windows); ledit.layout_view is one verb for the view — no args reads the current view, rect_um sets it, home resets to the cell's home view, and every call answers with the view AFTER the call plus has_window; ledit.save_image renders a cell/area to PNG/BMP/JPG — a user-requested artifact only, never verification evidence, same rule as KLayout's view.screenshot: verify with get_cell / status instead.
The real return from ledit.show_cell after pushing a five-way fan-out klink tapered-router cell, ROUTE_05_FANOUT, into L-Edit:
{"cell": "ROUTE_05_FANOUT", "window_opened": false, "via": "window", "file": "klink_site_demo"}
ledit.show_cell opens after ledit.push_cell_tree pushes ROUTE_05_FANOUT and the cells below it: the layer palette shows klink's layers created by name (with GDS numbers), and the cell window shows the pushed fan-out routes.{"cell": "ROUTE_05_FANOUT", "mode": "get", "rect_um": [-51.139, -4.598, 114.139, 52.598], "has_window": true, "file": "klink_site_demo"}
ledit.save_image render of the whole cell (L-Edit's own renderer, 1600×1000): five klink tapered routes, pads, and the zero-width Port triangles — rendered as hairlines, not filled triangles.Markers cross the bridge as zero-width wires
klink's Port / Anchor / Region markers have been zero-area pure marks (width_um=0 path outlines) since 0.5.6. Before 0.5.7, draw refused width_um == 0 wires itself; that refusal now comes from L-Edit (LWire_New), and the error names the reason when it fires. Verified on real L-Edit v16.3: zero-width wires ARE accepted and render as 1-px outlines — so klink's zero-width markers can now transfer whole. ledit.push_cell_tree's report counts the total in zero_width_wires.
The real report from pushing ROUTE_05_FANOUT and its whole subtree (including Port and CorridorAnchor marker cells) into L-Edit:
{"cell_count": 13, "ops": 57, "zero_width_wires": 10, "requests": 1, "unsupported_instances": []}
13 cells, 57 draw operations, in one batched request — 10 of those are zero-width wires, exactly the Port markers' triangle outlines. Set the view near one Port with ledit.layout_view, then capture that area with ledit.save_image:
layout_view(rect_um=...) to the area around a Port marker, then save_image: two pads, one route, and the Port triangle — which crossed the bridge as a zero-width wire, reads back as width_um: 0 with 4 points, and L-Edit renders it as a hairline rather than a filled triangle.Destructive commands: explicit targets, and they refuse
ledit.delete_cell deletes a cell by explicit name; unless force=true, it refuses when the cell is the visible cell, is instanced by other cells, or is a T-Cell generator, naming the referencing cells. The real refusal:
delete_cell failed: refusing to delete 'Port$17': it is instanced 1 time(s) by ROUTE_05_FANOUT
ledit.rename_cell renames, refusing when the new name already exists. ledit.delete_objects deletes shapes by layer and/or rect_um — only objects whose bounding box lies entirely inside the rect are removed, so a route merely crossing it stays; instances are never touched (use clear_cell to reset a whole cell). The real return (rename, then delete_objects):
{"old": "DRC_DEMO", "new": "DRC_DEMO_RENAMED"}
{"cell": "DRC_DEMO_RENAMED", "deleted": 1, "by_layer": {"Poly": 1}}
ledit.close_design closes an open design by name; a design with unsaved changes is refused unless discard=true — this is the only way to drop a klink-created scratch design (new_design), because closing a design's last window does not close the design itself:
{"file": "klink_site_demo", "closed": true, "discarded_changes": true}
ledit.push_cell_tree itself got safer too: every draw item is validated before it is sent, and when a batch fails partway, the cells it created in that call are deleted (rolled_back) and any existing cells it had already cleared are reported as clobbered — never a half-done result.
DRC and the GDS return path
ledit.run_drc runs L-Edit's own DRC with the design's loaded rule set (whole cell, or rect_um for an area), reporting only the error count and status — L-Edit v16.3's UPI does not expose violation geometry, so for geometry use ledit.export_gds and klink's KLayout-side DRC tools. ledit.drc_summary reads the last result without re-running; errors is null and status is "needed" before the first run.
A DRC demo: two Poly boxes 0.4 µm apart, with the design's 82 loaded rules:
drc_summary (before a run): {"cell": "DRC_DEMO", "errors": null, "status": "needed"}
run_drc (whole cell): {"cell": "DRC_DEMO", "errors": 1, "status": "failed", "rules": 82}
run_drc (area over left box): {"cell": "DRC_DEMO", "errors": 0, "status": "failed", "rules": 82, "rect_um": [-1, -1, 4, 6]}
run_drc on the whole cell reports errors: 1 / status: failed (82 rules); the area run covering only the left box reports errors: 0 — the count is right, but there is no violation shape or coordinate.ledit.export_gds writes the whole design, or one cell with its hierarchy, to a GDS file via LFile_ExportGDSII — the cheap L-Edit → KLayout return path: layout.file_info / layout.import_file on the KLayout side read it as-is, no padding needed in this direction. The real export plus the KLayout-side readback:
export_gds: {"bytes": 4096, "scope": "specified_cell", "cell": "ROUTE_05_FANOUT"}
klayout_file_info: {"top_cells": ["ROUTE_05_FANOUT"], "dbu": 0.001,
"layers": [{"layer":1,"datatype":0}, {"layer":11,"datatype":0},
{"layer":996,"datatype":99}, {"layer":999,"datatype":1},
{"layer":999,"datatype":99}]}
export_gds, opened in KLayout with layout.show_file: top cell ROUTE_05_FANOUT, 5 layers — matching the klayout_file_info readout above.T-Cells: parametric in both directions
A T-Cell's generator source lives in a cell property the bridge can read whole; parameter names parse deterministically from the generated DO-NOT-EDIT section. That opens both directions, with a single acceptance bar: byte-exact (sorted integer-nanometer boxes, element for element, with L-Edit's own generated geometry as ground truth).
python tcell_workflows.py read NFET_Generator # params + defaults
python tcell_workflows.py variants NFET_Generator --paramsets "[...]" --out ex.json
python tcell_workflows.py writeback MyGen --code gen.cpp --params "[...]"
python tcell_workflows.py verify MyGen --reference ref.py:boxes --paramsets "[...]"
python tcell_workflows.py fit MyGen --paramsets "[...]" --check "[...]" --out fit.json --register MY_DEVICE
python tcell_workflows.py to_pcell MyGen --reference ref.py:boxes --params-spec spec.json --paramsets ps.json --check chk.json --register MY_DEVICE
- T-Cell → KLayout, geometry-only:
fit(v0.2.2) harvests exemplars and fits the v3 repeat-group model — counts, pitch and positions as exact integer laws, byte-verified against fresh L-Edit variants at held-out points, then registered as a live KLayout PCell (placement byte-verified too). What the model cannot express exactly and uniquely it REFUSES, naming the box family — alternating/parity structure stays on theto_pcellcode route below. - T-Cell → KLayout, code route (
to_pcell):to_pcellis the scaffolded version of the code-porting route — all three stages have to pass before anything is registered; any stage failing exits non-zero with instructive text, never leaving a half-done result. Step 1: byte-exact differential of your ported reference generator (Python,entry(params) -> {layer_name: [[x1,y1,x2,y2] int-nm boxes]}, harvest-native, the same dict shapeverifyuses) against fresh L-Edit variants at several--paramsets. Step 2: register that same function, unmodified, as a native KLayout PCell (libraryklink_custom; the mechanism is a newklink.domains.structdevice.pcell_native, and registration goes through the explicitexec.pythonescape hatch — deliberately, no plugin RPC accepts code directly; layer names map to GDS L/D numbers through your design's own layer table, and any GDS number not in that table is refused — klink never invents one). Step 3: an acceptance loop — at every--checkpoint, place the registered PCell live in KLayout, harvest the drawn boxes back, and byte-compare against a fresh L-Edit variant. This route covers exactly the structure the v3 fitter REFUSES — parity-alternating fingers, M×L bilinear extents, count-stepped structure — with full L/W/M parameter fidelity. - KLayout → T-Cell: the agent emits UPI code →
writeback(also defines the parameter table) → L-Edit compiles it in place → a native parametric T-Cell, indistinguishable from a hand-written one.
Here is a real run against L-Edit's own stock NFET_Generator sample — the exact device whose parity/M×L structure the fitter REFUSES:
$ python tcell_workflows.py to_pcell NFET_Generator --reference nfet_ref.py:nfet_boxes \
--params-spec spec.json --paramsets ps.json --check chk.json --register NFET_KLINK
step 1/3: byte-exact verify -- nfet_boxes vs L-Edit NFET_Generator at 5 paramset(s) ...
{'L': 2, 'W': 5, 'M': 1}: 10 boxes -> BYTE-EXACT
{'L': 2, 'W': 8, 'M': 2}: 16 boxes -> BYTE-EXACT
{'L': 3, 'W': 12, 'M': 3}: 24 boxes -> BYTE-EXACT
{'L': 2.5, 'W': 9, 'M': 4}: 24 boxes -> BYTE-EXACT
{'L': 2, 'W': 9.5, 'M': 2}: 16 boxes -> BYTE-EXACT
VERDICT: ALL BYTE-EXACT
step 2/3: registering 'NFET_KLINK' in library 'klink_custom' ...
registered KLayout PCell NFET_KLINK (library klink_custom, params ['L', 'W', 'M'])
step 3/3: acceptance loop -- 3 --check point(s), live KLayout placement vs fresh L-Edit variant ...
[1/3] {'L': 4, 'W': 15, 'M': 5} ... BYTE-EXACT (34 boxes)
[2/3] {'L': 2, 'W': 6, 'M': 2} ... BYTE-EXACT (13 boxes)
[3/3] {'L': 3.5, 'W': 10.5, 'M': 3} ... BYTE-EXACT (20 boxes)
SUCCESS
to_pcell-registered native PCell at M=1 / M=2 / M=5: finger count follows M, contact row count follows a floor law in W, and the metal columns alternate hooking to the top vs. bottom rail — exactly the parity structure the fitter REFUSES. to_pcell carries it into KLayout unchanged, byte-exact.Known boundaries & troubleshooting
| Symptom / boundary | Meaning / fix |
|---|---|
Heartbeat stalls (stale hello.json) | A modal dialog is open in L-Edit (often a T-Cell compile error) — close it and the bridge resumes |
| Changed T-Cell code but geometry unchanged | L-Edit caches variants: use fresh parameter values or Tools → Regenerate T-Cells |
| Drawing into an existing cell doubles content | draw is append-only — regenerate via clear_cell or a fresh cell |
| Two L-Edit instances with the macro | single namespace for now; keep one instance |
A layer shows GDS -1 | it will not export correctly — bridge-created layers stamp numbers; written-back code should follow the need_layer pattern |
A tool reports the macro is too old / ERR_MACRO_TOO_OLD | reload the macro: Tools → klink: Bridge Stop, then Tools → Macro → Load Macro… (the error itself names the macro's path) |
run_drc has no violation geometry | by design — L-Edit v16.3's UPI gives only a count and status; for geometry, export_gds then use KLayout-side DRC tools |
| Closed the last window but the design is still there | by design — closing a window is not closing a design; use ledit.close_design to actually close it |
save_image is for people, not verification | verify with get_cell / status, same rule as KLayout screenshots — never use the picture as evidence |
Scope: parametric intelligence (fitting, porting, verification) runs on the klink/KLayout side; L-Edit receives native results (static cells or real T-Cells). The bridge is an external adapter shipped with klink, not an L-Edit plugin.