278 lines
14 KiB
Python
278 lines
14 KiB
Python
"""Nested three-level FixedPatchStepper (child=..., single chain): CPU guards and GPU gates.
|
|
|
|
Fixture = reviewed CPU probe: L0 16x16 periodic, L1 (4,4,12,12) -> 24x24 window,
|
|
L2 (8,8,16,16) in L1 window cells -> 24x24 window; 4x4 blocks on every level.
|
|
Run CPU and GPU classes in separate processes. GPU gates (frozen before any run):
|
|
- uniform moving equilibrium stays on all three levels' ACTIVE leaves, |dG| <= 1e-6;
|
|
- seeded non-equilibrium: every level's ACTIVE leaves finite, raw owner cores NaN on
|
|
OUTER/covered (INNER partly invalid by contract, not asserted);
|
|
- each L1 hop feeds L2 from the L1 buffer it is about to step (current, then the new
|
|
G written by the first hop), never from L0: 2 child advances per coarse step.
|
|
Numerical comparison against the CPU probe (G vs C(f)) is done by the acceptance run.
|
|
"""
|
|
from contextlib import nullcontext
|
|
from dataclasses import replace
|
|
import unittest
|
|
from unittest.mock import Mock, patch
|
|
|
|
import numpy as np
|
|
import pycuda.driver as cuda
|
|
|
|
from CelerisLab.config import LBMConfig
|
|
from CelerisLab.lbm.equilibrium import compute_feq_d2q9
|
|
from CelerisLab.lbm.refinement import ACTIVE, OUTER_GHOST
|
|
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
|
from test_refinement_blocked_fine import state, circle
|
|
from test_refinement_blocked_base import raw
|
|
|
|
CFG = LBMConfig(nx=16, ny=16, viscosity=.1)
|
|
L1, L2 = (4, 4, 12, 12), (8, 8, 16, 16)
|
|
B = (4, 4)
|
|
|
|
|
|
def levels(l1=L1, l2=L2, blocks=(B, B, B), objects=((), (), ()), jets=((), (), ())):
|
|
"""Level-list keywords for the scalar L1 patch with one scalar L2 child."""
|
|
return dict(refinements=[l1, l2], level_blocks=list(blocks),
|
|
level_objects=list(objects), level_jets=list(jets))
|
|
|
|
|
|
KW = levels()
|
|
|
|
|
|
def child_body(spec=None):
|
|
from CelerisLab.body.geometry.circle import CircleGeometry
|
|
from CelerisLab.body.objects import SimObject
|
|
return SimObject(99, geometry=CircleGeometry(8., 8., .8), center=(8., 8.), radius=.8,
|
|
surface_actuation=spec)
|
|
|
|
|
|
def child_jet(speed=.002):
|
|
from CelerisLab.body.jets import Jet, Tube, Parallel
|
|
return Jet(Tube((6.5, 8.), 0., 3., .3), Parallel(0.), amplitude=speed)
|
|
|
|
|
|
def mocked_child():
|
|
from CelerisLab.cuda import compiler
|
|
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
|
module = Mock(get_function=Mock(side_effect=lambda name: Mock(name=name)))
|
|
module.get_global.return_value = (Mock(), 20)
|
|
with patch.object(cuda, "Stream"), patch.object(cuda, "mem_get_info", return_value=(1 << 40, 1 << 40)), patch.object(compiler, "generate_config"), \
|
|
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
|
patch.object(compiler, "load_module", return_value=module), \
|
|
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
|
|
patch.object(cuda, "memcpy_htod"):
|
|
return FixedPatchStepper(CFG, context=context,
|
|
**levels(objects=((), (), (child_body(),)), jets=((), (), (child_jet(),))))
|
|
|
|
|
|
class ThreeLevelCPU(unittest.TestCase):
|
|
def test_rejections_before_cuda(self):
|
|
context = object() # never activated: every case must fail first
|
|
bad = (levels(blocks=(B, B, None)), levels(blocks=(B, None, B)),
|
|
levels(objects=((), (circle(),), ())), levels(l2=(4, 8, 16, 16)),
|
|
levels(blocks=(B, B, (5, 4))))
|
|
for kw in bad:
|
|
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
|
FixedPatchStepper(CFG, context=context, **kw)
|
|
# L2 bodies/Jets without an L2 level (one entry too many), and an L2 Jet without a body.
|
|
two_level = dict(refinements=[L1], level_blocks=[B, B])
|
|
for kw in (dict(two_level, level_objects=[(), (), (child_body(),)]),
|
|
dict(two_level, level_jets=[(), (), (child_jet(),)]),
|
|
levels(jets=((), (), (child_jet(),)))):
|
|
with self.assertRaises(ValueError):
|
|
FixedPatchStepper(CFG, context=context, **kw)
|
|
with self.assertRaises(ValueError): # several L1 patches + scalar child: not yet
|
|
FixedPatchStepper(LBMConfig(nx=32, ny=16, viscosity=.1), context=context,
|
|
**levels(l1=((4, 4, 12, 12), (20, 4, 28, 12))))
|
|
|
|
|
|
def test_four_halo_bfl_steps_and_fresh_middle_feeds(self):
|
|
run = mocked_child()
|
|
run._status = "ready"
|
|
f, g = run._levels[1:]
|
|
b = g.boundary
|
|
events, feeds = [], []
|
|
advance, gather = run._advance, run._gather_halo
|
|
def record_feed(index, buffer, **kw): # index 2: L1 hop feeds L2
|
|
if index == 2:
|
|
self.assertIs(buffer, (f.current, f.spare)[len(feeds)])
|
|
feeds.append(buffer)
|
|
advance(index, buffer, **kw)
|
|
def record_halo(level, buffer, **kw):
|
|
if level is g:
|
|
events.append(("halo", buffer))
|
|
gather(level, buffer, **kw)
|
|
b.kernel.side_effect = lambda buffer, *a, **k: events.append(("bfl", buffer))
|
|
def record_step(src, dst, halo, flags, kind, **kw):
|
|
self.assertIs(flags, b.flags)
|
|
self.assertIs(kind, g.kind)
|
|
events.append(("step", src))
|
|
g.kernel.side_effect = record_step
|
|
with patch.object(run, "_advance", side_effect=record_feed), \
|
|
patch.object(run, "_gather_halo", side_effect=record_halo):
|
|
run.step()
|
|
self.assertEqual(feeds, [f.current, f.spare])
|
|
expected = [(kind, buffer) for buffer in (g.current, g.spare, g.current, g.spare)
|
|
for kind in ("halo", "bfl", "step")]
|
|
self.assertEqual(events, expected)
|
|
self.assertGreater(len(np.unique(b.tables["block"])), 1)
|
|
# Negative control: reusing L1's old source at the midpoint must fail.
|
|
feeds.clear()
|
|
with patch.object(run, "_advance", side_effect=lambda index, buffer, **kw:
|
|
record_feed(index, f.current if index == 2 else buffer, **kw)):
|
|
with self.assertRaises(AssertionError):
|
|
run.step()
|
|
run.close()
|
|
|
|
def test_child_update_isolated_and_failure_atomic(self):
|
|
run = mocked_child()
|
|
b = run._levels[2].boundary
|
|
before = b.tables["surface_velocity"].copy()
|
|
with self.assertRaises(TypeError):
|
|
run.set_jet_amplitude(0, -.002) # level is required
|
|
with self.assertRaises(IndexError):
|
|
run.set_jet_amplitude(0, -.002, level=1) # level 1 has no Jets
|
|
for level in (0, 3, True):
|
|
with self.assertRaises(ValueError):
|
|
run.set_jet_amplitude(0, -.002, level=level)
|
|
with patch.object(cuda, "memcpy_htod") as upload:
|
|
run.set_jet_amplitude(0, -.002, level=2)
|
|
self.assertEqual(upload.call_count, 1)
|
|
self.assertIs(upload.call_args.args[0], b.device["surface_velocity"])
|
|
np.testing.assert_array_equal(b.tables["surface_velocity"], -before)
|
|
self.assertEqual(run.jets[1], ())
|
|
with patch.object(cuda, "memcpy_htod", side_effect=RuntimeError("copy failed")):
|
|
with self.assertRaisesRegex(RuntimeError, "copy failed"):
|
|
run.set_jet_amplitude(0, .002, level=2)
|
|
self.assertEqual(run._status, "failed")
|
|
self.assertEqual(b.jets[0].amplitude, -.002)
|
|
np.testing.assert_array_equal(b.tables["surface_velocity"], -before)
|
|
run.close()
|
|
|
|
def test_child_public_masks_include_solids(self):
|
|
from CelerisLab.lbm.descriptors import SOLID
|
|
run = mocked_child()
|
|
solid = run.boundaries[2].flags.reshape(run.hierarchy.pairs[1].fine_shape) & SOLID != 0
|
|
self.assertTrue(solid.any())
|
|
self.assertFalse(run.leaf[2][solid].any())
|
|
arrays = [np.ones(level.shape, np.float32) for level in run._levels]
|
|
run._mask_nonleaves(*arrays)
|
|
self.assertTrue(np.isnan(arrays[2][:, solid]).all())
|
|
with patch.object(cuda, "memcpy_htod") as upload:
|
|
run.initialize(*arrays)
|
|
sent = upload.call_args_list[-1].args[1]
|
|
dense = run.layouts[2].to_dense(sent)
|
|
self.assertTrue(np.isnan(dense[:, ~run.leaf[2]]).all())
|
|
run.close()
|
|
|
|
|
|
class ThreeLevelGPU(unittest.TestCase):
|
|
def test_equilibrium_is_kept_on_every_level(self):
|
|
from CelerisLab.cuda.context import CudaContext
|
|
feq = np.asarray(compute_feq_d2q9(1.0, .03, -.02), np.float32)[:, None, None]
|
|
with CudaContext(0) as ctx, FixedPatchStepper(CFG, context=ctx, **KW) as run:
|
|
shapes = ((16, 16), run.hierarchy.pairs[0].fine_shape, run.hierarchy.pairs[1].fine_shape)
|
|
run.initialize(*(np.broadcast_to(feq, (9, *s)).copy() for s in shapes))
|
|
run.step(8)
|
|
self.assertEqual(run.step_count, 8)
|
|
for level, g in enumerate(run.download()):
|
|
leaf = np.isfinite(g[0])
|
|
self.assertTrue(leaf.any(), f"level {level}")
|
|
np.testing.assert_allclose(g[:, leaf], np.broadcast_to(feq, g.shape)[:, leaf],
|
|
atol=1e-6, rtol=0, err_msg=f"level {level}")
|
|
|
|
def test_nonequilibrium_masks_and_fresh_middle_feeds(self):
|
|
from CelerisLab.cuda.context import CudaContext
|
|
with CudaContext(0) as ctx, FixedPatchStepper(CFG, context=ctx, **KW) as run:
|
|
p1, p2 = run.hierarchy.pairs[0], run.hierarchy.pairs[1]
|
|
run.initialize(state(16, 16, 1), state(*p1.fine_shape, 2), state(*p2.fine_shape, 3))
|
|
f = run._levels[1]
|
|
feeds, original = [], run._advance
|
|
def record(index, buffer, **kw): # index 2: L1 hop feeds L2
|
|
if index == 2:
|
|
feeds.append(int(buffer))
|
|
original(index, buffer, **kw)
|
|
with patch.object(run, "_advance", side_effect=record):
|
|
cur, spare = int(f.current), int(f.spare)
|
|
run.step(1)
|
|
self.assertEqual(feeds, [cur, spare]) # current G, then the new G of hop 1
|
|
run.step(7)
|
|
middle = run.hierarchy.kinds[1]
|
|
for g, leaf in zip(run.download(), (~p1.covered, middle == ACTIVE, p2.fine_kind == ACTIVE)):
|
|
self.assertTrue(np.isfinite(g[:, leaf]).all())
|
|
self.assertTrue(np.isnan(g[:, ~leaf]).all())
|
|
rc, rf, rg = raw(run)
|
|
self.assertTrue(np.isnan(rc[:, p1.covered]).all())
|
|
self.assertTrue(np.isfinite(rf[:, middle == ACTIVE]).all())
|
|
self.assertTrue(np.isnan(rf[:, middle == OUTER_GHOST]).all()) # incl. child-covered
|
|
self.assertTrue(np.isnan(rf[:, p2.covered]).all())
|
|
self.assertTrue(np.isfinite(rg[:, p2.fine_kind == ACTIVE]).all())
|
|
self.assertTrue(np.isnan(rg[:, p2.fine_kind == OUTER_GHOST]).all())
|
|
|
|
|
|
class ThreeLevelBodyGPU(unittest.TestCase):
|
|
"""Frozen gates; CPU mocks do not establish these numerical results."""
|
|
def run_case(self, mode, block, *, jets=(), spec=None, switch=False):
|
|
from CelerisLab.cuda.context import CudaContext
|
|
from CelerisLab.lbm.descriptors import SOLID
|
|
cfg = CFG if mode == "periodic" else replace(
|
|
CFG, velocity=0., wall_normal_velocity=.001, wall_slot_width=4,
|
|
y_wall_bc="on_site_velocity", top_wall_bc="reservoir")
|
|
with CudaContext(0) as ctx, FixedPatchStepper(
|
|
cfg, context=ctx, coarse_boundary=mode,
|
|
**levels(blocks=(B, B, block), objects=((), (), (child_body(spec),)),
|
|
jets=((), (), tuple(jets)))) as run:
|
|
run.initialize(*(state(*level.shape[1:], i + 1) for i, level in enumerate(run._levels)))
|
|
snapshots = []
|
|
for stop in (0, 1, 2, 4, 8):
|
|
run.step(stop - run.step_count)
|
|
if stop == 4 and switch:
|
|
run.set_jet_amplitude(0, -.002, level=2)
|
|
public, stored = run.download(), raw(run)
|
|
leaves = [~run.hierarchy.pairs[0].covered, run.leaf[1], run.leaf[2]]
|
|
if mode != "periodic":
|
|
leaves[0][[0, -1]] = False
|
|
for g, mask in zip(public, leaves):
|
|
self.assertTrue(np.isfinite(g[:, mask]).all())
|
|
self.assertTrue(np.isnan(g[:, ~mask]).all())
|
|
invalid = [run.hierarchy.pairs[0].covered.copy(), run.hierarchy.kinds[1] == OUTER_GHOST,
|
|
(run.hierarchy.pairs[1].fine_kind == OUTER_GHOST) |
|
|
((run.boundaries[2].flags.reshape(run.hierarchy.pairs[1].fine_shape) & SOLID) != 0)]
|
|
if mode != "periodic":
|
|
invalid[0][[0, -1]] = True
|
|
for g, mask in zip(stored, invalid):
|
|
self.assertTrue(np.isnan(g[:, mask]).all())
|
|
snapshots.append(public + tuple(stored))
|
|
return snapshots
|
|
|
|
def test_layouts_static_driven_switched_and_zero(self):
|
|
for mode in ("periodic", "slot_reservoir"):
|
|
cases = [self.run_case(mode, (4, 4), jets=jets, switch=switch)
|
|
for jets, switch in (((), False), ([child_jet()], False),
|
|
([child_jet()], True), ([child_jet(0.)], False))]
|
|
for a, b in zip(cases[0], cases[3]):
|
|
for x, y in zip(a, b):
|
|
np.testing.assert_array_equal(x, y)
|
|
self.assertFalse(np.array_equal(cases[1][-1][2], cases[2][-1][2], equal_nan=True))
|
|
for block in ((8, 4), (4, 8)):
|
|
for expected, (jets, switch) in zip(cases, (((), False), ([child_jet()], False),
|
|
([child_jet()], True), ([child_jet(0.)], False))):
|
|
actual = self.run_case(mode, block, jets=jets, switch=switch)
|
|
for a, b in zip(actual, expected):
|
|
for x, y in zip(a, b):
|
|
np.testing.assert_array_equal(x, y)
|
|
|
|
def test_legacy_surface_equivalence(self):
|
|
from CelerisLab.body.objects import SurfaceActuation
|
|
spec = SurfaceActuation(.7, .85, .002)
|
|
for mode in ("periodic", "slot_reservoir"):
|
|
legacy = self.run_case(mode, (4, 4), spec=spec)[-1][2]
|
|
driven = self.run_case(mode, (4, 4), jets=[spec.as_jet((8., 8.), .8)])[-1][2]
|
|
mask = np.isfinite(legacy)
|
|
np.testing.assert_array_equal(mask, np.isfinite(driven))
|
|
self.assertLessEqual(np.max(np.abs(legacy[mask] - driven[mask])), 1e-6)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|