- pyproject.toml + uv.lock: uv-managed dev environment (uv sync / uv run pytest). requirements.txt stays: KiCad builds the plugin's runtime venv from it and the PCM zip packages it. - README: em-dash and run-on cleanup (34 -> 22, the rest deliberate), split the 120-word THT solder-joint sentence, fix the documented ADAPTIVE_MAX_CELL_UM value (2 mm -> 1 mm, config has 1000 um), fix the *Packaging / publishing* cross-reference, dev sections now use uv sync / uv run. - LLM disclaimer: "most commits" carry the trailer (32 of 39), figures claim now excepts the hand-drawn hole cross-section, reference the UT3513+ measured-vs-computed validation. - .gitignore: local AI-tooling artifacts; deploy scripts exclude pyproject.toml/uv.lock; error-figure title punctuation aligned with the other window titles.
Fill Resistance — KiCad 10 plugin
Computes the DC or AC resistance of copper zone fills and traces between two contacts, single- or multi-layer: the chosen net's fills (teardrops included) and tracks on the selected copper layers are solved as coupled finite-difference sheets linked by the net's via and through-hole-pad barrels (18 µm plating, configurable). At a user-set frequency the exact 1D foil/barrel skin-effect correction is applied (AC results are a rigorous lower bound; see Model & limits). Shows per-layer rasterized maps, potential, current density, and power density, and reports per-via currents (via ampacity!) and total dissipation at a selectable test current. PNGs + a text summary are saved per run.
Real output on a synthetic two-layer net: current from a soldered
THT-pad contact (V+, injected at the drill-wall ring) squeezes past a
notch in the F.Cu pour, transfers through the stitching-via field into
the B.Cu pour and leaves at the V− lug. Per-via currents and the
hottest via are reported.
The matching potential map with equipotential contour lines: they
bunch where the field is strongest — nearly the whole 8.7 mV drop
happens around the notch on F.Cu.
Uses the KiCad IPC API (kicad-python / kipy), not the deprecated
SWIG API. Requires KiCad 10.0.1+.
Setup (one-time)
- Enable the API server: KiCad → Preferences → Plugins → check Enable KiCad API.
- Check the interpreter path on the same page: should point at the
KiCad 10 Python, e.g.
C:\Program Files\KiCad\10.0\bin\pythonw.exeon Windows or/usr/bin/python3on Linux (after a 9→10 upgrade it can point at KiCad 9). - Deploy (dev checkout; end users install the PCM zip instead, see
Packaging / publishing):
Linux / macOS (also works on Windows with developer mode):
powershell -ExecutionPolicy Bypass -File deploy.ps1 # junction (dev) powershell -ExecutionPolicy Bypass -File deploy.ps1 -Mode Copypython3 tools/deploy.py # symlink (dev) python3 tools/deploy.py --copy - Restart KiCad; first load builds the plugin venv (numpy, scipy, matplotlib, PySide6 — takes minutes; the Ω button appears when done). If stuck: Preferences → Plugins → Recreate Plugin Environment.
Usage
- Mark the current-injection terminals. Each terminal may have
multiple parts (all merged into one externally bonded contact):
- V+ rectangles on
User.1, V− rectangles onUser.2(marker layers, configurable viaELECTRODE_POS_LAYER/ELECTRODE_NEG_LAYER), any number per side, axis-aligned; - pads and vias (SMD pad: real copper shape on its own layer; through-hole pads and vias become barrel contacts: the current enters at the drill wall on every spanned layer, see below); selected pads/vias fill a side that has no rectangles;
- legacy: exactly 2 selected contacts with no marker rectangles still works; empty selection scans the whole board's marker layers.
- V+ rectangles on
- Select the contacts, click the Fill Resistance Ω button.
- In the dialog, pick the net (defaults to the selected pad's net), check the layers to include, set each contact's layer scope ("All selected layers" = bolted-lug/through contact), the test current, and optionally a grid cell size. Multiple layers are coupled through the net's via/pad barrels automatically.
- Read R / voltage drop / total power in the figure titles and status
bar. Outputs land in
<board dir>\fill_res_results\<timestamp>\: per-layer1_raster_map/2_potential/3_current_density/4_power_densityPNGs,summary.txt(incl. the busiest vias with per-via current and dissipation, and the current through each injection area — computed flux with the equipotential model, prescribed area share with the uniform model),geometry_dump.json.
Model & limits
-
Sheet model per layer: R□ = ρ/t, ρ = 1.68e-8 Ωm (20 °C), t from the board's physical stackup. Layer z-positions from the stackup drive the barrel lengths.
-
Via/pad barrels: thin-wall annulus, R = ρ·L/(π·d·t_plating),
VIA_PLATING_UM = 18infill_resistance/config.py. Vias are always plated. Each via also contributes its ring/pad copper (a full-thickness disc of the pad diameter on every spanned layer) and its drill mouth, area-weighted per cell: with the "vias filled + capped" checkbox (default on,VIAS_CAPPED) the mouth carries a thin copper cap (CAP_PLATING_UM = 15, fab spec) on the outer layers and is an open hole on inner layers; unchecked, mouths are open holes everywhere. The fab caps only small vias: drills above the dialog's "capped up to drill" threshold (defaultCAP_MAX_DRILL_MM = 0.5) keep open mouths even with capping selected. Layer-to-layer the cap never matters at DC (it is in parallel with the annular-ring contact, not in series), so the checkbox only affects in-plane conduction across outer-layer mouths. Sub-cell mouths scale their cells' sheet conductance by the true covered fraction (4×4 supersampling), so coarse grids see the correct small perturbation instead of a whole-cell hole. Barrels are gathered in single-layer runs too (drill mouths perforate a lone plane). THT pads are fully modeled: their exact copper shapes (incl. oblong pads, fetched from KiCad; the outer shape stands in for inner rings) are stamped onto every included layer, and every populated pad carries its full soldered joint on its SOLDER side (opposite the component; the component-side pad face stays bare). The hole holds the component lead (a cylinder of drill −THT_LEAD_CLEARANCE_MM, resistivityTHT_LEAD_RHO_OHM_M, copper by default; raise it for brass/steel leads) plus solder in the remaining annulus, both in parallel with the plating. The mouth copper stays conducting: it stands in for the solder plug, which is worth far more than the foil, so this is conservative. The pad face gets the average-thickness solder coat (exact pad shape) and the protruding-lead cone (see barrel contacts below; on oblong pads the cone tapers within the inscribed circle). Whether a hole is a via or a THT pad, the owning footprint's side, and its Do not populate flag are all read from KiCad. DNP pads get an open hole and a plating-only barrel, no joint. At f > 0 the thickness scaling is applied multiplicatively to the skin-corrected sheet conductance (approximation). Per layer a barrel attaches to the fill cell under it, or to the nearest copper cell within the pad footprint plus one grid cell — fills joined by thermal-relief spokes still connect; wider antipads do not, and the barrel bridges the layers above/below with the full barrel length. Barrels that reach fill on fewer than two layers carry no current and are reported.
The four hole types: capped small via, open large via, populated THT
pad with its full solder joint (lead ∥ solder ∥ plating in the hole,
one-sided pad coat, protruding-lead solder cone), and a DNP THT pad. -
The net's traces (straight and arc tracks, exact outline polygons incl. rounded ends) conduct together with the fills — dialog checkbox, on by default (
INCLUDE_TRACKS). Traces narrower thanTRACK_1D_FACTOR(3) grid cells are modeled as exact 1D resistor chains along their centerline — true arc length per link, so their series resistance carries no discretization error and no cell-size tuning is needed for thin traces. 1D-modeled traces show potential, power density, and |J| (the true in-trace density from the link currents, |ΔV|/(ρ·Δl)). THT pad copper is part of the conductor (exact shapes, see above); SMD pad copper other than the selected contacts is still not. -
Solder buildup on mask openings (dialog checkbox, off by default;
INCLUDE_MASK_BUILDUP): zones drawn onF.Mask/B.Maskare treated as mask openings that collectSOLDER_THICKNESS_UM(50 µm) of solder on the exposed pour, plus an optional user-defined added copper thickness (dialog field, e.g. a soldered busbar/wire). The sheet conductance there becomes t_Cu/ρ_Cu + t_solder/ρ_solder + t_extra/ρ_Cu (SAC305 ρ = 1.32e-7 Ωm: 50 µm solder ≈ 6.4 µm copper); interface faces use harmonic-mean conductances. Buildup areas render tin-gray on the raster map; |J| in them is referenced to the conductance-equivalent copper thickness. -
Barrel contacts: a selected via or through-hole pad injects at the drill-wall ring on every layer the barrel spans — the current physically enters through the lead/wire soldered into the hole, so the spreading resistance across the pad and surrounding pour is part of the result (both contact models; verified against R = ρ/(π·t)·acosh(d/2a) for two circular contacts on a sheet). A soldered THT joint additionally assumes the hole is filled with solder (core in parallel with the plating) and the pad face on the solder side carries an average-thickness solder coat (
SOLDER_THICKNESS_UM, 50 µm) over the modeled copper under the pad shape — the solder side is the side opposite the component (taken from the owning footprint; assumedB.Cuif it cannot be found), and the component-side pad face stays bare. There the clipped lead protrudesTHT_LEAD_PROTRUSION_MM(1.5 mm, 0 = off) and a solder cone wraps it: full protrusion height at the drill wall, tapering linearly to zero at the pad edge, applied as extra conduction-equivalent copper per cell. The tall solder column at the wall pulls the joint vicinity to lead potential — equivalent to extending the barrel wall vertically — while the taper carries the radial spreading. To model a probe pressed onto the pad face instead, draw a marker rectangle over the pad. -
Contact models (dialog /
CONTACT_MODEL): default uniform injection — a conductor pressed on top feeds the current orthogonally with uniform surface density, so |J| ramps across the contact area (R = ΔV̄/I from area-averaged terminal potentials); or equipotential — ideal bonded lug (Dirichlet). The two bracket a real contact: R_equipotential ≤ R_real ≤ R_uniform. If the selected fills form several disconnected copper groups that each touch both terminals (e.g. planes joined only through the bolted lugs), only the equipotential model is well-defined; the uniform model stops with an error instead of prescribing an arbitrary split.
|J| around the same 3×3 mm contact under both models: the ideal
bonded lug crowds the current at the contact edges (no in-sheet
current inside an equipotential region), the pressed conductor ramps
it across the contact area. -
Fields are reported at the dialog's test current; power scales with I².
-
Skin effect (f > 0): per-layer effective sheet resistance from the exact 1D foil-diffusion solution
Zs = τρ·coth(τt),τ = (1+j)/δ(SKIN_SIDES = 1in config: plane facing a return plane;2= isolated foil), and the analogous correction for the 18 µm barrel wall. Enter one frequency per run (e.g. a switching harmonic, with its RMS amplitude as the test current); suffixesk/Mare accepted. Caveat: only through-thickness crowding is modeled. Lateral (proximity-effect) redistribution needs a magneto-quasistatic solver and is not captured — since the resistance-driven distribution is the minimum-dissipation one, AC results are a rigorous lower bound. Rule of thumb for 70 µm foil: skin is negligible below ~300 kHz (δ = 173 µm at 142 kHz), ~+11 % at 1 MHz. At f > 0 the |J| maps are referenced to the skin-reduced conduction-equivalent thickness t/(R_AC/R_DC) — the density in the copper that actually conducts — not the geometric foil thickness. -
5-point FDM per layer on an auto-sized shared grid (~2 M fine cells with the uniform grid; ~8 M with the adaptive grid, whose unknown count no longer scales with the fine-cell count). Direct sparse solve up to 500 k unknowns, AMG-preconditioned CG (pyamg) above (Jacobi-CG if pyamg is missing). Discretization error typically ≲ 2 % at defaults; halve the cell size and compare to judge convergence.
-
Adaptive cells (dialog checkbox, on by default;
ADAPTIVE_CELLS): solves on a 2:1-balanced quadtree — fine cells at copper boundaries, electrodes, traces, via mouths and buildup, blocks up toADAPTIVE_MAX_CELL_UM(1 mm) in plane interiors (ADAPTIVE_GUARDsets the clearance a block needs to grow). The minimum element size is the grid cell size itself (auto / dialog /CELL_UM_OVERRIDE); the uniform limit reproduces the normal grid exactly. Large speed/memory wins on big pours. The raw coarse–fine interface flux bias (~0.5–2 % low) is removed by a deferred-correction pass (ADAPTIVE_CORRECTION_PASSES, default 1: reconstruct leaf gradients, move the tangential term to the RHS, re-solve on the reused factorization/AMG hierarchy) — measured residual deviation from the uniform grid ≲ 0.03 %, with the power-balance identity intact. All fields are expanded back to the fine grid for the maps and reports.
The raster map of the demo net: quadtree leaves drawn on the copper
(fine at boundaries, electrodes, via mouths and pads; coarse blocks
in plane interiors), the tin-gray solder coat of the THT-pad contact
P1, and the via field with its pad copper.
Measured vs. computed: we tested the plugin on a few real boards against a UT3513+ micro-ohm meter; the measured resistances were within ±20 % of the computed values. We attribute the deviation to imperfections of the testing setup (probe placement and probe contact resistance vs. the ideal modeled contacts) and to manufacturing inaccuracies — actual copper and plating thicknesses routinely deviate from nominal. Relative comparisons between layout variants are accordingly more trustworthy than absolute numbers.
Offline / development
Every run writes geometry_dump.json; re-solve without KiCad:
uv run python -m fill_resistance.standalone dump.json `
[--current 40] [--cell-um 50] [--layers F.Cu,In1.Cu] [--no-show] `
[--out DIR] [--force-iterative]
Dev environment, tests, headless extraction — uv
manages the venv from pyproject.toml/uv.lock (requirements.txt
stays: KiCad builds the plugin's runtime venv from it):
uv sync # one-time env setup
uv run pytest -q # incl. exact analytic cases
uv run python tools/api_probe.py # IPC API probe vs live KiCad
uv run python -m fill_resistance.board_io dump.json [NET] # extract only
Packaging / publishing
python tools/build_package.py builds the PCM addon zip in dist/
(installable right away via Plugin and Content Manager → Install from
File) plus dist/metadata-registry.json with the SHA-256 and sizes
filled in. To publish: upload the zip to a release, set download_url
(and the homepage resource in metadata.json), then submit the
registry copy as packages/th.co.b4l.fill-resistance/metadata.json in a
merge request to https://gitlab.com/kicad/addons/metadata. Icons are
regenerated with python tools/gen_icons.py; the README figures in
docs/img/ with uv run python tools/gen_readme_figs.py
(real solver output on small synthetic boards, plus the hand-drawn
hole cross-section).
License
GPL-3.0-or-later — see LICENSE.
Troubleshooting
- No toolbar button: venv still building (wait), or build failed → Recreate Plugin Environment; check the interpreter path (setup 2).
- "Could not connect to KiCad's IPC API": API server not enabled, or KiCad not running (no headless mode in KiCad 10).
- "KiCad is busy": a modal dialog is open in KiCad — close it, rerun.
- Windows don't appear: they may open behind KiCad (raised best-effort); PNGs are always saved regardless.
- Result seems too low/high: remember the model is fills + barrels only, with ideal contacts; measure electrode-to-electrode.
LLM disclaimer
This plugin was developed with an LLM: Anthropic's Claude (Claude
Code, model Claude Fable 5). The physics model, solver, tests, tooling
and this documentation (including the figures; all but the hand-drawn
hole cross-section are generated by the solver itself) were written by
the model, feature by feature, under human direction and review
(janik / B4L); most commits carry a Co-Authored-By: Claude trailer.
What keeps this honest: the test suite pins the numerics to exact
analytic references (strip and annulus resistances, the acosh spreading
resistance of two circular contacts, skin-effect limits, power-balance
identities) and to convergence/regression checks; run it with
uv run pytest. Real boards were measured against a UT3513+ micro-ohm
meter (see Measured vs. computed above). Nevertheless, an LLM wrote
this: read Model & limits
critically, treat surprising numbers with the usual engineering
suspicion, and cross-check against a hand estimate before trusting a
result with hardware. Bug reports are very welcome.