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 <noreply@anthropic.com>
This commit is contained in:
@@ -132,12 +132,14 @@ SWIG API. Requires KiCad **10.0.1+**.
|
|||||||
minimum-dissipation one, AC results are a rigorous **lower bound**.
|
minimum-dissipation one, AC results are a rigorous **lower bound**.
|
||||||
Rule of thumb for 70 µm foil: skin is negligible below ~300 kHz
|
Rule of thumb for 70 µm foil: skin is negligible below ~300 kHz
|
||||||
(δ = 173 µm at 142 kHz), ~+11 % at 1 MHz.
|
(δ = 173 µm at 142 kHz), ~+11 % at 1 MHz.
|
||||||
- 5-point FDM per layer on an auto-sized shared grid (~2 M cells total
|
- 5-point FDM per layer on an auto-sized shared grid (~2 M fine cells
|
||||||
across layers by default). Direct sparse solve up to 500 k unknowns,
|
with the uniform grid; ~8 M with the adaptive grid, whose unknown
|
||||||
AMG-preconditioned CG (pyamg) above — Jacobi-CG if pyamg is missing.
|
count no longer scales with them). Direct sparse solve up to 500 k
|
||||||
Discretization error typically ≲ 2 % at defaults — halve the cell size
|
unknowns, AMG-preconditioned CG (pyamg) above — Jacobi-CG if pyamg is
|
||||||
and compare to judge convergence.
|
missing. Discretization error typically ≲ 2 % at defaults — halve the
|
||||||
- **Adaptive cells** (dialog checkbox, off by default; `ADAPTIVE_CELLS`):
|
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,
|
solves on a 2:1-balanced quadtree — fine cells at copper boundaries,
|
||||||
electrodes, traces, via mouths and buildup, blocks up to
|
electrodes, traces, via mouths and buildup, blocks up to
|
||||||
`ADAPTIVE_MAX_CELL_UM` (2 mm) in plane interiors (`ADAPTIVE_GUARD`
|
`ADAPTIVE_MAX_CELL_UM` (2 mm) in plane interiors (`ADAPTIVE_GUARD`
|
||||||
|
|||||||
+11
@@ -1,4 +1,15 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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)
|
||||||
|
|||||||
@@ -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
|
ALWAYS_REFILL = False # refill zones even if KiCad says they are filled
|
||||||
|
|
||||||
# --- Adaptive grid ---
|
# --- 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,
|
# copper boundaries/electrodes/features,
|
||||||
# coarse plane interiors (dialog-toggleable).
|
# coarse plane interiors (dialog-toggleable).
|
||||||
# Slight low bias (~0.5-1% on feature-dense
|
# With the deferred-correction pass the
|
||||||
# boards); large speed/memory wins on pours
|
# 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
|
ADAPTIVE_MAX_CELL_UM = 2000.0 # coarsest leaf edge length. The MINIMUM
|
||||||
# element size is the grid cell size itself
|
# element size is the grid cell size itself
|
||||||
# (auto / dialog / CELL_UM_OVERRIDE). Rarely
|
# (auto / dialog / CELL_UM_OVERRIDE). Rarely
|
||||||
|
|||||||
@@ -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
|
h = config.CELL_UM_OVERRIDE * 1000.0
|
||||||
else:
|
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)
|
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
|
ncells = math.ceil(w / h) * math.ceil(ht / h) * nlayers
|
||||||
|
|||||||
@@ -47,9 +47,11 @@ def main(argv=None) -> int:
|
|||||||
ap.add_argument("--force-iterative", action="store_true",
|
ap.add_argument("--force-iterative", action="store_true",
|
||||||
help="use the iterative solver (AMG-CG, or Jacobi-CG "
|
help="use the iterative solver (AMG-CG, or Jacobi-CG "
|
||||||
"without pyamg) regardless of problem size")
|
"without pyamg) regardless of problem size")
|
||||||
ap.add_argument("--adaptive", action="store_true",
|
ap.add_argument("--adaptive", action=argparse.BooleanOptionalAction,
|
||||||
help="solve on the adaptive quadtree grid (coarse "
|
default=None,
|
||||||
"plane interiors)")
|
help="adaptive quadtree grid (coarse plane interiors); "
|
||||||
|
"default: config (on). --no-adaptive forces the "
|
||||||
|
"uniform reference grid")
|
||||||
args = ap.parse_args(argv)
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
if args.cell_um is not None:
|
if args.cell_um is not None:
|
||||||
@@ -58,8 +60,8 @@ def main(argv=None) -> int:
|
|||||||
config.INTERACTIVE = False
|
config.INTERACTIVE = False
|
||||||
if args.force_iterative:
|
if args.force_iterative:
|
||||||
config.SPSOLVE_MAX_UNKNOWNS = 0
|
config.SPSOLVE_MAX_UNKNOWNS = 0
|
||||||
if args.adaptive:
|
if args.adaptive is not None:
|
||||||
config.ADAPTIVE_CELLS = True
|
config.ADAPTIVE_CELLS = args.adaptive
|
||||||
|
|
||||||
problem = load_problem(args.dump)
|
problem = load_problem(args.dump)
|
||||||
if args.strip_buildup:
|
if args.strip_buildup:
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://go.kicad.org/pcm/schemas/v2",
|
"$schema": "https://go.kicad.org/pcm/schemas/v2",
|
||||||
"name": "Fill Resistance",
|
"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": "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",
|
"identifier": "th.co.b4l.fill-resistance",
|
||||||
"type": "plugin",
|
"type": "plugin",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -165,6 +165,22 @@ def test_part_currents_and_ac(monkeypatch):
|
|||||||
assert ada.rs_ratios == ref.rs_ratios
|
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):
|
def test_max_cell_size_respected(monkeypatch):
|
||||||
from fill_resistance.adaptive import _max_block
|
from fill_resistance.adaptive import _max_block
|
||||||
assert _max_block(100_000.0) == 16 # 2000 um / 100 um cells
|
assert _max_block(100_000.0) == 16 # 2000 um / 100 um cells
|
||||||
|
|||||||
Reference in New Issue
Block a user