Skip to content

v0.5.0

Latest

Choose a tag to compare

@rjwalters rjwalters released this 15 Sep 02:19
· 4524 commits to main since this release
6bf5610

235 commits on main since v0.4.0, cut under the 25-commit backstop in
RELEASING.md's "Release cadence" (operator ruling 2026-09-15,
issue #1563). Minor bump: the range carries no conventional-commit breaking
marker (!: / BREAKING CHANGE) and no schema_version value under src/
changed; compatibility notes, where they matter, are in the entries below.

  • Added: klt place-and-route response field nominal_hold_slack_ns
    (issue #1826) — the single, nominal-corner hold WNS
    (report_worst_slack_metric -hold), populated from the "place" stage
    onward, mirroring the existing setup-side worst_slack_ns. Distinct from
    the route-stage-only, corner-swept worst_hold_slack_ns aggregate (issue
    #949) — this field exists so a caller can get a hold slack number
    before a full route, not just hold_violation_count's pass/fail count.

  • Added: klt place-and-route response field unrouted_def_path
    (issue #1826) — a pre-route DEF, populated when target_stage itself is
    "place" or "cts", reusing (and, for "cts", newly adding) the
    write_def call each of those stages' own Tcl generator already runs.
    klt sta's new def/geometry_source: "placement_estimate" request
    fields (below) accept this path directly, closing the gap where an SDC-
    driven setup/hold slack characterization required a full route.

  • Added: klt sta request field geometry_source ("routed", the
    default, or "placement_estimate"; issue #1826), echoed back verbatim as
    the response's own geometry_source field. Declares whether def is a
    fully-routed signoff geometry or a pre-route (klt place-and-route
    "place"/"cts"-stage) estimate — a caller-supplied label, since a bare
    DEF file carries no stage-provenance metadata of its own. See
    docs/cli/sta.md's "Pre-route DEFs" section.

  • Added: klt arith-gen — generate a parallel-prefix adder as Verilog
    RTL from one of five named architectures (ripple, brent-kung,
    han-carlson, sklansky, kogge-stone) or from an explicit N×N binary
    cell map (issue #1722, new module klayout_tools/arith_gen.py). Pure
    Python string generation — no Yosys, no PDK, no engine. Writes the
    structural adder, a behavioural a + b + cin reference with identical
    ports, a self-checking testbench, a Yosys techmap -map rule file
    substituting the adder for same-width $add cells, and a ready-to-run
    klt equiv request pairing the reference (gold) against the adder (gate).
    Reports prefix_cells/logic_levels/max_fanout so the cell-count vs.
    depth vs. fanout trade is visible without synthesizing. The representation
    (cell map) and the measured-in-loop selection below are reimplemented from
    Lai et al., Scalable and Effective Arithmetic Tree Generation for Adder
    and Multiplier Designs
    (NeurIPS 2024, arXiv:2405.06758); their RL/MCTS
    search is deliberately not reimplemented. See
    docs/cli/arith-gen.md.

  • Added: klt synthesize request field arithmetic (issue #1722) —
    {"adders": "auto" | "default" | "<architecture>", "min_width": 8, "candidates": [...], "verify_adders": true}. Substitutes a generated
    prefix adder for Yosys's own $add expansion via a techmap -map rule
    inserted between hierarchy and synth, guarded by _TECHMAP_FAIL_ so
    only the probed widths are replaced. "auto" runs a full trial synthesis
    per candidate plus Yosys's own default expansion, and keeps the
    smallest candidate meeting constraints.clock_period_ns (with an explicit
    reported fallback when none does). Every substituted adder is proven
    equivalent to a behavioural a + b + cin by klt equiv before it is kept
    — scoped to the adder module, so the gate works on sequential designs too.
    Additive: the new response field arithmetic is null for every request
    without the field, and schema_version is unchanged. Measured on the
    modexp canary at WIDTH=16 (gf180mcu, clock_period_ns: 22): Yosys's
    own expansion missed the constraint at 23.79 ns while all five prefix
    architectures met it, and the selection kept the smallest of those.
    Multipliers (compressor trees) and search over cell maps are explicit
    non-goals. See docs/cli/synthesize.md's "Arithmetic architecture"
    section and docs/design/synthesize-qor-improvements-survey.md §3.8.

  • Added: klt sim --plot <dir> — renders one self-contained,
    dependency-free waveform SVG per non-sweep signal per corner (issue
    #1723), reusing klt trajectory --plot's no-plotting-library, string-
    built SVG approach (new module klayout_tools/sim_plot.py). Forces
    options.waveforms/keep_artifacts on for the run regardless of the
    request's own settings — a waveform can only be plotted once captured.
    ac plots use a log10-scaled sweep axis; a tran measurement whose
    .meas card's own signal matches a rendered signal additionally gets its
    from=/to= window shaded and its WHEN <signal>=<value> threshold
    drawn as a reference line. Every written file is listed in the response's
    new plots field and each corner's artifacts.plots; a measurement's
    rollup entry additionally gets a plot field (the SVG for its own
    worst_case corner, or null) — so a miss report names the picture to
    open next, without the numeric threshold-crossing extraction that #56
    covers separately. All additive/optional (present only when --plot is
    used); schema_version unchanged. Also hardens the pre-existing
    options.waveforms capture path: a corner whose rawfile
    parse_ascii_rawfile cannot parse (currently ac/sp's complex
    real,imag-encoded values — tracked as issue #1756, out of this issue's
    own scope) now reports a "warning" diagnostic and leaves
    artifacts.waveform/plots empty instead of raising and aborting the
    whole sweep. See docs/cli/sim.md's "Waveform plots" section.

  • Added: klt kb validate --recheck-measured — re-runs every distinct
    measured.figures[].testbench in kb/entries/*.json via klt sim and
    fails validation when a recorded figures[].value has drifted beyond a
    fixed 1% relative tolerance from what that testbench produces now, instead
    of only checking that the testbench path exists (issue #1726). Off by
    default and absent from the every-PR klt kb validate CI gate — actually
    simulating every measured entry is expensive — it is what the new nightly
    .github/workflows/kb-drift-canary.yml workflow
    (schedule + workflow_dispatch only, mirroring equiv-canary.yml's
    posture for an expensive non-blocking-on-PRs simulation gate) runs. A
    drifted figure fails that job and is never auto-corrected. klt kb validate's JSON payload shape is unchanged (drift shows up as additional
    entries[].errors[] strings, so no schema_version bump). Also raises
    examples/kb/pfd-charge-pump-tri-state/request.json's
    options.timeout_s from 60 to 900: that transient needs ~4 minutes of
    wall clock, so its own checked-in request could not reproduce the figures
    the KB entry records from it. See docs/cli/kb.md's "Recheck measured
    figures" section.

  • Added: klt sim --op-lint — a per-device operating-point sanity lint
    ("op-sanity") that answers "which MOSFET is not doing its job, and why?"
    instead of running the corner sweep (issue #1718). It takes the same
    request document (only netlist is required), runs one .op at one
    corner (--op-lint-corner picks which), and walks every MOS instance in
    the netlist reporting: off (|Vgs| < |Vth| with Id at/below a noise
    floor), triode (|Vds| < |Vdsat| plus a corner-aware margin
    defaulting to 2 kT/q at that corner's temperature), wiring smells
    (drain_tied_to_rail, gate_shorted_to_source, bulk_not_tied), and
    netlist hygiene (missing_node for a declared/.meas-referenced node the
    netlist never creates, floating_node for a single-terminal node). Each
    finding names the device, its four terminal nodes, the model-reported
    values, and a one-line suggestion; per-device Vth is read from the model
    (@m…[vth]), never a constant. Exit codes reuse klt sim's vocabulary
    against this mode's own verdict (0 no error-severity finding, 3 at
    least one, 4 the operating point itself is untrustworthy). The same
    entry point is exposed as a new klt eval gate kind, op-sanity — the
    cheapest useful analog gate, meant to sit before a sim gate in a
    descriptor. New module klayout_tools/op_sanity.py
    (schema_version: 1); no new dependency (it reuses the ngspice
    print @m…[param] path klt size already uses). The corner-sweep path is
    unchanged when --op-lint is absent. See docs/cli/sim.md's
    "Operating-point lint" and docs/cli/eval.md.

  • Added: klt extract --parasitics now reports a per-net by_layer[]
    breakdown alongside each net's scalar resistance_ohm/capacitance_ff
    totals (issue #1701) — {"layer", "resistance_ohm", "capacitance_ff"} per
    contributing conductor role ("diffusion"/"poly") or metal level
    ("metal<i>", 0-based), letting a caller composing a hierarchical netlist
    from separately-extracted sub-blocks attribute a net's R/C to a specific
    layer instead of hand-subtracting scalar totals. Purely additive (no
    schema_version bump per docs/json-contract.md's additive-field policy)
    and derived entirely from intermediates _compute_parasitics already
    computed internally — no new geometry pass. Summing by_layer[].resistance_ ohm/.capacitance_ff across one net's list reproduces that net's own
    scalar totals, except for a net named by --mom-net/--mom-rlc-net (those
    substitute the scalar totals with a value this lumped-RC breakdown did not
    compute). See docs/cli/extract.md's "Per-layer breakdown" section.

  • Fixed: the curated gf180mcu DRC deck now checks Metal4's own minimum
    width and spacing — metal4.width.1 and metal4.space.1, both 0.28 um
    (280 dbu) on layer (46, 0) (issue #1688). Every other routing metal
    (Metal1-Metal3, Metal5, MetalTop) already carried both, and Metal4
    was otherwise present in the deck (its Via3/Via4 enclosure rules and
    the MiM-cap stack), so a sub-0.28 um Metal4 wire — or two Metal4 wires
    closer than 0.28 um — previously came back clean rather than reported.
    Both values are transcribed from the same DRM table the sibling rules cite
    (google/gf180mcu-pdk,
    docs/physical_verification/design_manual/tables_clear/22_Metaln_58.csv,
    rules Mn.1/Mn.2a, whose published 0.28 (2 <= n <= 5) range covers
    n = 4), under the already-registered scope="7.13 Metaln" — so
    coverage.deck_scope is unchanged. The gf180mcu deck is now 46 rules (was
    44), and coverage.rules_skipped gains the two new ids on a stream with no
    Metal4 geometry.

  • Fixed: klt extract --def-net-names now recovers the real DEF net names
    of a composed layout's block — a DEF-merged P&R macro that klt gen-compose placed into a larger layout — instead of silently degrading
    every one of that block's nets to a synthesized $<id> placeholder (issue
    #1689). Composition never dropped the DEF net-name shape property; the scan
    that reads it looked in the wrong cell. KLayout's LEF/DEF reader stamps the
    property onto the routed geometry it draws in the merge's own top cell,
    and gen-compose's composed GDS places that exact cell one level below the
    composed top (<block_id>__<src_cell_name>, hierarchy preserved rather
    than flattened) — so the property sat one level below the only cell
    _def_net_name_probes scanned. The scan is now depth-1: the extraction top
    cell's own shapes (unchanged) plus exactly one level into each direct child
    cell instance, with each candidate probe point transformed into the
    composed top cell's own coordinate frame before it is resolved. A
    standalone (non-composed) layout's output is unchanged, and the original
    false-positive guard is kept for anything nested deeper — a standard cell's
    own internal annotation several levels down still is not read as a
    top-level net name. Known limitation: a block composed more than one
    level deep (a macro composed into a macro composed into a macro) is not
    reached; nothing in this codebase currently produces that shape, since
    gen-compose always places a source block exactly one level below its own
    composed top. See docs/cli/extract.md's
    "DEF-derived net names" section.

  • Added: klt sim --format json now reports
    environment.timeout_preflight_warning (part of issue #1686) — an
    advisory string emitted before the corner grid starts when the requested
    options.timeout_s looks implausible for a tran analysis's own declared
    step/window. A per-corner timeout_s protects against a hang, but
    nothing protected against a budget that can never be met: a real incident
    widened a tran window 33× while raising timeout_s only 6×, and all 45
    corners came back timeout after burning their full budget for ~45
    wall-clock hours with no usable result. The check compares timeout_s
    against the window / step timepoint count under a deliberately generous
    50 timepoints/second floor (not a real per-engine throughput estimate),
    is scoped to kind: "tran" only, and is advisory only — it never blocks
    the sweep
    ; a budget that looks plausible under this coarse floor can
    still be wildly insufficient in practice. Present only when the heuristic
    has something to say, absent for the common case — purely additive, and
    klt sim's schema_version stays 1. The stronger dispatch-time abort
    scoped by the same issue is not implemented (it needs a timed-out
    corner's reached_s, which no investigated mechanism can recover safely
    against this module's per-corner deck shape); issue #1694 tracks that open
    design question with the empirical findings. See
    docs/cli/sim.md's "Timeout-budget preflight" section.

  • Added: klt extract --parasitics gains --parasitics-net NET,
    repeatable
    (issue #1700) — it scopes the per-net ground R/C pass to
    the named nets instead of measuring every net in the design, and the
    request is echoed back verbatim as the additive
    parasitics.parasitics_nets field. The whole-layout default is the
    dominant cost of --parasitics on a large block (each net's geometry is
    queried once per conductor role), so a mixed-signal top cell can spend
    tens of minutes producing R/C for thousands of nets when the caller
    wanted it for a handful. Requires --parasitics (the flag alone is an
    error, same as --critical-net); a name matching no net with
    ground-eligible geometry is reported in warnings, not raised, and an
    anonymous net is named in the same backslash-escaped spelling
    parasitics.nets[].net reports (issue #1162). This is the only
    --parasitics flag that removes content from the report, so two
    consequences are deliberate rather than layout findings: an unnamed net
    gets no parasitics.nets[] entry, no injected R/C SPICE cards, and
    no contribution to total_resistance_ohm/total_capacitance_ff
    — the
    totals of a scoped run are partial by construction; and because both
    coupling passes are computed from the per-net geometry the ground pass
    caches, only a pair of both-named nets can couple — naming a
    crosstalk victim without its aggressor reports zero coupling for it. A
    --critical-net net left out of --parasitics-net gets its own
    warnings entry naming the responsible flag combination, instead of the
    generic "matches no net in this layout" wording that would misdirect the
    caller at their layout. The flag changes which nets are measured, never
    how: a named net's resistance_ohm/capacitance_ff are bit-for-bit
    what the full-layout pass reports for it. parasitics.parasitics_nets is
    [] when the flag was never given, and omitting the flag leaves every
    field byte-identical to before it existed — purely additive, and klt extract's schema_version stays 3. See
    docs/cli/extract.md's "Scoping the ground R/C
    pass to named nets" section.

  • Fixed: klt gen-compose's connectivity[] router now retries a leg
    onto routing.cross_block_layer_role when it is rejected for crossing a
    different net's already-accepted route, instead of failing the whole net
    outright (issue #1680). Two nets sharing one routing.layer_role purely
    because it is the request's one primary layer — not because either
    genuinely needs that physical plane — could not resolve a crossing between
    them before this: route_two_pin()'s own same-block cross-layer retry
    (#1168/#1393) never fired, since it only ever engages from its own checks
    3/4, one level below where the route-vs-route collision check (#1057/#1386)
    actually runs. The rejected leg is now re-routed once with the primary and
    cross layers swapped and accepted only if it also clears the collision
    check there; a leg that collides on both layers still fails the whole net.
    A genuine same-layer short with no routing.cross_block_layer_role
    configured is unaffected. See
    docs/cli/gen-compose.md's "Known limitations"
    section.

  • Fixed: klt gen-compose now warns when a block's declared bbox_um
    understates its own real drawn geometry, and that undeclared excess
    geometry physically overlaps another placed block (issue #1679). A
    blocks[].generator_report entry's bbox_um is trusted verbatim for
    placement math (never re-derived from its own stream) — but a block whose
    real geometry (e.g. an externally-produced macro's guard/seal ring) extends
    past what it declared could have that excess silently overlap a neighbour
    placed just outside the declared bbox but still inside the real one.
    klt drc reports this clean (a zero-clearance same-layer merge is not an
    illegal shape by any spacing rule); the corruption previously only
    surfaced later as a spurious klt extract merged_net_labels entry
    joining two of the macro's own unrelated nets, even though extracting the
    same, unmodified macro GDS directly (no composition) was clean.
    gen-compose now reads each block's own stream to compare its real placed
    bbox against its declared one, and — only for a block where they disagree —
    checks its real per-layer geometry against every other block's for an
    actual shape overlap, adding one warnings[] entry naming both blocks and
    the shared layer when found. Advisory-only (never raises, never blocks
    composition), applies to "row"/"explicit" placement alike. See
    docs/cli/gen-compose.md's "A declared
    bbox_um is trusted, not verified" section.

  • Fixed: klt gen-compose's obstacle-overlap routability check is now
    layer-scoped instead of layer-agnostic (issue #1656). The check used to
    test a candidate route's backbone against every other placed block's
    bbox, regardless of which layers that block actually draws shapes on —
    so a route on one layer (e.g. metal3, a router-only role no klt gen
    generator draws pads on) could be rejected, or detoured around (#1167),
    for "crossing" a neighboring block's bbox even when that block draws
    nothing at all on that layer: no physical short is possible, only a bbox
    intersection on paper. route_two_pin() now looks up each candidate
    obstacle block's own drawn geometry on the leg's effective route layer
    (reusing the same lazy per-block read_block_layer_geometry cache
    compose() already builds for #1520/#1527's own-block checks) and skips a
    block as an obstacle when it draws nothing there. A block that does draw
    something on that layer is still rejected/detoured around exactly as
    before — this removes false positives only, it does not weaken the check
    against a genuine obstacle. See
    docs/cli/gen-compose.md's "Known limitations"
    section.

  • Added: klt extract --format json now reports
    devices[].instance_path (issue #1666) — the chain of GDS-level cell
    placements each device's recognition geometry sits inside, outermost
    first: [{"cell": str, "array_index": [ia, ib] | null}, ...], with
    array_index giving the 0-based element position within a regular
    CellInstArray (null for a plain single placement) and [] meaning the
    geometry was drawn directly in the top cell. Extraction flattens the whole
    instance tree before device recognition runs, so until now nothing in the
    report distinguished devices coming from different repeated copies of the
    same leaf cell; this is the device-level counterpart to the net-level
    nets[].net_id/pin_index/label_positions_um disclosure added in issue
    #1540, and is resolved positionally after extraction from each device's
    own recorded position against the layout's instance tree. Purely additive
    disclosure — no device, net, parameter, or written netlist line changes,
    and klt extract's schema_version stays 3. See
    docs/cli/extract.md's "Per-device GDS instance
    attribution" section for the worked example, the rationale for choosing an
    additive field over a non-flattening --preserve-instances mode, and the
    caveat that [ia, ib]'s axis is the GDS file's rather than the authoring
    order's.

  • Fixed: klt lvs's internal _prune_extra_top_circuits now compares
    circuits by identity (circuit is not keep) instead of cell_index
    (issue #1657). Every netlist read directly from SPICE/Verilog text — every
    reference netlist, and the pre-extracted layout.netlist shape — has no
    backing kdb.Layout, so every circuit's cell_index previously read back
    as 0, making the old cell_index-based comparison always false and
    silently pruning nothing. This could surface as a spurious "mismatch"
    status when a reference or layout netlist declared an extra, unrelated
    top-level .subckt alongside the circuit pinned via top. Layout-backed
    netlists (distinct cell_index values) already pruned correctly and are
    unaffected.

  • Added: DrcRule.check accepts a new single-layer check kind,
    "isolated" (issue #1654), dispatching to KLayout's
    Region.isolated_check — spacing measured only between edges of
    different polygons, unlike "space" (Region.space_check), which also
    measures the width of a concave notch carved into a single merged polygon.
    klt drc can therefore emit "isolated" as a violations[].check value;
    this is additive value-set growth on an unchanged shape, so klt drc's
    schema_version stays 1 (see docs/json-contract.md's
    "Pre-1.0 caveat: value sets within an unchanged shape can grow").

  • Fixed: the sky130 curated deck's nwell.space.1 now uses
    check="isolated" instead of check="space" (issue #1654), matching the
    real semantics of its source rule — sky130.lydrc's nwell.2a is
    nwell.isolated(1.27, euclidian), an inter-polygon spacing check.
    Transcribing it as "space" also flagged a concave notch within a
    single merged nwell island, which nwell.2a does not consider a
    violation; on a production layout roughly a third of the reported
    nwell.space.1 violations were this same-polygon false-positive class.
    Genuine inter-well spacing violations are still caught unchanged. Two
    consumer-visible consequences: violations of this rule now carry
    check: "isolated" rather than check: "space", and nwell.space.1 no
    longer appears in tests/golden_deck/sky130/manifest.json (that
    manifest's sky130 scope is ("width", "space")). The rule itself is
    unchanged in id, layer, and 1.27 um threshold.

  • Fixed: klt lvs's reference.form: "gate-level-verilog" compare no
    longer reports a false topology "circuit could not be matched to a
    counterpart" mismatch for a layout-side power-only cell — a filler cell
    (sky130_fd_sc_hd__fill_*, inserted unconditionally whenever issue
    #1442's row-rail fallback fires) or a tap cell
    (sky130_fd_sc_hd__tapvpwrvgnd_1, inserted unconditionally by the
    tapcell stage) — that a gate-level-Verilog reference never instantiates
    at all (issue #1622). klt lvs now removes every layout circuit whose
    entire declared pin list is a power/ground pin of reference.library
    (derived per run from that library's own .subckt data, never a
    cell-name glob or a hardcoded per-PDK power-pin table), along with every
    instance of it, before the compare runs, disclosing each removal as a new
    severity: "warning", category: "topology.power_only_pruned"
    mismatches[] entry so a "match" reached this way stays auditable from
    the report alone. Scoped strictly to reference.form: "gate-level-verilog" — the same inference is unsound against a
    "plain-element"/"subckt-call" reference, whose pin names need not
    overlap the layout's. Additive; schema_version stays 1. See
    docs/cli/lvs.md's "topology.power_only_pruned"
    section.

  • Fixed: klt place-and-route no longer raises PlaceAndRouteError when
    reaching the "cts"/"route" stage against a
    gf180mcu_fd_sc_mcu7t5v0 (7-track) netlist (issue #1649). The module's six
    per-cell-library reference tables (_CTS_BUFFER_CELLS,
    _ROUTING_LAYER_RANGE, _ANTENNA_DIODE_CELLS, _TAPCELL_CELLS,
    _FILLER_CELLS, _POWER_PIN_PATTERNS) previously carried only a
    gf180mcu_fd_sc_mcu9t5v0 (9-track) entry, so any 7-track run's table
    lookup failed before OpenROAD did any real work. Each table now also
    carries a verified gf180mcu_fd_sc_mcu7t5v0 entry, sourced the same way
    as the existing 9-track ones: platforms/gf180/config.mk's
    TRACK_OPTION-templated variables for the CTS buffer/tapcell/filler/power
    pin-pattern cells, and that library's own LEF (CLASS .../ANTENNACELL
    and pin data) for the antenna-diode and routing-layer-range rationale.

  • Fixed: klt sta's spef_annotation.annotation_complete (issue #1624)
    no longer reports true for a SPEF OpenSTA's read_spef then discards.
    The field previously rested on a name-correlation check measured
    before read_spef, with a different name resolver (get_nets) than
    the SPEF reader's own — so a SPEF whose flattened names contain the SPEF
    divider character (e.g. a generate-block hierarchy flattened to
    g_slice[0].u_slice/_08_) could correlate perfectly and still be thrown
    away wholesale by the reader, yielding annotation_complete: true
    alongside timing numbers bit-identical to the unannotated run. klt sta
    now measures three further pieces of evidence around and after
    read_spef, each of which independently forces annotation_complete: false: reader_warning_count (+ a capped reader_warning_sample) —
    OpenSTA's own STA-1650/STA-1648 "net/instance not found" reader
    diagnostics, matched on both stdout and stderr; delay_changed — the
    identical report_checks -path_delay min_max -digits 6 -unconstrained report taken
    before and after read_spef, false when byte-identical; and
    unannotated_driver_count — report_parasitic_annotation's own count of
    drivers OpenSTA holds no parasitics for. A companion
    partially_unannotated_driver_count is reported but deliberately does
    not gate (a complete, correctly-read SPEF routinely reports a non-zero
    partial count). Unknown evidence (an OpenROAD build emitting none of these)
    degrades to null and never gates, so the pre-existing name-correlation
    verdict still applies. Additive; schema_version stays 1. See
    docs/cli/sta.md's "Annotation evidence" section.

  • Added: klt extract --deck sky130 now recognises sky130's drawn
    metal-resistor device family (issue #1621): res_generic_m1..
    res_generic_m5 (metN.drawing covered by the PDK's own metN.res
    resistor-ID mark, N = 1..5), extracting a bound r_ohm from each
    layer's own sheet resistance (0.120/0.120/0.047/0.047/0.029 Ω/□ for
    met1/met2/met3/met4/met5) instead of leaving the marked segment
    unrecognised. Previously, ingesting a subckt-call-form reference netlist
    that named one of these devices failed outright — there was no device
    class for it at all. --pdk leaves them as the bare R-card form (no
    curated subcircuit binding): sky130's own device library defines no
    .subckt for any metal flavour, only a bare .model ... r card, the
    same carve-out res_metal1/res_metal2 already document for sg13g2.
    klt lvs matches the new devices through the existing generic
    device-comparison path with no LVS code changes. klt gen has no
    generator for this family yet — filed as a follow-up. See
    docs/cli/extract.md's "Drawn resistors" table.

  • Added: klt extract --deck gf180mcu now recognises gf180mcu's drawn
    metal-resistor family (issue #1640, the gf180mcu counterpart of #1621
    above): rm1/rm2/rm3 (Metal1/Metal2/Metal3 covered by the PDK's
    own metal1_res/metal2_res/metal3_res resistor-ID marks, 0.09 Ω/□
    each) plus a tm6k/tm9k/tm11k/tm30k top-metal thickness flavour set
    (0.06/0.04/0.04/0.0095 Ω/□, caller-selectable via --deck-option metal_top=, 9K the PDK's own default — mirroring poly_res, issue
    #595). Previously, a marked metal segment extracted as an ordinary short
    and a reference netlist naming one of these devices failed to ingest at
    all. Unlike sky130's res_generic_mN carve-out, all seven of these
    gf180mcu classes have a real two-terminal .subckt in the vendored
    sm141064.ngspice device library, so --pdk binds each to a genuine X
    subcircuit call rather than a bare R card. klt lvs matches the new
    devices through the existing generic device-comparison path with no LVS
    code changes. See docs/cli/extract.md's "Drawn
    resistors" table.

  • Added: klt gen-compose's routing.cross_block_layer_role fallback
    plane (issue #1168) now has its own, independent width knob —
    routing.cross_block_width_um (issue #1620). Previously
    routing.width_um was validated against both routing.layer_role's own
    deck-minimum floor and routing.cross_block_layer_role's (issue #1501),
    so naming a cross-block layer with a stricter minimum silently forced
    every net's primary-plane routing wider too. routing.width_um is now
    floored against routing.layer_role only; a leg that actually falls back
    to routing.cross_block_layer_role draws at routing.cross_block_width_um
    instead, defaulting to that layer's own deck minimum when omitted and
    floored against it the same way, with the identical error shape. Additive;
    schema_version stays 1. Same behavior reachable from
    layout_plan_execute's request.routing.cross_block_width_um. See
    docs/cli/gen-compose.md's "Cross-block bus
    routing" section.

  • Added: klt synthesize now reports static leakage power (issue
    #1626): leakage_power_nw — sum(cell_leakage_power[cell_type] * instance_count[cell_type]) over the response's own
    instance_counts_by_type, in nanowatts — plus a per-cell-type
    leakage_by_type_nw breakdown, both read from the same resolved liberty
    already loaded for dfflibmap/abc -liberty (no second liberty fetch,
    no new PDK lookup). Static leakage only — dynamic/switching power needs an
    activity factor this command has no vectors to supply, and stays out of
    scope. Both fields are null — never a fabricated or partial-looking
    number — when the resolved liberty reports no cell_leakage_power scalar
    for any instantiated cell type (some libraries, e.g.
    gf180mcu_fd_sc_mcu9t5v0, report leakage only via per-input-state
    leakage_power() groups this command deliberately does not average into
    one number). Additive; schema_version stays 1. See
    docs/cli/synthesize.md's
    "leakage_power_nw/leakage_by_type_nw" section.

  • Fixed: klt lef-abstract (issue #1613) no longer crashes with an
    unhandled AttributeError: 'SimplePolygon' object has no attribute 'each_point_hull' when a routing layer's merged obstruction region
    contains a non-rectangular polygon. The OBS formatter called
    each_point_hull() — a method that exists only on kdb.Polygon, not on
    the kdb.SimplePolygon it was calling it on — so the command always
    succeeded on a purely rectangular layout and always failed on any design
    with L-shaped/T-shaped metal, or whose pin ports only partially overlap a
    metal run. Such shapes now emit as the LEF POLYGON they always should
    have; rectangular geometry still emits as RECT, and no JSON field
    changed.

  • Fixed: klt lef-abstract (issue #1614) now finds a declared pin's
    real drawn geometry when a PDK puts a routing layer's drawn metal and its
    pin/net text label on two different GDS datatypes of the same LEF layer
    (e.g. gf180mcu's Metal1: (34, 0) drawn, (34, 10) pin label). Pin
    resolution previously searched only the exact datatype the socket
    descriptor's pins[].layer declares — the same datatype klt socket-check reads the pin's text label from — so a pin satisfying
    socket-check's label-layer contract could never see real drawn metal on
    a sibling datatype, and was always synthesized as a placeholder box even
    when metal existed. _resolve_pins now searches every GDS datatype that
    shares the pin's resolved LEF layer name, mirroring _resolve_obs's
    existing per-LEF-layer union across datatypes. See
    docs/cli/lef-abstract.md.

  • Fixed: klt extract --parasitics --spef (issue #1627) no longer
    stamps the SPEF header's *DATE line with the write's own wall-clock
    time. *DATE is optional and informational-only per the IEEE 1481-1999
    SPEF grammar (no downstream reader parses it back), so it is now a fixed
    placeholder — two runs of identical extraction against an unchanged input
    layout now produce byte-identical .spef output, matching the
    reproducibility guarantee klt place-and-route's DEF→GDS merge already
    makes for GDS2 output (issue #1367).

  • Added: klt sta now reports worst_hold_slack_ns/
    total_negative_hold_slack_ns (issue #1625), the hold-side counterparts of
    the existing setup-only worst_slack_ns/total_negative_slack_ns — from
    report_worst_slack_metric -hold/report_tns_metric -hold, mirroring
    klt place-and-route's own worst_hold_slack_ns field name/pairing
    convention so the two commands' output correlates without a naming
    mismatch. Previously the only hold-side signal in the response was
    hold_violation_count, a count that could say a corner has a hold
    violation but not rank corners by hold margin or identify the binding hold
    corner in a characterization sweep. report_tns_metric -hold ->
    timing__hold__tns was verified live against a real openroad/orfs
    container (not assumed by analogy) before shipping. Purely additive; no
    schema_version bump. See docs/cli/sta.md.

  • Added: new verb klt clip (issue #1608): writes a subset of an
    existing GDSII/OASIS stream back out as its own top-cell stream, either a
    micrometre bounding-box region (--region, the same inline-JSON shape
    klt ring-check/klt components already accept) or a named cell's full
    subtree (--cell, any cell in the stream, not just a top cell). Fills a
    gap identified by #1603: handing a single device or region to an external
    tool (EM extraction, third-party meshing, or just isolating it for review)
    previously had no klt path — --region on klt ring-check/klt components only ever restricted analysis, it never wrote the clipped
    subset back out. Region-clip mode flattens every layer of the resolved top
    cell (clip_box() + region()/texts()) and intersects it with the
    window; cell-extraction mode copies the named cell's subtree verbatim
    (kdb.Cell.copy_tree()), preserving nested hierarchy exactly. Output is
    written via the existing shared write_layout() (deterministic, no
    embedded timestamp). Errors clearly on a nonexistent --cell, a --region
    that rounds to zero width/height at the input's own database unit, and a
    --region that matches no geometry. See
    docs/cli/clip.md.

  • Added: klt pdk stackup (issue #1609) — the resolved PDK variant's
    process cross-section as structured data: per conductor/via layer its
    elevation (z0_um/z1_um), thickness, sheet (or per-cut) resistance and
    derived conductivity_S_per_m; per dielectric its z-range and relative
    permittivity. This is the z-axis information a GDSII/OASIS file does not
    carry and an external E&M/field solver requires, and it closes the open
    question docs/design/em-field-sim-spike.md deferred ("where the sky130
    stackup table itself lives as a klt-owned asset"). Unlike klt pdk em-limits, this is not a pure live parse: real tech LEFs carry no
    elevation and no dielectric constant, so those come from a curated
    per-family table (src/klayout_tools/pdk_stackup.py) whose every entry
    cites the open, non-NDA'd published source it was transcribed from
    (open_pdks' own sky130A.tech height stanza / defaultareacap
    coefficients, sky130.xs, sky130A.map), while thickness and resistance
    are derived live from the install's tech LEFs at the selected --corner.
    A variant whose family has no curated entry is an explicit exit-1 error,
    not a partial stack. --thickness curated|tech-lef selects which of the
    two published film thicknesses drives the emitted geometry (default keeps
    the stack gap-free). Curated today: gf180mcu, sky130. See
    docs/cli/pdk.md.

  • Added: klt pdk stackup --pdk gf180mcuD (issue #1616) — a second
    curated family, gf180mcu, alongside sky130
    (supported_families() now ["gf180mcu", "sky130"]). Verified against a
    real volare-fetched gf180mcuD: elevation/thickness come from
    libs.tech/magic/gf180mcuD.tech's height stanza (five metal levels of
    uniform 0.55 µm, met5 at 1.0025 µm, and no local-interconnect layer
    — met1 is the first conductor, unlike sky130's li1/mcon), GDS
    layer/datatype from libs.tech/magic/gf180mcuD-GDS.tech's calma
    statements, and permittivity (ε_r = 4.0, distinct from sky130's 3.9)
    corroborated the same arithmetic way against this install's own
    defaultareacap coefficients. gf180mcu ships no KLayout XSection script
    analogous to sky130's xsect/sky130.xs, so dielectric slab boundaries are
    induced purely from the height stanza's contiguity and film material
    names are a disclosed generic assumption rather than an install-specific
    citation (see _GF180MCU_STACKUP's provenance note in
    src/klayout_tools/pdk_stackup.py). ihp-sg13g2 remains an uncurated
    variant and continues to error clearly, naming both curated families.

  • Added: klayout_tools.lef_header.parse_lef_header() now also reports a
    CUT layer's per-cut RESISTANCE as layers[].resistance_ohms (additive;
    null on ROUTING layers, which state RESISTANCE RPERSQ instead) —
    the only resistance a via layer declares, needed by klt pdk stackup.

  • Added: klt functional-verification --mutations <proposals> (issue
    #1592) — mutation testing as a test-quality gate: applies a set of
    hand- or agent-authored byte-exact single-point RTL mutations, one at a
    time, to an isolated from-scratch build+test of the same request, and
    reports how many the testbench actually caught. Ported (Apache-2.0
    attribution header, src/klayout_tools/_vendor/mutation_variants.py) from
    boldaxolotl/booley's
    no-HDL-parsing mutation seam, per
    docs/design/mutation-testing-spike.md
    (issue #1586). The baseline run is reused unchanged from the existing
    request/response contract; a baseline that itself fails aborts the whole
    run before any proposal is attempted (exit 1 — comparing a mutant against
    an already-broken baseline is meaningless). Each isolated per-mutant
    build+test is bounded by a klt-level subprocess timeout, independent of
    whatever cycle bound the testbench's own code has — a mutation that
    wedges the DUT's FSM is classified "killed" on timeout, per booley's own
    rule, rather than hanging the whole run. Adds an additive
    mutation_testing response block (proposal_count, valid_count,
    killed_count, survived_count, rejected_count, mutation_score,
    results[]) and extends the exit-code table: exit 3 when the baseline
    passed but at least one valid proposal survived, matching klt drc's
    "ran fine, found violations" convention. See
    docs/cli/functional-verification.md's
    new "Mutation testing: --mutations" section, and
    examples/functional-verification/proposals-modexp.json
    for a worked example reproducing the spike's own live finding: one
    comparison-boundary mutation on modexp.v's modular reduction step that
    survives the committed testbench unnoticed, alongside one bit-select
    mutation that is correctly killed.

  • Added: klt synthesize's response gains three additive fields (issue
    #1588): an always-present structural verdict ({latches, expected_latches, unexpected_latches, comb_loops, multi_driven, has_critical}) over inferred latches, combinational loops, and
    multiply-driven nets — all three drawn from the same Yosys synth/stat
    run this command already performs, no extra invocation; an always-present
    bounded, deterministic warnings summary ({total, by_category, representatives}) of the captured Yosys run log; and an optional
    baseline comparison ({ref, instance_count, area_um2, critical_path_ns, delta_pct}) against a prior run named by the request's new
    baseline.response_path/baseline.netlist_path. The CLI now exits 3
    when structural.has_critical is true (status stays "ok"),
    reversing this command's own prior "no exit code 3" decision — additive
    per docs/json-contract.md's rule that a command may define codes above
    2; no schema_version bump (every new field is additive). See
    docs/cli/synthesize.md's "structural"/"warnings"/"baseline"/"Exit
    codes" sections.

  • Fixed: klt components's --label-layers entries can now declare an
    optional "conductor" name (e.g. {"name": "m1pin", "layer": [34, 10], "conductor": "m1"}), scoping that label layer's texts to only that
    conductor's own shapes in a component. Previously every label layer's texts
    were matched against the union of every conductor/via layer present in a
    component, so a text on a lower conductor's pin/label layer was wrongly
    attributed to an unrelated, upper-layer component whose geometry merely
    crossed over it in XY with no via joining them — on any real multi-layer
    block with a power stitch or long route, this made labels an unreliable
    net-name/short detector (issue #1579). An unknown "conductor" name now
    raises ComponentsError (exit 1), matching how an unknown via "between"
    conductor is already validated. "conductor" is optional and omitting it
    keeps the original any-layer matching for backward compatibility — this is
    an additive JSON field, not a schema_version bump.

  • Changed: klt gen guard_ring's (and, since they compose the same
    ring-drawing code, mos_array's/diff_pair's/bjt_array's/
    esd_device's) gf180mcu geometry now draws a second implant ring,
    exactly coincident with the tap/collector ring itself, closing the same
    DF.12 ("COMP not covered by Nplus/Pplus is forbidden") coverage rule
    #1577 closed for each unit device's own drawn Comp body but did not
    reach the ring's Comp shape (issue #1580, a follow-up to #1577). The
    doping matches whether the ring encloses a well: a well-tied ring
    (guard_ring's own add_well default, or a flavor="pfet" well on
    mos_array/diff_pair) reuses the "well_tap_implant" role (Nplus)
    well_island already reuses for its own ring (issue #1421's precedent);
    a substrate-tied ring (no enclosing well, including bjt_array's
    collector ring) gets the opposite doping, "pplus" (the same p+ role
    #1577 added for a flavor="pfet" unit device's own source/drain). This is
    a real, deliberate geometry change on gf180mcu only, gated on a
    ring_implant_present-style flag — sky130/sg13g2/sg13cmos5l
    geometry is unaffected. Not verified against gf180mcu's own signoff DRC
    tooling
    (this sandbox has no real run_drc.py to check it against) —
    see docs/cli/gen.md's updated gf180mcu note.

  • Changed: klt gen mos_array's (and, since they compose the same
    unit-device drawing, diff_pair's/esd_device's) gf180mcu geometry now
    margins against six real gf180mcu signoff-DRC rule ids
    (DF.6_LV/PL.4_LV/PL.5a_LV/PL.5b_LV/CO.7/DF.12) that this repo's
    own curated gf180mcu DRC deck never transcribed, and that #1575 found
    and documented as real violations of the pre-existing default gf180mcu
    output (issue #1577). The unit device now draws: a gate-pad clearance off
    the diffusion edge; a symmetric bottom-edge gate endcap (the top edge
    already cleared these rules via the #461 landing pad); an extra
    S/D-contact-to-gate clearance floor; a source/drain implant
    (Nplus/Pplus, selected by flavor) covering every unit device's
    Comp body; and (for voltage_flavor: "medium_voltage") a wider
    Dualgate marker box. This is a real, deliberate geometry change on
    gf180mcu only — the reported U<i>_G port and the array/pair's overall
    footprint move — sky130/sg13g2/sg13cmos5l geometry is unaffected.
    Not verified against gf180mcu's own signoff DRC tooling (this sandbox
    has no real run_drc.py to check it against) — see
    docs/cli/gen.md's updated gf180mcu note for exactly
    what changed and what remains unverified.

  • Added: documented and worked-example recipe for drawing a unit MOS
    device narrower than mos_array/diff_pair's UNIT_MIN_W_UM contact-fit
    floor (0.42 µm) — issue #1574, the follow-on #322's own triage comment
    flagged as the trigger for revisiting the hard floor. Rather than adding a
    generator-level override, docs/cli/gen.md now documents the standard
    "dog-bone terminal" hand-drawn recipe (widen only the source/drain pads to
    the generator's own contact-fit pad size, keep an unwidened shoulder
    between each pad and the gate edge, draw the gate-crossing channel at the
    requested narrow width) directly beneath both generators' w_um rows, and
    examples/dogbone-terminal/ is a new runnable, checked-in worked example
    (generate.py) that hand-draws exactly this device with klayout.db on
    the same PDK role layers mos_array itself resolves to, places it beside
    a real klt gen mos_array unit device in one cell, and verifies the
    composed result klt drc-clean on both sky130 and gf180mcu
    (example_sky130.gds/example_gf180mcu.gds with their .drc.json
    reports checked in). No generator behavior changes — mos_array/
    diff_pair still reject w_um below UNIT_MIN_W_UM outright, exactly as
    before.

  • Added: klt gen cap_array now supports the gf180mcu PDK family
    (issue #1555 — the follow-on issue #1117 explicitly deferred when it
    scoped this generator to sky130, and the gf180mcu counterpart of #1455's
    sg13g2 support). It draws the same layer/datatype numbers
    klayout_tools.decks.gf180mcu's EXTRACTION_DECK.capacitors[0]
    (cap_mim_2f0_m4m5_noshield) already declares — FuseTop top plate over a
    Metal4 bottom plate, Via4/Metal5 top-plate via and landing pad — plus
    the CAP_MK/MIM_L_MK masks that entry's own top_plate_requires
    demands, so the output round-trips through klt extract --deck gf180mcu
    to that device class (and to its 1f0/1f5 density siblings via
    --deck-option mim_cap=…, issue #1151) instead of extracting as zero
    capacitors. Three per-family geometry floors keep the default output
    DRC-clean where the generator's generic values are too small for this
    family's MiM rules: the bottom plate is drawn at the DRM's own 1.06µm
    "virtual bottom plate" oversize (CapacitorDevice.bottom_plate_oversize_um,
    vs. mim.enclosing.fusetop.1's 0.6µm minimum), the top-plate via at
    via4.width.1's 0.26µm, and a requested params.spacing_um below
    mim.space.1's 1.2µm is widened to it rather than rejected —
    drc_hints.min_spacing_um reports the spacing actually drawn, with a new
    drc_hints.notes entry naming the widening. No other PDK family's drawn
    geometry, ports, or hints change (every floor applies as max(generic, floor) and only gf180mcu sets any). One related fix: cap_array's
    harness-computed cap_top_via_metal_min_w_um PCell parameter (added by
    #1455) was missing from gen.py's _HIDDEN_PARAMS, so it was advertised
    as a request param by klt gen --list despite never being documented as
    one; it and the three new floors are all hidden now.

  • Fixed: options.combine_devices_per_circuit (issue #1552) now carries
    the same two post-combine corrections the whole-netlist
    options.combine_devices path already applies (issue #1557): the
    deferred resistor fixed_offset_ohm correction (issue #559/#585) and the
    capacitor C sum-conservation check (issue #1497). Both were previously
    gated only inside the if combine_devices_enabled: block, which never
    runs for a combine_devices_per_circuit-only request (the two options
    are mutually exclusive), so a matched circuit's folded res_high_po
    resistors never received the deferred offset and a folded capacitor's C
    could be left silently inconsistent with its pre-combine sum. Both
    corrections are now applied per circuit, scoped to just the circuit(s)
    _combine_circuit_devices_safely actually combined cleanly — a circuit
    left uncombined, unmatched, or reported device.combine_incomplete is
    never touched. Existing mismatches[].category values are reused
    (device.combine_parameter_corrected); no new category, no response
    field changes, no schema_version bump.

  • Fixed: klt extract no longer writes an unsimulatable C card for a
    recognised capacitor device class (issue #1558). KLayout's default
    primitive-card writer appends the device class's own name as a trailing
    4th token (C$1 a b 1.119e-13 cap_mim_1f0_m4m5_noshield), which ngspice's
    native C element parser reads as a required capacitor .model
    reference — and no deck's capacitor class name is a real .model in any
    PDK model library (gf180mcu's cap_mim_*_m4m5_noshield, sky130's
    sky130_fd_pr__model__cap_mim*, sg13g2's cap_cmim/rfcmim are
    klt-internal class labels or, at best, subcircuit names a C card
    cannot reference), so the extracted netlist failed to elaborate at all
    (unknown parameter (…) / could not find a valid modelname). Unbound
    capacitor classes now write the bare, value-only card (C$1 a b 1.119e-13). Reachable both with --pdk (a recognised flavour with no
    curated pdk_models table row — e.g. gf180mcu's non-default
    cap_mim_1f0_m4m5_noshield/cap_mim_1f5_m4m5_noshield densities,
    selected via --deck-option mim_cap=…) and without it. The flavour
    identity is unchanged in devices[].class and in the netlist's own
    * device instance … comment; --parasitics' ground/coupling capacitor
    cards, --pdk-bound capacitor X cards, and the R/Q
    bare-primitive carve-outs (whose trailing token is a usable
    consumer-supplied-.model reference) are all byte-identical to before.

  • Added: klt pex now accepts --deck-option <key>=<value>
    (repeatable) and --pins <a,b,…>, passed straight through to the
    klt extract --parasitics run it drives internally (issue #1558). Before
    this, a design committed to a non-default deck flavour (gf180mcu's
    poly_res/mim_cap axes — one drawn geometry, several PDK-offered
    interpretations) was silently extracted against the deck's default
    flavour, with no warning, and every delta[] row was computed from the
    wrong parasitics; --pins is likewise how a caller resolves a
    pin_count_mismatch caused by flat extraction promoting more top-level
    pins than the schematic DUT declares. Both are off by default —
    byte-identical to before — and the resolved deck options are echoed in
    the existing provenance.deck.options field (no JSON shape change). Both
    flags share klt extract's own parsing/validation helpers, so an
    unrecognised key/value or a malformed entry is the same clean exit-1
    error in either command.

  • Fixed: klt extract --check <report> --rerun (issue #1559) no longer
    reports status: "drifted" for extractor-internal bookkeeping that is not
    a stable contract across builds — a permuted net_id, an anonymous $N
    net-name spelling changing, and/or parasitics.nets[] reordering. Both
    the committed and freshly re-run reports are canonicalized before
    diffing: net_id is stripped, an anonymous net's spelling is resolved to
    a name derived from its own sorted <device>.<terminal> attachment list
    (not the raw counter), and order-sensitive lists are re-sorted by the
    resulting canonical identity. A genuine content change (an R/C value, a
    device/pin count, a connectivity change) still reports status: "drifted" exactly as before — the fix only normalizes bookkeeping, never
    swallows real drift. docs/cli/extract.md gains a "Field classes:
    content, bookkeeping, tool metadata" table naming which report fields
    fall into each class, cross-referenced from docs/json-contract.md.

  • Added: klt lvs now accepts options.combine_devices_per_circuit
    (issue #1552): a {"<circuit-name-glob>": <boolean>} map applied via
    klayout.db.Circuit.combine_devices() to each side's own matching
    circuits, before either side's optional flatten_layout/
    flatten_reference structural flatten runs. It is a per-macro alternative
    to the existing whole-request options.combine_devices boolean, for a
    composed design (e.g. via klt gen-compose) whose own macros were
    each already independently verified under their own — possibly opposite —
    combine_devices setting: one already-verified macro needs folding of
    split/interleaved device legs, another needs to stay a literal
    device-for-device compare to avoid issue #1497's silent
    parameter-corruption risk at scale, and no single whole-request boolean
    can satisfy both once composed under a shared top-level rail. Keys are
    fnmatch-style glob patterns matched case-sensitively against each side's
    own circuit names (NetlistSpiceReader upper-cases names read back from
    SPICE), applied in declaration order (first match wins); a pattern that
    matches no circuit on a side is a new severity: "warning"
    mismatches[].category, combine_devices_per_circuit.unmatched —
    never an error, since a pattern legitimately naming a circuit that exists
    on only one side is not itself a mistake. Mutually exclusive with a truthy
    options.combine_devices (a clean application error, exit 1); an explicit
    combine_devices: false alongside it is a harmless no-op. Echoed under
    the new options.combine_devices_per_circuit response field (null when
    omitted) and reconstructed by --check --rerun. No schema_version bump
    — both the request field and the response field are additive. See
    docs/cli/lvs.md's "Composing macros with opposing combine_devices
    needs" section for the full worked example.

  • Fixed: klt gen res_array no longer draws a 1dbu-short (219nm) end
    contact when length_um puts the contact centre on an exact half-dbu grid
    tie — e.g. length_um=1.4965, which tripped gf180mcu's 220nm
    contact.width.1 minimum with no signal to the caller (issue #1551). The
    unit resistor's two end-contact boxes were built from raw cx ± CONTACT_SIZE_UM / 2 floats whose opposite edges were then rounded to the
    dbu grid independently, so floating-point noise could resolve one edge up
    and the other down (the same failure mode diagnosed for guard_ring in
    issue #685). They now go through the existing _snap_square_box_um helper,
    which snaps the centre to the grid before deriving the edges. No response
    field changes and no schema_version bump; drawn geometry is unchanged at
    every length_um that is not on a tie.

  • Added: klt gen res_array now exposes gf180mcu's high-sheet-rho
    poly-resistor flavours, not just the base ppolyf_u (issue #1550):
    params.flavor accepts "1k" / "2k" / "3k" alongside the existing
    "generic" default. All three new names draw identical geometry — SAB
    (49, 0) plus the Resistor high-sheet-rho marker (62, 0) over the
    RES_MK body, with no Pplus — matching ppolyf_u_1k's (and its _2k/
    _3k siblings') own requires set in
    klayout_tools.decks.gf180mcu.EXTRACTION_DECK.resistors exactly; the three
    device classes are distinguished only by which poly_res value klt extract --deck-option poly_res=<1k|2k|3k> selects at extraction time
    (1000/2000/3000 Ω/□ respectively), not by any drawn layer. Before this
    change, a res_array cell meant to model a real design's ppolyf_u_1k
    resistor extracted as the base ppolyf_u class at ~350 Ω/□ instead of
    1000 Ω/□ — a >2.5x resistance-recognition error with no drawing-side
    parameter to fix it. gf180mcu's existing default "generic" flavour is
    byte-identical to before. No schema_version bump — no field changed
    shape, and no request field changed meaning.

  • Added: klt extract's nets[] entries now carry net_id,
    pin_index, and label_positions_um (issue #1540) — all additive,
    no schema_version bump. A flat extraction of a block that instantiates
    the same leaf cell several times in a chain (e.g. a ring of identical
    2-input stages) collides several genuinely distinct nets onto one shared
    name (KLayout's own NetlistSpiceWriter disambiguates them at write
    time with a $1/$2/... suffix this tool does not control), and neither
    name nor the existing pin boolean can tell which collided entry is
    which. net_id is the net's own KLayout cluster_id (unique per net
    object, the same identity parasitics.nets[].net_id already uses for
    issue #765/#811's identical "several distinct nets, one label" shape);
    pin_index is that net's exact 0-based position in the written
    .SUBCKT/instance-line port order when it is a promoted pin (null
    otherwise), letting a caller resolve a specific .SUBCKT port index
    straight back to a nets[] entry with no separate klt lvs run against a
    reference schematic; label_positions_um reports every drawn label's own
    (x_um, y_um) position naming that net, so a caller with independent
    floorplan knowledge (a known pin location, e.g. from klt place-and-route's DEF) can positively identify which collided entry is
    the one it means. Also fixes _purge_preserving_named_nets to restore a
    device-free, pinned net by its own cluster_id rather than by name,
    so two distinct same-named survivors on one circuit no longer silently
    collapse onto a single recreated net (the identical collision shape at a
    smaller scale). See docs/cli/extract.md's "Net-name collisions from
    internally-repeated sub-cells" section for a worked example.

  • Added: klt gen --list-pdk-pcells and klt gen --pdk-pcell <library>/<cell> reach the PCell library a resolved PDK ships itself
    under libs.tech/klayout/python/, instead of only klt's own built-in
    generators (issue #1535). --list-pdk-pcells enumerates each vendor
    library's registered name, source package, cells, and every cell's
    parameter spec (name/type/default/description/hidden/settable);
    --pdk-pcell instantiates one and writes GDS through the same response
    envelope a built-in generator emits, plus an additive pdk_pcell
    {library, cell, package} object. klt is a thin passthrough here per
    docs/ARCHITECTURE.md's "wrap the proven engine" rule: it puts the PDK's
    own python/ directory on sys.path, imports the vendor package, lets the
    vendor's own pya.Library register itself under the vendor's own name, and
    drives KLayout's own Layout.add_pcell_variant() against the vendor's own
    declaration — the same sequence the PDK's own KLayout autoload macro
    performs. A vendor PCell is instantiated as an opaque cell
    (device_count is always 1, ports always []), because interpreting
    vendor geometry to infer devices or pins would be exactly the
    hand-transcription step this feature removes. A package that needs a
    third-party compat layer klt has no dependency on is reported, never
    vendored or reimplemented: --list-pdk-pcells lists it under unavailable
    with the missing module named (and still reports every library that did
    load), while --pdk-pcell fails as an application error (exit 1) naming
    the same module, never a traceback. Both real installs available at the
    time this landed hit that path — sky130A's cells is a plain-pya
    library (not Cadence-DLO/cni-based) but reaches import gdsfactory,
    and ihp-sg13g2's chain reaches from cni.tech import Tech. See
    docs/cli/gen.md's "PDK-shipped PCells" section.

  • Added: klt pdk find and klt pdk list report a has_pcell_library
    boolean per resolved variant (issue #1535), so a caller can discover that a
    PDK ships its own KLayout PyCell library without invoking klt gen --list-pdk-pcells. Additive field; it reports only that a Python package is
    present under libs.tech/klayout/python/, never that it imports in the
    current environment. See docs/cli/pdk.md.

  • Added: klt gen-compose's connectivity[].legs[] field lets a caller
    hand-route individual legs of a bundle (>2-pin) net by name (issue #1529).
    waypoints_um steers a 2-pin net's single backbone but was rejected
    outright on any net with more than two pins, since a bundle net's spanning
    tree has no single unambiguous leg for a caller-supplied path to belong
    to — the only workaround was decomposing the net into several 2-pin
    connectivity[] entries sharing one net name, which then had to stay
    pin-adjacent (each entry sharing a literal pin with the next) to avoid the
    route-vs-route collision check's accepted-leg exemption — keyed on sharing
    a pin, not a net name — rejecting two legs of the same net as if they
    were a short between different nets. legs[] is an array of
    {from_pin, to_pin, waypoints_um} objects, each naming one leg of the
    entry's own net (from_pin/to_pin must match two of that entry's own
    pins[]); every named leg is routed through the same
    routability-check path as any other leg (nothing is exempted, only
    steered) and seeded into the spanning tree before the automatic
    nearest-first search runs, so any pin not named in legs[] still
    completes automatically. Because every leg in one legs[] array belongs
    to the same net, they are never compared against each other by the
    route-vs-route collision check, so non-adjacent legs of one bundle net
    route cleanly with no pin-adjacency ordering required. Mutually exclusive
    with the top-level waypoints_um on the same entry (an application
    error, exit 1, if both are supplied). A named leg the router rejects
    (its path crosses an unrelated block, or collides with an already-routed
    net) does not fail the net by itself — the automatic search still runs —
    but, unlike a rejected auto-selected candidate, it stays in
    nets[].legs[] with routed: false and its own reason even when the
    net comes back status: "routed", so the automatic search can never
    silently re-route around the caller's own steering. See
    docs/cli/gen-compose.md's "Hand-routing individual legs of a bundle net
    with legs[]" section.

  • Fixed: klt gen-compose now rejects an inter-block leg whose escape
    from a coordinate-tapped blocks[].cell port would draw a silent short to
    a different net already present inside that same block, instead of
    silently drawing it (issue #1527). A blocks[].cell block (an existing
    GDS/OASIS stream this command did not generate) publishes no ports[] of
    its own — a caller taps a net by coordinate, hand-declaring a port
    directly on one of the block's own internal wires. route_two_pin()
    already models the block a leg starts/ends in by its bbox_um only, with
    an unavoidable margin (_port_edge_margin_um) exempting the port's own
    approach stub from that check — so the one region a coordinate-tapped
    leg is guaranteed to draw metal in (its own approach stub) was the one
    region with no obstacle model, and an escape direction that happened to
    run across a different net's metal already drawn inside the same block
    composed routed: true and DRC-clean (two overlapping shapes on one layer
    merge into one polygon — a short, not a spacing violation, so no rule deck
    can see it) while klt extract silently merged the two nets. This is now
    caught the same way the existing route-vs-route check (#1057) is: the leg
    is compared against its own block's other drawn shapes on the route layer
    (excluding only the shape its own port lands on), and rejected into
    unrouted_nets[] with a legs[].reason naming the block instead of being
    drawn. Scoped to blocks[].cell endpoints only — a generator_report
    block (e.g. mos_array's own unreported dummy matching columns,
    suppressed from klt extract's netlist by convention) can legitimately
    draw real geometry this check cannot tell apart from an obstacle, so it is
    left to the existing whole-block bbox check. See docs/cli/gen-compose.md's
    new bullet under "Known limitations", and the corrected "Geometry is
    advisory" note on what unrouted_nets: [] plus a clean klt drc does and
    does not prove for a coordinate-tapped composition.

  • Fixed: klt pex no longer fails every extracted-side corner with
    "Could not find include file" when -o/--output and/or --outdir are
    given as relative paths (issue #1525). The extracted-side testbench
    klt pex generates re-points its .include line at the freshly-extracted
    netlist; that line named the caller's -o value verbatim, but each klt sim corner runs ngspice -b from a corner-scoped working directory
    nested under --outdir, which resolves a relative .include against
    that directory, not the invocation's own cwd — so a relative -o that
    was perfectly valid from the caller's cwd resolved to nothing there. The
    extracted netlist path is now resolved to absolute internally before it is
    written into the generated testbench, regardless of how -o/--outdir
    were spelled; this run's own JSON report continues to render netlist as
    a repo-relative {path, scope} object (issue #1261), so the fix does not
    leak an absolute path into committed evidence. See "The DUT .include
    swap" in docs/cli/pex.md.

  • Fixed: klt gen-compose now rejects a via-drop landing pad (or
    stub-widen box) that would DRC-violate against its own placed block's
    drawn geometry, instead of silently drawing it (issue #1520). A
    blocks[].cell block (an existing GDS stream this command did not
    generate) is modelled by its bbox_um plus its declared ports[] —
    hand-declared, since a pre-existing stream never reports its own ports.
    Declaring a port on an internal wire is legitimate, but the fixed-size
    landing pad gen-compose draws there (independent of width_um) could
    land close enough to a different part of that same wire (e.g. a
    perpendicular leg near a corner) to violate the resolved deck's own
    same-layer minimum-spacing rule — a real klt drc finding even though
    both shapes are the same electrical node, since a rule-deck spacing check
    is net-agnostic. This is now caught via kdb.Region.notch_check (a
    same-polygon self-space check, as opposed to the existing route-vs-route
    overlap/inflate check, which cannot see it: the pad merges with the wire
    it lands on into one polygon, so there is no separate shape to compare
    against) before anything is drawn — the net is reported unroutable
    (unrouted_nets[], routed: false, a legs[].reason entry naming the
    block and the violated rule id) rather than composing routed: true
    with the violation left for a later klt drc run to discover. See
    docs/cli/gen-compose.md's new bullet under "Known limitations".

  • Added: klt mom --touchstone <path> writes a standard 2-port
    Touchstone (.s2p) file from the de-embedded S-parameters
    report["full_wave_sweep"][i]["s_parameters"] already computes (issue
    #1518) — the interchange format SPICE-family tools (ngspice, Xyce, ADS,
    …) expect for N-port network data. Follows Touchstone Rev 1.1 (# HZ S RI R <z0> option line, S11 S21 S12 S22-per-line data order, no lossy
    magnitude/phase round-trip). Raises a clear MomError (exit code 1) if
    the report has no S-parameters (ports unset in the spec file) or if the
    two ports' reference_impedance_ohm differ — Touchstone v1.1's single
    scalar R <z0> cannot represent two different real reference
    impedances, and this writer never silently averages them or emits the
    inconsistently-supported Touchstone v2.0 [Reference] syntax. See
    docs/cli/mom.md's "Exporting Touchstone (.s2p)" section.

  • Fixed: klt gen-compose no longer hard-refuses composing blocks whose
    GDS inputs disagree on dbu, as long as every disagreement is an exact
    integer ratio (issue #1514, a regression from #1512). klt gen's output
    dbu is now resolved from the target PDK's own tech LEF DATABASE MICRONS
    (#1512) — 0.0005um for a gf180mcu-family PDK instead of the prior fixed
    0.001um — but klt draw (deliberately PDK-unaware, #230) and any GDS
    from an older klt gen build still always write 0.001um, so composing
    either against a freshly-generated gf180mcu-family klt gen block used to
    hit the same "which does not match the composed cell's dbu" error
    #1496 fixed for the opposite pairing. gen-compose now resolves the
    composed layout's own dbu as the finest dbu among all blocks, and
    rescales every coarser block's geometry (and any internal hierarchy or
    array pitch) losslessly onto that grid via a kdb.ICplxTrans integer
    magnification — the same rescale mechanics klt place-and-route's
    _merge_gds_view already uses for the analogous DEF/LEF-merge problem
    (#1090). Composing blocks whose dbus differ by a non-integer ratio (a
    genuine cross-PDK composition mistake, not a reconcilable grid mismatch)
    still raises GenComposeError. Each rescaled block adds one entry to the
    response's warnings[] naming the block, its original dbu, and the
    composed dbu it was rescaled onto — additive field usage only, no
    schema_version bump.

  • Added: klt extract --pin-source-cells CELL[,CELL...] (issue #1513) is
    a third, positional declared-pin mechanism for composing several
    already-independently-verified blocks — at least one of them a
    placed-and-routed macro with generic internal pin labels (A, X, Q,
    Y, ...) — into one flat top-level layout via klt gen-compose plus
    hand-drawn interconnect, where the composition itself has no single
    governing DEF to anchor --def-pins on. --top-cell-pins cannot help
    (a composition's own hand-drawn interconnect labels necessarily live in
    an instanced sub-cell, not literally in the new top cell, so
    --top-cell-pins demotes them right alongside the genuine internal
    noise it targets); --pins/--def-pins match by string, so two
    independently-labelled macros that coincidentally share a generic pin
    name (e.g. both use CLK internally) both stay promoted once that
    string is declared, with no way to keep only the intended one.
    --pin-source-cells resolves each drawn pin-name label found anywhere
    under the top cell whose immediate owning cell is one of the named
    cells to its actual extracted net by probing that label's own
    composed-frame position, rather than by matching its text — demoting
    every currently-promoted pin not reached this way, exactly as
    --pins/--def-pins demote on a miss. Applied after --pins's and
    --def-pins's own reconciliations (when given), so it can only further
    restrict. Off by default (omitting the flag is byte-identical to
    extraction before this feature existed). klt lvs exposes the same
    control as the layout.pin_source_cells request field, mirroring
    layout.declared_pins/layout.top_cell_pins. See
    docs/cli/extract.md's "Pin-source cells" section. The same pass also
    documents (in docs/cli/extract.md's "Declared pin set" section) that a
    --format json-reported nets[].name (|-joined by
    spice_safe_net_name) must not be pasted directly into --pins, whose
    own matching reads the promoted net's ,-joined internal name instead.

  • Fixed: klt extract --parasitics's substrate DC-tie (issue #1263) now
    also declares its tied net(s) SPICE-global (issue #1503), closing the
    gap where an X-instantiated testbench's own vsubs (or vsubs_iso<n>)
    node was electrically disconnected from the extracted subcircuit's
    internal one -- the tie kept the instance's own node from floating (no
    more singular-matrix error), but the parasitic ground-capacitance model it
    anchors was silently computed against a node no testbench could reach or
    drive. A .GLOBAL <net> ... card is now written once, before the first
    .SUBCKT, for every synthesized substrate identity the extraction
    produced (the deck-wide substrate_net and each _iso<n> variant, issue
    #1128); the pin interface, pin_count, and every other parasitics count
    stay byte-identical (klt pex's pin_count_mismatch/flat_dut_mismatch
    diagnostics, issue #1258, are unaffected). Additive JSON field:
    parasitics.substrate_dc_tie.node_scope (always "global"), so a caller
    can detect the node-scoping guarantee without reading the generated SPICE.
    See docs/cli/extract.md's "Substrate DC reference" section.

  • Added: klt lvs's options.combine_devices: true now checks every
    combined capacitor device's C parameter against the pre-combine sum of
    the parallel group KLayout's own Netlist.combine_devices() folded it
    from, and corrects it in place when the two disagree (issue #1497). A
    parallel capacitor's C is mathematically a simple per-device sum — the
    same rule klt extract's C = area_cap_f_um2 * A + perim_cap_f_um * P
    deck formula relies on — so the total is a conserved quantity combining can
    only redistribute, never change; a reported observation (10/10 repeat
    calls against one real ~1000-device/~20-group extracted netlist) found
    KLayout's own combine sometimes leaving C at a single pre-combine
    instance's own value instead of the group's summed total, while the same
    group's secondary A/P parameters combined correctly — with no
    exception raised and no existing device.combine_incomplete warning to
    catch it (that category only covers the unrelated #1185/#466
    RuntimeError-on-partial-match failure mode, which never fires here).
    Neither that report's own reduction attempt nor a follow-up investigation
    here could force a reliable reproduction from a from-scratch synthetic
    netlist at a comparable scale, so this check is applied unconditionally as
    a defensive invariant rather than gated behind a confirmed repro. New
    mismatches[].category
    : device.combine_parameter_corrected, always
    severity: "warning" (never changes status), naming every corrected
    device and its before/after C values — see docs/cli/lvs.md's
    device.combine_parameter_corrected section. Independent of the existing
    device.combine_incomplete/options.combine_devices_max_attempts retry
    mitigation, which is unaffected.

  • Fixed: klt gen compose's router now validates routing.width_um and
    sizes its via-drop squares against the resolved PDK deck's own
    minimum-width DRC rules
    — the same ExtractionDeck/DrcRule set klt drc judges the composed layout with — instead of the documented 0.17um
    default and the PDK-independent _VIA_DROP_SIZE_UM (0.22um) constant
    (issue #1501). On a family whose minimums exceed those values (e.g.
    gf180mcu's metal1.width.1 = 0.23um, via1.width.1 = 0.26um), compose
    previously drew guaranteed-illegal geometry with no error or warning at
    generation time — 172 metal1.width.1 violations on the documented default,
    284 via1.width.1 violations on every metal2 route's via-drops, per the
    issue's own measured reproduction. compose now rejects a routing.width_um
    below the resolved deck's own minimum for the requested layer (and any
    cross_block_layer_role fallback layer), naming the violated rule and its
    threshold, and sizes each via-drop's square to max(_VIA_DROP_SIZE_UM, <deck's own via-layer width floor>) per distinct via layer actually drawn.
    A PDK family whose deck cannot be resolved, or a layer with no matching
    "width" rule, keeps the prior constants unchanged. No schema_version
    bump — compose's success-path payload shape is unchanged; the new failure
    mode reuses the existing GenComposeError application-error path.

  • Fixed: klt gen now writes its output GDS at the resolved PDK's own
    database unit
    , read from that PDK's tech LEF DATABASE MICRONS
    declaration, instead of a fixed 0.001um regardless of PDK (issue #1496).
    For gf180mcu (DATABASE MICRONS 2000) a generated cell is now written at
    0.0005um — the same dbu klt place-and-route's DEF→GDS merge already
    derived from the same tech LEF — so klt gen-family blocks and
    klt place-and-route-family macros built for the same PDK can finally be
    placed in one klt gen-compose request, which requires a single shared dbu
    across every block and previously refused the mix outright with
    has dbu=0.0005, which does not match the composed cell's dbu=0.001. A PDK
    whose tech LEF is missing, unreadable, or declares no DATABASE MICRONS
    falls back to 0.001um, so no request that previously succeeded can now
    fail; sky130 (DATABASE MICRONS 1000) output is unchanged. A finer dbu
    changes only the integer grid the shapes are stored on — every generator's
    documented default params stays klt drc --deck gf180mcu clean at
    0.0005um and its drawn micrometre geometry is identical. Composing blocks
    built against genuinely different PDKs still raises the dbu-mismatch
    error: that is a real composition mistake, not a tooling artefact.

  • Added: klt gen and klt gen-compose responses now carry dbu_um, the
    database unit their output stream was written at, so a caller can confirm
    two blocks agree before composing them without a separate klt stats
    round-trip (issue #1496). Additive field on both commands — no
    schema_version bump, per docs/json-contract.md.

  • Documented: klt gen diff_pair places both legs at the same x column
    per terminal
    — the common-centroid checkerboard puts one Q1 and one
    Q2 sub-instance in every column at the same x0, so Q1_<n>_S/_D/_G
    and that column's Q2_* ports report identical x_um and differ only in
    y_um. Previously a caller only discovered this by reading gen.py or by
    hitting the routing conflict: a one-column-per-pin floorplan (one
    horizontal track per net, one vertical routing column per pin) cannot give
    the two legs' distinct nets separate columns (issue #1495). The caveat, why
    it is inherent to the interleave rather than a fixable oversight, and the
    routing styles that do work are now in docs/cli/gen.md's diff_pair
    section, and surfaced in klt gen --list (the diff_pair summary and its
    splits param description — both are free-text fields, so this is not a
    schema change). No geometry change: every generated cell is byte-for-byte
    what it was.

  • Fixed: klt gen cap_array's reported C<i>_TOP port now lands on a
    routable escape pad instead of the unit cell's own interior centre (issue
    #1494). The bottom plate spans almost the entire unit footprint (only
    CAP_BOTTOM_PLATE_MARGIN_UM, 0.5um, clear on each side), so the old centre
    position sat directly over it -- a caller stepping a via stack down from
    there to route on lower metal had no way to avoid landing on the bottom
    plate's own sheet, silently merging the two plate nets (invisible to DRC,
    since nothing about it violates a same-layer spacing/width rule; only
    extraction would report the merge, with the real top-plate net left
    anonymous). _cap_unit_layout now draws a same-layer stub running north
    from the original via-landing pad, clear past the bottom plate's own top
    edge, ending in a second pad whose own bounding box no longer touches --
    let alone overlaps -- the bottom plate; C<i>_TOP is reported there
    instead, with a geometrically real direction_deg (due north) rather than
    the previous fixed placeholder. Both currently-supported families (sky130,
    sg13g2) draw this escape by default -- no opt-in param, matching this
    repo's existing ring_gap_side precedent of not gating correct behavior
    behind a flag. A hypothetical family with plates but no top-plate-via-metal
    layer (not exercised by any currently-supported family) keeps the old,
    unroutable centre position and gains a drc_hints.notes warning instead.
    No schema_version bump -- no field changed shape, only the C<i>_TOP
    port's x_um/y_um/direction_deg values.

  • Fixed: klt lvs's reference.form: "subckt-call" conversion no longer
    silently mis-scales a bare (unsuffixed, non-exponent) L=/W= literal
    on a device subcircuit call by 1e6 for a deck whose real schematic-flow
    netlists carry an ambient SPICE .option scale=1.0u (issue #1492).
    normalize_reference_netlist previously always read a bare literal as SI
    metres, so a real sky130 xschem/ngspice netlist's L=0.15 (meaning 0.15
    um under sky130's own .option scale=1.0u convention, confirmed against a
    real fetched open_pdks install) converted to L=150000U — a plausible-
    looking but wildly wrong device geometry that produced an indistinguishable
    device.unmatched/net.unmatched avalanche with no diagnostic pointing at
    units. A bare literal is now resolved per reference.deck's own
    klayout_tools.pdk_models.geometry_style_for_family convention: sky130
    reads it as already-micrometres; gf180mcu/sg13g2/sg13cmos5l (which
    set no ambient scale) are unchanged, still SI metres. An explicit unit
    suffix (L=0.15u) or exponent (L=0.15e-6) was already unambiguous and is
    unaffected either way. With no reference.deck given, a bare literal is
    now a loud NormalizeError (surfacing as the usual application-level klt lvs error) naming the offending device/parameter/value, instead of a
    silent metres assumption.

  • Fixed: klt extract --def-net-names now recovers the real DEF net name
    for an unrouted single-pin net — a tie-cell output, or any
    synthesis-inserted constant driver whose net has one instance pin and
    nothing to route to (issue #1488). KLayout's LEF/DEF reader stamps its
    net-name shape property only onto the routed geometry it draws in the top
    cell, so such a net had no name carrier anywhere and extracted as a
    synthesized $<id>; several structurally identical ones in a design are
    exactly what makes a downstream klt lvs run report an ambiguous-pairing
    topology warning per net. klt place-and-route's DEF→GDS merge now
    synthesizes the missing carrier from the net's own pin geometry (the DEF
    COMPONENTS instance name KLayout records on each placement, the LEF
    PIN/PORT rectangle, and the instance's placement transform), as a 2 dbu
    marker shape drawn inside the already-drawn conductor — a geometric no-op
    for DRC/LVS/extraction, needing no change to what --def-net-names reads.
    Nets a routed-metal shape already names are untouched, so issue #951's
    behaviour is unchanged. Additive response field on klt place-and-route: def_net_names (single_pin_markers,
    unresolved_single_pin_nets), null unless stage_reached is "route",
    mirroring layer_map. A single-pin net whose pin geometry cannot be
    resolved (no open_pdks layer-map file, an undeclared LEF macro/pin, a pin
    centre not covered by drawn conductor) is named in that field and keeps the
    previous $<id> fallback rather than failing the merge.

  • Added: klt gen mos_array gains add_guard_ring (default false,
    preserving byte-for-byte existing geometry) plus ring_gap_side/
    ring_gap_um/ring_gap_offset_um/ring_padding_um (issue #1493),
    matching diff_pair's existing param set and composing the same
    _ring_layout/ring-gap machinery (#434). This closes the "guard/tap ring
    around a matched device group" idiom mos_array — unlike diff_pair/
    esd_device/bjt_array — previously had no way to draw for itself: a
    caller needed a separately generated guard_ring block placed inside the
    array's cavity, which klt gen-compose could never route to (every
    backbone segment crossing the enclosing block's bbox was rejected as a
    collision, with no way to express "this block encloses that one"). With
    the ring composed directly into the array's own geometry, reporting its
    own TAP_*/GAP_* ports, a route from the array's own device port to its
    own ring's tap port is an ordinary same-block self-net — already covered
    by the existing pin-block edge-margin allowance, so no gen-compose
    change was needed. Deferred on sg13cmos5l (no "tap" role in that
    family's curated deck) only when a request actually sets
    add_guard_ring: true — every other mos_array request on that family is
    unaffected. A general gen-compose-level declared-enclosure relationship
    for a ring around a separately-generated block remains a non-goal for
    this issue.

Merged since 0.4.0, by commit type

Every commit on main in v0.4.0..v0.5.0 (235 commits), grouped by its
conventional-commit prefix; the trailing (#N) is the squash-merged PR.
The narrative entries above cite the originating issue where one exists.

Added

  • feat(sta): from-scratch netlist input mode for klt sta (issue #1825) (#1832)
  • feat(place-and-route,sta): pre-route timing checkpoint (nominal hold slack + unrouted DEF) (#1831)
  • feat(wave): port VCD ingest and implement klt wave build (#1607)
  • feat(wave): port bwave FST query engine, implement klt-wave query (#1602)
  • feat(design-agent): device-level sky130 relaxation oscillator, 18/18 corners (#1822)
  • feat(synthesize,place-and-route): add sg13g2_stdcell platform-table entries (#1791)
  • feat(benchmarks): swap design-agent reference solutions to real sky130 devices (#1782)
  • feat(synthesize): generate prefix adders and pick one by measured synthesis (#1772)
  • feat(gen): populate res_array metal_level for gf180mcu rm1/rm2/rm3 (#1774)
  • feat(gen): populate res_array metal_level for sg13g2 res_metal1/res_metal2 (#1769)
  • feat(benchmarks): add charge-pump-pll hard-tier design-agent task (#1767)
  • feat(benchmarks): add tool-using interactive design-agent provider (#1763)
  • feat(loom): propagate a held PR's loom:operator onto its linked issue(s) (#1762)
  • feat(sim): add klt sim --plot waveform SVG rendering (issue #1723) (#1757)
  • feat(benchmarks): add hard-tier voltage-controlled relaxation oscillator (VCO) design-agent task (#1753)
  • feat(benchmarks): add medium-tier cascode, integrator and Schmitt trigger tasks (#1750)
  • feat(benchmarks): add medium-tier design-agent tasks (5T OTA + two-stage Miller OTA) (#1746)
  • feat(benchmarks): add hard-tier RC relaxation oscillator design-agent task (#1735) (#1745)
  • feat(kb): nightly CI drift-check that re-runs measured testbenches (#1742)
  • feat(benchmarks): add live-agent candidate provider to design-agent benchmark (#1732) (#1740)
  • feat(sim): add per-device operating-point sanity lint (klt sim --op-lint, eval op-sanity gate) (#1730)
  • feat(benchmarks): add design-agent benchmark harness (schema + runner + easy-tier tasks) (#1729)
  • feat(gen-compose): add gf180mcu metal4/via3 routing roles (#1670) (#1675)
  • feat(extract): add devices[].instance_path, attributing each device to its originating GDS instance (#1672)
  • feat(extract): recognise gf180mcu's drawn metal-resistor family (issue #1640) (#1662)
  • feat(drc): add isolated check kind, fix nwell.space.1 notch false positive (#1661)
  • feat(gen): add metal_level param to res_array for sky130 res_generic_mN (#1639) (#1660)
  • feat(gen-compose): decouple cross_block_layer_role's width from routing.width_um (#1620) (#1648)
  • feat(extract): recognise sky130's drawn metal-resistor family (issue #1621) (#1641)
  • feat(pdk): provision SG13G2 ngspice/OSDI sim toolchain (issue #1628) (#1644)
  • feat(synthesize): report static leakage power (issue #1626) (#1635)
  • feat(sta): add hold-side worst/negative slack fields to klt sta (#1625) (#1634)
  • feat(pdk): curate a gf180mcu cross-section table for klt pdk stackup (#1629)
  • feat(pdk): add klt pdk stackup — PDK cross-section for field solvers (#1609) (#1615)
  • feat(clip): add klt clip to write a bbox region or named cell out as its own stream (#1618)
  • feat(pdk): surface PDK PCell importability probe via klt pdk pcell-check (#1611)
  • feat(functional-verification): add --mutations mutation testing (issue #1592) (#1604)
  • feat(docker): add eda-sim overlay image on the loom-worker base (#1584)
  • feat(gen-compose): support multi-level via-drop ladders (#1567) (#1570)
  • feat(gen): add gf180mcu MiM-capacitor support to cap_array (#1561)
  • feat(lvs): add options.combine_devices_per_circuit for composed macros (#1556)
  • feat(gen): expose gf180mcu high-sheet-rho poly-resistor flavours in res_array (#1553)
  • feat(extract): disclose positional pin/net identity for internally-repeated sub-cell name collisions (#1540) (#1543)
  • feat(gen): instantiate a PDK's own shipped PCell library via --list-pdk-pcells/--pdk-pcell (#1538)
  • feat(gen-compose): steer individual legs of a bundle net with connectivity[].legs[] (#1529) (#1536)
  • feat(mom): add --touchstone (.s2p) export for two-port S-parameters (#1521)
  • feat(extract): add --pin-source-cells for gen-compose'd multi-macro pin declaration (#1515)
  • feat(gen): add add_guard_ring to mos_array (issue #1493) (#1499)

Fixed

  • fix(buildgate): skip real ngspice/openroad tiers locally, raise timeout (#1651) (#1659)
  • fix(pdk): resolve IHP sg13g2_stdcell's naming convention in synthesize/place-and-route liberty/LEF resolvers (#1796)
  • fix(dep-recheck): recognize curator:named-dependency markers in idempotency check (#1788)
  • fix(synthesize): bound _run_yosys with a timeout to avoid indefinite hangs (#1777)
  • fix(equiv): apply WASI-sandboxed-yosys hint to equiv.py too (#1755) (#1768)
  • fix(sim): parse_ascii_rawfile handles ac/sp Flags: complex rawfiles (#1761)
  • fix: scope champion-issue-promo tier backlog counts to loom:issue only (#1716)
  • fix(extract): recover DEF net names one level below a composed macro's top (#1696)
  • fix(decks): add gf180mcu metal4.width.1 / metal4.space.1 (#1688) (#1692)
  • fix(extract): --pins matches any component label of a multi-label net (#1690)
  • fix(gen-compose): warn when a block's declared bbox_um understates its real geometry (#1679) (#1684)
  • fix(gen-compose): retry route-vs-route collision onto cross_block_layer_role (#1683)
  • fix(gen-compose): exempt a hollow block's cavity from the obstacle check (#1682)
  • fix(gen-compose): scope obstacle-overlap check to layers a block actually draws (#1678)
  • fix(loom): guard against inline-code-quoted closing keywords in PR bodies (#1677)
  • fix(loom): match #N anywhere on named-dependency checklist lines (#1669) (#1671)
  • fix(lvs): prune power-only layout circuits before gate-level-verilog compare (#1663)
  • fix(lvs): compare circuits by identity in _prune_extra_top_circuits (#1664)
  • fix(ci): make ci-apt-install.sh's mirror rewrite adaptive, not one-directional (#1665) (#1668)
  • fix(place-and-route): add gf180mcu_fd_sc_mcu7t5v0 table entries (#1653)
  • fix(lef-abstract): search every GDS datatype sharing a pin's LEF layer name (#1636)
  • fix(sta): verify SPEF annotation after read_spef, not just net names (#1647)
  • fix(lef-abstract): emit non-rectangular OBS polygons instead of crashing (#1642)
  • fix(pdk-pcell): reach ihp-sg13g2's cni compat shim via sys.path hint + tkinter workaround (#1638)
  • fix(extract): make SPEF *DATE header a fixed placeholder, not wall-clock (#1631)
  • fix: unescape DEF identifiers in extract's --def-net-connections (#1632)
  • fix(gen): cover gf180mcu guard/tap/collector-ring Comp shapes with implant (#1580) (#1583)
  • fix(components): scope --label-layers texts to their own conductor (#1582)
  • fix(gen): close mos_array's gf180mcu unit-device signoff-DRC gap (#1577) (#1581)
  • fix(dep-recheck): extract Dependencies section by last match, any heading level (#1571)
  • fix(extract): normalize bookkeeping fields before --check --rerun diff (#1562)
  • fix: write bare C cards and add klt pex --deck-option/--pins passthrough (#1564)
  • fix(lvs): carry #559/#1497 resistor-offset and capacitor-C corrections into combine_devices_per_circuit (#1560)
  • fix(gen): snap res_array end contacts to the dbu grid before deriving edges (#1554)
  • fix(gen-compose): reject unrecognized connectivity[]/legs[]/endpoint keys (#1549)
  • fix(loom): restore FORMULA_VERSION guard reverted by resync (#1546) (#1547)
  • fix(loom): add FORMULA_VERSION guard to dep-recheck-fingerprint.sh (#1544) (#1545)
  • fix(loom): repair curator.md's dep-recheck-fingerprint.sh recipe after CLI resync (#1542)
  • fix(lvs): scope net-mismatch merge/split classification per weakly-connected component (#1539)
  • fix(curator): derive dep-recheck CONCLUSION_HASH from canonical state, not prose (#1534)
  • fix(gen-compose): reject inter-block legs that short to their own block's other metal (#1527) (#1530)
  • fix(pex): resolve extracted-side .include path to absolute before writing testbench (#1526)
  • fix(curator): add regression guard for dep-recheck idempotency violations (#1523) (#1524)
  • fix(gen-compose): reject via-drop pads that self-notch their own block's wire (#1522)
  • fix(gen-compose): reconcile integer-ratio dbu mismatches instead of refusing (#1516)
  • fix(gen): resolve output dbu from the PDK's tech LEF DATABASE MICRONS (#1512)
  • fix(extract): declare substrate DC-tie nets SPICE-global so instantiated testbenches share the node (#1503) (#1511)
  • fix(pnr): skip clock_tree_synthesis for a zero-fanout clock instead of segfaulting (#1506) (#1510)
  • fix(lvs): correct capacitor C left unmerged by combine_devices() (#1497) (#1508)
  • fix(layout-plan): forward routing.cross_block_layer_role instead of silently dropping it (#1509)
  • fix(gen): floor routing width and via-drop size at the resolved deck's own DRC minimums (#1507)
  • fix(lvs): resolve bare L=/W= geometry literals per deck's .option scale convention (#1505)
  • fix(gen): give cap_array's C_TOP port a routable escape pad (#1500)
  • fix(extract): recover DEF net names for unrouted single-pin nets (#1498)
  • fix(lvs): expand nf>1 MOS subckt calls into parallel plain-element fingers (#1489)

Changed

  • refactor(decks): split decks/init.py into rules/extraction/parasitics submodules (#1824)
  • refactor(place-and-route): split DEF->GDS merge subsystem into place_and_route_gds_merge.py (#1823)
  • refactor(cli): extract checks-cluster parsers into _add_X_parser() helpers (#1807)
  • refactor(place-and-route): split corner-sweep/post-route-SPEF STA into place_and_route_sta.py (#1819)
  • refactor(cli): extract misc-cluster parsers into _add_X_parser() helpers (#1811)
  • refactor(cli): extract 5 generation-verb parsers into add_parser() helpers (#1818)
  • refactor(cli): extract analysis-cluster parsers into add_parser() helpers (#1817)
  • refactor(cli): extract 5 inspection-verb parsers into add_parser() helpers (#1802)
  • refactor(lvs): split netgen engine subsystem into lvs_netgen.py (#1806)
  • refactor(cli): extract 'extraction/verification' cluster parsers into add*_parser() helpers (#1809)
  • perf(benchmarks): cross-step cache for design-agent-benchmark reference solutions (#1794)
  • perf(benchmarks): cache deterministic reference-provider attempts, re-tune CI timeout from measured run (#1792)
  • refactor(cli): consolidate duplicated _print_rerun_text into output.render_rerun_drift (#1787)
  • refactor(benchmarks): split interactive agent provider into its own module (#1770)
  • refactor(sim): split fleet shard/merge + remote dispatch into sim_remote.py (#1765)
  • refactor: dedup SPICE + continuation-line fold into _paths.py (#1741) (#1751)
  • refactor(lvs): split mismatch-classification subsystem into lvs_mismatch.py (#1724)
  • refactor(gen-compose): split routing subsystem into gen_compose_routing.py (#1717)
  • refactor: dedupe _tcl_net_list/_count_spef_nets_annotated into _paths.py (#1715)
  • refactor(gen): split ten build_pcell() factories into gen_pcells/ package (#1713)
  • refactor: extract _load_block_cell to dedupe gen_compose GDS-load block (#1711)
  • extract: add --parasitics-net to scope the ground R/C pass to named nets (#1705)
  • extract: expose per-net R/C breakdown by layer in --parasitics JSON (#1701) (#1706)
  • sim: add coarse timeout-budget preflight warning (part of #1686) (#1695)
  • refactor: deduplicate _validate_vias between power.py and erc.py (#1693)
  • refactor(gen): split _build_pcell_classes into per-family factories (#1650)
  • refactor: remove unused build_identity.git_commit()/is_release() accessors (#1646)
  • refactor: dedupe _run_openroad/_count_violations/_openroad_version into _openroad_engine.py (#1643)
  • klt synthesize: additive structural verdict, warnings summary, baseline QoR delta (#1605)

Docs

  • docs(design-agent): split-polarity dual comparator clears the sky130A ss/-40C/1.62V headroom ceiling (#1815)
  • docs(kb): document sky130 binned nfet L=0.15/nf>=3 BSIM4 fatal footgun (#1812)
  • kb: add reference netlist + measured figures for sky130-lc-vco-cross-coupled (#1805)
  • kb: add reference netlist + measured figures for multi-ro-xor-entropy-source (#1778)
  • kb: add reference netlists + measured figures for ro-puf, metastability-trng, ring-oscillator-jitter-trng (#1776)
  • kb: add reference netlists + measured figures for beta-multiplier-bias-cell, cmos-subthreshold-voltage-reference and cmos-ring-vco-current-starved (#1773)
  • kb: add reference netlists + measured figures for rx-front-end-termination-buffer and sram-power-up-puf (#1760)
  • kb: add reference netlists + measured figures for folded-cascode-ota and cml-tx-line-driver (#1754)
  • kb: add reference netlists + measured figures for two-stage-miller-ota and strongarm-latch-comparator (#1747)
  • kb: add reference netlists + measured figures for five-transistor-ota and inverter-based-comparator (#1737)
  • kb: add measured figures block + numeric search filters (av_db>=40 style) (#1727)
  • docs: document daemon dispatch race claiming a reverted promotion (#1710) (#1712)
  • docs: design spike — attribute --parasitics R/C to top-cell-drawn vs. instance-inherited geometry (#1707)
  • docs(extract): document parasitics star addressing and device-card node order (#1676)
  • docs: port per-concern RTL/testbench review guides into docs/guides/digital-review/ (#1612)
  • docs(design): survey klt wave query surface and trace formats (#1598)
  • docs(guides): port booley's RTL mutation-proposer guide (Apache-2.0) (#1597)
  • docs(design): port-vs-build decision record for klt wave (#1596)
  • docs(design): propose the klt wave build/query JSON contract (#1595)
  • docs(design): survey mutation testing as a test-quality gate for functional-verification (#1594)
  • docs(gen,drc): disclose gf180mcu mos_array's real-signoff-DRC gap (#1575) (#1578)
  • docs(gen): add dog-bone terminal recipe for W below UNIT_MIN_W_UM (#1576)
  • docs(layout_plan): update stale "Phase C not-yet-built" claims (#1566)
  • docs(loom): document local Loom test-suite wiring convention (#1532) (#1537)
  • docs(gen): state diff_pair's shared per-terminal x column in docs and --list (#1504)

Chores

  • test(place-and-route): gate a real ihp-sg13g2 GCD integration test (#1784) (#1816)
  • chore(deps-dev): bump the npm-minor-patch group across 1 directory with 5 updates (#1573)
  • chore(deps): bump flate2 (#1482)
  • ci: declare python3-click for the pinned SymbiYosys launcher (#1685)
  • ci: run native-techmap on the dedicated heavy runner (2am#29) (#1246)
  • chore(decks): regenerate deck history table for v0.4.0
  • chore: resync installed Loom surfaces (72 commits, no PR)