From 07ab59baadb375f5cd58bcb2f27630326dda0a74 Mon Sep 17 00:00:00 2001 From: janik Date: Wed, 15 Jul 2026 17:57:12 +0700 Subject: [PATCH] Adaptive grid on by default; finer auto cell size under it ADAPTIVE_CELLS defaults to True (dialog checkbox stays; untick or --no-adaptive for the uniform reference grid). With the adaptive grid the auto cell sizer targets TARGET_CELLS_ADAPTIVE (8M fine cells, ~2x finer h) since unknowns no longer scale with the fine cell count - memory of the masks/field arrays is the new bound. The test suite pins ADAPTIVE_CELLS off via an autouse conftest fixture: the exact-value tests define the uniform reference grid; adaptive tests opt in per test. Co-Authored-By: Claude Fable 5 --- README.md | 14 ++++++++------ conftest.py | 11 +++++++++++ fill_resistance/config.py | 11 ++++++++--- fill_resistance/raster.py | 6 +++++- fill_resistance/standalone.py | 12 +++++++----- metadata.json | 2 +- tests/test_adaptive.py | 16 ++++++++++++++++ 7 files changed, 56 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fb9e159..d2e15a6 100644 --- a/README.md +++ b/README.md @@ -132,12 +132,14 @@ SWIG API. Requires KiCad **10.0.1+**. minimum-dissipation one, AC results are a rigorous **lower bound**. Rule of thumb for 70 µm foil: skin is negligible below ~300 kHz (δ = 173 µm at 142 kHz), ~+11 % at 1 MHz. -- 5-point FDM per layer on an auto-sized shared grid (~2 M cells total - across layers by default). Direct sparse solve up to 500 k unknowns, - AMG-preconditioned CG (pyamg) above — Jacobi-CG if pyamg is missing. - Discretization error typically ≲ 2 % at defaults — halve the cell size - and compare to judge convergence. -- **Adaptive cells** (dialog checkbox, off by default; `ADAPTIVE_CELLS`): +- 5-point FDM per layer on an auto-sized shared grid (~2 M fine cells + with the uniform grid; ~8 M with the adaptive grid, whose unknown + count no longer scales with them). Direct sparse solve up to 500 k + unknowns, AMG-preconditioned CG (pyamg) above — Jacobi-CG if pyamg is + missing. Discretization error typically ≲ 2 % at defaults — halve the + cell size and compare to judge convergence. +- **Adaptive cells** (dialog checkbox, **on by default**; + `ADAPTIVE_CELLS`): solves on a 2:1-balanced quadtree — fine cells at copper boundaries, electrodes, traces, via mouths and buildup, blocks up to `ADAPTIVE_MAX_CELL_UM` (2 mm) in plane interiors (`ADAPTIVE_GUARD` diff --git a/conftest.py b/conftest.py index 2ca7a99..7a283b8 100644 --- a/conftest.py +++ b/conftest.py @@ -1,4 +1,15 @@ import os import sys +import pytest + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + + +@pytest.fixture(autouse=True) +def _uniform_grid_default(monkeypatch): + """The exact-value test suite is the UNIFORM reference grid; the + adaptive default (config.ADAPTIVE_CELLS = True) is pinned off here. + Adaptive tests opt back in per test via monkeypatch.""" + from fill_resistance import config + monkeypatch.setattr(config, "ADAPTIVE_CELLS", False) diff --git a/fill_resistance/config.py b/fill_resistance/config.py index 00997e3..e298db5 100644 --- a/fill_resistance/config.py +++ b/fill_resistance/config.py @@ -56,11 +56,16 @@ ELECTRODE_NEG_LAYER = "User.2" # rectangles on this layer mark V- contact parts ALWAYS_REFILL = False # refill zones even if KiCad says they are filled # --- Adaptive grid --- -ADAPTIVE_CELLS = False # solve on a 2:1-balanced quadtree: fine at +ADAPTIVE_CELLS = True # solve on a 2:1-balanced quadtree: fine at # copper boundaries/electrodes/features, # coarse plane interiors (dialog-toggleable). - # Slight low bias (~0.5-1% on feature-dense - # boards); large speed/memory wins on pours + # With the deferred-correction pass the + # deviation from the uniform grid is <0.03% + # measured; untick for the reference grid +TARGET_CELLS_ADAPTIVE = 8_000_000 # auto cell-size budget with the adaptive + # grid: unknowns no longer scale with the + # fine cell count, so the auto sizer picks + # a ~2x finer h (memory-bound: masks/fields) ADAPTIVE_MAX_CELL_UM = 2000.0 # coarsest leaf edge length. The MINIMUM # element size is the grid cell size itself # (auto / dialog / CELL_UM_OVERRIDE). Rarely diff --git a/fill_resistance/raster.py b/fill_resistance/raster.py index e3e296e..b5f678c 100644 --- a/fill_resistance/raster.py +++ b/fill_resistance/raster.py @@ -97,7 +97,11 @@ def choose_cell_size(bbox_nm: tuple[int, int, int, int], nlayers: int) -> float: ) h = config.CELL_UM_OVERRIDE * 1000.0 else: - h = math.sqrt(w * ht * nlayers / config.TARGET_CELLS) + # the adaptive grid decouples unknowns from the fine cell count, + # so its auto sizing affords a larger fine-cell budget (finer h) + target = (config.TARGET_CELLS_ADAPTIVE if config.ADAPTIVE_CELLS + else config.TARGET_CELLS) + h = math.sqrt(w * ht * nlayers / target) h = min(max(h, config.MIN_CELL_UM * 1000.0), config.MAX_CELL_UM * 1000.0) ncells = math.ceil(w / h) * math.ceil(ht / h) * nlayers diff --git a/fill_resistance/standalone.py b/fill_resistance/standalone.py index d43569f..1e7ba3a 100644 --- a/fill_resistance/standalone.py +++ b/fill_resistance/standalone.py @@ -47,9 +47,11 @@ def main(argv=None) -> int: 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("--adaptive", action="store_true", - help="solve on the adaptive quadtree grid (coarse " - "plane interiors)") + 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.cell_um is not None: @@ -58,8 +60,8 @@ def main(argv=None) -> int: config.INTERACTIVE = False if args.force_iterative: config.SPSOLVE_MAX_UNKNOWNS = 0 - if args.adaptive: - config.ADAPTIVE_CELLS = True + if args.adaptive is not None: + config.ADAPTIVE_CELLS = args.adaptive problem = load_problem(args.dump) if args.strip_buildup: diff --git a/metadata.json b/metadata.json index 8e8fd7f..4c1b17c 100644 --- a/metadata.json +++ b/metadata.json @@ -2,7 +2,7 @@ "$schema": "https://go.kicad.org/pcm/schemas/v2", "name": "Fill Resistance", "description": "DC/AC resistance of copper zone fills and traces between two contacts, single- or multi-layer with via coupling; current and power density maps.", - "description_full": "Computes the DC or AC resistance of copper zone fills and traces between two contacts (marker rectangles on User.1/User.2 and/or selected pads), single- or multi-layer: the chosen net's fills and tracks are solved as coupled finite-difference sheets linked by the net's via and through-hole-pad barrels; traces narrower than the grid become exact 1D resistor chains.\n\nShows per-layer rasterized maps, potential, current density and power density, reports per-via currents (via ampacity) and total dissipation at a selectable test current. At a user-set frequency the exact 1D foil/barrel skin-effect correction is applied (AC results are a rigorous lower bound). PNGs, a text summary and a re-solvable geometry dump are saved per run.\n\nNote: the first load builds the plugin's Python environment (numpy, scipy, pyamg, matplotlib, PySide6) and can take several minutes.", + "description_full": "Computes the DC or AC resistance of copper zone fills and traces between two contacts (marker rectangles on User.1/User.2 and/or selected pads), single- or multi-layer: the chosen net's fills and tracks are solved as coupled finite-difference sheets linked by the net's via and through-hole-pad barrels; traces narrower than the grid become exact 1D resistor chains, and an adaptive multi-resolution grid (fine at features, coarse plane interiors, deferred-corrected) keeps large boards fast.\n\nShows per-layer rasterized maps, potential, current density and power density, reports per-via currents (via ampacity) and total dissipation at a selectable test current. At a user-set frequency the exact 1D foil/barrel skin-effect correction is applied (AC results are a rigorous lower bound). PNGs, a text summary and a re-solvable geometry dump are saved per run.\n\nNote: the first load builds the plugin's Python environment (numpy, scipy, pyamg, matplotlib, PySide6) and can take several minutes.", "identifier": "th.co.b4l.fill-resistance", "type": "plugin", "author": { diff --git a/tests/test_adaptive.py b/tests/test_adaptive.py index 42c8b94..6cc20dc 100644 --- a/tests/test_adaptive.py +++ b/tests/test_adaptive.py @@ -165,6 +165,22 @@ def test_part_currents_and_ac(monkeypatch): assert ada.rs_ratios == ref.rs_ratios +def test_auto_cell_size_finer_with_adaptive(monkeypatch): + """The auto sizer affords a larger fine-cell budget (finer h) when + the adaptive grid is on.""" + from fill_resistance import raster + p = make_problem([([(0, 0), (300, 0), (300, 200), (0, 200)], [])], + rect1_mm=(0, 90, 2, 110), rect2_mm=(298, 90, 300, 110)) + monkeypatch.setattr(config, "ADAPTIVE_CELLS", False) + h_uniform = raster.choose_cell_size(p.copper_bbox(), 1) + monkeypatch.setattr(config, "ADAPTIVE_CELLS", True) + h_adaptive = raster.choose_cell_size(p.copper_bbox(), 1) + assert h_adaptive < h_uniform + assert h_adaptive == pytest.approx( + h_uniform / (config.TARGET_CELLS_ADAPTIVE + / config.TARGET_CELLS) ** 0.5, rel=1e-9) + + def test_max_cell_size_respected(monkeypatch): from fill_resistance.adaptive import _max_block assert _max_block(100_000.0) == 16 # 2000 um / 100 um cells