FixedPatchSet2D.fine_layout rejects non-positive, float, bool or wrong-length block sizes with ValueError before any modulo (was ZeroDivisionError/TypeError). The face gap-4 GPU test now checks raw owner cores directly by kind/covered for every patch (ACTIVE finite, OUTER/holes/covered NaN; INNER not asserted), not only the masked download and reversed-order parity. README documents the body-free multipatch call, scope and level-wide fine shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
7.4 KiB
Python
149 lines
7.4 KiB
Python
"""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, also on
|
|
raw owner cores per patch (INNER partly invalid by contract, not asserted).
|
|
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, OUTER_GHOST, 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
|
|
support = s.fine_layout((4, 4)).owned
|
|
outer = support & (s.fine_kind == OUTER_GHOST)
|
|
holes = ~support
|
|
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())
|
|
# Raw owner cores, independent of the download mask (every patch).
|
|
# INNER may be partly NaN after hop 2 by contract: not asserted.
|
|
rc, rf = a[2], a[3]
|
|
label = f"raw step {step}"
|
|
self.assertTrue(np.isfinite(rc[:, ~s.covered]).all(), label + " coarse leaves")
|
|
if step: # covered parent cores are poisoned by the parent step
|
|
self.assertTrue(np.isnan(rc[:, s.covered]).all(), label + " covered")
|
|
self.assertTrue(np.isfinite(rf[:, leaf]).all(), label + " fine ACTIVE")
|
|
self.assertTrue(np.isnan(rf[:, outer]).all(), label + " fine OUTER")
|
|
self.assertTrue(np.isnan(rf[:, holes]).all(), label + " fine holes")
|
|
for i, p in enumerate(s.patches):
|
|
active = window(s, i, rf)[:, p.fine_kind == ACTIVE]
|
|
self.assertTrue(np.isfinite(active).all(), f"{label} patch {i} ACTIVE")
|
|
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()
|