Files
kicad-zone-resistance/fill_resistance/standalone.py
T
janik 92bb29637f Model sub-resolution traces as exact 1D resistor chains
Tracks are now first-class Problem objects (TrackSeg: centerline +
width, dump schema v5), so the wide/narrow decision replays at raster
time: traces at least TRACK_1D_FACTOR (3) cells wide rasterize from
their outline as before; narrower ones mark the cells their centerline
crosses as copper and connect them with explicit conductance links
carrying the trace's TRUE arc length per link - no staircase inflation
for diagonals or arcs, and no discretization error in the trace R, at
any grid size. Links across cells already joined by pour faces are
skipped (union, not sum); chain-only cells get no sheet faces (their
copper is narrower than a cell). Electrodes, via barrels, connectivity
restriction and the skin-effect scaling all work on chain cells
unchanged.

This removes the need to shrink the cell size for thin traces: a 0.2 mm
bridge at 500 um cells now matches its finely-rasterized ground truth
within a few percent (tested), including diagonal and arc traces.

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

83 lines
3.4 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("--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.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())