uv dev environment, README writing pass, publish hygiene
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.
This commit is contained in:
2026-07-17 13:24:08 +07:00
parent f59ada94e0
commit d9ca118e1b
7 changed files with 1339 additions and 40 deletions
+8
View File
@@ -3,3 +3,11 @@ __pycache__/
*.pyc
.pytest_cache/
dist/
# local AI-tooling artifacts, never publish
.claude/
.claude-flow/
.swarm/
.mcp.json
CLAUDE.md
ruvector.db
+39 -37
View File
@@ -6,8 +6,8 @@ between two contacts, **single- or multi-layer**: the chosen net's fills
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**, reports **per-via
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.
@@ -15,7 +15,7 @@ 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
the B.Cu pour and leaves at the V lug. Per-via currents and the
hottest via are reported.*
![Potential on the two-layer demo net](docs/img/demo-potential.png)
@@ -35,7 +35,7 @@ SWIG API. Requires KiCad **10.0.1+**.
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*):
*Packaging / publishing*):
```powershell
powershell -ExecutionPolicy Bypass -File deploy.ps1 # junction (dev)
powershell -ExecutionPolicy Bypass -File deploy.ps1 -Mode Copy
@@ -52,13 +52,13 @@ SWIG API. Requires KiCad **10.0.1+**.
## Usage
1. Mark the current-injection terminals. Each terminal may have
**multiple parts** (all merged into one externally-bonded contact):
**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)
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.
@@ -93,7 +93,7 @@ SWIG API. Requires KiCad **10.0.1+**.
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) the checkbox
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
@@ -103,18 +103,18 @@ SWIG API. Requires KiCad **10.0.1+**.
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
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 plug — conservative,
the plug is worth far more than the foil); the pad face gets the
average-thickness solder coat (exact pad shape) and the
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
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
@@ -194,7 +194,7 @@ SWIG API. Requires KiCad **10.0.1+**.
(`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.
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
@@ -206,15 +206,15 @@ SWIG API. Requires KiCad **10.0.1+**.
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 them). 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.
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` (2 mm) in plane interiors (`ADAPTIVE_GUARD`
`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
@@ -246,20 +246,20 @@ accordingly more trustworthy than absolute numbers.
Every run writes `geometry_dump.json`; re-solve without KiCad:
```powershell
.venv\Scripts\python.exe -m fill_resistance.standalone dump.json `
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 (Windows shown; on
Linux/macOS use `.venv/bin/python`):
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 venv --python 3.11 .venv
uv pip install --python .venv\Scripts\python.exe kicad-python numpy scipy pyamg matplotlib pytest
.venv\Scripts\python.exe -m pytest tests -q # incl. exact analytic cases
.venv\Scripts\python.exe tools\api_probe.py # IPC API probe vs live KiCad
.venv\Scripts\python.exe -m fill_resistance.board_io dump.json [NET] # extract only
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
@@ -272,7 +272,7 @@ filled in. To publish: upload the zip to a release, set `download_url`
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 `.venv\Scripts\python.exe tools\gen_readme_figs.py`
`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).
@@ -294,18 +294,20 @@ GPL-3.0-or-later — see [LICENSE](LICENSE).
## LLM disclaimer
This plugin was developed with an LLM Anthropic's **Claude** (Claude
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, which are generated by
the solver itself) were written by the model, feature by feature, under
human direction and review (janik / B4L); commits carry a
`Co-Authored-By: Claude` trailer.
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
`pytest tests`. Nevertheless, an LLM wrote this: read *Model & limits*
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.
+2 -1
View File
@@ -30,7 +30,8 @@ if ($Mode -eq 'Junction') {
New-Item -ItemType Junction -Path $dst -Target $src | Out-Null
Write-Host "junction created: $dst -> $src"
} else {
$exclude = @('.venv', '.git', 'tests', 'tools', '__pycache__', '.pytest_cache')
$exclude = @('.venv', '.git', 'tests', 'tools', '__pycache__', '.pytest_cache',
'pyproject.toml', 'uv.lock')
New-Item -ItemType Directory -Force $dst | Out-Null
Get-ChildItem $src -Force | Where-Object { $exclude -notcontains $_.Name } |
ForEach-Object { Copy-Item $_.FullName -Destination $dst -Recurse -Force }
+1 -1
View File
@@ -416,7 +416,7 @@ def fig_power(result, stack, e1, e2, problem):
def fig_error(message: str):
fig, ax = plt.subplots(figsize=(9, 4.5), layout="constrained")
ax.axis("off")
ax.set_title("Fill Resistance ERROR", color="#b02a2a",
ax.set_title("Fill Resistance - ERROR", color="#b02a2a",
fontsize=14, fontweight="bold", loc="left")
wrapped = "\n".join(
textwrap.fill(line, width=90) for line in message.splitlines()
+28
View File
@@ -0,0 +1,28 @@
# Development environment only (uv sync / uv run). The KiCad plugin
# manager builds the runtime venv itself from requirements.txt — keep
# the dependency list there in sync with [project.dependencies].
[project]
name = "fill-resistance"
version = "1.1.0"
description = "DC/AC resistance of copper zone fills and traces between two contacts (KiCad 10 plugin)"
license = "GPL-3.0-or-later"
requires-python = ">=3.11"
dependencies = [
"kicad-python>=0.7.0",
"numpy",
"scipy",
"pyamg",
"matplotlib",
"PySide6",
]
[dependency-groups]
dev = [
"pytest",
]
[tool.uv]
package = false
[tool.pytest.ini_options]
testpaths = ["tests"]
+1 -1
View File
@@ -18,7 +18,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
COPY_EXCLUDE = {".venv", ".git", "tests", "tools", "dist", "resources",
"__pycache__", ".pytest_cache", "conftest.py", "deploy.ps1",
".gitignore", "metadata.json"}
".gitignore", "metadata.json", "pyproject.toml", "uv.lock"}
def plugins_dir(kicad_version: str) -> Path:
Generated
+1260
View File
File diff suppressed because it is too large Load Diff