fasterhenry

What was compared against what, with the measured residuals. This page is rendered from docs/validation.md at build time, so it matches the repository. The oracle comparison script is tools/fasthenry_compare.py, run by fasthenry-oracle.yml.

Validation

The measured agreement between fasterhenry and its independent references, for the M0 milestone. Regenerate everything with:

python3 -m venv /tmp/pypeec-venv && /tmp/pypeec-venv/bin/pip install pypeec==5.8.0
/tmp/pypeec-venv/bin/python tools/pypeec_reference.py --out tools/pypeec_reference.json
cargo test -p fasterhenry --test spiral_validation -- --nocapture

CI reruns the same steps on the Linux legs (the reference is regenerated there, never committed) and asserts the test did not take its skip path.

How CI asserts a gate actually ran

Every PyPEEC- or reference-backed gate in this document prints a positive marker on stdout once its comparison has completed and passed — SPIRAL GATE PASSED, SLOT GATE PASSED, CONTACT GATE PASSED, CROSS-SECTION GATE PASSED — and the CI step counts occurrences of that marker against an exact expected number. Two details make that necessary rather than decorative (#59):

Between them, the relative path and the invisible skip meant the spiral and slot gates took their "reference absent" path on every CI run from their introduction until #59, while the job stayed green.

Fixture

The 2-turn square spiral klayout-tools validates its MoM PEEC against (docs/design/mom-validation.md §5 in that repository), so numbers are comparable across the two projects: starting side 60 µm, 15 µm pitch growth per turn — segment centreline lengths 60, 60, 75, 75, 90, 90, 105, 105 µm — 2 × 2 µm copper cross-section (σ = 5.8e7 S/m), port closing the spiral from its end back to its start. The engine uses 4 × 4 filaments per segment; klayout-tools used 1 µm filament size (2 × 2).

Measured (2026-09-20, fasterhenry post-#18, pypeec 5.8.0)

quantityfasterhenryPyPEEC 5.8.0GreenhouseMohan m-Wklt MoM PEEC (reference)
L (1 kHz)0.637460 nH0.634505 nH0.638215 nH0.515 nH0.637718 nH
R (DC)2.844828 Ω2.816871 Ω———
comparisonstated tolerancemeasured
L vs PyPEEC2 %0.47 %
L vs Greenhouse5 %0.12 %
R vs PyPEEC (sanity band)25 %0.99 %
R vs analytic series sum0.5 %< 0.1 %

Mohan's modified-Wheeler estimate is reported but not asserted: at −19 % it sits at the edge of its ±20 % accuracy class for a spiral this small, which is the expected behaviour of a current-sheet expression here, not a defect of the engine.

Oracles

Ground planes (issue #22)

Plane validation is gated by the independent PyPEEC slot differential. The trace-over-plane fixture supplies convergence and coarse sanity checks; the earlier 5–10 % method-of-images accuracy claim is retired (issue #37).

The trace-over-plane fixture uses a 0.2 mm × 35 µm trace, 8 mm long, 0.5 mm above a 10 × 6 × 0.035 mm plane, with vias snapped to the mesh. Its DC resistance is bounded between 3 mΩ and the trace-alone resistance, and low-frequency loop inductance is checked for grid convergence (2.4 % measured from 10 × 3 to 40 × 12, bound 5 %) and LF/RF ordering. trace_over_plane_rf_image_estimate_is_a_coarse_sanity_check reports RF inductance against the rectangle-image + via estimate: 3.3051 nH versus 5.2397 nH at 1 GHz (36.92 % relative deviation), with the existing 45 % coarse sanity bound retained. The vias, snap offsets, finite plane, and single-filament plane bars are outside what that estimate bounds at 5–10 %; this diagnostic does not establish an image-oracle accuracy guarantee.

The separate slot-differential fixture is a 1.2 × 0.8 × 0.02 mm copper plane with a 0.2 × 0.64 mm slot, driven across its short ends; comparing L(slotted) − L(solid) cancels the differently modelled drive contacts on both sides, isolating the slot's physics.

quantityfasterhenry (96 × 64)PyPEEC 5 µm voxelsrel
ΔL (slot)0.173660 nH0.165443 nH4.97 % (bound 5.5 %)
ΔR (slot)2.280 mΩ2.222 mΩ2.6 % (bound 15 %)

Convergence, measured: fasterhenry's ΔL is 0.184 / 0.174 / 0.172 nH at 48 × 32 / 96 × 64 / 192 × 128 (cell-centre mesh, ~1/n); PyPEEC moves 0.2 % from 5 µm to 2.5 µm voxels. The ~4 % residual at full convergence is the two discretizations' method bias; the bound leaves headroom rather than encoding today's number. CI regenerates both PyPEEC references and runs the gate in release mode, asserting it printed its SLOT GATE PASSED marker exactly once (the positive assertion described above).

Margin is thin — read a failure here as sensitivity before assuming a bug (measured 2026-09-25, recorded against 26fba08, 2026-09-27). The gate only started actually executing in CI with issue #59 / PR #62; before that it silently took its "reference absent" skip path. The first real measurements leave less room than the bounds suggest:

checkmeasuredboundheadroom
ΔL vs PyPEEC4.97 %5.5 %0.5 pp (~10 %)
ΔL grid sensitivity (48 × 32 vs 96 × 64)5.91 %7 %1.1 pp (~16 %)
ΔR vs PyPEEC2.6 %15 %comfortable

Source: CI run 36162393794 (Rust (ubuntu-latest), plane-validation step), reproduced exactly on a developer workstation (fh dL 0.173660 nH to all printed digits). These figures are deterministic across machines, so the thin margin is not run-to-run noise — but it also means the two ΔL checks will fail the moment anything shifts the method bias by more than half a point: a PyPEEC version bump (pinned to 5.8.0 via PYPEEC_VERSION in .github/workflows/ci.yml), a change to the plane mesh or the port model, a different reference voxel size, or a change to the 96 × 64 / 48 × 32 grids the gate compares.

If the gate fails, check for that class of change first. Most of the 4.97 % is fasterhenry's own discretization error, not disagreement with PyPEEC — fasterhenry's ΔL is still converging (0.184 / 0.174 / 0.172 nH) while PyPEEC moves only 0.2 % from 5 µm to 2.5 µm voxels — so a finer fasterhenry grid moves the number down and a coarser one moves it up, independently of whether the physics changed. A failure accompanied by one of the changes above is expected sensitivity to be re-baselined deliberately (re-measure, then decide whether the bound or the mesh moves); a failure with none of them is a real regression. The bounds themselves (grid_relative < 0.07, relative < 0.055 in fasterhenry/tests/plane_validation.rs) are deliberately left as-is: loosening them to buy CI stability would weaken the independent-oracle accuracy claim the gate exists to defend.

The --fixture plane / --fixture plane-solid modes of tools/pypeec_reference.py regenerate the references (--voxel-um 5, ~10 s each locally).

The gate is the most expensive one in CI: the differential is two 96 × 64 solves plus a 48 × 32 pair for the grid-sensitivity check — 12 128 plane bars on the reference grid — and it is essentially single-threaded. On the first two runs that actually executed it (2026-09-25) the whole plane step — four PyPEEC reference generations plus the gate — took 2 min 33 s on run 36162393794 and 4 min 32 s on run 36163545213, with the ubuntu-latest leg at 9 min and 16 min respectively. That ~1.8x spread is hosted-runner variance, not the gate: every step of the slower run scaled alike, and both runs agree exactly on the physics (fh dL 0.173660 nH). The rust job's timeout-minutes is sized against the slower observation. For reference the same gate costs 4 min 16 s wall and 2.5 GB peak RSS on a loaded 8-core workstation.

Graded contact regions (issue #36, measured 2026-09-25)

A uniform cell-centre plane converges like 1/n (the ΔL series above), so resolving a via landing by refining everywhere is ruinous. A ContactRegion refines locally instead: inside the rectangle the mesh is uniform at the fine cell, and outside it each cell grows by a geometric ratio until it reaches the background cell.

The contact-dominated fixture is the same 1.2 × 0.8 × 0.02 mm sheet, driven between two 25 µm via landings inside it rather than across its short ends, so the port impedance is dominated by the current crowding under the contacts. It is meshed three ways: a 100 µm background graded to 25 µm under each landing (ratio = 2, 5 fine cells per axis per region), a fully uniform 25 µm plane, and the bare 100 µm background. Landings 0.825 mm apart:

meshplane barsL (1 kHz)R (DC)rel Lrel R
graded (100 → 25 µm)5800.274043 nH1.809 mΩ0.10 %0.80 %
uniform 100 µm1720.308044 nH1.520 mΩ12.52 %15.30 %
uniform 25 µm (reference)2 9920.273774 nH1.794 mΩ——

Grading reaches the fully-fine answer inside 1 % on both L and R at 5.2× fewer filaments (2 992 → 580), and is 125× closer than the background mesh it is built on. Bounds: 1 % on each, ≥ 4× saving.

The same fixture carries an independent PyPEEC separation differential: L(far) − L(near) for landings 0.825 mm and 0.225 mm apart, which cancels the pad terms the two solvers model differently (a cell-centre node here, a 25 µm voxel pad there).

quantityfasterhenry (graded, 580 bars)PyPEEC 5 µm voxelsrel
ΔL (separation)0.224802 nH0.224530 nH0.12 % (bound 3 %)
ΔR (separation)0.7180 mΩ0.7129 mΩ0.72 % (bound 15 %)

Grading moves that differential by 0.06 % against the fully uniform 25 µm mesh, so the comparison measures physics rather than the mesh. The --fixture contact / --fixture contact-near modes of tools/pypeec_reference.py regenerate the references (--voxel-um 5, ~3 min each locally).

Both contact gates are release-mode: the fully-fine reference is 2 992 filaments, ~12 s optimized and ~10 min unoptimized, so the first skips itself in a debug build. Each prints CONTACT GATE PASSED on success and CI asserts it counted two — the same positive assertion every gate here now carries (see "How CI asserts a gate actually ran" above).

Surface-graded filaments (issue #23, measured 2026-09-21)

A 400 mm × 40 mm × 200 µm copper trace (σ = 5.8e7 S/m) uses one filament across its width and 14 across its thickness at a 2:1 inward ratio. Against the one-dimensional slab solution Z_int = l k coth(kt/2)/(2 σ w), k = (1+j)/δ, its resistance errors at t/δ = 5, 10, 20 are +0.17 %, −1.03 %, −1.14 %. The analytic formula models internal impedance; the solver's imaginary port impedance includes the much larger external inductance and is compared through frequency-dependent differences. Its extracted internal inductance is within 14.3 % of the slab expression over t/δ = 2 … 20, including the high frequency L_int ∝ δ transition. The DC partial inductance agrees with the Rosa/Grover rectangular-bar closed form to better than 1e-4 relative.

On a separate 200 µm square trace, graded 4 × 4 at 10:1 differs from uniform 16 × 16 by 0.097 %, 0.002 %, <0.001 % in complex impedance at δ/t = 1, 3, 10, using 16 instead of 256 filaments. The Criterion skin_resistance_accuracy bench reports resistance error against a 128-layer uniform reference together with assembly-and-solve timing for 4, 8, 12 and 16 layers.

The slab is a one-dimensional reference. It does not certify grids graded across the width of a finite trace; those are validated against the independent two-dimensional reference in the next section. Near-singular kernel handling is tracked in issue #30.

Width-graded filaments (issue #32, measured 2026-09-22)

The slab above has no edges, so it cannot tell whether a grid graded across the width resolves the current crowding into a finite trace's edges. That needs a two-dimensional oracle. The infinite-width slab is not used as one here.

Reference: tools/cross_section_reference.py

The reference is a finite-width cross-section solve that shares no code with the crate. It needs only numpy.

To regenerate it (about 35 s of CPU):

python3 tools/cross_section_reference.py --out target/cross_section_reference.json
t/δfR' (Ω/m)±ΔL' (H/m)±
0.39.826 kHz1.752614e-2<0.001 %−1.184873e-90.033 %
1109.2 kHz2.316251e-20.004 %−1.707837e-80.010 %
3982.6 kHz4.212494e-20.073 %−2.490836e-80.022 %
1010.92 MHz1.421672e-10.490 %−2.923833e-80.045 %

(R'_DC = 1.7241e-2 Ω/m.)

Comparison: fasterhenry/tests/width_graded_skin_validation.rs

The 3-D solve produces the same per-unit-length quantity from a 1-segment trace by differencing two lengths: z' = (Z(200 mm) − Z(100 mm)) / 100 mm. The common l ln l and end-correction parts of the partial inductances cancel in that difference. Doubling the lengths moves R' by at most 0.008 % and ΔL' by at most 0.042 %; a reference-free test asserts that this stays below 0.1 %. L'_DC is read from the same grid at 1 Hz.

Signed errors against the reference, in %:

gridt/δ = 0.3: R' / ΔL'1310
uniform(36, 10)−0.01 / +0.49−0.46 / +0.39−8.70 / +2.14−28.24 / +5.66
graded(36, 10, 1.2)−0.01 / +1.00−0.01 / +0.12−0.56 / +0.18−4.00 / +0.35
graded(18, 5, 1.44)−0.02 / +3.74+0.00 / +0.39−2.24 / +0.59−17.98 / +1.42
graded(72, 20, 1.0954)−0.00 / +0.29−0.01 / +0.06−0.15 / +0.07−0.98 / +0.11
graded(36, 10, 1.5)−0.02 / +3.89+0.11 / +0.28−0.23 / +0.19−0.23 / +0.14
SkinDepthGrading(f, 0.5, 2) (7, 20, 52, 112 filaments)−0.11 / +13.39−0.10 / +1.34−2.31 / +0.88+0.27 / +0.30

The nw ≥ 4, ratio ≥ 1.2 case from the issue. At 36 × 10, width grading at 1.2 raises R' at t/δ = 10 by a factor of 1.34 over the uniform grid. That matches the 6.65 mΩ vs 5.00 mΩ (1.33) first reported. The reference shows the uniform grid is the inaccurate one: its 139 µm edge cells cannot resolve the edge crowding, so it reads R' 28 % low. The graded grid is 4 % low. Width grading therefore needs no fix.

Convergence-based accuracy bound (asserted). The refinement family graded(18, 5, 1.44) → (36, 10, 1.2) → (72, 20, √1.2) halves every cell while keeping the grading profile fixed. At every frequency, the test asserts three things for both quantities:

Supported regime (documented and asserted). Grading is not free:

Both limits are asserted as bounds, so a change in them is noticed.

CI. CI regenerates the reference on ubuntu-latest and runs the comparison in release mode. The step fails unless all four reference-backed tests print their CROSS-SECTION GATE PASSED line. It counts that positive marker instead of only grepping for SKIPPED. Without a reference, the ordinary workspace test prints SKIPPED and returns before any solve.

FASTERHENRY_CROSS_SECTION_REFERENCE=target/cross_section_reference.json \
  cargo test --release -p fasterhenry --test width_graded_skin_validation -- --nocapture

Filament currents (issue #294)

fasterhenry/tests/currents.rs checks the current output (--currents, MeshSystem::currents, IterativeSystem::currents) against: