Guide · L-Edit Bridge

Drive Tanner L-Edit both ways with an agent

A load-as-source UPI macro plus a JSON file-exchange transport: no sockets, no firewall prompts, nothing to compile; written against the old-version API subset (older versions compatible by design — tested on v16.x). Agents get white-box read/write access — all the way down to T-Cell generator code. 0.5.7 turns the bridge from a drawing transport into a full editor control surface: navigation (windows, view, cell show/hide), destructive commands with explicit targets, a validated and rollback-capable hierarchy push, L-Edit's own DRC, and GDS export back to KLayout (macro 0.5.6 → 0.5.8 — reload the macro in L-Edit after upgrading).

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 ping shows 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

AreaCommands
Design filesnew_design (bootstraps a .tdb even with nothing open) · open_design · save_design
Layersensure_layer (GDS numbers stamped; new layers auto-colored: solid fill, no outline) · set_layer_style · get_layers (fill color, special-layer flags)
Drawingcreate_cell · draw (box/polygon/wire/circle, batch) · place_instance (incl. nx×ny arrays) · clear_cell
Full readoutget_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)
Navigationshow_cell · set_cell_hidden · list_windows · close_window · layout_view · save_image
Destructivedelete_cell · rename_cell · delete_objects · close_design
Verificationrun_drc · drc_summary · export_gds
Process knowledgeget_drc_rules — the design's whole rule table, machine-readable
T-Cellsget_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 in not_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=True by 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 in unsupported_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"}
The pushed ROUTE_05_FANOUT cell open in an L-Edit v16.3 window; the layer palette lists klink's named layers (M1_DEVICE_OR_PAD, KLINK_ROUTES, KLINK_PORTS, ...) with their GDS numbers, and the cell window shows the pushed fan-out routes
The real L-Edit v16.3 window 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 ROUTE_05_FANOUT cell (L-Edit's own renderer, 1600x1000): five routes from klink's tapered router, pads, and the width-0 Port triangles as hairlines
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:

ledit.layout_view then ledit.save_image render of the area around a Port marker: two pads, one route, and the Port marker triangle rendered as a hairline (zero-width wire, read back with width_um 0, 4 points)
Zoomed with 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]}
Two Poly boxes 0.4 micron apart; ledit.run_drc with the design's 82 rules reports error count 1 and status failed, while the area run covering only the left box reports error count 0
Two Poly boxes deliberately spaced 0.4 µm apart, tripping a spacing rule: 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}]}
The GDS written by ledit.export_gds opened in KLayout (layout.show_file), top cell ROUTE_05_FANOUT, 5 layers, with KLayout's scale bar
The GDS written by 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 the to_pcell code route below.
  • T-Cell → KLayout, code route (to_pcell): to_pcell is 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 shape verify uses) against fresh L-Edit variants at several --paramsets. Step 2: register that same function, unmodified, as a native KLayout PCell (library klink_custom; the mechanism is a new klink.domains.structdevice.pcell_native, and registration goes through the explicit exec.python escape 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 --check point, 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
Three instances of a to_pcell-registered native PCell in KLayout, labelled M=1, M=2, M=5, finger count increasing with M, contact row count following a floor law in W, and metal columns alternating between hooking to the top and bottom rail
A synthetic demonstration device (its own arbitrary dimensions, not any real process) drawn live by a 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 / boundaryMeaning / 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 unchangedL-Edit caches variants: use fresh parameter values or Tools → Regenerate T-Cells
Drawing into an existing cell doubles contentdraw is append-only — regenerate via clear_cell or a fresh cell
Two L-Edit instances with the macrosingle namespace for now; keep one instance
A layer shows GDS -1it 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_OLDreload the macro: Tools → klink: Bridge Stop, then Tools → Macro → Load Macro… (the error itself names the macro's path)
run_drc has no violation geometryby 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 thereby design — closing a window is not closing a design; use ledit.close_design to actually close it
save_image is for people, not verificationverify 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.