"""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())