92bb29637f
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>
83 lines
3.4 KiB
Python
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())
|