Step-by-step tutorial · nanodevice · EBL

Step-by-step: drawing an EBL wraparound nanodevice

This walks through example_template/nanodevice/ebl_wraparound.py from the public gallery (for the demo's function, requirements, and measured numbers, see Examples · EBL wraparound) -- the second tutorial after the Hall bar tutorial. It's the same offline, self-contained device pattern with Port/Anchor intent markers, but this time it picks up the thread the Hall bar tutorial left dangling: writefield obstacle avoidance. Electron-beam lithography exposes a chip one rectangular writefield at a time, and the stitch boundary between adjacent fields carries alignment error -- so wiring may only cross a writefield boundary through a pre-planned "crossing window." klink expresses that as a batch of keepout geometry plus corridor Anchors, not something a router guesses at runtime.

Prerequisites

  • Install klink: pip install klayout-klink (one command installs klink plus its Rust kernels -- no third-party scientific libraries needed).
  • KLayout running with the klink plugin loaded (RPC port defaults to 8765).
  • Scaffold a project and enter it: klink init nano-demo && cd nano-demo -- the full script is python example_template/nanodevice/ebl_wraparound.py --live (add --keep to keep the generated cell around for inspection). In a repo checkout the equivalent is python -m examples_klink.public.demos.nanodevice.ebl_wraparound --live --keep.
This tutorial breaks the single batched call inside build_wraparound_demo() into 7 observable stages. Construction order is fixed: 4 corner alignment crosses (3 items each) -> the flake polygon -> the keepout box + its label -> 8 contact groups (7 items each, in this fixed order: contact box / M1 stub / via / M2 wraparound path / taper transition / pad / label) -> stitch patches -> writefield keepout boxes. The tutorial slices shape_items along this real order -- there is no tutorial-only drawing logic here.

1Create the cell + plan the layers

Every layer here is example-owned (WRAP_LAYERS in example_template/nanodevice/ebl_wraparound.py) -- klink ships no process layers of its own. The real script uses a generic _ensure_layers helper that scans every (layer, datatype) pair that actually appears in shape_items and creates it, including klink's reserved writefield keepout layer 900/0 (it shows up automatically because the writefield obstacle boxes are already shape items -- no separate declaration needed):

LayerPurpose
30/0Flake (the device's active region)
10/0M1 (near-field contact wiring)
11/0M2 (wraparound routing to distant pads)
40/0Via (contact via)
20/0Probe pads
6/0Labels + alignment marks + local keepout box
113/0Writefield stitch patches
900/0klink's reserved writefield keepout layer (appears automatically)
999/99klink's reserved Port marker layer
999/1klink's reserved Anchor marker layer
from klink import KLinkClient
from klink.domains.nanodevice.devices.wraparound import (
    ANCHOR_LAYER, CELL, PORT_LAYER, build_wraparound_demo,
)

def _ensure_layers(client, items):
    seen = set()
    for item in items:
        key = (int(item["layer"]), int(item.get("datatype", 0)))
        if key not in seen:
            seen.add(key)
            client.layer_ensure(key[0], key[1], name=f"NANODEVICE_{key[0]}_{key[1]}")

with KLinkClient(port=PORT).connect() as c:
    bundle = build_wraparound_demo(WRAP_LAYERS)
    c.cell_create(CELL)
    _ensure_layers(c, bundle["shape_items"])
    c.call("port.set_layer", {"layer": PORT_LAYER})
    c.call("anchor.set_layer", {"layer": ANCHOR_LAYER})
Empty cell with every layer already created, no geometry on the canvas yet
Step 1 · Empty cell right after the layer plan, framed on the full 460x340 um chip area; nothing is drawn yet.

2Draw the flake + local keepout + alignment marks

An irregular polygon flake sits at the center -- the device's real active region -- wrapped in a local keepout box ([-54,-44,54,44], labeled "flake + local keepout") that keeps later wiring at a safe distance. Four alignment crosses sit in the corners for the EBL system's global alignment pass:

c.shape_insert_many(CELL, bundle["shape_items"][0:15])  # 12 alignment-cross items + flake polygon + keepout box + text
An irregular polygon flake sits at the center of the frame, wrapped in a red keepout box, with a red alignment cross in each corner
Step 2 · Flake (30/0) + local keepout box (6/0) + four corner alignment crosses (6/0).

3Draw the near-field contacts + vias

Eight narrow M1 stubs (C0-C7) reach out from the flake, each running diagonally from a contact point near the flake edge to its own via -- near-field wiring, at roughly the flake's own scale:

contact_block = bundle["shape_items"][15:71]  # 8 groups x 7 items
stage3 = [it for i, it in enumerate(contact_block) if i % 7 in (0, 1, 2)]  # contact box / M1 stub / via
c.shape_insert_many(CELL, stage3)
Eight thin blue traces radiate outward from around the flake, each ending in a small square via
Step 3 · 8 M1 near-field contact stubs (10/0) + 8 vias (40/0), radiating outward from the keepout box like spider legs.

4Route the wraparound to the probe pads

This is where the name "wraparound" comes from: instead of a straight line from via to pad, each route first runs horizontally or vertically out toward the chip boundary, then turns toward its distant probe pad -- this spreads all 8 pads evenly around the chip perimeter while dodging the flake and neighboring routes:

stage4 = [it for i, it in enumerate(contact_block) if i % 7 in (3, 4, 5, 6)]  # M2 wraparound path / taper / pad / label
c.shape_insert_many(CELL, stage4)
Eight M2 traces turn a right angle from each via and wrap around to large probe pads outside the flake, pads labeled C0 through C7
Step 4 · 8 M2 wraparound routes (11/0, with tapered transitions) + 8 probe pads (20/0, labeled C0-C7).

5Mark Port + Anchor intent

This is where it diverges from the Hall bar tutorial: here the geometry is drawn by hand first, and Port/Anchor markers are added after the fact -- not as an upstream routing intent. Writefield stitch corridors are a hard constraint, so the path shape has to be pinned to the corridor layout before anything runs; klink's route solver isn't the right tool for that kind of constrained, pre-determined geometry. Port/Anchor here record intent for the validation pass in stage 7:

for port in bundle["port_marks"]:          # 16: 8 contacts + 8 pads
    payload = dict(port); payload["cell"] = CELL
    c.call("port.mark", payload)
for anchor in bundle["anchor_marks"][0:8]:  # 8 via waypoint anchors
    payload = dict(anchor); payload["cell"] = CELL
    c.call("anchor.mark", payload)
Red Port marker boxes appear next to every contact point and pad, red Anchor text labels wrap_0 through wrap_7 appear at every via
Step 5 · 16 Ports (8 contacts + 8 pads, 999/99) + 8 via waypoint Anchors (999/1, labeled via (wrap_0) ... via (wrap_7)).

6Plan the writefield obstacles

This is the real difference from the Hall bar tutorial. klink.domains.nanodevice.ebl.writefield.plan_writefields() divides the 460x340 um chip into a 4x4 grid of 115 um writefields, and draws a keepout obstacle box (klink's reserved layer 900/0) along every writefield boundary -- except at 11 pre-planned "crossing windows." Wiring may only cross a writefield boundary at one of these windows; the windows themselves are marked with corridor-kind Anchors:

obstacle_items = bundle["shape_items"][71 + bundle["patch_report"]["patch_count"]:]  # 17 keepout boxes on 900/0
c.shape_insert_many(CELL, obstacle_items)

for anchor in bundle["anchor_marks"][8:]:   # 11 corridor window anchors
    payload = dict(anchor); payload["cell"] = CELL
    c.call("anchor.mark", payload)
The chip is divided by black grid lines into a 4x4 writefield grid, with red-striped keepout walls along the grid lines; the walls have gaps labeled with WF-prefixed red text, and the routed traces pass exactly through those gaps
Step 6 · 4x4 writefield grid + boundary keepout walls (red stripes, 900/0) + 11 corridor-window Anchors (labeled WF_XL_MID, WF_Y0_WIDE, etc.) -- every route drawn so far passes cleanly through a window gap, never through the wall.

7Add stitch patches + finish

Field-stitching in real lithography carries alignment error, so generate_wf_patches() (ported from Klayout-Router's Auto-patching macro) drops a square patch everywhere a drawn route crosses a writefield boundary, to compensate. Last, look at the whole chip and one detail:

patch_items = bundle["shape_items"][71:71 + bundle["patch_report"]["patch_count"]]
c.shape_insert_many(CELL, patch_items)
The finished EBL wraparound layout in full: central flake, eight radiating near-field traces, eight wraparound routes, the writefield grid, keepout walls, corridor windows, and the probe pads
Step 7a · Finished overview (view.zoom_fit): flake + 8 contacts + 8 wraparound routes + writefield grid/keepout/corridors + 8 pads + 12 stitch patches.
Zoomed-in view of a route crossing a vertical writefield boundary, with a gray square stitch patch at the crossing, and the boundary wall reappearing outside the window
Step 7b · Detail crop: the C0 net's M2 wraparound route crossing the x=-115 writefield boundary, landing exactly inside the WF_XL_MID corridor window. The gray square is the stitch patch; note the keepout wall reappearing just above the window.
Same spot, with an orange box and arrow pointing at the exact location of the stitch patch
Step 7c · Same spot, orange highlight: this 7x7 um gray square is this route's only stitch patch on this writefield boundary.

Verify, don't just look

As the tutorials home page says: screenshots are for people, not proof of done. Here's the actual build report from this run:

{
  "shape_items": 100,
  "ports": 16,
  "anchors": 19,
  "patches": 12,
  "wf_obstacles": 17,
  "wf_crossings": 20,
  "wf_crossing_violations": 0,
  "route_centerline_overlaps": 0
}

wf_crossings=20 with wf_crossing_violations=0 means all 20 boundary crossings made by the routed geometry land inside a pre-planned corridor window -- none of them punch straight through a keepout wall. route_centerline_overlaps=0 means the 8 different nets' route centerlines never overlap, so there's no short-risk between them. Together, that's what "this layout respects the writefield constraint" actually rests on -- not "it looks like it crosses cleanly."

Next steps

If you haven't read the first tutorial yet, start with Step-by-step: drawing a Hall bar device -- it's the prerequisite for this one: the same Port/Anchor/geometry-first loop, but there the wiring is mark-first, solve-second through klink's routing backend, instead of hand-drawn-first, marked-after like here. Edit WRAP_LAYERS and the crossing_windows passed into build_wraparound_demo to your own writefield size and corridor placement, rerun, and confirm wf_crossing_violations stays at 0. The full Port/Anchor field reference is in the MCP reference; for more on offline devices see Core concepts & tutorials · Offline device tutorial.