Files
kicad-zone-resistance/fill_resistance/standalone.py
T
janik d34d3ade51 Model via ring copper and drill mouths (capping), dialog-toggleable
Each via now contributes its ring/pad copper (full-thickness disc of
the pad diameter on every spanned layer) and its drill mouth: with
"vias filled + capped" (dialog checkbox, default on, VIAS_CAPPED) the
mouth carries a CAP_PLATING_UM (15 um) thin copper cap on the outer
layers and is an open hole on inner layers; unchecked, mouths are open
everywhere. Mouth coverage is area-weighted per cell (4x4
supersampling) through a per-cell thickness map feeding the existing
harmonic-mean face machinery, so sub-cell mouths perturb the sheet by
their true covered fraction instead of whole cells. Fully swallowed
cells leave the mask; the barrel then attaches through the ring via the
existing pad-footprint search. THT-pad copper and drills stay outside
the model. Ring discs paint before 1D trace chains (chains see them as
regular copper), mouths after wide tracks (drills go through trace
copper). standalone gains --uncapped.

Tests: cap==foil identity against the feature-off reference, strict
R(solid) < R(cap) < R(hole) ordering, ring bridging a fill gap that a
ringless barrel cannot cross, gentle sub-cell perturbation at coarse
grids, and JSON roundtrip of the new fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 16:42:21 +07:00

88 lines
3.6 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
from .errors import UserFacingError
from .geometry import load_problem
from .skin import 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=float, default=None,
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)")
ap.add_argument("--cell-um", type=float, 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)")
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("--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")
args = ap.parse_args(argv)
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
problem = load_problem(args.dump)
if args.strip_buildup:
problem.buildups = []
if args.uncapped:
problem.vias_capped = False
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
try:
pipeline.run(problem, outdir, show=not args.no_show,
i_test=args.current, freq_hz=args.freq,
contact_model=args.contact_model)
except UserFacingError as e:
print(f"ERROR: {e}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())