d34d3ade51
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>
88 lines
3.6 KiB
Python
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())
|