Native RC extraction¶
rustroad extract turns the routed HEAD revision of a workspace into detailed
IEEE 1481 SPEF. It is a read-only command. It reads the stored routing records
(routing.rbin), the placed pin/obstruction geometry and the supply metal, and
a per-PDK, per-corner RC profile built from PDK data files. Two independent
black-box oracles check it: OpenRCX (scripts/compare_spef.py) and Magic
(scripts/magic_rc.py); scripts/rc_oracles.py runs all three together. A
finite-difference field solver on the PDK's process stack
(scripts/field_solver.py) measures what no 2.5-D extractor models: the
three-dimensional loading of wire crossings. No reference source was read,
and no native coefficient was fitted to either oracle's output.
Scope is block level. Supply nets are not extracted. Pin capacitance stays in
Liberty. The routed metal is extracted completely: wire bodies, the half-width
end extensions, the via landing pads and routing patches. A crossing is counted
from both conductors' cross-sections, including pins, obstructions and supply
metal that a routed wire crosses. Metal on another layer that overlaps a wire
face without forming a calibration plate (stacked, offset or barely wider
metal) couples over the overlapped area. Via cut capacitance, diagonal coupling
and the fringe of such non-plate overlaps are not modeled (see "Limitations").
SI, IR/EM, OCV and CPPR remain unchecked. DRC/LVS status is unchecked.
Command¶
rustroad extract --workspace W --rc-profile fixtures/pdk/sky130-rc-max.json \
--corner max --out /path/outside/W/routed_max.spef \
[--max-spef-mib 32] [--max-segment-um 25] [--max-rc-nodes 500000] \
[--max-coupling-caps 1000000] [--max-extract-work 200000000] \
[--max-rc-elements 1000000] [--max-extract-candidates 4096] \
[--extract-memory-mib 2048] [--max-extract-shapes 2000000] \
[--max-extract-bin-entries 8000000]
- The workspace is opened under its shared lock and never written.
--outinside the workspace fails withOUTPUT_IN_WORKSPACE. The SPEF is written to a temporary file in the target directory, fsynced, then renamed. A failure removes the temporary file and leaves no output. --cornermust equal the profile's corner (CORNER_MISMATCHotherwise). The profile'stechnology_lef_sha256must equal the workspace's imported technology LEF. Every routing and cut layer the router used must be in the profile. A routed width different from the calibrated width is an explicit error (non-default widths are unsupported).- The byte cap (
--max-spef-mib, at most 16,384) is enforced while writing. Shape, node, element, coupling, candidate, bin and work budgets fail withBUDGET_EXCEEDED. Errors use the existing{status, code, message}stderr envelope and exit code 2. - stdout is a JSON report (
rustroad.extract.v1): output path, sha256 and bytes; profile identity;scope(corner,wire_model: rc_tree_per_net,coupling: cross_net_spefplus the reason, and how each structure is treated);stats; unannotated signal nets; and diagnostics. Nets with no pin (pinless_signal_nets) and nets with exactly one pin (single_pin_signal_nets: an unused input port or an unloaded output pin; the router routes nets of two or more terminals only) have nothing to extract and are listed separately, never as unannotated. - Candidate shapes per wire run (
--max-extract-candidates, default 4,096) were a fixed cap; they are a budget now. Per run the extractor holds the candidate list, interval events, ordered active set and plane list, with vector growth and hash/tree occupancy included: 256 B per candidate (CANDIDATE_SCRATCH_BYTES), one run at a time. The ceiling is 48 MiB of that scratch, 196,608 candidates (MAX_CANDIDATES_CEILING). An interval sweep skips disjoint candidates while keeping shape-ID accumulation order. Index work and active-candidate comparisons debit--max-extract-work. The report statesmax_candidates_per_run,candidate_scratch_bytesand the observedstats.max_run_candidates.--max-rc-elements(resistors + nodes + 2 x couplings, explicitly bounded by the whole-process memory allowance) is a command-line budget too. The track recipe of the gate passes 65,536 candidates (16 MiB scratch with the interval sweep) and 8 M elements.
Cell pin and obstruction geometry is shared per master and decoded only for
local queries. The default virtual-shape budget is 256 million and the separate
instance-index budget is 64 million entries; they can be set with
--max-virtual-shapes and --max-instance-bin-entries. Crossing conductors use
one marking bit per virtual shape and generate their contribution strips as
needed. They do not create a flat design-sized rectangle/strip array.
Before RC-tree construction, the extractor checks the loaded process footprint,
with 25% headroom, plus graph, retained-shape, coupling, spatial-index and scratch
allowances against --extract-memory-mib. Runtime peak RSS is checked during
extraction. Larger object limits still fail when their combined estimate exceeds
the memory allowance. stats.memory reports the estimate and cap. The CLI reads
the workspace once, converting the segment length under the same snapshot lock.
Owned-conductor bins use a shared insertion-ordered entry array and fixed
headers. The estimate accounts for entry-vector reallocation and bounds header
count by each routing layer's actual conductor extent. Virtual cell geometry
keeps its separate index. Candidate order and per-bin work counts are preserved.
SPEF formatting retains the numeric RC network once and generates labeled nets one at a time. A compact coupling incidence table preserves record order; the name map borrows instance names instead of copying them for every pin. The existing byte and graph budgets still apply.
Repeated runs are byte-identical. The SPEF *DATE is a fixed string, and the
JSON report carries provenance.
Candidate queries use the lateral calibration window on the wire's own layer and its exact footprint on every other modelled layer. All accepted shape IDs and interval boundaries are retained, including shapes with no direct final capacitance contribution; removing such boundaries can change f64 rounding. Candidates are still processed in shape-ID order. The query change adds no per-run storage or worker concurrency: the run16 replay estimate is 3.46 GiB, including 16 MiB scratch (65,536 candidates at 256 bytes), one corner and one extraction worker. Builds and tests use two workers under a 4 GiB OS cap.
After metal construction and crossing discovery, an immutable BVH indexes only the retained shape IDs. The mutable conductor bins are released first; virtual cell geometry stays separate. Admission takes the maximum of the old bin and new BVH allowances, including a bounded construction/query stack. For four million retained shapes and 15 layers the BVH allowance is about 112 MiB, below the existing 192 MiB bin-entry allowance. Build/query probes consume the original work budget; candidate and electrical accumulation order stay fixed.
The final virtual-cell query index uses twice the smallest per-layer calibration
window when it fits. Construction keeps the original index order until all metal
tiling and crossing discovery are complete. Admission checks the exact proposed
index storage and a conservative bound covering every later query, including
the largest window and one-DBU margin. A tiny window, excessive query extent or
insufficient memory retains the coarse index. The report records the selected
stats.memory.instance_query_bin_dbu; the process memory cap stays unchanged.
RC profile (rustroad.rc-profile.v1)¶
scripts/build_rc_profile.py writes fixtures/pdk/{sky130,gf180}-rc-{min,nom,max}.json.
--check fails if a tracked profile differs from a rebuild;
--compare-ref e0d074d fails unless every field that existed at that revision
is unchanged, apart from the intentional changes it lists with their PDK
evidence (round 3: the four GF180 max cut resistances, see "Data
decisions"); diagnostics may only gain entries. It reads PDK data files and
records each one's path, sha256 and size in sources:
| role | Sky130 | GF180 |
|---|---|---|
| capacitance calibration table | libs.tech/librelane/rules.openrcx.sky130A.<c>.calibre |
libs.tech/librelane/rules.openrcx.gf180mcuD.<c> |
| nominal calibration table (corner ratio of the overlap data; min/max profiles) | ...sky130A.nom.calibre |
...gf180mcuD.nom |
| resistance, overlap data and cross-check | libs.tech/magic/sky130A.tech (extract section) |
libs.tech/magic/gf180mcuD.tech |
| layer order and widths | sky130_fd_sc_hd__nom.tlef |
gf180mcu_fd_sc_mcu7t5v0__nom.tlef |
Corners map as follows:
| profile | calibration table | Magic variant | used with Liberty |
|---|---|---|---|
min |
*.min |
(lrlc): low R, low C |
ff (hold) |
nom |
*.nom |
(): nominal |
tt |
max |
*.max |
(hrhc): high R, high C |
ss (setup and hold) |
Schema:
layers[]:name,rules_metal(the 1-based calibration metal index, checked against the LEF routing-layer order andLayerCount),width_um(checked against LEFWIDTH),sheet_resistance_ohm_per_square(from the Magicresistline of the corner variant),contexts[],area_cap_pf_per_um2andoverlap_area[]. Each context is{over, under, rows}:overis the calibration index of the nearest conductor below (0 = substrate), andunderis the index of the nearest conductor above or null. Each row is[edge_spacing_um, coupling_pf_per_um, ground_pf_per_um_per_side]. Eachoverlap_areaentry is{over, pf_per_um2, magic_nominal_af_per_um2, table_corner_ratio, table_corner_ratio_source}.cuts[]:name,lower,upper,resistance_ohm_per_cutandsource(the Magiccontactvalue). The cut-to-contact mapping is checked against the Magiccontact/typessections.- Also recorded:
corner,pdk,library,pdk_version,technology_lef_sha256,units(um/pF/ohm),magic_variant,lef_parasitics,model(text),cross_checks,diagnosticsandgenerator.
A profile without area_cap_pf_per_um2 (from before round 2) still loads;
the overlap-area term is then off and the report says so
(OVERLAP_AREA_UNAVAILABLE).
Data decisions:
- Table rows are per side. The isolated row read as "per side" (isolated
total = 2 x last-row ground) agrees with the Magic area + perimeter model
within -11 %..+28 % on all layers, corners and both PDKs. Read as "total",
the disagreement would be -36 %..-56 %. Coupling at minimum spacing agrees
with the Magic sidewall model within 8 % on every Sky130 layer at nominal.
GF180 differs more (-40 %..+54 %); that is a disagreement between two PDK
data sources and is recorded, not resolved (
cross_checks). - Resistance is the foundry sheet resistance. The fourth column of each
calibration row is not used (its ratio to the sheet resistance differs per
layer, so its unit cannot be established from data alone). Sky130 LEF
RPERSQagrees with the Magic nominal values and is recorded as a cross-check. - Non-plate overlap area comes from the Magic overlap rules. The
calibration tables only describe a wire over or under an infinite plate; they
have no data for metal that covers part of a wire face. For that case the
profile carries the PDK's own area capacitances: Magic nominal
defaultareacap(face to substrate) anddefaultoverlap(face over a lower routing layer). They are moved to each corner with the calibration table's own ratio, so every capacitance term follows the table's corners: the substrate row's isolated value (min/nom, max/nom) for the area capacitance, and the plate excess of the same layer pair (OVER-k minus OVER-0, isolated) for the overlap; where a distant plate's excess is not positive (Sky130 met4 over li1) the substrate ratio is used and named intable_corner_ratio_source. Magic's own corner variants are not used for this term: Sky130(hrhc)puts met2 over met1 at 313 aF/um^2 against 133.9 at nominal (2.3x), the table's plate excess moves 1.57x. - GF180's technology LEF states that its parasitic values are not correct. They
are never read (
lef_parasitics: placeholder_excluded). - The GF180 Magic
(lrlc)block lists every contact resistance as 0, which means no data. The min profile therefore uses the nominal 4.5 ohm and states this indiagnostics(VIA_R_CORNER_UNAVAILABLE). - The GF180 Magic
(hrhc)contact block is a placeholder too (round 3). It gives every one of its 20 contact names (diffusion, poly, via and MIM contacts) one value, 15 ohm, while the nominal block distinguishes them (4.5 to 8 ohm); the diffusion resistances of the same block are a blanket 15 ohm/sq as well. A block that gives every contact type one value although the nominal block distinguishes them is not corner data, so the max profile also keeps the nominal 4.5 ohm per cut (equal to the technology LEF) and says so (VIA_R_CORNER_UNAVAILABLE). Sky130's corner blocks distinguish their contacts (and agree in direction with the Sky130 SPICE corner models) and are kept. The previous choice (15 ohm) remains available as theround2_dataablation variant;rc_data_sources.pytabulates every variant's block. DIAGUNDERtables and theRESOVERtables are not used.- Which data source each quantity follows, and how far the PDK's sources
disagree, is tabulated in "Data sources" (
docs/evidence/rc-data-sources.json).
Physical model¶
Each routed net is built from its stored records: wires, fixed vias and pin-access points.
- Graph. Nodes are
(layer, x, y)in integer DBU: record ends, via landings and access points. A wire record is split at every node of its net and layer that lies inside it, and repeated metal (collinear overlapping records) counts once (duplicate_wire_edges). So lattice edges and compact segments of any length give the same graph. A node is a junction unless it is the interior point of a straight run on one layer: degree 2, collinear, no via and no pin. Each maximal straight run between junctions is one resistor, unless it is longer than--max-segment-um: then it is cut intoceil(L / limit)equal pieces at exact coordinates, and a cut point that is not a stored node becomes an internal node (segment_cut_nodes). The cut points depend only on the run, not on how its metal was stored (unit testlong_runs_split_at_the_same_coordinates_however_stored). Coincident pins get a 0-ohm link; the timing engine contracts zero-ohm edges. - Tree. A breadth-first walk starts at the single driver, an output pin or
an input port. Neighbours are visited in node-key order. A conductor that
closes a loop keeps its capacitance but not its resistance, and is counted
in
opened_loop_resistorswith anRC_LOOP_OPENEDdiagnostic; the timing engine rejects meshes. Disconnected nodes are an error. - Resistance. A run's resistance is
sheet * centerline_length / width. A via's resistance is the per-cut value divided by the number of cuts. The centreline already runs to the via centre, so pads and end extensions add no resistance. - Metal. The routed metal of a net is tiled without overlap: the body of each run (its centreline extent times the width), then pieces: the half-width end extensions, every via landing pad (the LEF via geometry on routing layers) and every routing patch, each minus the same-net metal already present on its layer, cut into disjoint rectangles by exact integer subtraction (at most 256 per rectangle). The union of bodies and pieces is exactly the routed metal. Pieces add capacitance only. Each piece has two side strips, one along x and one along y, so all four of its edges take the per-side model below.
- Capacitance. Every strip (a body or a side strip of a piece) is cut into intervals at the extents of nearby shapes: other bodies and pieces, instance pins, port pins, obstructions and supply metal on routing layers. For each interval and each of its two sides:
- Exposure. A side touching or inside same-net metal (an inner corner,
the net's own pin) carries no capacitance (
covered_side_intervals). - Lateral. The nearest same-layer shape within the table range gives
the row at that edge spacing (linear interpolation, clamped to the first
row): coupling
ccand groundcgper side. With no neighbour, the side takes the isolated last row. A same-net neighbour shields the side. - Plates. A conductor on another layer is the calibration plate below
(above) where the metal of its layer covers the strip and reaches at least
one strip width beyond both side edges: crossings and clearly wider
shapes. The nearest such layer below and above selects the context
(over, under); the conductor under each side edge owns that side's plate share. A missing exact context falls back to(over, -), then(0, under), then(0, -), and is counted. - Split. A plate below shields the substrate: the whole context value
cgof the side goes to it. A plate above takes the excess over what goes down (the substrate sharecg_sub, the OVER-0 row at the same spacing, or the OVER-only row when there is a plate below too). With no plate,cgis ground. - Crossings count from both conductors. The upper conductor's OVER cut
covers the lower one's width; the lower conductor's UNDER cut covers the
upper one's width and adds its excess as coupling to the upper one. This
holds whoever owns the lower or upper metal: a pin, an obstruction or
supply metal that a routed conductor on another layer overlaps gets two
strips like a piece (
passive_strips,passive_intervals) and counts its own cut toward the routed conductor that is its plate (to that net's ground, or as coupling for another net's pin). Its own ground and lateral sides are counted from the routed side, as before. The overlap area of a crossing is in both cuts, which overstates a crossing by aboutarea_cap * w_upper * w_lower(2.6 aF for Sky130 met2/met1, against 24-45 aF per crossing). This two-cut construction is a 2.5-D model: per the 3-D field solve ("Crossings in three dimensions") it is accurate for dense crossings, under-counts sparse crossings and over-counts crossings under a wire with close same-layer neighbours. That 3-D loading is not modeled. - Non-plate overlap area. Where the nearest metal of another layer covers
part of a strip's face without being a plate (stacked, offset or barely
wider metal, partial pad overlaps), the upper conductor's face couples to
it over the covered width at the profile's
overlap_areavalue (counted once, from the upper face; a passive conductor above a routed one is counted from the routed one), and the covered face loses itsarea_cap_pf_per_um2to the substrate (floored at zero per interval). Nearer layers shield farther ones across the face. The fringe of such overlaps is not modeled.overlap_area_um2andnonspanning_plane_intervalscount it. - Ownership of lateral pairs. Every routed-net shape models its own exposed sides, so a lateral pair is counted half from each side. Pins, obstructions and supply metal are counted fully from the routed side. A partner with no signal net (supply, obstruction, unconnected pin) is ground. A partner pin is attached at that pin's tree node, a piece at its node, and a body at its end nearest the interval. On the strip's own side, each value is split between its two end nodes by position (pi model).
- Routing patches.
Routes.patches(DEF routingRECT) are pieces of step 4 attached to the RC node of the same-net conductor they overlap or touch: largest overlap area first, then the lowest shape index. A floating patch, a patch on a net without an RC tree and a patch off a routing layer are explicit errors. Patches do not conduct in the native tree (see "Limitations"). - Coupling output. Cross-net capacitors are written as SPEF coupling caps,
listed in both nets'
*CAPsections. So each net's declared total equals its ground plus coupling. RustSTA grounds coupling at factor 1, so for timing this equals a grounded model.
JSON stats added in round 2: duplicate_wire_edges, segment_cut_nodes,
passive_strips, passive_intervals, overlap_area_um2,
overlap_pairs_without_data. scope adds passive_conductors; parallel_overlap
and cell_internal_metal describe the overlap-area term.
SPEF conventions¶
- Header:
*SPEF "IEEE 1481-1999", unitsNS,PF,OHM,HENRY, divider/, delimiter:, bus delimiter[],*DESIGN_FLOW "NAME_SCOPE LOCAL" "PIN_CAP NONE". *NAME_MAP: routed nets*1..*Nin connectivity net order. Then come the instances, in order of first use.- Nodes: internal junctions are
*<net>:<k>, with k = 1.. in tree order from the driver; instance pins are*<inst>:<PIN>; ports are bare bit names (a[0]). Characters other than[A-Za-z0-9_]and the bus brackets are backslash-escaped. *PORTSlists routed signal ports.*CONNhas*Pfor ports (I/O/B) and*Ifor pins with the LEF pin direction and*D <master>.*CAPlists ground caps for every node (zero allowed), then couplings. Each coupling names the net's own node first.*RESlists tree edges.- Values have 6 significant digits. The
*D_NETtotal is the sum of the rounded values listed, so RustSTA's total check (1e-4 relative) is exact.
Resources (Rule 9)¶
Estimate made before implementation, at the default caps with one corner and one thread (the extractor is serial and deterministic):
- 2 M shapes x 56 B = 112 MiB (bodies, pieces and passive shapes share this cap; a run adds at most two end pieces and a via at most one pad per routing layer before subtraction);
- passive strips: at most 2 per passive shape, bounded by 2 x the shape cap, 64 B each = 256 MiB worst case;
- 8 M bin entries x 4 B, plus the map, < 128 MiB;
- 500 k nodes (virtual cut nodes included) with keys and adjacency, about 64 MiB;
- 1 M couplings x about 64 B (BTreeMap) = 64 MiB;
- SPEF model strings < 64 MiB; the file is streamed through a 256 KiB buffer;
- the workspace read itself is under 16 MiB of
design.json, about 200 MiB parsed.
That is under 1 GiB peak, under the 2 GiB per-test-binary limit. There are no
paths, cuts or cones. Each net is one linear walk; the neighbour search is
bounded by the window, max_candidates and max_work; the passive-strip
search and the face coverage are charged to max_work.
The estimate stays under 4 GiB, so no scale lock is needed. Builds and tests
use CARGO_BUILD_JOBS=4, RUST_TEST_THREADS=4 and
systemd-run --user --scope -p MemoryMax=8G -p MemorySwapMax=2G.
Verification¶
- Unit tests (
rustroad-core::extract, values derived by hand from the test rows): - an analytic straight wire (10 ohm; ground 2 x 4e-5 x 10 um plus the two end extensions);
- a parallel pair (coupling counted once, per-side ground);
- the exact union perimeter of an L corner and of a via with pads;
- sides inside the net's own pin are not exposed;
- a crossing counted from both cross-sections;
- a crossed grounded rail counts its own cut (2 x 3e-5 x 0.2 um), and the same rail as a routed conductor gives the crossing wire the same total;
- a non-plate overlap couples over the covered face once (3e-4 pF/um^2 x 0.1 um x 10.1 um) and removes 1e-4 pF/um^2 of the face's substrate capacitance, leaving the lower wire unchanged;
- narrower parallel metal below is not a plate;
- compact segments extract exactly like lattice edges, and a run longer than the segment limit is cut at the same exact coordinates whether it is stored as lattice edges or as one record through a T junction plus an overlapping duplicate (resistors 2/2/3/3/3 ohm; two virtual cut nodes; two duplicate edges);
- patches, floating/unrouted/cut-layer patches, branch/via/pin trees, loop opening, budgets, profile-width mismatch.
- Integration tests (
crates/rustroad-workspace/tests/extraction.rs): byte-identical rerun; unchanged workspace tree; one D_NET and one tree per net; refusals that leave no output and no temporary file. - Python tests (
scripts/tests/test_rc_oracles.py): GDS records; Magic.extparsing; instance mapping; back-annotation; the target bounds; legacy profiles drop only the new fields; LEF OBS stripping; signal-pin shrinking (supply pins and OBS untouched); report rows carry the bound their verdict uses (fidelity 1 ps); blanket corner contact blocks are detected and the tracked GF180 max profile keeps the nominal via resistance; the field solver's self-test and stack parsing; design totals and the between/below/above classification (69 tests). - Campaign self-checks and controls (
scripts/rc_oracles.py): native byte-identical rerun; read-only workspace; refusals; independent resistance accounting from the route records (collinear records merged; nets whose record graph has a loop are left out and listed); Magic determinism; back-annotation fidelity (OpenRCX's per-net totals on the native trees must time within 1 ps of OpenRCX's own SPEF); OpenRCX design total unchanged at-coupling_threshold 0; OpenRCX per-net C unchanged without the cell LEF OBS; no net loses more than 0.1 fF of OpenRCX capacitance with every signal pin of the cell LEF shrunk to a 10 nm square; negative controls (x3 resistance, dropped D_NET, renamed net, dropped*I, planted open in the GDS). - Canonical structures (
scripts/rc_structures.py,docs/evidence/rc-structures.json): the native column is the extractor itself (crates/rustroad-core/examples/extract_structures.rscallsextract_cpu_refwith the tracked profile), not a formula. Round 3 adds a monotonicity check (adding conductors must not lower a wire's total) and an OpenRCX via probe. - Field solver (
scripts/field_solver.py selftest, also in the Python tests): parallel plates exact to 1e-9, symmetric capacitance matrix, monotone totals, the 2-D grid and the quarter-domain 3-D grid agree within 1 % on a long wire; grid and domain convergence of the crossing result is recorded in the evidence.
Oracles¶
OpenRCX (pinned openroad/orfs image f152917e) reads the DEF the flow
writes (maximal collinear runs, geometry-identical routing to the native GDS),
the PDK technology and cell LEF and the calibration table of the corner, with
-max_res 0 -no_merge_via_res and either the flow's -coupling_threshold 0.1
or -coupling_threshold 0. The campaign checks per design that its nominal
SPEF at 0.1 equals the flow's own OpenRCX SPEF net for net
(openrcx.flow_reference_nominal). It reads the same
calibration tables as the native profile; its resistance comes from the
tables' RESOVER rows and the LEF via resistances (see "Data sources").
Black-box behaviour (inputs varied, outputs compared; no source read). Each item is a campaign control or a canonical structure, re-run every campaign:
- Cell pin metal is not a partner of any net. The same DEF with every signal pin of the cell LEF shrunk to a 10 nm square lowers no net's C beyond the control threshold; it can raise C and change R, because OpenRCX merges a net's own wire metal over its own pin into the pin while the pin is full size. Native and Magic couple routed wires to neighbouring pin metal.
- Cell obstructions are not extracted. A cell LEF stripped of every OBS block gives identical per-net C.
- Via landing metal is not extracted. On a single wire, a via (or a two-via stack) adds exactly what splitting the wire path at that point adds (the extra end extension); the pads themselves add nothing. Native and Magic extract the pads.
- At
-coupling_threshold 0.1a small coupling is grounded in one net and dropped from the other. The design total (sum(ground) + sum(coupling)/2) is identical to the threshold-0 run at every corner while per-net totals drop. The native SPEF writes every coupling in both nets, so the like-for-like OpenRCX configuration for per-net C and timing is threshold 0 ("lossless"). Both are reported. - Capacitance monotonicity can be violated. In one canonical GF180 structure (a Metal3 wire with minimum-space neighbours on both sides, over Metal2 crossings at 2 um pitch) OpenRCX lowers the wire's total when the crossings are added. With every other conductor at 0 V a conductor's capacitance is an energy minimum over a set that adding conductors only shrinks, so it cannot decrease; native and Magic increase.
- A crossing is two grounded increments. Crossing couplings never appear in its SPEF; each wire gets its own ground increment, which does not grow with the lower conductor's width until that conductor is a wider plate.
- Stacked parallel wires are full plates. Magic and native use the PDK's overlap data for an aligned pair instead.
- OpenRCX can write two parallel resistors at a pin access; RustSTA rejects
meshes, so those resistors are opened (counted in
openrcx.<corner>.loops_opened_for_sta) before timing.
So OpenRCX is geometry-identical to the native extractor for the routed wires and the supply metal, but not for the cell pins, cell obstructions and via pads, which the native extractor and Magic both include.
Magic (pinned signoff image 4c48415d8bd0, Magic 8.3.678) is independent
in both data and algorithm for capacitance (its own area, perimeter, sidewall,
overlap and sideoverlap rules per corner variant) and sees the real cell
layouts. The harness (scripts/magic_rc.py):
- exports the native GDS, DEF, routes and connectivity of the workspace (read-only commands);
- capacitance: reads the GDS with the GDS via structures flattened on input
(
gds flatglob VIA_*), extracts the hierarchy once (instance transforms and a standalone extraction of every cell master) and then the flattened design. A net's routing capacitance is its flat node capacitance (substrate plus every coupling record) minus the standalone capacitance of its cell pin nodes, which Liberty already holds. Values are read from the raw.extrecords with exactly rounded sums; - resistance: extracts a routing-only terminal layout (cell instances removed,
one port cell at every routed pin access point) with
extract do resistanceand zeroextresistthresholds; terminals are matched by position; - maps every Magic node to a native net through the instance placement and the pin names, and refuses any open, short, merged or unmapped net;
- corner styles follow the profile's
magic_variant.
Magic has no SPEF writer, so timing against Magic back-annotates its per-net totals onto the native RC trees (every node capacitance and resistor of a net scaled by one factor; couplings grounded at their own node). The fidelity control does the same with OpenRCX's totals and compares with OpenRCX's own SPEF (bound 1 ps).
Oracle campaign¶
scripts/rc_oracles.py campaign extracts every routed workspace listed in
CASES.json natively at min/nom/max, runs OpenRCX at the flow and lossless
coupling thresholds and Magic at the profile's corner variants, times every
SPEF source in RustSTA (Magic by back-annotation) and writes the comparison:
python3 scripts/rc_oracles.py campaign --workspace RUN --base-rev empty --inputs CASES.json \
--rustroad target/release/rustroad --sta-bin STA_DIR \
--variant e0d074d=BASE_BIN:legacy --variant round1=ROUND1_BIN:legacy \
--variant round2_data=target/release/rustroad:profiles=PROFILES_AT_66411bd
python3 scripts/rc_oracles.py evidence --summary RUN/summary.json --out docs/evidence/extraction-oracles-<design>.json
The targets are fixed: total C +/-10 %, per-net C +/-20 % for nets >= 5 fF,
per-net R +/-20 % for nets >= 20 ohm and an endpoint slack delta <= 10 ps
(the larger of the setup and hold delta), each evaluated against each oracle.
The back-annotation fidelity control must stay within 1 ps; when it fails the
campaign status is failed, and Magic timing deltas are bounded only to the
fidelity value. --variant NAME=BIN[:legacy|:profiles=DIR] adds ablation
binaries or profile sets to the same campaign. A missed comparison is
attributed net by net (one net at a time set to the oracle's per-net totals on
the native trees), capped by --attribution-max-nets.
Neither oracle is geometry-identical to the native extractor: OpenRCX extracts the same routed wires and supply metal but none of the cell pins, cell obstructions and via pads (controls above), and Magic extracts the real cell layouts with its own data. A target missed against one oracle is therefore reported with the net attribution and the oracle-to-oracle difference, never closed by fitting a coefficient to either oracle.
Campaign receipts of earlier rounds (docs/evidence/extraction-oracles-*.json,
extraction-comparison.json, extraction-requalification.json) are
historical evidence kept for reproducibility, not current benchmarks.
Crossings in three dimensions¶
The calibration tables are two-dimensional; every extractor here builds a
crossing from 2-D cross-sections (native: both wires' cuts; OpenRCX and Magic:
their own 2.5-D rules). scripts/field_solver.py solves the 3-D Laplace
problem of an upper wire crossed by perpendicular lower wires at a given pitch,
optionally with same-layer neighbours of the upper wire, on the PDK's own
process stack (Sky130 KLayout cross-section, GF180 KLayout 2.5-D file) in a
homogeneous dielectric, and compares the upper wire's increment per crossing
with the native two-cut construction built from the same solver's 2-D values
(docs/evidence/rc-field-solver.json, which also records grid and domain
convergence). In a homogeneous dielectric every capacitance scales with the
permittivity, so the ratio does not depend on it; the real stack's layered
dielectrics would shift it, which is why the ratio is reported and not applied.
- For dense crossings (minimum pitch, no neighbours) the two-cut construction agrees with the 3-D solve.
- Sparse crossings load the upper wire more than the two-cut model says: a grounded line just below a wire intercepts field that would reach the substrate farther below, over a length set by that height, not by the line's width.
- Close same-layer neighbours absorb that field: with minimum-space neighbours the 3-D increment is below the two-cut value.
So the narrow-crossing loading is an unmodeled native structure, and the 2.5-D oracles bound it only loosely. A 3-D crossing term is not adopted: it needs the full 3-D environment (cell metal below, irregular pitches, finite wires) that the idealised factors do not contain, and an oracle that models it.
Resistance against Magic¶
Magic uses the same Magic sheet and contact values as the native profile except where the profile falls back to nominal values (GF180 min and max contacts), so for resistance it checks geometry and topology. Its network starts at the terminal square, so a terminal on a pin via adds the via's contact plus half a square of the pin layer; that element is taken out on both sides before the comparison. A routing patch that bridges two regions conducts in Magic but not in the native tree (patches add capacitance only). At GF180 min and max, Magic's contacts are its placeholder blocks while the native profile keeps the nominal value, so that comparison reflects the data choice.
Data sources¶
scripts/rc_data_sources.py (docs/evidence/rc-data-sources.json) tabulates
every interconnect value the PDK ships, per corner. Selected rows:
| PDK | quantity | corner | native | Magic | LEF | SPICE corner model | calibration table (reference) | other table variants |
|---|---|---|---|---|---|---|---|---|
| sky130 | met1/met2 sheet, ohm/sq | min | 0.105 | 0.105 | 0.125 | 0.111 | 0.131 (calibre) | 0.106 (magic), 0.105 (spef_extractor) |
| sky130 | met1/met2 sheet, ohm/sq | nom | 0.125 | 0.125 | 0.125 | 0.125 | 0.173 (calibre) | 0.126, 0.125 |
| sky130 | met1/met2 sheet, ohm/sq | max | 0.145 | 0.145 | 0.125 | 0.139 | 0.233 (calibre) | 0.147, 0.145 |
| sky130 | met3/met4 sheet, ohm/sq | nom | 0.047 | 0.047 | 0.047 | 0.047 | 0.051 | 0.047, 0.047 |
| sky130 | via (M1M2) ohm/cut | min / nom / max | 2.0 / 4.5 / 15 | 2.0 / 4.5 / 15 | 4.5 | 2.75 / 4.5 / 11.85 | - | - |
| sky130 | mcon ohm/cut | min / nom / max | 1.6 / 9.3 / 23 | 1.6 / 9.3 / 23 | 9.3 | 3.91 / 9.3 / 18.61 | - | - |
| gf180 | Metal1-4 sheet, ohm/sq | min / nom / max | 0.076 / 0.090 / 0.104 | 0.076 / 0.090 / 0.104 | 0.090 | 0.078 / 0.090 / 0.102 | 0.063 / 0.091 / 0.145 (plain) | 0.077 / 0.091 / 0.105 (magic) |
| gf180 | Via1-4 ohm/cut | min / nom / max | 4.5 / 4.5 / 4.5 | 0 / 4.5 / 15 (placeholder blocks) | 4.5 | - | - | - |
Magic contact blocks (magic_contact_blocks in the evidence): Sky130 gives 8
distinct values among 26 contact names in every variant; GF180 gives 4 at
nominal and one value for all 20 names in (hrhc)/(hrlc) (15 ohm) and
(lrhc)/(lrlc) (0).
The calibration table's resistance is its RESOVER row (ohm per unit length
at the table width); times the width it is the sheet value shown. The OpenRCX
reference uses that row, and the LEF via resistance at every corner (observed
in its SPEF: 9.3 / 4.5 / 3.41 ohm on Sky130, 4.5 on GF180).
Which source the native profile follows, and why. Resistance follows the
Magic extraction rules of the corner variant ((lrlc) / () / (hrhc)) where
they are corner data:
- at nominal they agree with the technology LEF, the SPICE interconnect model
and two of the three shipped calibration variants (
magic,spef_extractor). The Sky130calibrevariant, the one the OpenRCX flow reads, is the outlier: met1/met2 0.173 ohm/sq (+39 %) at nominal, 0.233 (+61 % on Magic, +68 % on SPICE) at max. No other PDK source supports it, so the native profile does not adopt it; - the Magic rules are a complete per-corner set, contacts included; the SPICE corner models are the other complete set and lie between Magic's extremes and the nominal value. GF180's SPICE sheet corners agree with Magic within 2 %;
- GF180's Magic corner contact blocks are placeholders (one value for every contact type; zeros at the low corners), and no other GF180 source has via corner data, so GF180 vias keep the nominal 4.5 ohm at every corner.
Capacitance follows the calibration table (calibre for Sky130, the plain
table for GF180), the same table OpenRCX reads. The isolated-wire value of
that table against Magic's area + perimeter rules (aF/um at default width):
| PDK | corner | table / Magic - 1 over the routing layers |
|---|---|---|
| sky130 | min | +2.2 .. +25.0 % |
| sky130 | nom | -3.7 .. +14.5 % (Magic nominal equals the LEF CPERSQDIST/EDGECAPACITANCE) |
| sky130 | max | -11.1 .. +4.8 % |
| gf180 | min | +3.0 .. +26.7 % |
| gf180 | nom | +5.4 .. +27.3 % |
| gf180 | max | +4.2 .. +27.6 % |
The overlap-area term (non-plate overlaps) uses the Magic nominal overlap and area capacitance, moved to the corner by the table. For a fully covered face it adds 25-45 % of the table's full-plate excess for the same pair (area only, no fringe), nominal values:
| PDK | upper over lower | Magic overlap aF/um^2 | Magic area cap aF/um^2 | covered-face increment aF/um (default width) | table full-plate excess aF/um | corner ratio min / max |
|---|---|---|---|---|---|---|
| sky130 | met1 over li1 | 114.2 | 25.78 | 12.4 | 25.4 | 0.73 / 1.36 |
| sky130 | met2 over met1 | 133.9 | 17.50 | 16.3 | 37.9 | 0.71 / 1.57 |
| sky130 | met3 over met1 | 34.5 | 12.37 | 6.7 | 16.1 | 0.90 / 1.23 |
| sky130 | met3 over met2 | 86.2 | 12.37 | 22.1 | 47.7 | 0.82 / 1.32 |
| gf180 | Metal2 over Metal1 | 59.0 | 15.02 | 12.3 | 56.6 | 0.80 / 1.28 |
| gf180 | Metal3 over Metal1 | 20.2 | 10.09 | 2.8 | 23.8 | 0.86 / 1.17 |
| gf180 | Metal3 over Metal2 | 59.0 | 10.09 | 13.7 | 61.3 | 0.81 / 1.26 |
Canonical structures¶
scripts/rc_structures.py (docs/evidence/rc-structures.json) puts the same
canonical geometry through both oracles and the native extractor itself, at
the nominal corner with 40 um wires at default width: isolated wires,
minimum-space pairs, single crossings, crossings of rails and planes, wires
along rails, wires over crossing arrays with and without minimum-space
neighbours, and stacked and offset wires. It also checks monotonicity (adding
conductors must not lower a wire's total) and probes how OpenRCX treats vias.
The native column is the extractor, not a formula.
Reproduction¶
free -h
export CARGO_BUILD_JOBS=4 RUST_TEST_THREADS=4
systemd-run --user --scope -p MemoryMax=8G -p MemorySwapMax=2G timeout 900s \
cargo build --release --bins --examples
python3 scripts/build_rc_profile.py --check
python3 scripts/build_rc_profile.py --compare-ref e0d074d # lists the intentional GF180 max cut change
python3 scripts/extraction_ablation.py --out .work/abl --build # ablation binaries
python3 scripts/verify_routing.py --workspace .work/runs/flowA --base-rev empty \
--rustroad target/release/rustroad --rusteda STA/rusteda --ruststa STA/ruststa --producer policy
systemd-run --user --scope -p MemoryMax=8G -p MemorySwapMax=2G /usr/bin/time -v \
timeout 3000s python3 scripts/rc_oracles.py campaign --workspace .work/runs/r3/pre \
--base-rev empty --inputs CASES.json --rustroad target/release/rustroad --sta-bin STA \
[--variant NAME=BIN[:legacy|:profiles=DIR] ...] [--attribution-max-nets 30]
python3 scripts/rc_oracles.py report --summary .work/runs/r3/pre/summary.json
python3 scripts/rc_oracles.py evidence --summary .work/runs/r3/pre/summary.json --out X.json [--compact]
python3 scripts/rc_structures.py --out .work/runs/structures \
--native-bin target/release/examples/extract_structures --evidence docs/evidence/rc-structures.json
systemd-run --user --scope -p MemoryMax=8G -p MemorySwapMax=2G \
python3 scripts/field_solver.py crossings --out docs/evidence/rc-field-solver.json --markdown
python3 scripts/magic_rc.py controls --pdk sky130 --out .work/runs/magic_controls/sky130
python3 scripts/rc_data_sources.py --out docs/evidence/rc-data-sources.json --markdown
python3 -m pytest -q scripts/tests
CASES.json lists, per PDK, the routed workspace, the post-route netlist and
SDC, and optionally the flow's OpenRCX nominal SPEF. For a schema-6 workspace
(scale stream) the campaign needs an extractor built from this branch merged
with feat/rtl2gds-scale.
Limitations¶
- Only default-width wires and fixed single-layer-cut vias are supported. The calibration tables have one width per layer.
- Crossings are 2.5-D (two-cut). The 3-D loading of sparse crossings and its reduction next to close neighbours are not modeled (field-solver evidence).
- Via cut capacitance is not modeled; the pads are.
- Non-plate overlaps carry their area capacitance only; their edge fringe
(Magic
sideoverlap) is left out. A routed upper conductor that has a calibration plate farther below does not count a nearer non-plate overlap. - Diagonal adjacent-layer coupling (
DIAGUNDER) is not modeled. - The overlap area of a crossing is in both cuts (about
area_cap * w_upper * w_lower, 2.6 aF for Sky130 met2/met1). - Piece and passive strips count area per strip, so their area term is approximate (under 5 % of a pad's or end's capacitance); the overlap-area term of a piece is counted on its x strip only.
- A lateral pair is split half-and-half; where one side's nearest shape differs from the other's, the pair is counted unevenly.
- Same-net metal is joined only through the stored record graph. Routing patches add capacitance but do not conduct.
- GF180 via resistance has no corner data in the PDK; nominal is used at min and max.
- Magic timing is a back-annotation of Magic's per-net totals onto the native trees; it is accurate only to the fidelity control, because an RC-driver model sees the distribution of R and C along a net, which one scale factor per net cannot reproduce.
- The field solver uses a homogeneous dielectric and idealised environments (infinite wires, periodic crossings); its factors are evidence, not model data.
- Electrical arithmetic runs serially in
cpu_ref; candidate queries can use bounded CPU workers. There is no GPU path.
Bounded CPU candidate queries¶
extract --extract-query-workers N opts into candidate-only concurrency (1
by default, ceiling 16). The effective worker count cannot exceed --threads,
--extract-query-batch-runs (default 8, ceiling 64), or the workers admitted
under both --extract-memory-mib and --extract-query-scratch-mib (default 32,
ceiling 64 MiB). If fewer than two workers fit, extraction uses the serial path.
The original memory admission and instance-query bin selection happen first.
The extra scratch is added to the reported working and total memory estimates.
Each worker has bounded candidate deduplication and one generation-mark vector
with one u64 per physical instance. All workers borrow the same immutable
retained-shape BVH, instance bins, templates and shape IDs. A tile holds at most
batch_runs * max_candidates IDs, with exact reserved capacities and checked
candidate caps; it never retains electrical contribution transcripts. Conservative
admission is workers * (64 * max_candidates + 8 * instances + 2 MiB) plus
8 * batch_runs * max_candidates + 4096 bytes. The stack term covers active
query workers; pool resources already exist before the original live-memory probe.
The original run iterator stays lazy. Query results are consumed in its original order, and each candidate list is sorted by the original shape ID. All routing layers, including shapes without a direct electrical contribution, remain in the query. Interval formation, passive strip effects, individual ground/coupling additions, final threshold handling and statistical floating-point additions all run in the original serial order. Integer work charges and maxima are unchanged.
A tile runs concurrently only when a conservative bound on every query,
interval-index operation and active-interval comparison fits the remaining
original max_work. A query visits each retained BVH node/ID at most once and
each instance/virtual shape at most once per layer. For n candidates, there are
at most 2*n events and 2*n+1 intervals with at most n active IDs. Overflow
or insufficient remaining work selects the original serial path, preserving its
budget rejection order. Results and errors are consumed in run order.
Opt-in receipts expose stats.parallel_queries with requested/admitted workers,
original memory estimate, admitted scratch, tile bound, and parallel/serial tile
counts. Default serial receipts keep their original shape.