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 <noreply@anthropic.com>
This commit is contained in:
@@ -385,6 +385,25 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack,
|
|||||||
part_currents1 = part_currents(parts1, e1n, int(e1.sum()))
|
part_currents1 = part_currents(parts1, e1n, int(e1.sum()))
|
||||||
part_currents2 = part_currents(parts2, e2n, int(e2.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)
|
V3 = np.full((L, ny, nx), np.nan)
|
||||||
J3 = np.full((L, ny, nx), np.nan)
|
J3 = np.full((L, ny, nx), np.nan)
|
||||||
Parea = 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
|
ids = g_.id_grid
|
||||||
m = stack.masks[li]
|
m = stack.masks[li]
|
||||||
Vl = Vflat[offs[li]:offs[li + 1]]
|
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)
|
sel = (e_axis >= 0) & (e_layer == li)
|
||||||
la = (edges.a[sel] - offs[li]).astype(np.int64)
|
la = (edges.a[sel] - offs[li]).astype(np.int64)
|
||||||
|
|||||||
@@ -66,11 +66,12 @@ TARGET_CELLS_ADAPTIVE = 8_000_000 # auto cell-size budget with the adaptive
|
|||||||
# grid: unknowns no longer scale with the
|
# grid: unknowns no longer scale with the
|
||||||
# fine cell count, so the auto sizer picks
|
# fine cell count, so the auto sizer picks
|
||||||
# a ~2x finer h (memory-bound: masks/fields)
|
# a ~2x finer h (memory-bound: masks/fields)
|
||||||
ADAPTIVE_MAX_CELL_UM = 2000.0 # coarsest leaf edge length. The MINIMUM
|
ADAPTIVE_MAX_CELL_UM = 1000.0 # coarsest leaf edge length (also the
|
||||||
# element size is the grid cell size itself
|
# granularity of the potential/field maps
|
||||||
# (auto / dialog / CELL_UM_OVERRIDE). Rarely
|
# on plane interiors). The MINIMUM element
|
||||||
# needs tuning: the guard distance already
|
# size is the grid cell size itself (auto /
|
||||||
# caps leaf growth near features
|
# 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
|
ADAPTIVE_GUARD = 4 # a leaf of size s needs >= GUARD*s cells of
|
||||||
# clearance to the nearest feature
|
# clearance to the nearest feature
|
||||||
ADAPTIVE_CORRECTION_PASSES = 1 # deferred-correction re-solves fixing the
|
ADAPTIVE_CORRECTION_PASSES = 1 # deferred-correction re-solves fixing the
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ _E1_COLOR = "#c8385a"
|
|||||||
_E2_COLOR = "#2f6fb0"
|
_E2_COLOR = "#2f6fb0"
|
||||||
_VIA_COLOR = "#2d6b45"
|
_VIA_COLOR = "#2d6b45"
|
||||||
_SOLDER = "#9aa3ad" # tin-gray: solder buildup areas
|
_SOLDER = "#9aa3ad" # tin-gray: solder buildup areas
|
||||||
|
_MESH = "#a56c33" # darker copper: adaptive leaf boundaries
|
||||||
_INK = "#3a3a3a"
|
_INK = "#3a3a3a"
|
||||||
_GRID_INK = "#b8b4ae"
|
_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):
|
def fig_raster(stack, e1, e2, problem, result=None):
|
||||||
fig, axes = _layer_fig(stack, "Fill Resistance - rasterized map")
|
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_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):
|
for li, ax in enumerate(axes):
|
||||||
codes = np.zeros(stack.shape2d, dtype=np.uint8)
|
codes = np.zeros(stack.shape2d, dtype=np.uint8)
|
||||||
codes[stack.masks[li]] = 1
|
codes[stack.masks[li]] = 1
|
||||||
if has_buildup:
|
if has_buildup:
|
||||||
codes[stack.buildup[li]] = 4
|
codes[stack.buildup[li]] = 4
|
||||||
|
if has_mesh:
|
||||||
|
codes[stack.mesh[li]] = 5
|
||||||
codes[e1[li]] = 2
|
codes[e1[li]] = 2
|
||||||
codes[e2[li]] = 3
|
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")
|
extent=stack.extent_mm(), interpolation="nearest")
|
||||||
_via_markers(ax, problem, problem.layers[li])
|
_via_markers(ax, problem, problem.layers[li])
|
||||||
if result is not None and (result.part_currents1
|
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])
|
_electrode_labels(ax, stack, e1[li], e2[li])
|
||||||
handles = [Patch(fc=_COPPER, label="copper"),
|
handles = [Patch(fc=_COPPER, label="copper"),
|
||||||
Patch(fc=_VIA_COLOR, label="vias")]
|
Patch(fc=_VIA_COLOR, label="vias")]
|
||||||
|
if has_mesh:
|
||||||
|
handles.append(Patch(fc=_MESH, label="adaptive mesh (coarse leaves)"))
|
||||||
if has_buildup:
|
if has_buildup:
|
||||||
handles.append(Patch(
|
handles.append(Patch(
|
||||||
fc=_SOLDER,
|
fc=_SOLDER,
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class RasterStack:
|
|||||||
# copper-thickness factor (via
|
# copper-thickness factor (via
|
||||||
# mouths: cap-thin or partially
|
# mouths: cap-thin or partially
|
||||||
# drilled cells); None = all 1
|
# drilled cells); None = all 1
|
||||||
|
mesh: np.ndarray | None = None # bool (L, ny, nx): adaptive leaf
|
||||||
|
# boundaries (drawn on the raster map)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nlayers(self) -> int:
|
def nlayers(self) -> int:
|
||||||
|
|||||||
+18
-2
@@ -124,7 +124,8 @@ def test_1d_trace_bridge(monkeypatch):
|
|||||||
|
|
||||||
ref = _run(prob(), 0.5, adaptive=False, monkeypatch=monkeypatch)
|
ref = _run(prob(), 0.5, adaptive=False, monkeypatch=monkeypatch)
|
||||||
ada = _run(prob(), 0.5, adaptive=True, 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)
|
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):
|
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) == 8 # 1000 um / 100 um cells
|
||||||
monkeypatch.setattr(config, "ADAPTIVE_MAX_CELL_UM", 250.0)
|
monkeypatch.setattr(config, "ADAPTIVE_MAX_CELL_UM", 250.0)
|
||||||
assert _max_block(100_000.0) == 2
|
assert _max_block(100_000.0) == 2
|
||||||
monkeypatch.setattr(config, "ADAPTIVE_MAX_CELL_UM", 50.0)
|
monkeypatch.setattr(config, "ADAPTIVE_MAX_CELL_UM", 50.0)
|
||||||
assert _max_block(100_000.0) == 1 # never below the fine cell
|
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
|
||||||
Reference in New Issue
Block a user