diff --git a/tests/validation/multigrid/test_exchange_window.py b/tests/validation/multigrid/test_exchange_window.py index a66bdde..09a09f0 100644 --- a/tests/validation/multigrid/test_exchange_window.py +++ b/tests/validation/multigrid/test_exchange_window.py @@ -109,7 +109,7 @@ class ExchangeWindowGPU(unittest.TestCase): from test_raw_exchange_gpu import BOUNDS as RB, CFG, circle as rc, jet, state from CelerisLab.cuda.context import CudaContext with CudaContext(0) as ctx: - kw = dict(context=ctx, refinements=[RB], level_blocks=[None, (16, 20)], + kw = dict(context=ctx, refinements=[RB], level_blocks=[(12, 8), (16, 20)], level_objects=[(), (rc(),)], level_jets=[(), (jet(),)], observe_raw_exchange=True) with FixedPatchStepper(CFG, observe_window_exchange=True, **kw) as windowed, \ diff --git a/tests/validation/multigrid/test_fine_surface_gpu.py b/tests/validation/multigrid/test_fine_surface_gpu.py index e9f806f..c30e8c7 100644 --- a/tests/validation/multigrid/test_fine_surface_gpu.py +++ b/tests/validation/multigrid/test_fine_surface_gpu.py @@ -279,6 +279,61 @@ def test_fine_surface_four_hops(speed): np.testing.assert_array_equal(got, wanted) +def run_blocked_patch(speed): + """All-blocked run (whole-level tiles) of the same case; fine ROI after each fine hop.""" + cfg = LBMConfig(nx=24, ny=24, viscosity=.05, velocity=0, rho=1) + source = object_spec(speed) + with CudaContext(0) as context: + with implementation.FixedPatchStepper(cfg, context=context, refinements=[BOUNDS], + level_blocks=[(24, 24), (40, 40)], + level_objects=[(), (source,)], + observe_raw_exchange=True) as backend: + c, f = backend._levels + flags = backend.fine_boundary.flags.reshape(40, 40) + active = np.zeros((40, 40), bool) + active[4:-4, 4:-4] = True + active &= ((flags & FLUID) != 0) & ((flags & SOLID) == 0) + yy, xx = np.mgrid[:40, :40] + roi = active & ((xx-20)**2+(yy-20)**2 <= 9**2) + backend.initialize(rest((24, 24)), rest((40, 40))) + traces, original = [], f.kernel + + def fine(*args, **kwargs): + original(*args, **kwargs) + backend._stream.synchronize() + advanced = f.layout.to_dense(read(args[1], f.layout.blocked_shape)) + assert np.isfinite(advanced[:, active]).all() + assert np.isnan(advanced[:, (flags & SOLID) != 0]).all() + traces.append(advanced[:, roi].copy()) + + with patch.object(f, "kernel", fine): + backend.step(2) + assert len(traces) == 4 and backend.step_count == 2 + mass = backend.read_raw_exchange(0)["total"]["mass_into_fluid"] + return traces, roi, backend.download(), float(mass) + + +@pytest.mark.parametrize("speed", [0., .002, -.002]) +def test_fine_surface_blocked_matches_uniform_four_hops(speed): + """Blocked storage: fine ROI after each of four hops against the uniform 48^2 solver + (same tolerances as the dense test); blowing/suction drives mass with its sign; + zero actuation equals the passive circle bit for bit.""" + actual, roi, final, mass = run_blocked_patch(speed) + reference = uniform_reference(speed, roi) + for got, wanted in zip(actual, reference): + np.testing.assert_allclose(got, wanted, atol=3e-6, rtol=0) + np.testing.assert_allclose(MOMENTS@got.astype(float), MOMENTS@wanted.astype(float), + atol=2e-5, rtol=0) + if speed: + assert speed*mass > 0 + else: + passive, passive_roi, passive_final, _ = run_blocked_patch(None) + np.testing.assert_array_equal(passive_roi, roi) + for got, wanted in zip(actual+list(final), passive+list(passive_final)): + np.testing.assert_array_equal(got, wanted) + print(f"blocked speed={speed} four hops vs uniform PASS mass_into_fluid={mass:+.9g}", flush=True) + + @pytest.mark.parametrize("missing", ["surface_velocity_gpu", "link_exchange_gpu"]) def test_missing_surface_device_buffer_rejects_before_launch(missing): backend = implementation.FixedPatchStepper.__new__(implementation.FixedPatchStepper) @@ -328,6 +383,7 @@ def test_cross_interface_donor_rejects_before_cuda(): with patch.object(cuda, "mem_alloc") as allocate: with pytest.raises(ValueError, match="donor crosses"): implementation.FixedPatchStepper(cfg, context=context, refinements=[BOUNDS], + level_blocks=[(24, 24), (40, 40)], level_objects=[(), (source,)]) context.activate.assert_not_called() allocate.assert_not_called() diff --git a/tests/validation/multigrid/test_raw_exchange_gpu.py b/tests/validation/multigrid/test_raw_exchange_gpu.py index 724fbaa..c544cc2 100644 --- a/tests/validation/multigrid/test_raw_exchange_gpu.py +++ b/tests/validation/multigrid/test_raw_exchange_gpu.py @@ -92,8 +92,9 @@ def census(run, index, buffer): def fine(block): - """Two-level circle+jet construction on BOUNDS; block None keeps the dense fine level.""" - return dict(refinements=[BOUNDS], level_blocks=[None, block], + """Two-level circle+jet construction on BOUNDS, all-blocked with a (12, 8) root tile; + block None is the dense route (dense reference test only, retired with the dense route).""" + return dict(refinements=[BOUNDS], level_blocks=[None if block is None else (12, 8), block], level_objects=[(), (circle(),)], level_jets=[(), (jet(),)]) class RawExchangeGPU(unittest.TestCase): diff --git a/tests/validation/multigrid/test_refinement_blocked_fine.py b/tests/validation/multigrid/test_refinement_blocked_fine.py index d82d40a..33560d8 100644 --- a/tests/validation/multigrid/test_refinement_blocked_fine.py +++ b/tests/validation/multigrid/test_refinement_blocked_fine.py @@ -88,15 +88,13 @@ def seam_cells(layout, mapped): def raw(backend): - """Unmasked device arrays; blocked fine storage unpacked from owner cores.""" - coarse, fine = backend._levels - out = [np.empty(coarse.shape, np.float32)] - out.append(np.empty(fine.shape if backend.fine_layout is None - else backend.fine_layout.blocked_shape, np.float32)) - cuda.memcpy_dtoh(out[0], coarse.current) - cuda.memcpy_dtoh(out[1], fine.current) - if backend.fine_layout is not None: - out[1] = backend.fine_layout.to_dense(out[1]) + """Unmasked device arrays of every level; blocked storage unpacked from owner cores.""" + out = [] + for level in backend._levels: + array = np.empty(level.shape if level.layout is None else level.layout.blocked_shape, + np.float32) + cuda.memcpy_dtoh(array, level.current) + out.append(array if level.layout is None else level.layout.to_dense(array)) return out diff --git a/tests/validation/multigrid/test_refinement_jets.py b/tests/validation/multigrid/test_refinement_jets.py index de99afe..c8fe76c 100644 --- a/tests/validation/multigrid/test_refinement_jets.py +++ b/tests/validation/multigrid/test_refinement_jets.py @@ -3,11 +3,13 @@ Jets use the same coarse cell-edge frame as fine_objects. The equivalent jet of Astra's actuated fixture is SurfaceActuation(0.7, 0.85, U).as_jet((16, 12), 2.15). Frozen GPU gates (8 coarse steps, both coarse boundary modes): - G1 jet run: dense vs 1x1/2x2/4x2 fine blocks bit-identical (values + NaN masks); - G2 jet vs legacy actuation (dense): max |dG| over finite fine ACTIVE fluid + G1 jet run: 1x1/2x2/4x2 fine blocks (blocked (12, 8) root) bit-identical to the + FINE_BLOCKS[0] reference (values + NaN masks); dense leg kept until dense retirement; + G2 jet vs legacy actuation (blocked reference): max |dG| over finite fine ACTIVE fluid populations <= 1e-6 (velocity rows differ only by FP32 rounding; reported); - G3 amplitude change after 4 steps (+U -> -U), 4 more steps: dense vs blocked - bit-identical and the run differs from the unchanged run. + G3 amplitude change after 4 steps (+U -> -U), 4 more steps: blocked layouts + bit-identical to the FINE_BLOCKS[0] reference (dense leg until retirement) and the + run differs from the unchanged run. """ from contextlib import nullcontext import unittest @@ -26,6 +28,11 @@ from test_refinement_blocked_fine import (BOUNDS, FINE_BLOCKS, NX, NY, STEPS, ac SPEED = .002 +def root_and(fine_block): + """All-blocked levels with a (12, 8) root tile; fine_block None is the dense route.""" + return [None if fine_block is None else (12, 8), fine_block] + + def jet(speed=SPEED): return actuation(speed).as_jet((16.0, 12.0), 2.15) @@ -42,7 +49,7 @@ def mocked(objects, jets, fine_block): patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \ patch.object(cuda, "memcpy_htod", htod): backend = FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS], - level_blocks=[None, fine_block], level_objects=[(), tuple(objects)], + level_blocks=root_and(fine_block), level_objects=[(), tuple(objects)], level_jets=[(), tuple(jets)]) return backend, htod @@ -161,7 +168,7 @@ def run(mode, fine_block, *, objects, jets, switch=None): patch_ = FixedPatch2D.from_config(cfg, BOUNDS) with CudaContext(0) as context, FixedPatchStepper( cfg, context=context, coarse_boundary=mode, refinements=[BOUNDS], - level_blocks=[None, fine_block], level_objects=[(), tuple(objects)], + level_blocks=root_and(fine_block), level_objects=[(), tuple(objects)], level_jets=[(), tuple(jets)]) as backend: backend.initialize(state(NY, NX, 1), state(*patch_.fine_shape, 2)) if switch is None: @@ -179,29 +186,32 @@ class JetStepperGPU(unittest.TestCase): patch_, boundary = fine_circle(mode) fluid = (patch_.fine_kind == ACTIVE) & ( (boundary.flags.reshape(patch_.fine_shape) & SOLID) == 0) - dense = run(mode, None, objects=[circle()], jets=[jet()]) - self.assertTrue(np.isfinite(dense[1][1][:, fluid]).all(), mode) - for fine_block in FINE_BLOCKS: + reference = run(mode, FINE_BLOCKS[0], objects=[circle()], jets=[jet()]) + self.assertTrue(np.isfinite(reference[1][1][:, fluid]).all(), mode) + for fine_block in FINE_BLOCKS[1:]: public, raw_arrays = run(mode, fine_block, objects=[circle()], jets=[jet()]) - for a, b in zip(public + tuple(raw_arrays), dense[0] + tuple(dense[1])): + for a, b in zip(public + tuple(raw_arrays), reference[0] + tuple(reference[1])): np.testing.assert_array_equal(a, b, err_msg=f"G1 {mode} {fine_block}") - legacy = run(mode, None, objects=[circle(actuation(SPEED))], jets=()) - diff = np.abs(dense[1][1][:, fluid] - legacy[1][1][:, fluid]).max() + dense = run(mode, None, objects=[circle()], jets=[jet()]) # dense leg (D2 retires) + for a, b in zip(dense[0] + tuple(dense[1]), reference[0] + tuple(reference[1])): + np.testing.assert_array_equal(a, b, err_msg=f"G1 {mode} dense") + legacy = run(mode, FINE_BLOCKS[0], objects=[circle(actuation(SPEED))], jets=()) + diff = np.abs(reference[1][1][:, fluid] - legacy[1][1][:, fluid]).max() print(f"G2 {mode}: jet vs legacy fine max |dG| = {diff:.3e}", flush=True) self.assertLessEqual(diff, 1e-6, mode) def test_g3_amplitude_switch_layouts_identical(self): for mode in ("periodic", "slot_reservoir"): - dense = run(mode, None, objects=[circle()], jets=[jet()], switch=-SPEED) - steady = run(mode, None, objects=[circle()], jets=[jet()]) - self.assertFalse(np.array_equal(dense[1][1], steady[1][1], equal_nan=True), mode) - for fine_block in FINE_BLOCKS: + reference = run(mode, FINE_BLOCKS[0], objects=[circle()], jets=[jet()], switch=-SPEED) + steady = run(mode, FINE_BLOCKS[0], objects=[circle()], jets=[jet()]) + self.assertFalse(np.array_equal(reference[1][1], steady[1][1], equal_nan=True), mode) + for fine_block in (*FINE_BLOCKS[1:], None): # None: dense leg (D2 retires) public, raw_arrays = run(mode, fine_block, objects=[circle()], jets=[jet()], switch=-SPEED) - for a, b in zip(public + tuple(raw_arrays), dense[0] + tuple(dense[1])): + for a, b in zip(public + tuple(raw_arrays), reference[0] + tuple(reference[1])): np.testing.assert_array_equal(a, b, err_msg=f"G3 {mode} {fine_block}") - print(f"G3 {mode}: amplitude switch dense vs {len(FINE_BLOCKS)} layouts identical", - flush=True) + print(f"G3 {mode}: amplitude switch, {len(FINE_BLOCKS) - 1} layouts + dense " + f"identical to {FINE_BLOCKS[0]}", flush=True) if __name__ == "__main__":