QtCore's libraries (glib, icu) can load while QtWidgets/QtGui still
miss libGL - matplotlib's qt backend imports all three, so the old
probe could promise QtAgg and lose even the error figure at
switch_backend, one library layer deeper than the NixOS fix. The
regression test's mock now mirrors the real failure (bare package and
QtCore import fine, GUI modules raise): both the bare-import and the
QtCore-probe variants now fail it, verified by mutation. Also drop two
redundant f-prefixes on placeholder-less strings in report.py.
First successful Linux field test (NixOS 26.05, Plasma 6, KiCad 10).
Getting there peeled three layers the error messages misattribute:
the FHS env itself (libgthread), then libs the xcb-cursor hint hides
(the xcb-util family, and libzstd for libQt6Core - in zstd.out, not
zstd's default output), and finally Plasma's QT_PLUGIN_PATH feeding
the system Qt's platform plugins to the wheel's bundled Qt, a
private-ABI mismatch that fails exactly like a missing library.
New docs/NIXOS.md carries the verified buildFHSEnv wrapper, the
steam-run quick test (now with env -u QT_PLUGIN_PATH), and a guide
for chasing future missing sonames. README upgrades Linux from
untested to works and links the doc. The in-plugin error no longer
suggests programs.nix-ld: the venv python is a Nix binary, so the
nix-ld shim never engages for it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field-tested: under steam-run the PySide6 wheel loads, but Qt aborts
at platform-plugin init - the Steam runtime predates Qt 6.5, which
hard-requires libxcb-cursor0 for xcb (and its wayland stack is too
old for the wayland plugin). Document the working invocation: point
LD_LIBRARY_PATH at xcb-util-cursor on top of steam-run, or build a
proper buildFHSEnv wrapper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First NixOS attempt: pip wheel PySide6 cannot load libgthread-2.0.so.0
because NixOS has no FHS library paths. Nothing inside the venv can
fix that (KiCad installs wheels only) - but the plugin made it worse
twice over:
- The backend probe imported bare PySide6, whose pure-Python __init__
succeeds even when QtCore's .so cannot load - so matplotlib was
promised QtAgg and the error figure died at switch_backend, taking
the failure report with it. The probe now imports <binding>.QtCore
and falls through to Tk/Agg on a broken Qt.
- The user got a raw ImportError traceback. A cannot-open-shared-object
failure during the kipy/dialog imports now raises a UserFacingError
that names the actual fixes: run KiCad in an FHS environment
(steam-run) or enable nix-ld with Qt runtime libraries. The README
Linux notes carry the same guidance.
141 passed on the dev stack and the Python 3.9 mac-equivalent stack.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
macOS is now field-tested and working; the README platform notes say
so instead of untested. Linux remains audited-only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second Mac run (KiCad demo board opened from the mounted installer
image) got past the dialog and died twice:
1. make_output_dir crashed with OSError 30: the demos volume is a
read-only filesystem. Now falls back to a temp directory named
after the board, with the path printed (the Messages panel showed
it is actually read there).
2. The error figure - and every other figure - could never render:
matplotlib refused with Cannot load backend TkAgg ... as qt is
currently running. macOS bundled Python ships tkinter, so the
old tk-first probe picked TkAgg while the PySide6 dialog and
progress window had already made this a Qt process. Windows never
saw it because KiCad Python there has no tkinter. Qt now comes
first - PySide6 is a hard dependency, so it is always there.
Both covered by tests that fail on the old code: the dev env has both
toolkits installed, so the backend-order assertion is exercised for
real, and the RO fallback is simulated by denying mkdir under the
board dir. 140 passed on the dev stack and on the Python 3.9 +
numpy 2.0 / scipy 1.13 / matplotlib 3.9 / PySide6 6.10 stack that a
Mac venv resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First real run on a Mac died before the dialog could open:
config.py line 15: CELL_UM_OVERRIDE: float | None = None
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'
KiCad macOS bundles Python 3.9, where PEP 604 unions in annotations
are evaluated at import time unless deferred - config.py was the one
annotated module without the __future__ import (errors.py has no
annotations, __init__.py is empty).
A new tripwire test walks every shipped module with ast and fails if
a file uses annotations without deferring them, so the next module
added (progress.py was born only last week) cannot regress this
silently while the Windows suite stays green.
Verified for real this time, not audited: the full suite passes under
CPython 3.9.25 with the exact stack a Mac venv resolves (numpy 2.0.2,
scipy 1.13.1, matplotlib 3.9.4, PySide6 6.10.0, pyamg 5.2.1) - 137
passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The setup section and platform notes claimed all three OSes work; the
Linux and macOS statements come from a dependency/code audit only -
nobody has run the plugin there. Say so and ask for reports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The README assumed Windows throughout. Setup now gives the interpreter
path, deploy command and venv location for Windows, macOS and Linux
(venv paths verified against KiCad 10 sources: GetUserCachePath +
python-environments), plus a Platform notes section: macOS bundles
Python 3.9.13 so pip resolves an older wheel stack (KiCad installs with
--only-binary :all:), and windows there may open behind KiCad; Linux
needs python3-venv on Debian/Ubuntu.
pyamg has never published Linux aarch64 wheels, and with KiCad's
wheels-only pip one unresolvable requirement kills the whole venv
build - so an environment marker skips pyamg there and the solver
falls back to Jacobi-CG, which it already supports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
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>
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.
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.
akkuman/gitea-release-action takes body/body_path but does not fall
back to the tag annotation, so v1.2.0 published with empty notes.
Write the tag body to a file and pass it; fetch-depth 0 so the
annotated tag object is present to read it from.
kipy's Board.remove_items discards the DeleteItemsResponse, and the
proto warns the overall status may read OK even when nothing was
deleted - a locked image comes back IDS_IMMUTABLE. remove_overlays
went through the proto layer already for create; do the same for
delete, so a locked overlay is reported instead of silently surviving
while a second image is stacked on top of it.
Slots that a narrower run does not write kept the previous solve's
heatmap and read as current; clear them. The whole push is now one
commit, so a single undo reverts it rather than one layer of it.
_pad_polygons probed F.Cu before B.Cu regardless of which side the
joint protrudes from, so a pad sized differently per copper layer had
its solder coat measured from the wrong face; probe the solder side
first. Pads on no single copper layer are now noted rather than
silently skipped.
_fail could report nothing at all: before the output directory exists
no PNG is written, and with no GUI toolkit plots only opens saved
PNGs - the broken-plugin-environment case the docstring promises to
cover. Fall back to the temp dir, and never let reporting mask the
fault. Frequency input now keeps its specific rejection reason, as
the other numeric fields do.
It is a right-click context-menu item on the plugin row in
Preferences -> PCB Editor -> Action Plugins, not a control on the
Plugins page. Also document the manual venv-delete fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After a solve, the per-layer current-density maps can be pushed into
the open board as reference images on User.9..User.12 (stackup order,
top first) - visible right in the editor, toggled like any layer,
never plotted to gerbers. Dialog checkbox, default OFF; every push
replaces all reference images on those layers.
Rendering (fill_resistance/overlay.py, KiCad-free and tested headless):
one pixel per grid cell, opaque over copper with the log scale lifted
off the colormap's near-black bottom (dark canvas), transparent
elsewhere, one pixel of half-alpha edge bleed so the overlay reaches
the drawn outline instead of stopping half a cell short. Pushing lives
in board_io (ReferenceImage via the IPC API, KiCad >= 10.0.1; scale =
width / (pixels * 1 inch / 300 PPI), position = image center); kipy
0.7.1 swallows creation errors, so the per-item status is read from
the raw CreateItemsResponse. pipeline.run takes an optional overlay
callback; failures are reported, never fatal.
tools/kicad_overlay_test.py pushes a fiducial alignment pattern (KiCad
bbox readback verified placement to half a pixel); tools/
kicad_heatmap_overlay.py runs the whole thing headless against the
open board, filtering marker rectangles of other nets' analyses via
the exact copper test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mouth of a populated THT pad kept only foil copper on every layer:
on the component side and inner layers the joint looked (and conducted)
like plain plane, and current had to crowd through the single barrel
attachment cell. The filled hole - lead cylinder plus solder bore - now
adds conduction-equivalent copper of the full hole depth on EVERY
spanned layer (the pin continues beyond both mouths, so each layer sees
the whole plug cross-section). Side to side the joint differs only by
the solder: coat and cone stay on the protrusion side.
Cone and plug contributions accumulate ADDITIVELY (stack.t_extra_nm)
and fold into thick_scale once; multiplying the factors would overstate
mouth cells carrying both. The raster map draws filled mouths in a
darker tin with their own legend entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every barrel was drawn with the same green dot under one 'vias' legend
label, making through-hole pads read as vias. Pad barrels (kind='pad')
now draw violet with a 'THT pad barrels' entry; the legend lists only
the barrel kinds present.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The drill's y dimension was discarded (padstack.drill.diameter.x only),
so a milled slot became a round hole of its x size - contact rings,
drill mouths and lead cones painted circles larger than the oblong pad
itself, and the cone was skipped outright (pad_min <= drill).
Slots now keep their true stadium shape, rotated with the pad (KiCad
CCW, y down): Electrode/ViaLink carry the end-cap offset vector,
drill_nm becomes the slot WIDTH, and a shared slot_distance() reduces
to the plain radius for round holes. The barrel wall ring, mouth
coverage, cone taper and the solver's attachment search all follow the
slot; barrel_resistance uses the stadium perimeter and bore area. The
stitching-coat fallback becomes a capsule along the slot (inscribed
disc when the axis is unknown) instead of a largest-dimension disc.
Slot fields round-trip through the JSON dumps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pads are the junctions where traces and thermal-relief spokes actually
meet; without their copper a multi-track junction necks down to the
accidental overlap of the rounded track ends, or is severed outright.
gather_smd_pad_copper fetches the exact shape of every undrilled pad
on the net (one API call per pad, layer from the padstack) and
build_problem stamps them onto their own layer (INCLUDE_SMD_PADS).
Selected SMD-pad contacts get their real copper as a side effect
(previously electrode-shape intersected whatever lay underneath).
Dead-end pads become floating islands that the existing connectivity
restriction drops.
Closes the documented SMD-pad-copper limitation (prompted by the
padne comparison).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- test_stitching_pad_mid_plane_close: solder-filled THT stitching pad
mid-pour, adaptive vs uniform. Fails at 13.8% low on the pre-pinning
adaptive path, passes at <0.1% with it.
- normalize_decimal / parse_frequency: decimal commas parse, thousands-
separator patterns raise instead of silently scaling 1000x.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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.
- adaptive: barrel links could attach to coarse leaves (up to 1 mm),
making the whole leaf equipotential and deleting the local spreading
resistance - via-field results read up to ~13% low. Barrel attachment
cells are now pinned into the keep-fine set before the quadtree is
built; the guard ring grades around them.
- skin/dialog: the decimal-comma rewrite turned '1,500' into 1.5, a
silent 1000x error in frequency, test current or cell size. Ambiguous
comma patterns (thousands separators, multiple commas, mixed with a
dot) now raise with a message; a real decimal comma ('1,5') still
parses.
- dialog: Selection.adaptive default now matches the documented
on-by-default; the adaptive accuracy claim is aligned to the measured
0.03% quoted in README and config.
Real boards vs. a UT3513+ micro-ohm meter, agreement within +/-20%,
attributed to test-setup imperfections and manufacturing tolerances.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Barrel contacts for vias/THT pads (drill-wall ring injection, verified
against the analytic acosh spreading resistance), full solder-joint
modeling of every populated THT hole (lead conductor, solder fill,
one-sided pad coat, protruding-lead cone, exact pad shapes and DNP
flags read from KiCad), configurable cap-drill threshold, and README
model figures. The release now also attaches the docs/img figures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The overlaid contour lines belonged on the potential plot the plugin
already produces; demo-potential.png is that figure for the same demo
solve.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shared contour levels across both layers, so line density reads as
field strength (the caption points at the notch carrying nearly the
whole drop).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/img, embedded in the README with captions:
- demo-current / demo-raster: real pipeline output on a synthetic
two-layer net (soldered THT-pad contact, notched pour, stitching-via
field) showing the current-density map and the raster map with the
adaptive-mesh overlay and the one-sided solder coat
- contact-models: |J| around the same contact under the equipotential
and uniform-injection models (the bracket, with both R values)
- hole-model: hand-drawn cross-section of the four hole types (capped
via, open via, populated THT joint with lead/solder/cone/coat, DNP)
tools/gen_readme_figs.py regenerates all four; noted in Packaging.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- THT pad copper is now part of the conductor: the exact pad outline
(incl. oblong/custom shapes) is fetched from KiCad once per pad and
stamped onto every included layer (the outer shape stands in for
inner rings). Annular rings bridge antipads, and joints land on real
copper instead of only pour coverage.
- The internal lead conductor is modeled in every solder-filled hole:
a cylinder of drill - THT_LEAD_CLEARANCE_MM (0.25 fab rule) with
THT_LEAD_RHO_OHM_M (copper default; config for brass/steel leads),
in parallel with the solder annulus and the plating.
- Drill mouths of THT pads: populated pads keep conducting mouth
copper (stands in for the solder plug - conservative, the plug is
worth ~200 um of copper equivalent); DNP pad holes are cut open on
every layer like uncapped via mouths.
- Oblong pads: the coat uses the exact pad shape; the lead cone tapers
within the inscribed circle (new pad_min_nm on ViaLink/Electrode) so
the long axis is not overstated sideways.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pad face is wetted where the joint is: the protrusion side,
opposite the component (already read from the owning footprint). The
component-side pad face stays bare - contact_solder_buildups and
tht_joint_buildups no longer coat both outer layers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No size-threshold guessing: whether a hole is a via or a THT pad
already comes from KiCad (board.get_vias vs drilled board.get_pads).
Every populated THT pad of the net now carries the complete joint the
contacts got: solder-filled barrel, average-thickness coat over a
pad-diameter disc on the outer layers, and the protruding-lead cone on
the side opposite its owning footprint. The footprint side and the
Do-not-populate flag are read from KiCad (footprint pads store absolute
positions, so owner lookup is an exact (x, y, number) map); DNP pads
stay plating-only with no joint. Contact pads are deduplicated by
barrel center so their cone/coat is never applied twice.
Barrels are now gathered in single-layer runs too: via rings and drill
mouths perforate a lone plane, THT joints stiffen it locally.
ViaLink gains solder_filled + protrusion_side (legacy dumps load with
the old every-THT-pad-filled semantics).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The clipped lead of a soldered THT contact protrudes
THT_LEAD_PROTRUSION_MM (1.5 mm default, 0 disables) out of the hole on
the side opposite the component, and a solder cone wraps it: full
protrusion height at the drill wall, tapering linearly to zero at the
pad edge. Painted as per-cell extra conduction-equivalent copper via
stack.thick_scale - the tall solder column at the wall pulls the joint
vicinity to lead potential (equivalent to extending the barrel wall
vertically), the taper carries the radial spreading. DC-exact additive
conductance; at f > 0 the factor multiplies the skin-corrected sheet
conductance like the via mouths (documented approximation).
The protrusion side is looked up from the owning footprint (pads store
absolute positions; component on F.Cu -> lead tents on B.Cu), with a
logged B.Cu fallback. Dump schema gains protrusion_side and
tht_protrusion_nm (defaults keep older v6 dumps loading).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Selected vias and through-hole pads inject at the drill-wall ring on
every spanned layer (both contact models), not the whole pad face,
so the pad/pour spreading resistance is part of the result. Vias are
now selectable as contacts.
- Soldered THT joints: the hole is modeled solder-filled (core in
parallel with the plating, also for stitching THT barrels) and the
pad face carries an average-thickness solder coat over the modeled
copper (SOLDER_THICKNESS_UM).
- Vias with drills above a configurable threshold (dialog field,
default CAP_MAX_DRILL_MM = 0.5) keep open mouths even with capping
selected - the fab caps only small vias.
- Geometry dump schema v6: electrode barrel fields, cap_max_drill_nm.
- Verified against R = rho/(pi t)*acosh(d/2a) for two circular contacts
on a sheet (+2.6% at h = 0.15 mm, a = 1 mm; uniform model above the
equipotential one as required by the contact bracket).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Swarm-review bug fixes: empty-layer crashes on the adaptive grid,
teardrop fills included in the copper model, exact track bbox, correct
AC current-density normalization, true 1D-trace current density.
Mutable tags on third-party actions could be repointed at code that
runs with the repo token; pin checkout, upload-artifact and
gitea-release-action to their current commits.
Every push to main builds the addon zip + registry metadata as a CI
artifact; pushing a v<version> tag (checked against metadata.json)
creates a release with both attached, matching the download_url
convention build_package.py already writes.
Follow the swarm-review fixes: chain cells display the true in-trace
current density, teardrop zones conduct, and AC current-density maps
are referenced to the skin-reduced equivalent thickness.
- adaptive: skip layers with zero quadtree leaves in the connectivity
restriction and mesh-boundary loops (IndexError on boards where a
selected layer has no copper)
- board_io: accept ZT_TEARDROP zones as conducting copper (KiCad types
teardrop fills ZT_TEARDROP, never ZT_COPPER, so they were dropped)
- geometry: copper_bbox uses the exact stroke bbox (centerline extrema
+ half width) instead of a 100 um chord tessellation that could
undershoot arc/cap extrema past the raster guard margin
- solver/adaptive: reference |J| to the conduction-equivalent thickness
sigma*rho in every branch (the uniform branch used geometric t, so AC
plots changed scale ~rs_ratio depending on unrelated per-cell maps)
- solver/adaptive/raster: chain cells no longer show phantom sheet-face
currents; store dl per chain link and overlay the true 1D density
|dV|/(rho*dl) (exact at any frequency: AC scaling of link conductance
and cross-section cancels)
- test_quadtree: compare edge lists pair-for-pair (the independent
column sort destroyed endpoint association)
Multi-layer figures (raster map, potential, current density, power
density) get a layer checkbox panel on interactive backends: unticking
a layer removes its row and the remaining rows grow to fill the window
(constrained layout reflows). At least one layer stays visible; saved
PNGs always contain every layer, with the panel hidden.
The four figure builders are restructured around per-layer painters
plus a finalize hook (legend/colorbar/annotations), driven by a shared
_layer_windows redraw loop; single-layer figures and non-interactive
runs are unchanged. The toggle path is exercised headlessly through
the real CheckButtons callback in the visual check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two display fixes for the adaptive grid, from field feedback:
- Equipotential contours showed leaf-sized staircase corners on plane
interiors: the potential is now expanded piecewise-LINEARLY from each
leaf's reconstructed gradient instead of constant-per-leaf, and the
default ADAPTIVE_MAX_CELL_UM drops 2 mm -> 1 mm (interior leaves
beyond that buy almost nothing).
- The raster map now overlays the adaptive mesh: boundaries of coarse
leaves draw in darker copper (fine regions stay plain = fully
resolved), with a legend entry. Uniform-grid runs are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ADAPTIVE_CELLS defaults to True (dialog checkbox stays; untick or
--no-adaptive for the uniform reference grid). With the adaptive grid
the auto cell sizer targets TARGET_CELLS_ADAPTIVE (8M fine cells,
~2x finer h) since unknowns no longer scale with the fine cell count -
memory of the masks/field arrays is the new bound.
The test suite pins ADAPTIVE_CELLS off via an autouse conftest fixture:
the exact-value tests define the uniform reference grid; adaptive tests
opt in per test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>