Files
kicad-zone-resistance/fill_resistance/standalone.py
T
janikandClaude Fable 5 26b1cfaa45
tests / archlinux:latest (push) Successful in 39s
tests / debian:12 (push) Successful in 1m17s
tests / fedora:latest (push) Successful in 1m8s
tests / ubuntu:24.04 (push) Successful in 1m23s
tests / ubuntu-latest · py3.11 (push) Successful in 1m25s
tests / ubuntu-latest · py3.13 (push) Successful in 1m5s
tests / NixOS (FHS wrapper from docs/NIXOS.md) (push) Skipped
Build PCM package / build (push) Successful in 11s
Release 1.4.0: PDN mode, the config-file workflow, and the dialog editor
Multiple Thevenin supplies and prescribed-current loads on one net,
solved in absolute volts with the Tellegen power balance verified per
run; a source-sink pair table (effective copper resistance per
supply x load pair plus an exactly-summing proportional-sharing loss
attribution), in summary.txt and as its own figure. Bonded terminals
short a package's contacts into one lug so the per-pin split becomes
a solve outcome. Geometry dumps carry the terminal set (schema v8).

The dialog gained a Classic/PDN mode selector and a full PDN editor:
per-role supply/load tables built from the marker rectangles (or a
config's terminal set, which never pins mode or net), with Component
hints, per-terminal Layer scopes, Active checkboxes, comments, a
per-net row filter, resizable tables and a scrolling, screen-sized
dialog. Numbers accept SI suffixes (50m, 4.7k) everywhere.

fill_res_config.json fully specifies a run (classic or PDN) with
validation, comments, named side-by-side configs (the one called
default auto-loads), Load/Save buttons with an editable file name,
and saves that never drop anything drawn on the board.

347 tests, green on Python 3.13 and on the 3.9 macOS wheel stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 17:01:24 +07:00

153 lines
6.9 KiB
Python

"""Offline runner: solve a geometry_dump.json without KiCad.
python -m fill_resistance.standalone dump.json [--current 40]
[--cell-um 50] [--layers F.Cu,In1.Cu] [--no-show] [--out DIR]
[--force-iterative]
This is the dev loop and the convergence-study tool (KiCad 10 has no
headless API server, so the plugin path always needs the GUI).
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from . import config, pipeline, progress
from .errors import UserFacingError
from .geometry import load_problem
from .skin import parse_engineering, parse_frequency
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("dump", type=Path, help="geometry_dump.json from a plugin run")
ap.add_argument("--current", type=parse_engineering, default=None,
help="test current [A], SI suffixes ok (500m = 0.5) "
"(default: config TEST_CURRENT_A)")
ap.add_argument("--config", type=Path, default=None, metavar="JSON",
help="fill_res_config.json whose run parameters act "
"as defaults under the explicit flags here. Only "
"re-solve parameters apply - the dump already "
"bakes the geometry and physics")
ap.add_argument("--freq", type=parse_frequency, default=None,
help="frequency, e.g. 142k or 1.5M (default: DC). "
"Skin resistance only, a lower bound - not AC "
"impedance (no proximity, no inductance)")
ap.add_argument("--cell-um", type=parse_engineering, default=None,
help="force grid cell size [um]")
ap.add_argument("--layers", type=str, default=None,
help="comma-separated subset of layers to include")
ap.add_argument("--out", type=Path, default=None,
help="output directory (default: next to the dump)")
ap.add_argument("--no-show", action="store_true",
help="save PNGs only, no windows")
ap.add_argument("--contact-model", choices=["uniform", "equipotential"],
default=None, help="contact model (default: config); "
"ignored for PDN dumps (models fixed there)")
ap.add_argument("--v-nominal", type=parse_engineering, default=None,
help="PDN dumps: default supply open-circuit voltage "
"[V] (default: config PDN_V_NOMINAL); supplies "
"with their own v_oc keep it")
ap.add_argument("--strip-buildup", action="store_true",
help="ignore solder buildup stored in the dump")
ap.add_argument("--uncapped", action="store_true",
help="treat vias as uncapped (open drill mouths on "
"all layers)")
ap.add_argument("--cap-max-drill", type=float, default=None,
metavar="MM",
help="cap only vias with drill <= this [mm]; larger "
"drills stay open (default: from the dump)")
ap.add_argument("--extra-cu-um", type=float, default=None,
help="override the added copper in mask openings [um]")
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); "
"default: config (on). --no-adaptive forces the "
"uniform reference grid")
args = ap.parse_args(argv)
if args.config is not None:
from .configfile import load_config
try:
cfg = load_config(args.config)
except UserFacingError as e:
print(f"ERROR: {e}", file=sys.stderr)
return 1
# config file values fill in only where no explicit flag was
# given (CLI wins); geometry/physics stay as baked into the dump
if args.current is None and cfg.current_a is not None:
args.current = cfg.current_a
if args.freq is None and cfg.freq_hz is not None:
args.freq = cfg.freq_hz
if args.cell_um is None and cfg.cell_um_given:
args.cell_um = cfg.cell_um
if args.adaptive is None and cfg.adaptive is not None:
args.adaptive = cfg.adaptive
if args.contact_model is None and cfg.contact_model is not None:
args.contact_model = cfg.contact_model
if args.v_nominal is None and cfg.v_nominal is not None:
args.v_nominal = cfg.v_nominal
if args.layers is None and cfg.layers:
args.layers = ",".join(cfg.layers)
if args.freq is None:
args.freq = 0.0
if args.cell_um is not None:
config.CELL_UM_OVERRIDE = args.cell_um
if args.no_show:
config.INTERACTIVE = False
if args.force_iterative:
config.SPSOLVE_MAX_UNKNOWNS = 0
if args.adaptive is not None:
config.ADAPTIVE_CELLS = args.adaptive
problem = load_problem(args.dump)
if args.strip_buildup:
problem.buildups = []
if args.uncapped:
problem.vias_capped = False
if args.cap_max_drill is not None:
problem.cap_max_drill_nm = int(args.cap_max_drill * 1e6)
if args.extra_cu_um is not None:
problem.extra_cu_nm = int(args.extra_cu_um * 1000)
if args.layers:
keep = [s.strip() for s in args.layers.split(",")]
problem.layers = [l for l in problem.layers if l.layer_name in keep]
problem.tracks = [t for t in problem.tracks if t.layer_name in keep]
if not problem.layers:
print(f"ERROR: no layer of the dump matches --layers {args.layers}",
file=sys.stderr)
return 1
outdir = args.out if args.out is not None else args.dump.parent
if args.progress:
progress.start()
try:
if problem.terminals:
print(f"PDN dump: {len(problem.terminals)} terminals "
f"(supplies/loads from the dump; --current is ignored)")
pipeline.run(problem, outdir, show=not args.no_show,
i_test=args.current, freq_hz=args.freq,
contact_model=args.contact_model,
v_nominal=args.v_nominal)
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
if __name__ == "__main__":
sys.exit(main())