From 1a28f2a593dfceb79149b8b7d59386a905d33a67 Mon Sep 17 00:00:00 2001 From: janik Date: Wed, 15 Jul 2026 18:12:52 +0700 Subject: [PATCH] Smooth adaptive potential maps and draw the mesh on the raster figure Two display fixes for the adaptive grid, from field feedback: - Equipotential contours showed leaf-sized staircase corners on plane interiors: the potential is now expanded piecewise-LINEARLY from each leaf's reconstructed gradient instead of constant-per-leaf, and the default ADAPTIVE_MAX_CELL_UM drops 2 mm -> 1 mm (interior leaves beyond that buy almost nothing). - The raster map now overlays the adaptive mesh: boundaries of coarse leaves draw in darker copper (fine regions stay plain = fully resolved), with a legend entry. Uniform-grid runs are unchanged. Co-Authored-By: Claude Fable 5 --- fill_resistance/adaptive.py | 25 ++++++++++++++++++++++++- fill_resistance/config.py | 11 ++++++----- fill_resistance/plots.py | 11 +++++++++-- fill_resistance/raster.py | 2 ++ tests/test_adaptive.py | 22 +++++++++++++++++++--- 5 files changed, 60 insertions(+), 11 deletions(-) diff --git a/fill_resistance/adaptive.py b/fill_resistance/adaptive.py index 6290254..3eef0a8 100644 --- a/fill_resistance/adaptive.py +++ b/fill_resistance/adaptive.py @@ -385,6 +385,25 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack, part_currents1 = part_currents(parts1, e1n, int(e1.sum())) part_currents2 = part_currents(parts2, e2n, int(e2.sum())) + # leaf boundaries for the raster map: draw the coarse mesh structure + # (fine regions stay plain copper = fully resolved) + stack.mesh = np.zeros_like(stack.masks) + for li in range(L): + ids = grids[li].id_grid + b = np.zeros_like(stack.masks[li]) + b[:, 1:] |= ids[:, 1:] != ids[:, :-1] + b[1:, :] |= ids[1:, :] != ids[:-1, :] + coarse = grids[li].size[np.maximum(ids, 0)] >= 2 + stack.mesh[li] = b & coarse & stack.masks[li] + + # piecewise-LINEAR potential expansion from the leaf gradients of the + # final solution: constant-per-leaf expansion shows leaf-sized + # staircase corners in the equipotential contours on coarse interiors + if faces.any(): + dgx, dgy = _leaf_gradients(N, fa, fb, cxg, cyg, Vflat) + else: + dgx = dgy = np.zeros(N) + V3 = np.full((L, ny, nx), np.nan) J3 = np.full((L, ny, nx), np.nan) Parea = np.full((L, ny, nx), np.nan) @@ -393,7 +412,11 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack, ids = g_.id_grid m = stack.masks[li] Vl = Vflat[offs[li]:offs[li + 1]] - V3[li][m] = Vl[ids[m]] * s + ii, jj = np.nonzero(m) + gid = offs[li] + ids[ii, jj] + V3[li][ii, jj] = (Vflat[gid] + + dgx[gid] * (jj + 0.5 - cxg[gid]) + + dgy[gid] * (ii + 0.5 - cyg[gid])) * s sel = (e_axis >= 0) & (e_layer == li) la = (edges.a[sel] - offs[li]).astype(np.int64) diff --git a/fill_resistance/config.py b/fill_resistance/config.py index e298db5..8bde3b6 100644 --- a/fill_resistance/config.py +++ b/fill_resistance/config.py @@ -66,11 +66,12 @@ 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 - # needs tuning: the guard distance already - # caps leaf growth near features +ADAPTIVE_MAX_CELL_UM = 1000.0 # coarsest leaf edge length (also the + # granularity of the potential/field maps + # on plane interiors). The MINIMUM element + # size is the grid cell size itself (auto / + # dialog / CELL_UM_OVERRIDE). The guard + # distance caps leaf growth near features ADAPTIVE_GUARD = 4 # a leaf of size s needs >= GUARD*s cells of # clearance to the nearest feature ADAPTIVE_CORRECTION_PASSES = 1 # deferred-correction re-solves fixing the diff --git a/fill_resistance/plots.py b/fill_resistance/plots.py index 7434cef..57c2d0d 100644 --- a/fill_resistance/plots.py +++ b/fill_resistance/plots.py @@ -49,6 +49,7 @@ _E1_COLOR = "#c8385a" _E2_COLOR = "#2f6fb0" _VIA_COLOR = "#2d6b45" _SOLDER = "#9aa3ad" # tin-gray: solder buildup areas +_MESH = "#a56c33" # darker copper: adaptive leaf boundaries _INK = "#3a3a3a" _GRID_INK = "#b8b4ae" @@ -154,16 +155,20 @@ def _injection_area_labels(ax, li, layer_name, problem, result): def fig_raster(stack, e1, e2, problem, result=None): fig, axes = _layer_fig(stack, "Fill Resistance - rasterized map") - cmap = ListedColormap([_BG, _COPPER, _E1_COLOR, _E2_COLOR, _SOLDER]) + cmap = ListedColormap([_BG, _COPPER, _E1_COLOR, _E2_COLOR, _SOLDER, + _MESH]) has_buildup = stack.buildup is not None and stack.buildup.any() + has_mesh = stack.mesh is not None and stack.mesh.any() for li, ax in enumerate(axes): codes = np.zeros(stack.shape2d, dtype=np.uint8) codes[stack.masks[li]] = 1 if has_buildup: codes[stack.buildup[li]] = 4 + if has_mesh: + codes[stack.mesh[li]] = 5 codes[e1[li]] = 2 codes[e2[li]] = 3 - ax.imshow(codes, cmap=cmap, vmin=0, vmax=4, origin="upper", + ax.imshow(codes, cmap=cmap, vmin=0, vmax=5, origin="upper", extent=stack.extent_mm(), interpolation="nearest") _via_markers(ax, problem, problem.layers[li]) if result is not None and (result.part_currents1 @@ -174,6 +179,8 @@ def fig_raster(stack, e1, e2, problem, result=None): _electrode_labels(ax, stack, e1[li], e2[li]) handles = [Patch(fc=_COPPER, label="copper"), Patch(fc=_VIA_COLOR, label="vias")] + if has_mesh: + handles.append(Patch(fc=_MESH, label="adaptive mesh (coarse leaves)")) if has_buildup: handles.append(Patch( fc=_SOLDER, diff --git a/fill_resistance/raster.py b/fill_resistance/raster.py index b5f678c..c308908 100644 --- a/fill_resistance/raster.py +++ b/fill_resistance/raster.py @@ -46,6 +46,8 @@ class RasterStack: # copper-thickness factor (via # mouths: cap-thin or partially # drilled cells); None = all 1 + mesh: np.ndarray | None = None # bool (L, ny, nx): adaptive leaf + # boundaries (drawn on the raster map) @property def nlayers(self) -> int: diff --git a/tests/test_adaptive.py b/tests/test_adaptive.py index 6cc20dc..32806ed 100644 --- a/tests/test_adaptive.py +++ b/tests/test_adaptive.py @@ -124,7 +124,8 @@ def test_1d_trace_bridge(monkeypatch): ref = _run(prob(), 0.5, adaptive=False, monkeypatch=monkeypatch) ada = _run(prob(), 0.5, adaptive=True, monkeypatch=monkeypatch) - assert ada.n_free < 0.6 * ref.n_free + # max leaf = 1 mm = 2 cells at h = 0.5, so the coarsening is modest + assert ada.n_free < 0.7 * ref.n_free assert ada.R_ohm == pytest.approx(ref.R_ohm, rel=2e-3) @@ -183,8 +184,23 @@ def test_auto_cell_size_finer_with_adaptive(monkeypatch): 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 + assert _max_block(100_000.0) == 8 # 1000 um / 100 um cells monkeypatch.setattr(config, "ADAPTIVE_MAX_CELL_UM", 250.0) assert _max_block(100_000.0) == 2 monkeypatch.setattr(config, "ADAPTIVE_MAX_CELL_UM", 50.0) - assert _max_block(100_000.0) == 1 # never below the fine cell \ No newline at end of file + assert _max_block(100_000.0) == 1 # never below the fine cell + + +def test_potential_expansion_is_smooth(monkeypatch): + """The potential map is expanded piecewise-linearly from the leaf + gradients: on a strip it must track the uniform-grid potential to a + small fraction of the total span (constant-per-leaf expansion would + show leaf-sized steps of ~1-2% of the span).""" + p = strip_problem(length=50, width=10, e_len=5) + ref = _run(p, 0.25, adaptive=False, monkeypatch=monkeypatch) + p2 = strip_problem(length=50, width=10, e_len=5) + ada = _run(p2, 0.25, adaptive=True, monkeypatch=monkeypatch) + span = np.nanmax(ref.V) + both = np.isfinite(ref.V) & np.isfinite(ada.V) + dev = np.abs(ada.V[both] - ref.V[both]).max() + assert dev < 0.005 * span \ No newline at end of file