0afb55216b
- adaptive: skip layers with zero quadtree leaves in the connectivity restriction and mesh-boundary loops (IndexError on boards where a selected layer has no copper) - board_io: accept ZT_TEARDROP zones as conducting copper (KiCad types teardrop fills ZT_TEARDROP, never ZT_COPPER, so they were dropped) - geometry: copper_bbox uses the exact stroke bbox (centerline extrema + half width) instead of a 100 um chord tessellation that could undershoot arc/cap extrema past the raster guard margin - solver/adaptive: reference |J| to the conduction-equivalent thickness sigma*rho in every branch (the uniform branch used geometric t, so AC plots changed scale ~rs_ratio depending on unrelated per-cell maps) - solver/adaptive/raster: chain cells no longer show phantom sheet-face currents; store dl per chain link and overlay the true 1D density |dV|/(rho*dl) (exact at any frequency: AC scaling of link conductance and cross-section cancels) - test_quadtree: compare edge lists pair-for-pair (the independent column sort destroyed endpoint association)
142 lines
5.6 KiB
Python
142 lines
5.6 KiB
Python
"""Quadtree grid engine tests (phase 1): exact uniform limit against the
|
|
production graph and solver, partition/alignment/balance invariants, and
|
|
adaptive-vs-fine R agreement."""
|
|
import numpy as np
|
|
import pytest
|
|
from scipy import ndimage
|
|
|
|
from fill_resistance import quadtree, raster, solver
|
|
from tests.util import NM, make_problem, strip_problem
|
|
|
|
|
|
def _plate_with_holes(n=5, size_mm=40.0):
|
|
holes = []
|
|
pitch = size_mm / n
|
|
for i in range(n):
|
|
for j in range(n):
|
|
x, y = pitch * (i + 0.4), pitch * (j + 0.4)
|
|
holes.append([(x, y), (x + 1, y), (x + 1, y + 1), (x, y + 1)])
|
|
outline = [(0, 0), (size_mm, 0), (size_mm, size_mm), (0, size_mm)]
|
|
return make_problem([(outline, holes)],
|
|
rect1_mm=(0, 15, 2, 25),
|
|
rect2_mm=(size_mm - 2, 15, size_mm, 25))
|
|
|
|
|
|
def _solve_on_leaves(problem, stack, e1, e2, grid):
|
|
"""Equipotential mini-solve on the leaf graph, using the production
|
|
assembly and linear solver."""
|
|
ia, ib, g = quadtree.leaf_edges(grid, problem.sigma_s(0))
|
|
state = np.ones(grid.n, dtype=np.uint8)
|
|
for e, code in ((e1[0], 2), (e2[0], 3)):
|
|
ids = grid.id_grid[e]
|
|
state[ids[ids >= 0]] = code
|
|
edges = solver.Edges(a=ia, b=ib, w=g,
|
|
via_index=np.full(len(ia), -1, dtype=np.int32))
|
|
A, rhs, _ = solver._assemble(state, edges, None)
|
|
x, _ = solver.solve_system(A, rhs)
|
|
V = np.zeros(grid.n)
|
|
V[state == 2] = 1.0
|
|
V[state == 1] = x
|
|
Ie = g * (V[ia] - V[ib])
|
|
sa, sb = state[ia], state[ib]
|
|
I1 = float(Ie[sa == 2].sum() - Ie[sb == 2].sum())
|
|
I2 = float(Ie[sb == 3].sum() - Ie[sa == 3].sum())
|
|
return 1.0 / (0.5 * (I1 + I2))
|
|
|
|
|
|
def test_uniform_limit_graph_identical():
|
|
"""max_block=1: one leaf per cell, and the edge list matches the
|
|
production in-plane graph exactly (same pairs, conductance sigma)."""
|
|
p = _plate_with_holes()
|
|
stack = raster.rasterize_stack(p, 0.5 * NM)
|
|
grid = quadtree.build_leaves(stack.masks[0], max_block=1)
|
|
assert int(stack.masks.sum()) == grid.n
|
|
assert (grid.size == 1).all()
|
|
|
|
ny, nx = stack.shape2d
|
|
flat_of_leaf = grid.y0.astype(np.int64) * nx + grid.x0
|
|
ia, ib, g = quadtree.leaf_edges(grid, p.sigma_s(0))
|
|
ours = np.stack([
|
|
np.minimum(flat_of_leaf[ia], flat_of_leaf[ib]),
|
|
np.maximum(flat_of_leaf[ia], flat_of_leaf[ib])], axis=1)
|
|
|
|
edges = solver.build_edges(stack, p, [p.sigma_s(0)])
|
|
ref = np.stack([np.minimum(edges.a, edges.b),
|
|
np.maximum(edges.a, edges.b)], axis=1)
|
|
assert ours.shape == ref.shape
|
|
assert np.array_equal(np.sort(ours.view("i8,i8"), order=["f0", "f1"],
|
|
axis=0),
|
|
np.sort(ref.view("i8,i8"), order=["f0", "f1"],
|
|
axis=0))
|
|
assert np.allclose(g, p.sigma_s(0), rtol=0, atol=0)
|
|
|
|
|
|
def test_uniform_limit_R_matches_production():
|
|
p = strip_problem(length=50, width=10, e_len=5)
|
|
stack = raster.rasterize_stack(p, 0.25 * NM)
|
|
e1, e2 = raster.electrode_masks(stack, p)
|
|
ref = solver.run_solve(p, stack, e1, e2, 1.0,
|
|
contact_model="equipotential")
|
|
stack2 = raster.rasterize_stack(p, 0.25 * NM)
|
|
e1b, e2b = raster.electrode_masks(stack2, p)
|
|
grid = quadtree.build_leaves(stack2.masks[0], max_block=1)
|
|
R = _solve_on_leaves(p, stack2, e1b, e2b, grid)
|
|
assert R == pytest.approx(ref.R_ohm, rel=1e-12)
|
|
|
|
|
|
def test_partition_alignment_and_balance():
|
|
p = _plate_with_holes()
|
|
stack = raster.rasterize_stack(p, 0.1 * NM)
|
|
mask = stack.masks[0]
|
|
grid = quadtree.build_leaves(mask, max_block=32)
|
|
|
|
# exact partition of the copper
|
|
assert int((grid.size.astype(np.int64) ** 2).sum()) == int(mask.sum())
|
|
assert (grid.id_grid >= 0).sum() == int(mask.sum())
|
|
assert not (grid.id_grid[~mask] >= 0).any()
|
|
counts = np.bincount(grid.id_grid[grid.id_grid >= 0], minlength=grid.n)
|
|
assert np.array_equal(counts, grid.size.astype(np.int64) ** 2)
|
|
|
|
# power-of-two sizes, aligned to their own size
|
|
assert np.array_equal(grid.size & (grid.size - 1),
|
|
np.zeros_like(grid.size))
|
|
assert (grid.y0 % grid.size == 0).all()
|
|
assert (grid.x0 % grid.size == 0).all()
|
|
|
|
# 2:1 balance and real coarsening (max size is geometry-limited by
|
|
# the guard distance, not by max_block, on this feature-dense plate)
|
|
assert quadtree.balanced(grid)
|
|
assert grid.n < 0.5 * int(mask.sum())
|
|
assert grid.size.max() >= 4
|
|
|
|
|
|
def test_boundary_and_keep_fine_stay_fine():
|
|
p = _plate_with_holes()
|
|
stack = raster.rasterize_stack(p, 0.1 * NM)
|
|
mask = stack.masks[0]
|
|
keep = np.zeros_like(mask)
|
|
keep[50:60, 50:60] = True
|
|
grid = quadtree.build_leaves(mask, keep_fine=keep, max_block=32)
|
|
|
|
boundary = mask & ndimage.binary_dilation(~mask)
|
|
assert (grid.size[grid.id_grid[boundary]] == 1).all()
|
|
assert (grid.size[grid.id_grid[keep & mask]] == 1).all()
|
|
|
|
|
|
def test_adaptive_R_close_to_fine():
|
|
"""Adaptive leaves reproduce the fine-uniform R within 1% on the
|
|
holey plate (features everywhere - the adversarial case)."""
|
|
p = _plate_with_holes()
|
|
stack = raster.rasterize_stack(p, 0.1 * NM)
|
|
e1, e2 = raster.electrode_masks(stack, p)
|
|
ref = solver.run_solve(p, stack, e1, e2, 1.0,
|
|
contact_model="equipotential")
|
|
|
|
stack2 = raster.rasterize_stack(p, 0.1 * NM)
|
|
e1b, e2b = raster.electrode_masks(stack2, p)
|
|
grid = quadtree.build_leaves(stack2.masks[0],
|
|
keep_fine=(e1b[0] | e2b[0]))
|
|
R = _solve_on_leaves(p, stack2, e1b, e2b, grid)
|
|
assert grid.n < 0.4 * ref.solve_info.n_unknowns
|
|
assert R == pytest.approx(ref.R_ohm, rel=0.01)
|