6 Commits
Author SHA1 Message Date
grabowski d05d523995 Release 1.2.2
Build PCM package / build (push) Successful in 7s
2026-07-22 16:56:26 +07:00
grabowski 24a77da491 Merge remote-tracking branch 'origin/main' 2026-07-22 16:55:24 +07:00
grabowski 979b69960f Show a busy window while the solve runs
On OK the dialog closed and nothing appeared until the figures did,
which on a real board is minutes of looking like the plugin did
nothing. Put a small always-on-top window up for that stretch: the
stage now running, elapsed seconds, and Cancel.

Qt only repaints while the event loop runs and the solve owns the
thread, so the window pumps events itself - from inside the CG/AMG
iteration callback, which is where the time actually goes. That is
also where Cancel is noticed. The state is module-level because the
tick happens several frames deep in scipy/pyamg, and threading a
handle through those signatures for a progress bar is not worth it;
it stays inert until start(), so the standalone runner and the tests
are unaffected.

The window covers the figure work too, not just the solve: laying out
labels and writing four PNGs at full DPI is seconds on a real board -
10-15 of them on a large one - and closing before that left the same
silent gap one step later.
2026-07-22 16:55:19 +07:00
janikandClaude Fable 5 b806d31a9a Advertise DC resistance only; frame f>0 as a skin-only estimate
Build PCM package / build (push) Successful in 10s
Skin resistance is a small fraction of real AC impedance (proximity
and inductance dominate), so AC must not appear in the descriptions.
README headline, PCM/plugin metadata, pyproject, dialog note, CLI
help and the summary label now all say: skin-only lower bound on the
resistance rise, not an AC impedance simulation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:35:30 +07:00
grabowski d7c3089031 Release notes come from a file in the repo, not the tag
Build PCM package / build (push) Successful in 9s
Deriving the body from the tag annotation does not survive CI: the
release action does not fall back to it (v1.2.0 published empty), and
checkout leaves the tag lightweight, so %(contents) yields the commit
message instead - which is what v1.2.1 first published. Keep the notes
at docs/release-notes/v<version>.md, pass that as body_path, and fail
the run when it is missing.
2026-07-22 16:27:31 +07:00
grabowski 4dd33e6f43 Actually fetch the tag the release notes come from
Build PCM package / build (push) Successful in 9s
checkout fetches with --no-tags unless fetch-tags is set, at any
fetch-depth, so the notes step read no tag and wrote an empty file -
v1.2.1 published with empty notes despite the previous commit. Fail
the step rather than publish empty notes a third time.
2026-07-22 16:24:02 +07:00
18 changed files with 324 additions and 51 deletions
+13 -9
View File
@@ -13,8 +13,6 @@ jobs:
# third-party actions pinned to commit SHAs: mutable tags could be
# repointed at malicious code that runs with repo/token access
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # the annotated tag carries the release notes
- name: Check tag matches metadata.json version
if: startsWith(github.ref, 'refs/tags/v')
@@ -37,20 +35,26 @@ jobs:
dist/*.zip
dist/metadata-registry.json
# the action does not fall back to the tag annotation, so an
# un-bodied release publishes with empty notes (as v1.2.0 did)
- name: Release notes from the annotated tag
# The release body comes from a file in the repo: the action does
# not fall back to the tag annotation (v1.2.0 published empty), and
# reading the annotation here is unreliable - checkout leaves the
# tag lightweight, so %(contents) yields the commit message instead.
- name: Check the release notes exist
if: startsWith(github.ref, 'refs/tags/v')
run: |
git tag -l --format='%(contents:body)' "$GITHUB_REF_NAME" \
> release-notes.md
cat release-notes.md
notes="docs/release-notes/${GITHUB_REF_NAME}.md"
if [ ! -s "$notes" ]; then
echo "$notes is missing or empty - write the release notes" \
"before tagging" >&2
exit 1
fi
cat "$notes"
- name: Create release with the zip, registry metadata and figures
if: startsWith(github.ref, 'refs/tags/v')
uses: akkuman/gitea-release-action@b8d9144f302c68610911db1aaf722708d5c02d94 # v1
with:
body_path: release-notes.md
body_path: docs/release-notes/${{ github.ref_name }}.md
files: |
dist/*.zip
dist/metadata-registry.json
+17 -11
View File
@@ -1,15 +1,17 @@
# Fill Resistance — KiCad 10 plugin
Computes the **DC or AC resistance of copper zone fills and traces**
Computes the **DC 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.
and through-hole-pad barrels** (18 µm plating, configurable). 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. An optional **skin-effect correction** (exact 1D
foil/barrel solution at a user-set frequency) estimates the resistive
skin rise only — it is **not** an AC impedance simulation (no proximity
effect, no inductance; see *Model & limits*).
![Current density on a two-layer demo net](docs/img/demo-current.png)
*Real output on a synthetic two-layer net: current from a soldered
@@ -236,10 +238,14 @@ SWIG API. Requires KiCad **10.0.1+**.
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**.
**Caveat:** this is **not an AC impedance simulation** — skin
resistance is only a small part of real AC behavior. 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, the f > 0 resistance is a rigorous **lower
bound** — and inductance, usually the dominant term of a real AC
impedance, is absent entirely.
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
+12 -4
View File
@@ -4,7 +4,7 @@ The PCM addon zip is built by CI (`.gitea/workflows/build-pcm.yml`).
Every push to `main` builds it as a downloadable artifact; pushing a
`v<version>` tag additionally creates a Gitea release with the zip
attached. The release job checks that the tag matches `metadata.json`
and fails on a mismatch.
and that the release notes exist, and fails on either mismatch.
## Steps
@@ -23,17 +23,25 @@ and fails on a mismatch.
]
```
2. **Commit, tag, push** (tag = `v` + the manifest version):
2. **Write the release notes** at `docs/release-notes/v<version>.md`.
This file becomes the release description verbatim; the job fails if
it is missing or empty (the release action publishes empty notes
rather than falling back to the tag message, which is how v1.2.0
shipped with a blank description). Say what changed for a user of
the previous version — in particular, whether results move for an
unchanged board.
3. **Commit, tag, push** (tag = `v` + the manifest version):
```powershell
git add metadata.json
git add metadata.json docs/release-notes/v1.0.2.md
git commit -m "Release 1.0.2"
git tag v1.0.2
git push
git push origin v1.0.2
```
3. **Verify**: the Actions run for the tag builds
4. **Verify**: the Actions run for the tag builds
`th.co.b4l.fill-resistance_<version>.zip` and publishes it at
<https://git.b4l.co.th/B4L/kicad-zone-resistance/releases>, together
with `metadata-registry.json`. The zip installs directly via
+36
View File
@@ -0,0 +1,36 @@
Bug-fix release. Results are unchanged from 1.2.0 for a board that
solves cleanly; the fixes are in the in-KiCad overlay push, pad copper
selection and error reporting.
Note for anyone coming from 1.1.0 or earlier: 1.2.0 changed the physics
model (exact SMD and THT pad copper, populated THT holes conducting as
their solder plug and lead, slotted holes as true stadiums) and fixed an
adaptive barrel-refinement bug that could make via-field results read up
to ~13% low. Numbers for an unchanged board differ from 1.1.0 - re-run
any board you track across versions.
Fixed:
- Overlay push: a locked reference image silently survived removal and a
new one was stacked on top of it. KiCad reports the failure per item
while the overall request still reads OK; it is now checked, and the
layer is reported and skipped instead.
- Overlay push: a run covering fewer layers than the previous one left
the earlier solve's heatmap on the unused slots, where it read as
current. Those slots are now cleared.
- Overlay push: the whole push is one commit, so a single undo reverts
it rather than just the last layer.
- Through-hole pad copper was always read from F.Cu even when the joint
protrudes on B.Cu, mis-sizing the modelled solder coat for pads sized
differently per copper layer. The solder side is now probed first.
- A failure before the output directory existed - a broken plugin
Python environment, typically - reported nothing at all on screen.
The error figure now falls back to the temp directory.
- Pads sitting on no single copper layer are noted rather than silently
skipped, and the frequency field keeps its specific rejection reason
("1,500" is a thousands separator, "-5" is negative) as the other
numeric fields already did.
The KiCad overlay push remains experimental and opt-in (off by default).
It writes reference images to User.9-User.12 and replaces what is on
those layers.
+40
View File
@@ -0,0 +1,40 @@
Results are unchanged from 1.2.1 for the same board and settings. This
release is about what the plugin tells you while it works, and about no
longer overstating what a frequency result means.
Progress while solving:
- The dialog used to close on OK and leave nothing on screen until the
figures appeared - minutes, on a real board, with no sign the plugin
was doing anything. A small window now stays up for that whole
stretch: the stage running, elapsed seconds, and Cancel.
- It covers the figure work as well as the solve. Laying out labels and
writing the four PNGs at full resolution is seconds on a modest board
and 10-15 on a large one, and that used to be silent too.
- Cancel stops the solve and returns you to the board with no error
figure - the run simply reports that it was cancelled.
Frequency results are described honestly:
- Nothing advertises "AC resistance" any more. At f > 0 the plugin
applies the exact 1D foil and barrel skin-effect correction and
nothing else: proximity redistribution and inductance are not
modelled, so the number is a lower bound on the resistive rise, not
an AC impedance simulation. The README headline, the PCM and plugin
descriptions, the dialog note, the CLI help and the summary line all
say so now.
- The computation itself has not changed - only its description. A
frequency result from 1.2.1 is the same number, previously labelled
in a way that invited it to be read as an impedance.
Also in this release:
- The offline runner takes --progress, so the same busy window can be
used outside KiCad.
- The frequency field keeps its specific reason for rejecting an input
("1,500" is a thousands separator, "-5" is negative) instead of a
generic "cannot parse".
The in-KiCad |J| overlay push remains experimental and opt-in, off by
default. It writes reference images to User.9-User.12 and replaces what
is on those layers.
+4 -2
View File
@@ -34,7 +34,7 @@ import numpy as np
from scipy import sparse
from scipy.sparse import csgraph
from . import config, quadtree, skin
from . import config, progress, quadtree, skin
from . import solver as sv
from .errors import ConnectivityError
from .geometry import Problem
@@ -300,9 +300,11 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack,
corr = np.zeros(len(edges.a))
faces = e_axis >= 0
fa, fb = edges.a[faces], edges.b[faces]
for _ in range(max(0, int(config.ADAPTIVE_CORRECTION_PASSES))):
passes = max(0, int(config.ADAPTIVE_CORRECTION_PASSES))
for p in range(passes):
if not faces.any():
break
progress.stage(f"correction pass {p + 1}/{passes} ...")
gx, gy = _leaf_gradients(N, fa, fb, cxg, cyg, Vflat)
gt = np.where(e_axis[faces] == 0, 0.5 * (gy[fa] + gy[fb]),
0.5 * (gx[fa] + gx[fb]))
+4 -4
View File
@@ -137,10 +137,10 @@ class _Dialog(QDialog):
lay = QVBoxLayout(self)
lay.addLayout(form)
note = QLabel("Multiple layers are coupled through the net's "
"via/through-pad barrels. At f > 0 the foil-thickness "
"skin effect is applied per layer; lateral (proximity) "
"redistribution is not modeled, so AC results are a "
"lower bound.")
"via/through-pad barrels. f > 0 applies only the "
"foil-thickness skin effect (a lower bound on the "
"resistance rise) - not an AC impedance simulation: "
"proximity and inductance are not modeled.")
note.setWordWrap(True)
note.setStyleSheet("color: gray; font-size: 10px;")
lay.addWidget(note)
+8 -1
View File
@@ -12,7 +12,7 @@ from __future__ import annotations
import sys
import traceback
from . import config, pipeline, report
from . import config, pipeline, progress, report
from .errors import CandidateError, UserFacingError
@@ -89,6 +89,9 @@ def main() -> None:
if selection is None:
print("cancelled")
return
# the solve owns the thread from here; without this the plugin
# looks like it did nothing until the figures appear
progress.start()
if selection.contact1 != "auto":
for e in es1:
@@ -122,10 +125,14 @@ def main() -> None:
freq_hz=selection.freq_hz,
contact_model=selection.contact_model,
overlay=overlay_cb)
except progress.Cancelled:
print("cancelled") # user's own doing: no error figure
except UserFacingError as e:
_fail(str(e), outdir)
except Exception:
_fail(traceback.format_exc(), outdir)
finally:
progress.done() # also on the error paths
if __name__ == "__main__":
+6 -5
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from pathlib import Path
from . import config, plots, raster, report, solver
from . import config, plots, progress, raster, report, solver
from .errors import UserFacingError
from .geometry import Problem
from .solver import Result
@@ -20,8 +20,8 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
if i_test <= 0:
raise UserFacingError(f"Test current must be > 0 A (got {i_test:g}).")
h = raster.choose_cell_size(problem.copper_bbox(), len(problem.layers))
print(f"rasterizing {len(problem.layers)} layer(s) at cell size "
f"{h / 1000:.1f} um ...")
progress.stage(f"rasterizing {len(problem.layers)} layer(s) at cell "
f"size {h / 1000:.1f} um ...")
stack = raster.rasterize_stack(problem, h)
print(f"grid {stack.shape2d[1]}x{stack.shape2d[0]}x{stack.nlayers}, "
f"{int(stack.masks.sum())} copper cells, {len(problem.vias)} "
@@ -30,7 +30,7 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
e1, e2 = raster.electrode_masks(stack, problem)
parts1, parts2 = raster.electrode_partition(stack, problem)
print(f"solving @ {i_test:g} A"
progress.stage(f"solving @ {i_test:g} A"
+ (f", {freq_hz:g} Hz" if freq_hz > 0 else " DC") + " ...")
result = solver.run_solve(problem, stack, e1, e2, i_test, freq_hz,
contact_model, parts1, parts2)
@@ -51,6 +51,7 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
except Exception as e:
print(f"overlay push failed: {e}")
progress.stage("rendering figures ...")
figs = [
(plots.fig_raster(stack, e1, e2, problem, result), "1_raster_map"),
(plots.fig_potential(result, stack, e1, e2, problem), "2_potential"),
@@ -58,5 +59,5 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
"3_current_density"),
(plots.fig_power(result, stack, e1, e2, problem), "4_power_density"),
]
plots.save_and_show(figs, outdir, show=show)
plots.save_and_show(figs, outdir, show=show) # closes the window itself
return result
+10 -1
View File
@@ -43,7 +43,7 @@ from matplotlib.gridspec import GridSpec # noqa: E402
from matplotlib.patches import Patch # noqa: E402
from matplotlib.widgets import CheckButtons # noqa: E402
from . import config # noqa: E402
from . import config, progress # noqa: E402
_BG = "#f5f3f0"
_COPPER = "#c98b4e"
@@ -514,11 +514,15 @@ def save_and_show(figs_named: list[tuple], outdir: Path | None,
show: bool = True) -> list[Path]:
"""figs_named: [(figure, basename), ...]. Saves first, then shows."""
saved = []
progress.stage("laying out figures ...", echo=False)
for fig, _ in figs_named:
_resolve_label_overlaps(fig)
if outdir is not None:
outdir.mkdir(parents=True, exist_ok=True)
for fig, name in figs_named:
# full-DPI savefig with tight bounding boxes is seconds per
# figure - the progress window has to stay up for it
progress.stage(f"saving {name}.png ...", echo=False)
panel = getattr(fig, "_layer_panel", None)
if panel is not None:
panel.set_visible(False) # PNGs carry no checkboxes
@@ -531,13 +535,18 @@ def save_and_show(figs_named: list[tuple], outdir: Path | None,
print(f"saved {p}")
if show and config.INTERACTIVE:
if INTERACTIVE_BACKEND:
progress.stage("opening the figure windows ...", echo=False)
for fig, _ in figs_named:
_fit_to_screen(fig)
progress.done() # last thing before the figures are up
_raise_windows()
plt.show()
else:
progress.done()
for p in saved:
_open_in_viewer(p)
else:
progress.done()
plt.close("all")
return saved
+145
View File
@@ -0,0 +1,145 @@
"""Busy window for the stretch between the dialog closing and the
figures appearing.
The solve is seconds to minutes on a real board, and until now nothing
was on screen for it: the dialog vanished on OK and the plugin looked
like it had done nothing. This puts a small always-on-top window up for
that stretch - current stage, elapsed time, and a Cancel button.
The state is module-level rather than an object threaded through the
call chain: the linear solve is where the time actually goes, and it
calls tick() from inside a scipy/pyamg iteration callback several
frames deep. Inactive until start() succeeds, so every call is a no-op
for the standalone runner and the tests.
Qt only repaints when the event loop runs, and the solve owns the
thread, so tick() pumps events itself. That is also where a click on
Cancel is noticed - it raises Cancelled at the next tick.
"""
from __future__ import annotations
import time
_win = None
_label = None
_text = ""
_t0 = 0.0
_last = 0.0
_cancelled = False
TICK_INTERVAL_S = 0.05 # ~20 fps: enough to look alive, cheap
class Cancelled(Exception):
"""The user closed the progress window. Not a failure - the caller
reports it like a cancelled dialog, with no error figure."""
def start(title: str = "Fill Resistance") -> bool:
"""Show the window. False (and inert) if Qt is unavailable."""
global _win, _label, _t0, _last, _cancelled, _text
if _win is not None:
return True
try:
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (QApplication, QDialog,
QDialogButtonBox, QLabel,
QProgressBar, QVBoxLayout)
except Exception:
return False
try:
app = QApplication.instance() or QApplication([])
win = QDialog()
win.setWindowTitle(title)
win.setWindowFlag(Qt.WindowStaysOnTopHint, True)
# no close button: closing is Cancel, and Cancel is the only way
# to stop a solve that owns the thread
win.setWindowFlag(Qt.WindowCloseButtonHint, False)
label = QLabel("starting ...")
bar = QProgressBar()
bar.setRange(0, 0) # indeterminate: no total to show
buttons = QDialogButtonBox(QDialogButtonBox.Cancel)
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(bar)
layout.addWidget(buttons)
win.setLayout(layout)
buttons.rejected.connect(_cancel)
win.rejected.connect(_cancel)
win.setMinimumWidth(340)
win.show()
win.raise_()
win.activateWindow()
app.processEvents()
except Exception:
return False
_win, _label, _t0, _last, _cancelled, _text = win, label, \
time.monotonic(), 0.0, False, ""
return True
def _cancel() -> None:
global _cancelled
_cancelled = True
def stage(text: str, echo: bool = True) -> None:
"""Name the phase now running. Always repaints - stages are rare.
echo=False for phases that already print their own line (saving a
PNG prints the path), so the window updates without doubling stdout.
"""
global _text
_text = text
if echo:
print(text)
if _win is not None:
_refresh()
def tick() -> None:
"""Called from inside the solve. Throttled, so it is safe to call
every iteration."""
global _last
if _win is None:
return
now = time.monotonic()
if now - _last < TICK_INTERVAL_S:
return
_last = now
_refresh()
def _refresh() -> None:
from PySide6.QtWidgets import QApplication
elapsed = time.monotonic() - _t0
if _label is not None:
_label.setText(f"{_text}\n{elapsed:.0f} s elapsed")
app = QApplication.instance()
if app is not None:
app.processEvents()
if _cancelled:
raise Cancelled()
def done() -> None:
"""Take the window down. Idempotent - callers use it in a finally."""
global _win, _label, _text, _cancelled
win, _win, _label, _text = _win, None, None, ""
_cancelled = False
if win is None:
return
try:
win.close()
win.deleteLater()
from PySide6.QtWidgets import QApplication
app = QApplication.instance()
if app is not None:
app.processEvents()
except Exception:
pass
+2 -1
View File
@@ -59,7 +59,8 @@ def write_summary(outdir: Path, problem: Problem, stack: RasterStack,
+ (f"{result.freq_hz:g} Hz (skin depth {result.skin_depth_um:.0f} um)"
if result.freq_hz > 0 else "DC")),
f"RESISTANCE: {result.R_ohm * 1000:.6g} mOhm"
+ (" (AC LOWER BOUND: lateral/proximity redistribution not modeled)"
+ (" (SKIN-ONLY LOWER BOUND: no proximity/inductance - "
"not AC impedance)"
if result.freq_hz > 0 else ""),
f"VOLTAGE DROP: {result.R_ohm * result.i_test * 1000:.4g} mV "
f"@ {result.i_test:g} A",
+6 -3
View File
@@ -45,7 +45,7 @@ from scipy import sparse
from scipy.sparse import csgraph
from scipy.sparse import linalg as sla
from . import config, skin
from . import config, progress, skin
from .errors import ConnectivityError, ElectrodeError, SolverError
from .geometry import Problem, slot_distance
from .raster import RasterStack, electrodes_touch
@@ -375,12 +375,14 @@ class PreparedSolver:
def solve(self, b: np.ndarray) -> tuple[np.ndarray, SolveInfo]:
if self._lu is not None:
progress.tick() # direct solve: one shot, no iterations
return self._lu.solve(b), SolveInfo(method="spsolve",
n_unknowns=self.n)
if self._ml is not None:
residuals: list[float] = []
x = self._ml.solve(b, tol=config.AMG_TOL, maxiter=300,
accel="cg", residuals=residuals)
accel="cg", residuals=residuals,
callback=lambda _: progress.tick())
res = float(np.linalg.norm(b - self._A @ x)
/ max(np.linalg.norm(b), 1e-300))
if not np.isfinite(res) or res > 1e-6:
@@ -404,7 +406,7 @@ def _solve_amg(A: sparse.csr_matrix, b: np.ndarray) -> tuple[np.ndarray, SolveIn
ml = pyamg.smoothed_aggregation_solver(A.tocsr(), max_coarse=500)
residuals: list[float] = []
x = ml.solve(b, tol=config.AMG_TOL, maxiter=300, accel="cg",
residuals=residuals)
residuals=residuals, callback=lambda _: progress.tick())
res = float(np.linalg.norm(b - A @ x) / max(np.linalg.norm(b), 1e-300))
if not np.isfinite(res) or res > 1e-6:
raise SolverError(
@@ -428,6 +430,7 @@ def _solve_cg_jacobi(A: sparse.csr_matrix, b: np.ndarray) -> tuple[np.ndarray, S
def count(_):
nonlocal iters
iters += 1
progress.tick()
try:
x, code = sla.cg(A, b, M=M, rtol=config.CG_TOL,
+13 -2
View File
@@ -13,7 +13,7 @@ import argparse
import sys
from pathlib import Path
from . import config, pipeline
from . import config, pipeline, progress
from .errors import UserFacingError
from .geometry import load_problem
from .skin import parse_frequency
@@ -26,7 +26,8 @@ def main(argv=None) -> int:
help="test current [A] (default: config TEST_CURRENT_A)")
ap.add_argument("--freq", type=parse_frequency, default=0.0,
help="frequency, e.g. 142k or 1.5M (default: DC). "
"AC results are a lower bound (skin per foil only)")
"Skin resistance only, a lower bound - not AC "
"impedance (no proximity, no inductance)")
ap.add_argument("--cell-um", type=float, default=None,
help="force grid cell size [um]")
ap.add_argument("--layers", type=str, default=None,
@@ -51,6 +52,9 @@ def main(argv=None) -> int:
ap.add_argument("--force-iterative", action="store_true",
help="use the iterative solver (AMG-CG, or Jacobi-CG "
"without pyamg) regardless of problem size")
ap.add_argument("--progress", action="store_true",
help="show the busy window during the solve, as the "
"KiCad plugin does (needs a GUI)")
ap.add_argument("--adaptive", action=argparse.BooleanOptionalAction,
default=None,
help="adaptive quadtree grid (coarse plane interiors); "
@@ -86,13 +90,20 @@ def main(argv=None) -> int:
return 1
outdir = args.out if args.out is not None else args.dump.parent
if args.progress:
progress.start()
try:
pipeline.run(problem, outdir, show=not args.no_show,
i_test=args.current, freq_hz=args.freq,
contact_model=args.contact_model)
except progress.Cancelled:
print("cancelled")
return 1
except UserFacingError as e:
print(f"ERROR: {e}", file=sys.stderr)
return 1
finally:
progress.done()
return 0
+3 -3
View File
@@ -1,8 +1,8 @@
{
"$schema": "https://go.kicad.org/pcm/schemas/v2",
"name": "Fill Resistance",
"description": "DC/AC resistance of copper zone fills and traces between two contacts, single- or multi-layer with via coupling; current and power density maps.",
"description_full": "Computes the DC or AC resistance of copper zone fills and traces between two contacts (marker rectangles on User.1/User.2 and/or selected pads/vias), single- or multi-layer: the chosen net's fills and tracks are solved as coupled finite-difference sheets linked by the net's via and through-hole-pad barrels. Selected vias/THT pads inject at the drill-wall barrel, and every populated THT hole carries its full solder joint (component lead, solder fill, one-sided pad coat and protruding-lead cone) with exact pad shapes and do-not-populate flags read from KiCad, conducting in-plane as its solder plug and lead on every layer it spans. Every net pad's exact copper shape is stamped on the layers it sits on, SMD as well as through-hole, and oblong (slotted) holes are modelled as their true stadium shape rather than an approximating circle. Traces narrower than the grid become exact 1D resistor chains, and an adaptive multi-resolution grid (fine at features, coarse plane interiors, deferred-corrected) keeps large boards fast.\n\nShows per-layer rasterized maps, potential, current density and power density, reports per-via currents (via ampacity) and total dissipation at a selectable test current. At a user-set frequency the exact 1D foil/barrel skin-effect correction is applied (AC results are a rigorous lower bound). PNGs, a text summary and a re-solvable geometry dump are saved per run.\n\nNote: the first load builds the plugin's Python environment (numpy, scipy, pyamg, matplotlib, PySide6) and can take several minutes.",
"description": "DC resistance of copper zone fills and traces between two contacts, single- or multi-layer with via coupling; current and power density maps.",
"description_full": "Computes the DC resistance of copper zone fills and traces between two contacts (marker rectangles on User.1/User.2 and/or selected pads/vias), single- or multi-layer: the chosen net's fills and tracks are solved as coupled finite-difference sheets linked by the net's via and through-hole-pad barrels. Selected vias/THT pads inject at the drill-wall barrel, and every populated THT hole carries its full solder joint (component lead, solder fill, one-sided pad coat and protruding-lead cone) with exact pad shapes and do-not-populate flags read from KiCad, conducting in-plane as its solder plug and lead on every layer it spans. Every net pad's exact copper shape is stamped on the layers it sits on, SMD as well as through-hole, and oblong (slotted) holes are modelled as their true stadium shape rather than an approximating circle. Traces narrower than the grid become exact 1D resistor chains, and an adaptive multi-resolution grid (fine at features, coarse plane interiors, deferred-corrected) keeps large boards fast.\n\nShows per-layer rasterized maps, potential, current density and power density, reports per-via currents (via ampacity) and total dissipation at a selectable test current. An optional skin-effect correction (exact 1D foil/barrel solution at a user-set frequency) estimates the resistive skin rise only - proximity redistribution and inductance are not modeled, so this is not an AC impedance simulation. PNGs, a text summary and a re-solvable geometry dump are saved per run.\n\nNote: the first load builds the plugin's Python environment (numpy, scipy, pyamg, matplotlib, PySide6) and can take several minutes.",
"identifier": "th.co.b4l.fill-resistance",
"type": "plugin",
"author": {
@@ -17,7 +17,7 @@
},
"versions": [
{
"version": "1.2.1",
"version": "1.2.2",
"status": "stable",
"kicad_version": "10.0",
"runtime": "ipc"
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://go.kicad.org/api/schemas/v1",
"identifier": "th.co.b4l.fill-resistance",
"name": "Fill Resistance",
"description": "DC/AC resistance of copper zone fills and traces between two contacts (marker rectangles or pads), single- or multi-layer with via coupling",
"description": "DC resistance of copper zone fills and traces between two contacts (marker rectangles or pads), single- or multi-layer with via coupling",
"runtime": {
"type": "python"
},
+2 -2
View File
@@ -3,8 +3,8 @@
# the dependency list there in sync with [project.dependencies].
[project]
name = "fill-resistance"
version = "1.2.1"
description = "DC/AC resistance of copper zone fills and traces between two contacts (KiCad 10 plugin)"
version = "1.2.2"
description = "DC resistance of copper zone fills and traces between two contacts (KiCad 10 plugin)"
license = "GPL-3.0-or-later"
requires-python = ">=3.11"
dependencies = [
Generated
+1 -1
View File
@@ -216,7 +216,7 @@ wheels = [
[[package]]
name = "fill-resistance"
version = "1.2.1"
version = "1.2.2"
source = { virtual = "." }
dependencies = [
{ name = "kicad-python" },