Multipatch: CPU guards and frozen GPU gates through the new interface
One rectangle via the multipatch path is bit-identical to the old single-patch blocked path (both modes, raw masks); S1 two-patch 16x32 equals the 16x16 one-patch run tiled (bitwise), in both rectangle orders; face gap-4 pair is order-invariant with exact covered/hole masks. Mass printed as a diagnostic only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4c8240ea13
commit
0397b6ebb7
@@ -0,0 +1,131 @@
|
||||
"""Opt-in multipatch FixedPatchStepper (FixedPatchSet2D): CPU guards plus GPU gates.
|
||||
|
||||
Run CPU and GPU classes in separate processes. GPU gates (frozen before any run):
|
||||
- one rectangle through the multipatch interface is bit-identical to the old
|
||||
single-patch blocked path (both boundary modes, raw owner masks included);
|
||||
- S1 translation copy: two patches on 16x32 equal the 16x16 one-patch run tiled
|
||||
twice, bit for bit, every step;
|
||||
- reversed rectangle order gives bit-identical results;
|
||||
- face gap-4 pair (16x24): ACTIVE leaves finite, covered/hole masks exact.
|
||||
Weighted leaf mass is printed as a diagnostic only.
|
||||
"""
|
||||
from dataclasses import replace
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.refinement import ACTIVE, FixedPatch2D, FixedPatchSet2D
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
from test_refinement_blocked_fine import BOUNDS, NX, NY, config, state, circle
|
||||
from test_refinement_blocked_base import exact, raw
|
||||
|
||||
FACE = ((4, 4, 8, 12), (12, 4, 16, 12))
|
||||
S1 = ((4, 4, 12, 12), (20, 4, 28, 12))
|
||||
|
||||
|
||||
def level_fine(patch_set, windows):
|
||||
"""Level-wide fine array with each patch window filled from `windows`."""
|
||||
out = np.full((9, *patch_set.fine_shape), np.nan, np.float32)
|
||||
for p, w in zip(patch_set.patches, windows):
|
||||
y0, x0 = p.fine_level_origin
|
||||
out[:, y0:y0 + p.fine_shape[0], x0:x0 + p.fine_shape[1]] = w
|
||||
return out
|
||||
|
||||
|
||||
def window(patch_set, index, fine):
|
||||
p = patch_set.patches[index]
|
||||
y0, x0 = p.fine_level_origin
|
||||
return fine[:, y0:y0 + p.fine_shape[0], x0:x0 + p.fine_shape[1]]
|
||||
|
||||
|
||||
class MultipatchCPU(unittest.TestCase):
|
||||
def test_rejections_before_cuda(self):
|
||||
cfg = config("periodic")
|
||||
context = object() # never activated: every case must fail first
|
||||
for kw in (dict(), dict(fine_block=(8, 4)), dict(base_block=(4, 4)),
|
||||
dict(fine_block=(8, 4), base_block=(4, 4), fine_objects=[circle()]),
|
||||
dict(fine_block=(5, 4), base_block=(4, 4))):
|
||||
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
||||
FixedPatchStepper(cfg, ((4, 4, 8, 12), (14, 4, 18, 12)), context=context, **kw)
|
||||
with self.assertRaisesRegex(ValueError, "closer"):
|
||||
FixedPatchStepper(cfg, ((4, 4, 8, 12), (10, 4, 14, 12)), context=context,
|
||||
fine_block=(8, 4), base_block=(4, 4))
|
||||
|
||||
|
||||
class MultipatchGPU(unittest.TestCase):
|
||||
STEPS = (0, 1, 2, 8)
|
||||
|
||||
def run_steps(self, context, cfg, bounds, coarse, fine, **kw):
|
||||
out = []
|
||||
with FixedPatchStepper(cfg, bounds, context=context, **kw) as backend:
|
||||
backend.initialize(coarse, fine)
|
||||
for step in self.STEPS:
|
||||
backend.step(step - backend.step_count)
|
||||
out.append(backend.download() + raw(backend))
|
||||
return out
|
||||
|
||||
def test_one_rectangle_matches_single_patch_path(self):
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
with CudaContext(0) as context:
|
||||
for mode in ("periodic", "slot_reservoir"):
|
||||
cfg = config(mode)
|
||||
p = FixedPatch2D.from_config(cfg, BOUNDS)
|
||||
s = FixedPatchSet2D.from_config(cfg, (BOUNDS,))
|
||||
coarse, fine = state(NY, NX, 1), state(*p.fine_shape, 2)
|
||||
kw = dict(coarse_boundary=mode, fine_block=(8, 4), base_block=(3, 4))
|
||||
old = self.run_steps(context, cfg, BOUNDS, coarse, fine, **kw)
|
||||
new = self.run_steps(context, cfg, (BOUNDS,), coarse, level_fine(s, [fine]), **kw)
|
||||
for step, a, b in zip(self.STEPS, old, new):
|
||||
label = f"{mode} step {step}"
|
||||
exact(self, b[0], a[0], label + " coarse")
|
||||
exact(self, window(s, 0, b[1]), a[1], label + " fine")
|
||||
exact(self, b[2], a[2], label + " raw coarse")
|
||||
exact(self, window(s, 0, b[3]), a[3], label + " raw fine")
|
||||
|
||||
def test_s1_translation_copy_and_order(self):
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
cfg16 = LBMConfig(nx=16, ny=16, viscosity=.1)
|
||||
cfg32 = replace(cfg16, nx=32)
|
||||
one = FixedPatchSet2D.from_config(cfg16, (S1[0],))
|
||||
two = FixedPatchSet2D.from_config(cfg32, S1)
|
||||
coarse, fine = state(16, 16, 3), state(*one.patches[0].fine_shape, 4)
|
||||
kw = dict(fine_block=(4, 4), base_block=(4, 4))
|
||||
with CudaContext(0) as context:
|
||||
ref = self.run_steps(context, cfg16, (S1[0],), coarse, level_fine(one, [fine]), **kw)
|
||||
tiled = np.concatenate([coarse, coarse], axis=2)
|
||||
for bounds in (S1, S1[::-1]):
|
||||
got = self.run_steps(context, cfg32, bounds, tiled,
|
||||
level_fine(two, [fine, fine]), **kw)
|
||||
for step, a, b in zip(self.STEPS, ref, got):
|
||||
label = f"order {bounds[0]} step {step}"
|
||||
exact(self, b[0], np.concatenate([a[0], a[0]], axis=2), label + " coarse")
|
||||
for i in (0, 1):
|
||||
exact(self, window(two, i, b[1]), window(one, 0, a[1]), f"{label} fine {i}")
|
||||
|
||||
def test_face_gap4_finite_masks_and_order(self):
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
cfg = LBMConfig(nx=24, ny=16, viscosity=.1)
|
||||
s = FixedPatchSet2D.from_config(cfg, FACE)
|
||||
coarse = state(16, 24, 5)
|
||||
fine = level_fine(s, [state(*p.fine_shape, 6 + i) for i, p in enumerate(s.patches)])
|
||||
kw = dict(fine_block=(4, 4), base_block=(4, 4))
|
||||
leaf = s.fine_kind == ACTIVE
|
||||
with CudaContext(0) as context:
|
||||
runs = [self.run_steps(context, cfg, bounds, coarse, fine, **kw)
|
||||
for bounds in (FACE, FACE[::-1])]
|
||||
for step, a, b in zip(self.STEPS, *runs):
|
||||
for x, y in zip(a, b):
|
||||
exact(self, y, x, f"reversed order step {step}")
|
||||
c, f = a[0], a[1]
|
||||
self.assertTrue(np.isfinite(c[:, ~s.covered]).all())
|
||||
self.assertTrue(np.isnan(c[:, s.covered]).all())
|
||||
self.assertTrue(np.isfinite(f[:, leaf]).all())
|
||||
self.assertTrue(np.isnan(f[:, ~leaf]).all())
|
||||
mass = float(np.nansum(c, dtype=np.float64) + .25 * np.nansum(f, dtype=np.float64))
|
||||
print(f"face gap4 step {step}: weighted leaf mass {mass:.9g} (diagnostic)", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user