d9ca118e1b
Build PCM package / build (push) Successful in 8s
- 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.
314 lines
18 KiB
Markdown
314 lines
18 KiB
Markdown
# 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)
|
||
|
||
1. **Enable the API server**: KiCad → Preferences → Plugins → check
|
||
*Enable KiCad API*.
|
||
2. **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.exe`
|
||
on Windows or `/usr/bin/python3` on Linux (after a 9→10 upgrade it
|
||
can point at KiCad 9).
|
||
3. **Deploy** (dev checkout; end users install the PCM zip instead, see
|
||
*Packaging / publishing*):
|
||
```powershell
|
||
powershell -ExecutionPolicy Bypass -File deploy.ps1 # junction (dev)
|
||
powershell -ExecutionPolicy Bypass -File deploy.ps1 -Mode Copy
|
||
```
|
||
Linux / macOS (also works on Windows with developer mode):
|
||
```bash
|
||
python3 tools/deploy.py # symlink (dev)
|
||
python3 tools/deploy.py --copy
|
||
```
|
||
4. **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
|
||
|
||
1. 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 on `User.2`**
|
||
(marker layers, configurable via `ELECTRODE_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.
|
||
2. **Select the contacts**, click the **Fill Resistance** Ω button.
|
||
3. 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.
|
||
4. Read R / voltage drop / total power in the figure titles and status
|
||
bar. Outputs land in `<board dir>\fill_res_results\<timestamp>\`:
|
||
per-layer `1_raster_map` / `2_potential` / `3_current_density` /
|
||
`4_power_density` PNGs, `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 = 18` in `fill_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 (default
|
||
`CAP_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`, resistivity `THT_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 than
|
||
`TRACK_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 on `F.Mask`/`B.Mask`
|
||
are treated as mask openings that collect `SOLDER_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; assumed `B.Cu` if it cannot be found),
|
||
and the component-side pad face stays bare. There the **clipped
|
||
lead protrudes** `THT_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 = 1` in 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); suffixes `k`/`M` are 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 to
|
||
`ADAPTIVE_MAX_CELL_UM` (1 mm) in plane interiors (`ADAPTIVE_GUARD`
|
||
sets 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:
|
||
|
||
```powershell
|
||
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](https://docs.astral.sh/uv/)
|
||
manages the venv from `pyproject.toml`/`uv.lock` (`requirements.txt`
|
||
stays: KiCad builds the plugin's runtime venv from it):
|
||
|
||
```powershell
|
||
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](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.
|