PDK-agnostic generators for placed-and-routed digital block layouts built from standard cells. Describe your PDK once in a small JSON tech file; each block type is a single Python function that returns a block that is already placed, routed and ready to stream out to GDS.
| Block | Function | Status |
|---|---|---|
| N → 2^N decoder (optional enable) | dla.blocks.make_decoder(n_bits, enable=False) |
✅ 4–10 bits |
| Std-cell library (inv, nand2/3, nor2/3, and2, aoi21, fillers) | dla.stdcell.make_library(tech, outdir) |
✅ transistor level |
| MUX, register file, shift register, … | make_... |
planned |
No PDK data is included. A ready-made tech file is provided for the open
IHP SG13G2 PDK; for any other library
write your own (see tech/TECH_FILE.md).
pip install klayout # GDS / connectivity / DRC / PNG steps (or use --no-gds)
git clone --depth 1 https://github.com/IHP-GmbH/IHP-Open-PDK.git
export PDK_ROOT=$PWD/IHP-Open-PDK
python gen.py --tech tech/ihp_sg13g2.json decoder --bits 8 --enable # -> out/dec8to256en/
export DLA_TECH=tech/ihp_sg13g2.json
python gen.py decoder --bits 10 --name row_dec
python gen.py --tech tech/ihp_sg13g2.json stdcell # own std cells -> out/dla_lib/
python gen.py --tech tech/ihp_sg13g2.json sram --rows 4 --cols 4 # 6T bitcell + array -> out/sram_4x4/
# SRAM sizing/[sim ] = heuristic ratio model (fixed MU_P/VM): minimum legal widths meeting CR/PR targets.
# SNM, write margin, delay and power still need SPICE with the PDK device models.
python gen.py --tech out/dla_lib/dla_lib.tech.json decoder --bits 6 # decoder from those cells
python -m pytest -q tests # 31 testsFrom Python:
from dla import make_decoder, write_all
from dla.tech import load
tech = load("tech/ihp_sg13g2.json")
blk = make_decoder(6, enable=True, tech=tech) # placed + routed Block
print(blk.stats()) # cells, fill, shapes, vias, size (um)
write_all(blk, "out/dec6to64en") # JSON, layout netlist, RTL, testbenchExample run:
[tech] ihp_sg13g2: site 0.480 x 3.780, grid 0.480 x 0.500 (x offset 0.000), cells inv=sg13g2_inv_1, ...
[gen ] dec8to256en: 313 cells (+118 fill), 42.7 x 83.9 um, 6151 shapes, 1419 vias (0.2s)
[sim ] exhaustive gate-level check: PASS
[gds ] out/dec8to256en/dec8to256en.gds (43.2 x 83.9 um)
[conn] 2063 probes: missing 0, opens 0, shorts 0 -> PASS
[drc ] metal/via rules: 0 violations
RESULT: PASS
| File | Contents |
|---|---|
<top>.json |
Block JSON: instances, routing shapes, vias, pins, layer map |
<top>_layout.v |
Structural netlist with exactly the connectivity drawn in the layout |
<top>_rtl.v, <top>_tb.v |
Behavioural reference model and an exhaustive testbench |
<top>.gds |
Final GDS (std cells instantiated by name from the library GDS) |
overview.png, predecoder.png, array_corner.png |
Layout snapshots |
- Gate-level simulation in pure Python: every address, with EN = 0 and 1.
- Metal-level connectivity: pin-layer/via1/vertical/via2/horizontal nets are extracted from the GDS and compared with the intended nets (opens, shorts, missing metal at pins, rails and labels).
- Metal/via DRC driven by the tech file: width, spacing, wide-metal spacing with parallel run length, minimum area, via size and spacing, rectangular via enclosure, via keep-outs and stacking.
These are fast sanity checks, not sign-off. Always run your PDK's own DRC/LVS deck
on the GDS (the generated _tb.v runs with your library's Verilog models).
dla.spice is a PDK-independent layer: a composable testbench builder plus a batch runner for
ngspice. It does not know about SRAM or any PDK; model files
come in through include / raw statements.
from dla.spice import SpiceDeck, SpiceRunner
deck = (SpiceDeck("rc lowpass")
.include("models/my_pdk.lib") # any path, spaces are fine
.add("VIN in 0 PULSE(0 1 0 1n 1n 10m 20m)", "R1 in out 1k", "C1 out 0 1u")
.tran("10u", "10m")
.measure("tran", "t50", "WHEN v(out)=0.5 RISE=1"))
result = SpiceRunner(timeout=120).run(deck, "out/spice/rc", required=deck.measure_names)
print(result.require("T50")) # 6.93e-04; names are case-insensitive- Deck:
add(raw statements),include,model,op,dc,tran,measure. The title is line 1, then statements, analyses and.measurecards, and.endis written once (.endcannot be passed toadd). Arguments are validated before the deck changes. - Executable:
SpiceRunner(command=...)if given (a path, or a list such as a wrapper script plus its fixed arguments), else$DLA_SPICE, elsengspice(thenngspice_con) onPATH. An explicit command or$DLA_SPICEthat does not exist is an error, never a silent fallback. It is started directly (no shell) as<cmd> -b -o <log> <deck>with host-native absolute paths and stdin closed, so a wrapper (e.g. for WSL) must translate paths itself. - Outputs:
<name>.spand<name>.logare kept inworkdir(default: a new temp directory; seeresult.deck_path/result.log_path). A stale log is deleted first. - Measures:
name = valuelines are parsed to finite floats with lower-case keys. SPICE suffixes work (M= milli,MEG= mega,u,n, ...).failed,nanandinfresults land inresult.invalidinstead ofresult.measures. - Errors (all subclass
SpiceError, and carry.resultwhen a run happened):SpiceUnavailable,SpiceTimeout,SpiceFailure(non-zero exit, or a log reporting an aborted simulation) andSpiceMeasureError(arequired=measure is missing, failed or non-finite). Messages include stderr/log excerpts and the log path.
smoke_testbench() is an RC transient deck for checking an installation:
from dla.spice import SpiceRunner, smoke_testbench
deck = smoke_testbench()
r = SpiceRunner().run(deck, "out/spice/smoke", required=deck.measure_names)
# vfinal ~ 0.99988, t50 ~ 6.93e-4 s, trise ~ 2.197e-3 spytest tests/test_spice.py uses a fake simulator; its real-ngspice test is skipped unless
ngspice is installed.
Every generated system (decoder/block, std-cell library, SRAM) can emit PDK-aware SPICE data. It is driven
by the optional spice section of the tech file (tech/TECH_FILE.md): model files, vdd, temperature,
corner and the cell SPICE library. Nothing is guessed: what is not configured is reported as unavailable.
python gen.py --tech my.json stdcell # + <lib>.spice.json, <lib>_schematics.svg, schematics/*.svg
python gen.py --tech out/dla_lib/dla_lib.tech.json decoder --bits 4 # + <name>.spice (structural), .spice.json, _schematic.svg
python gen.py --tech my.json sram --rows 4 --cols 4 # + .spice.json, _schematic.svg, _bitcell_schematic.svg
python gen.py --tech my.json --simulate auto stdcell # run ngspice if configured + available, else record why not
python gen.py --tech my.json --simulate require --spice-cmd "C:/ngspice/bin/ngspice_con.exe" sram- Structural SPICE. Decoder/block instances are written as
X<inst> ... <cell>using the pin order of the real.SUBCKTinspice.cell_library(power pins default to the tech supplies); the used sub-circuits are copied verbatim. A missing cell or cell library skips the netlist and says why. Std-cell and SRAM keep their generated transistor-level SPICE. - Metadata.
<name>.spice.json(dla-spice-metadata/1): provenance (SHA-256 of tech/model files), includes/corner/VDD/temperature, sub-circuit and device counts, pins, andsimulationwith status, checks and measurements.simulation.statusisnot_requested,unavailable,passedorfailed; numbers appear only when a simulator produced them. - Schematics.
dla.schematicdraws circuit diagrams from SPICE data as dependency-free, deterministic SVG: transistor-level cells (pull-up above / pull-down below each stage, series stacked, parallel side by side, VDD/VSS rails), gate-level block diagrams (levelised; large blocks are collapsed into counted groups) and the SRAM array + 6T bit-cell. Real measurements and the simulation status are annotated on the drawing. These are circuit schematics; the layout PNGs (overview.png, ...) are separate files. - Simulation policy.
--simulate off(default) never starts a simulator.autoruns when models andvddare configured and a simulator exists, otherwise recordsunavailableand exits 0.requireexits non-zero unless every check passed. A failing check, a simulator error, a timeout or an unparsable/failed.measureis alwaysfailedwith the reason and log path. Checks are DC: truth tables for cells, addressed-output decoding for decoders, hold/read-disturb/write state checks for the 6T bit-cell. Delay arcs are measured only whenspice.testbench.edgeandloadare set. - API.
make_library(..., spice=SpiceOptions("auto")),make_sram(..., spice=...),write_all(blk, outdir, spice=...); the default (spice=None) writes exactly the old files.
- Copy
tech/ihp_sg13g2.json, fill in your LEF/GDS paths, GDS layer numbers, the rules of the three metals and two vias, and the cell names for the rolesinv,nand2,nor2,nor3andfill. Keep tech files for proprietary PDKs outside the repository (or underlocal/, which git ignores). python gen.py --tech local/my_pdk.json decoder --bits 4— the[tech]line shows the derived site, routing grid and track offset; the run fails early with a clear message if a pin cannot be reached on the track grid.- Run your PDK's sign-off DRC on the produced GDS.
Requirements on the library and the full schema are in
tech/TECH_FILE.md.
| PDK | What was checked | Result |
|---|---|---|
| IHP SG13G2 (open) | all 14 decoders (4–10 bits, EN on/off): built-in checks, official IHP KLayout DRC deck (FEOL + BEOL, density excluded), Icarus Verilog with the IHP cell models | all clean / PASS |
python gen.py --tech <tech.json> stdcell builds a std-cell library at transistor level
from the front-end rules in the tech file's stdcell section (see
tech/TECH_FILE.md). No std-cell library is needed, so a PDK that
ships none still gets the decoder (and future blocks).
[cell] dla_inv_1 3 sites (1.44 um), 2 devices, gate order A
[cell] dla_nand2_1 4 sites (1.92 um), 4 devices, gate order A B
[cell] dla_nor3_1 6 sites (2.88 um), 6 devices, gate order C B A
...
[sim ] switch-level truth tables: PASS
[drc ] front-end + metal-1 rules, 12 cells + abutment test: 0 violations
[lvs ] device extraction vs SPICE: PASS
[tech] out/dla_lib/dla_lib.tech.json (use with --tech to build blocks from these cells)
- Netlist: each cell is a list of static CMOS stages, a series/parallel pull-down expression per stage; the pull-up is its dual. An exhaustive switch-level simulation checks every network against its truth table.
- Gate order: dynamic programming over common Euler trails of the NMOS and PMOS diffusion graphs (same gate order in both rows, fewest diffusion breaks).
- Template (derived from the rules): p+ tap / VSS rail, NMOS band, channel with the poly-contact window, PMOS band in the n-well, n+ tap / VDD rail. Cells abut in R0/MX rows like any library.
- Placement + routing: vertical poly columns at contacted / uncontacted pitch; metal-1 straps over contacted diffusion; an A* router with geometric legality (spacing, wide spacing, notches, min area) wires the rest on metal 1 and gives every pin an access pad around a VIA1 on the block router's track grid.
- Views:
<lib>.gds(pure-Python writer, plus an abutment test cell),.lef,.spice,.vand<lib>.tech.jsonfor the block generators. - Checks (KLayout): front-end + metal-1 DRC from the tech-file rules, and LVS (MOS extraction, compared with the SPICE netlist including W/L and pin names).
| Cell (IHP SG13G2 rules) | Sites | Cell | Sites | |
|---|---|---|---|---|
| inv_1 / inv_2 | 3 / 5 | nor2_1 / nor3_1 | 4 / 6 | |
| nand2_1 / nand3_1 | 4 / 6 | and2_1 / aoi21_1 | 6 / 6 |
inv_1 and nand2_1 match the widths of the IHP library cells. The generator has also been run on a second, proprietary bulk-CMOS PDK (not included) that forbids VIA1 near contacts and stacked vias: the library and a decoder built from it were DRC clean there too. A dec6to64en built from the generated cells passes the gate-level, connectivity, metal and front-end checks.
The n address bits are split into na = n // 2 row bits (A[na-1:0]) and
nb = n - na column bits (A[n-1:na]), giving R = 2^na rows × C = 2^nb columns.
- Predecoders: a k-bit group is split into
lo = k // 2andhi = k - lobits; each sub-group is decoded with nor2/nor3 of true/inverted literals (a 1-bit sub-group needs no gate) and combined by nand2 into the active-lowpa_n[R](rows) andpb_n[C](columns). - Array:
Y[c·R + r] = nor2(pa_n[r], pb_n[c]). - Enable:
ENnbecomes one more input of the column group's low sub-predecoder (nor2 → nor3); EN = 0 forces everypb_nhigh, so every Y is 0. - Only nor2/nor3 are needed, so a group has at most 5 bits → n = 4 … 10.
H = row height, y = 0 is the bottom of the array.
y = R*H +-------+---------+--------------------------------+-------+
| strap | channel | row nand2 | nor2 x C | strap | <- R rows, R0/MX alternating
| VDD | | | one horizontal wire| VDD | (rails shared)
y = 0 | VSS | +--------------------------------+ VSS |
y = -H | | | column nand2 x C | | <- one vertical wire / column
y = -3H | | column predecoder row (inv, nor, [EN]) | |
y = -5H | | row predecoder row | |
y = -6H +-------+---- inputs A[*], EN, VDD, VSS (bottom edge) -----+
- Regular wiring is drawn directly: one horizontal-layer wire per row joins the row nand2 output to all nor2 A pins; each column nand2 is shifted so its output lands on the B-pin track of its array column, so one vertical wire feeds the whole column.
- Everything else (literals, predecoded signals, inputs) is routed by an A* maze router on the derived grid (vertical layer on tracks, horizontal layer on grid lines). The grid guarantees that different nets on neighbouring nodes are DRC clean, so the router only tracks node ownership.
- Power: vertical-layer straps over filler columns at both ends, VIA1 onto every row rail.
- Auto-retry: if routing fails, the left channel is widened and the block is regenerated.
- Outputs
Y[*]are in-block pins on the vertical layer at each nor2 output — the block is a macro meant to abut a memory array or word-line driver column.
| Block | Logic cells | W × H (µm) |
|---|---|---|
| dec4to16 | 28 | 17.8 × 38.5 |
| dec6to64 | 94 | 26.4 × 53.6 |
| dec8to256 / en | 312 / 313 | 42.7 × 83.9 |
| dec10to1024 / en | 1122 / 1123 | 75.4 × 144.3 |
dla/
tech.py tech-file loader: LEF parser, derived routing grid, automatic pin access
layout.py Block: rows and placement by logic role, wiring helpers, power straps,
A* maze router, filler insertion
blocks/
decoder.py make_decoder() <- one file / function per block type
stdcell/ std-cell generator: rules (template), netlist (networks, switch sim),
euler (gate order), cell (placement + metal-1 router), writers
(GDS/LEF/SPICE/Verilog/tech), check (KLayout DRC + LVS), library
export.py block JSON, structural Verilog, RTL, testbench
sim.py pure-Python gate-level simulator (exhaustive checks)
gds.py KLayout back end: GDS build, connectivity, tech-driven DRC, PNG
gen.py command-line interface
tech/ tech-file reference and the IHP SG13G2 example
klayout_scripts/ wrappers for the KLayout application (klayout -b -r ...)
tests/ pytest suite (needs DLA_TECH)
- Create
dla/blocks/<block>.pywithmake_<block>(..., tech=None) -> Block: create aBlock→add_row/place(name, role, site, row, pins)→ draw fixed wiring →power_straps→maze_route→fill_rows, then setports,boundaryandmeta["block"]. Use logic roles, never cell names. - If it needs a new logic role (e.g.
dff), add it toTech.ROLES,sim.GATESand the tech files. - Register it in
BLOCKS(dla/blocks/__init__.py), add agen.pysub-command, a functional check and tests.
-
Std cells: no Liberty (.lib) timing characterization yet; only static CMOS cells (no flip-flops, transmission gates); every transistor of a row has the same width. The built-in front-end DRC covers the rules in the tech file, not antenna / density / latch-up: run the PDK sign-off decks on the library.
-
No timing analysis. In large decoders one nand2 drives a whole row of nor2s through a long horizontal wire; add buffering if row delay matters.
-
Built-in DRC covers the three metals and two vias only; front-end, density and antenna rules need the PDK deck.
-
No transistor-level LVS: connectivity is compared at metal level; std-cell internals are assumed correct.
-
Libraries that need explicit well-tap cells are not supported yet.
- Python ≥ 3.8
klayoutPython package ≥ 0.28 for the GDS steps (optional with--no-gds)pytestfor the tests
MIT License, Copyright (c) 2026 Beomjoon Kim. See LICENSE.
The example tech file tech/ihp_sg13g2.json only references the IHP SG13G2 PDK,
which is distributed separately by IHP under the Apache License 2.0.