Files
CelerisLab/tests/validation/multigrid/test_refinement_multipatch.py

232 lines
12 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 levels(bounds, blocks=(None, None), objects=(), jets=(), **rest):
"""Two-level constructor keywords: one L1 rectangle or set, per-level blocks, L1 bodies/Jets."""
return dict(refinements=[bounds], level_blocks=list(blocks),
level_objects=[(), tuple(objects)], level_jets=[(), tuple(jets)], **rest)
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_offset
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_offset
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(blocks=(None, (8, 4))), dict(blocks=((4, 4), None)),
dict(blocks=((4, 4), (8, 4)), objects=[circle()]),
dict(blocks=((4, 4), (5, 4)))):
with self.subTest(kw=kw), self.assertRaises(ValueError):
FixedPatchStepper(cfg, context=context, **levels(((4, 4, 8, 12), (14, 4, 18, 12)), **kw))
with self.assertRaisesRegex(ValueError, "margin"):
FixedPatchStepper(cfg, context=context,
**levels(((1, 4, 8, 12), (10, 4, 14, 12)), blocks=((4, 4), (8, 4))))
from CelerisLab.body.jets import Jet, Parallel, Tube
cfg32 = LBMConfig(nx=32, ny=16, viscosity=.1)
span = Jet(Tube((5., 8.), 0., 22., .6), Parallel(0.), amplitude=0.)
with self.assertRaisesRegex(ValueError, "one jet"): # same obj_id cannot bypass it
FixedPatchStepper(cfg32, context=context,
**levels(((2, 2, 14, 14), (18, 2, 30, 14)), blocks=((4, 4), (4, 4)),
objects=[body(8., 8.), body(24., 8.)], jets=[span]))
class MultipatchGPU(unittest.TestCase):
STEPS = (0, 1, 2, 8)
def run_steps(self, context, cfg, bounds, coarse, fine, **kw):
out = []
with FixedPatchStepper(cfg, context=context, **levels(bounds, **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, blocks=((3, 4), (8, 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(blocks=((4, 4), (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(blocks=((4, 4), (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)
def body(x, y, obj_id=0):
from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.objects import SimObject
return SimObject(obj_id, geometry=CircleGeometry(x, y, 2.15), center=(x, y), radius=2.15)
class MultipatchBodiesGPU(unittest.TestCase):
"""fine_objects/jets on several patches (frozen gates, bitwise)."""
PATCHES = ((2, 2, 14, 14), (18, 2, 30, 14))
STEPS = (0, 1, 2, 8)
def run_steps(self, context, cfg, bounds, coarse, fine, switch=None, **kw):
out = []
with FixedPatchStepper(cfg, context=context, **levels(bounds, **kw)) as backend:
backend.initialize(coarse, fine)
for step in self.STEPS:
if switch is not None and step == 8:
for index, amplitude in switch:
backend.set_jet_amplitude(index, amplitude, level=1)
backend.step(step - backend.step_count)
out.append(backend.download() + raw(backend))
return out
def test_one_body_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, blocks=((3, 4), (8, 4)), objects=[circle()])
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, window(s, 0, b[3]), a[3], label + " raw fine")
def test_two_bodies_and_jets_equal_tiled_single(self):
"""Translation copy across a block seam, with jets and an amplitude switch."""
from CelerisLab.body.jets import Jet, Parallel, Tube
from CelerisLab.cuda.context import CudaContext
cfg16 = LBMConfig(nx=16, ny=16, viscosity=.1)
cfg32 = replace(cfg16, nx=32)
one = FixedPatchSet2D.from_config(cfg16, self.PATCHES[:1])
two = FixedPatchSet2D.from_config(cfg32, self.PATCHES)
coarse, fine = state(16, 16, 3), state(*one.patches[0].fine_shape, 4)
jet = lambda x: Jet(Tube((x, 8.), 0., 6., .6), Parallel(0.), amplitude=.002)
kw = dict(blocks=((4, 4), (4, 4)))
with CudaContext(0) as context:
for jets in ((), (jet(5.),)):
ref = self.run_steps(context, cfg16, self.PATCHES[:1], coarse, level_fine(one, [fine]),
switch=[(0, -.001)] if jets else None,
objects=[body(8., 8.)], jets=jets, **kw)
both = jets and (jet(5.), jet(21.))
got = self.run_steps(context, cfg32, self.PATCHES, np.concatenate([coarse, coarse], 2),
level_fine(two, [fine, fine]),
switch=[(0, -.001), (1, -.001)] if jets else None,
objects=[body(8., 8., 5), body(24., 8., 5)],
jets=both or (), **kw)
for step, a, b in zip(self.STEPS, ref, got):
label = f"jets={len(jets)} 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}")
exact(self, window(two, i, b[3]), window(one, 0, a[3]), f"{label} raw fine {i}")
if __name__ == "__main__":
unittest.main()