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 *.pyc
.pytest_cache/ .pytest_cache/
dist/ 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 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 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 skin-effect correction is applied (AC results are a rigorous lower
bound see *Model & limits*). Shows per-layer rasterized maps, bound; see *Model & limits*). Shows per-layer rasterized maps,
potential, current density, and **power density**, reports **per-via potential, current density, and **power density**, and reports **per-via
currents** (via ampacity!) and total dissipation at a **selectable test currents** (via ampacity!) and total dissipation at a **selectable test
current**. PNGs + a text summary are saved per run. 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 *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 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 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.* hottest via are reported.*
![Potential on the two-layer demo net](docs/img/demo-potential.png) ![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 on Windows or `/usr/bin/python3` on Linux (after a 9→10 upgrade it
can point at KiCad 9). can point at KiCad 9).
3. **Deploy** (dev checkout; end users install the PCM zip instead, see 3. **Deploy** (dev checkout; end users install the PCM zip instead, see
*Packaging*): *Packaging / publishing*):
```powershell ```powershell
powershell -ExecutionPolicy Bypass -File deploy.ps1 # junction (dev) powershell -ExecutionPolicy Bypass -File deploy.ps1 # junction (dev)
powershell -ExecutionPolicy Bypass -File deploy.ps1 -Mode Copy powershell -ExecutionPolicy Bypass -File deploy.ps1 -Mode Copy
@@ -52,13 +52,13 @@ SWIG API. Requires KiCad **10.0.1+**.
## Usage ## Usage
1. Mark the current-injection terminals. Each terminal may have 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`** - **V+ rectangles on `User.1`**, **V rectangles on `User.2`**
(marker layers, configurable via `ELECTRODE_POS_LAYER` / (marker layers, configurable via `ELECTRODE_POS_LAYER` /
`ELECTRODE_NEG_LAYER`), any number per side, axis-aligned; `ELECTRODE_NEG_LAYER`), any number per side, axis-aligned;
- **pads and vias** (SMD pad: real copper shape on its own layer; - **pads and vias** (SMD pad: real copper shape on its own layer;
through-hole pads and vias become **barrel contacts** the current through-hole pads and vias become **barrel contacts**: the current
enters at the drill wall on every spanned layer, see below) enters at the drill wall on every spanned layer, see below);
selected pads/vias fill a side that has no rectangles; selected pads/vias fill a side that has no rectangles;
- legacy: exactly 2 selected contacts with no marker rectangles still - legacy: exactly 2 selected contacts with no marker rectangles still
works; empty selection scans the whole board's marker layers. 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 dialog's **"capped up to drill"** threshold (default
`CAP_MAX_DRILL_MM = 0.5`) keep open mouths even with capping `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 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 only affects in-plane conduction across outer-layer mouths. Sub-cell
mouths scale their cells' sheet conductance by the true covered mouths scale their cells' sheet conductance by the true covered
fraction (4×4 supersampling), so coarse grids see the correct small 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 oblong pads, fetched from KiCad; the outer shape stands in for inner
rings) are stamped onto every included layer, and every **populated** rings) are stamped onto every included layer, and every **populated**
pad carries its full **soldered joint** on its SOLDER side (opposite 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 holds the **component lead** (a cylinder of drill
`THT_LEAD_CLEARANCE_MM`, resistivity `THT_LEAD_RHO_OHM_M`, copper by `THT_LEAD_CLEARANCE_MM`, resistivity `THT_LEAD_RHO_OHM_M`, copper by
default raise it for brass/steel leads) **plus solder** in the default; raise it for brass/steel leads) **plus solder** in the
remaining annulus, both in parallel with the plating; the mouth remaining annulus, both in parallel with the plating. The mouth
copper stays conducting (it stands in for the plug — conservative, copper stays conducting: it stands in for the solder plug, which is
the plug is worth far more than the foil); the pad face gets the worth far more than the foil, so this is conservative. The pad face
average-thickness solder coat (exact pad shape) and the gets the average-thickness solder coat (exact pad shape) and the
protruding-lead cone (see barrel contacts below; on oblong pads 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 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** 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 a plating-only barrel, no joint. At f > 0 the thickness scaling is
applied multiplicatively to the skin-corrected sheet conductance applied multiplicatively to the skin-corrected sheet conductance
(approximation). Per layer a barrel attaches to (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` = (`SKIN_SIDES = 1` in config: plane facing a return plane; `2` =
isolated foil), and the analogous correction for the 18 µm barrel wall. 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 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 **Caveat:** only through-thickness crowding is modeled. Lateral
(proximity-effect) redistribution needs a magneto-quasistatic solver (proximity-effect) redistribution needs a magneto-quasistatic solver
and is not captured — since the resistance-driven distribution is the 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. not the geometric foil thickness.
- 5-point FDM per layer on an auto-sized shared grid (~2 M fine cells - 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 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 count no longer scales with the fine-cell count). Direct sparse solve
unknowns, AMG-preconditioned CG (pyamg) above Jacobi-CG if pyamg is up to 500 k unknowns, AMG-preconditioned CG (pyamg) above (Jacobi-CG
missing. Discretization error typically ≲ 2 % at defaults — halve the if pyamg is missing). Discretization error typically ≲ 2 % at
cell size and compare to judge convergence. defaults; halve the cell size and compare to judge convergence.
- **Adaptive cells** (dialog checkbox, **on by default**; - **Adaptive cells** (dialog checkbox, **on by default**;
`ADAPTIVE_CELLS`): `ADAPTIVE_CELLS`):
solves on a 2:1-balanced quadtree — fine cells at copper boundaries, solves on a 2:1-balanced quadtree — fine cells at copper boundaries,
electrodes, traces, via mouths and buildup, blocks up to 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 sets the clearance a block needs to grow). The **minimum element size
is the grid cell size itself** (auto / dialog / `CELL_UM_OVERRIDE`); is the grid cell size itself** (auto / dialog / `CELL_UM_OVERRIDE`);
the uniform limit reproduces the normal grid exactly. Large 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: Every run writes `geometry_dump.json`; re-solve without KiCad:
```powershell ```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] ` [--current 40] [--cell-um 50] [--layers F.Cu,In1.Cu] [--no-show] `
[--out DIR] [--force-iterative] [--out DIR] [--force-iterative]
``` ```
Dev environment, tests, headless extraction (Windows shown; on Dev environment, tests, headless extraction — [uv](https://docs.astral.sh/uv/)
Linux/macOS use `.venv/bin/python`): manages the venv from `pyproject.toml`/`uv.lock` (`requirements.txt`
stays: KiCad builds the plugin's runtime venv from it):
```powershell ```powershell
uv venv --python 3.11 .venv uv sync # one-time env setup
uv pip install --python .venv\Scripts\python.exe kicad-python numpy scipy pyamg matplotlib pytest uv run pytest -q # incl. exact analytic cases
.venv\Scripts\python.exe -m pytest tests -q # incl. exact analytic cases uv run python tools/api_probe.py # IPC API probe vs live KiCad
.venv\Scripts\python.exe tools\api_probe.py # IPC API probe vs live KiCad uv run python -m fill_resistance.board_io dump.json [NET] # extract only
.venv\Scripts\python.exe -m fill_resistance.board_io dump.json [NET] # extract only
``` ```
## Packaging / publishing ## 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 registry copy as `packages/th.co.b4l.fill-resistance/metadata.json` in a
merge request to <https://gitlab.com/kicad/addons/metadata>. Icons are merge request to <https://gitlab.com/kicad/addons/metadata>. Icons are
regenerated with `python tools/gen_icons.py`; the README figures in 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 (real solver output on small synthetic boards, plus the hand-drawn
hole cross-section). hole cross-section).
@@ -294,18 +294,20 @@ GPL-3.0-or-later — see [LICENSE](LICENSE).
## LLM disclaimer ## 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 Code, model Claude Fable 5). The physics model, solver, tests, tooling
and this documentation (including the figures, which are generated by and this documentation (including the figures; all but the hand-drawn
the solver itself) were written by the model, feature by feature, under hole cross-section are generated by the solver itself) were written by
human direction and review (janik / B4L); commits carry a the model, feature by feature, under human direction and review
`Co-Authored-By: Claude` trailer. (janik / B4L); most commits carry a `Co-Authored-By: Claude` trailer.
What keeps this honest: the test suite pins the numerics to exact What keeps this honest: the test suite pins the numerics to exact
analytic references (strip and annulus resistances, the acosh spreading analytic references (strip and annulus resistances, the acosh spreading
resistance of two circular contacts, skin-effect limits, power-balance resistance of two circular contacts, skin-effect limits, power-balance
identities) and to convergence/regression checks run it with identities) and to convergence/regression checks; run it with
`pytest tests`. Nevertheless, an LLM wrote this: read *Model & limits* `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 critically, treat surprising numbers with the usual engineering
suspicion, and cross-check against a hand estimate before trusting a suspicion, and cross-check against a hand estimate before trusting a
result with hardware. Bug reports are very welcome. 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 New-Item -ItemType Junction -Path $dst -Target $src | Out-Null
Write-Host "junction created: $dst -> $src" Write-Host "junction created: $dst -> $src"
} else { } 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 New-Item -ItemType Directory -Force $dst | Out-Null
Get-ChildItem $src -Force | Where-Object { $exclude -notcontains $_.Name } | Get-ChildItem $src -Force | Where-Object { $exclude -notcontains $_.Name } |
ForEach-Object { Copy-Item $_.FullName -Destination $dst -Recurse -Force } 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): def fig_error(message: str):
fig, ax = plt.subplots(figsize=(9, 4.5), layout="constrained") fig, ax = plt.subplots(figsize=(9, 4.5), layout="constrained")
ax.axis("off") 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") fontsize=14, fontweight="bold", loc="left")
wrapped = "\n".join( wrapped = "\n".join(
textwrap.fill(line, width=90) for line in message.splitlines() 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 ROOT = Path(__file__).resolve().parent.parent
COPY_EXCLUDE = {".venv", ".git", "tests", "tools", "dist", "resources", COPY_EXCLUDE = {".venv", ".git", "tests", "tools", "dist", "resources",
"__pycache__", ".pytest_cache", "conftest.py", "deploy.ps1", "__pycache__", ".pytest_cache", "conftest.py", "deploy.ps1",
".gitignore", "metadata.json"} ".gitignore", "metadata.json", "pyproject.toml", "uv.lock"}
def plugins_dir(kicad_version: str) -> Path: def plugins_dir(kicad_version: str) -> Path:
Generated
+1260
View File
File diff suppressed because it is too large Load Diff