Initial import: KiCad zone resistance plugin

DC/AC resistance, power dissipation, and via/injection-area currents of
copper zone fills. KiCad 10 IPC-API plugin (kicad-python/kipy):
multi-layer via-coupled FDM solver, multi-part terminals via User.1/User.2
marker layers, pads as contacts, uniform-injection and equipotential
contact models, per-foil skin effect, optional solder/copper buildup on
mask openings. 54-case test suite incl. exact analytic references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-07-14 17:22:00 +07:00
commit 06c62e04f8
33 changed files with 4000 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
# Fill Resistance — KiCad 10 plugin
Computes the **DC resistance of copper zone fills** between two contacts,
**single- or multi-layer**: the chosen net's fills 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). Shows
per-layer rasterized maps, potential, current density, and **power
density**, reports **per-via currents** (via ampacity!) and total
dissipation at a **selectable test current**. PNGs + a text summary are
saved per run.
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 be
`C:\Program Files\KiCad\10.0\bin\pythonw.exe` (after a 9→10 upgrade it
can point at KiCad 9).
3. **Deploy**:
```powershell
powershell -ExecutionPolicy Bypass -File deploy.ps1 # junction (dev)
powershell -ExecutionPolicy Bypass -File deploy.ps1 -Mode 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** (real copper shape; through-hole pad contacts all layers,
SMD pad its own layer) — selected pads 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; capped vs uncapped does not change the layer-to-layer DC path
(the ≥5 µm cap sits over the hole mouth in parallel with the
annular-ring contact, not in series). A barrel passing an antipad
bridges the layers above/below with the full barrel length.
- Tracks and pad copper (other than the selected contacts) are **not**
part of the conductor model — zone fills + barrels only.
- **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.
- **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.
- 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` 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.
- 5-point FDM per layer on an auto-sized shared grid (~500 k cells total
across layers by default). Direct sparse solve up to 700 k unknowns,
Jacobi-CG above. Discretization error typically ≲ 2 % at defaults —
halve the cell size and compare to judge convergence.
## Offline / development
Every run writes `geometry_dump.json`; re-solve without KiCad:
```powershell
.venv\Scripts\python.exe -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:
```powershell
uv venv --python 3.11 .venv
uv pip install --python .venv\Scripts\python.exe kicad-python numpy scipy matplotlib pytest
.venv\Scripts\python.exe -m pytest tests -q # incl. exact analytic cases
.venv\Scripts\python.exe smoke\smoke_probe.py # IPC API probe vs live KiCad
.venv\Scripts\python.exe -m fill_resistance.board_io dump.json [NET] # extract only
```
## 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.