Speed up rasterization ~40x and large solves ~2x
- Hybrid rasterizer: PIL scanline fill for the bulk, with cells in a ~2 px band around each ring edge re-tested exactly against the polygon - cell-for-cell identical to the old center-in-polygon pass (equivalence test added) but O(vertices + cells) instead of O(vertices x cells). Measured 4.5 s -> 0.11 s at 1.45M cells with 8.8k polygon vertices. - AMG-preconditioned CG (pyamg, new requirement) above 500k unknowns: measured 7.0 s vs 15.3 s spsolve at 1.4M unknowns at a fraction of the memory, R identical to 1e-6; the old Jacobi-CG (kept as fallback when pyamg is missing) needed tens of minutes there. spsolve stays the default below 500k where it is exact and fastest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,35 @@ def test_exact_cell_count_square_with_hole():
|
||||
assert int(stack.masks[0].sum()) == 100 - 16
|
||||
|
||||
|
||||
def test_hybrid_raster_matches_exact_point_test():
|
||||
"""The PIL-fill + exact-edge-band rasterizer must be cell-for-cell
|
||||
identical to a pure center-in-polygon pass, including awkward
|
||||
fractional offsets, concave lobes and a hole."""
|
||||
from matplotlib.path import Path as MplPath
|
||||
ang = np.linspace(0, 2 * np.pi, 257, endpoint=False)
|
||||
r = 7.3 + 1.7 * np.sin(5 * ang) + 0.9 * np.cos(9 * ang + 0.4)
|
||||
blob = np.stack([20.05 + r * np.cos(ang), 20.13 + r * np.sin(ang)],
|
||||
axis=1)
|
||||
hole = np.stack([20.4 + 2.1 * np.cos(ang), 19.8 + 2.2 * np.sin(ang)],
|
||||
axis=1)
|
||||
p = make_problem([(blob.tolist(), [hole.tolist()])],
|
||||
rect1_mm=(14, 19, 16, 21), rect2_mm=(24, 19, 26, 21))
|
||||
stack = _stack(p, 0.25)
|
||||
|
||||
ny, nx = stack.shape2d
|
||||
xg, yg = stack.cell_centers(0, ny, 0, nx)
|
||||
pts = np.column_stack([xg.ravel(), yg.ravel()])
|
||||
|
||||
def exact(ring):
|
||||
verts = np.vstack([ring, ring[:1]])
|
||||
return MplPath(verts, closed=True).contains_points(pts).reshape(
|
||||
ny, nx)
|
||||
|
||||
poly = p.layers[0].polygons[0]
|
||||
ref = exact(poly.outline) & ~exact(poly.holes[0])
|
||||
assert np.array_equal(stack.masks[0], ref)
|
||||
|
||||
|
||||
def test_margin_cells_are_empty():
|
||||
p = strip_problem()
|
||||
stack = _stack(p, 0.5)
|
||||
|
||||
+12
-1
@@ -79,10 +79,21 @@ def test_hole_increases_resistance_and_converges():
|
||||
assert abs(r_a.R_ohm - r_b.R_ohm) < 0.01 * r_b.R_ohm
|
||||
|
||||
|
||||
def test_cg_path_matches_direct(monkeypatch):
|
||||
def test_iterative_paths_match_direct(monkeypatch):
|
||||
"""AMG-CG (default iterative) and Jacobi-CG (pyamg-missing fallback)
|
||||
both reproduce the direct solve."""
|
||||
p = strip_problem(length=50, width=10, e_len=5)
|
||||
r_direct, _ = _solve(p, 0.25)
|
||||
monkeypatch.setattr(config, "SPSOLVE_MAX_UNKNOWNS", 0)
|
||||
r_amg, _ = _solve(p, 0.25)
|
||||
assert r_amg.solve_info.method == "amg+cg"
|
||||
assert r_amg.R_ohm == pytest.approx(r_direct.R_ohm, rel=1e-6)
|
||||
assert r_amg.mismatch_rel < 1e-5
|
||||
|
||||
def no_pyamg(A, b):
|
||||
raise ImportError("pyamg unavailable")
|
||||
|
||||
monkeypatch.setattr(solver, "_solve_amg", no_pyamg)
|
||||
r_cg, _ = _solve(p, 0.25)
|
||||
assert r_cg.solve_info.method == "cg+jacobi"
|
||||
assert r_cg.R_ohm == pytest.approx(r_direct.R_ohm, rel=1e-6)
|
||||
|
||||
Reference in New Issue
Block a user