Support stationary bodies and geometry Jets on nested L2
This commit is contained in:
@@ -10,8 +10,10 @@ Run CPU and GPU classes in separate processes. GPU gates (frozen before any run)
|
||||
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 patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
@@ -28,6 +30,32 @@ L1, L2 = (4, 4, 12, 12), (8, 8, 16, 16)
|
||||
KW = dict(fine_block=(4, 4), base_block=(4, 4), child=L2, child_block=(4, 4))
|
||||
|
||||
|
||||
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_v2 as 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(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, L1, context=context, child_objects=[child_body()],
|
||||
child_jets=[child_jet()], **KW)
|
||||
|
||||
|
||||
class ThreeLevelCPU(unittest.TestCase):
|
||||
def test_rejections_before_cuda(self):
|
||||
context = object() # never activated: every case must fail first
|
||||
@@ -37,11 +65,92 @@ class ThreeLevelCPU(unittest.TestCase):
|
||||
for kw in bad:
|
||||
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
||||
FixedPatchStepper(CFG, L1, context=context, **kw)
|
||||
for kw in (dict(child_objects=[child_body()]), dict(child_jets=[child_jet()]),
|
||||
dict(KW, child_jets=[child_jet()])):
|
||||
with self.assertRaises(ValueError):
|
||||
FixedPatchStepper(CFG, L1, context=context, **kw)
|
||||
with self.assertRaises(ValueError): # several L1 patches + child: not yet
|
||||
FixedPatchStepper(LBMConfig(nx=32, ny=16, viscosity=.1), ((4, 4, 12, 12), (20, 4, 28, 12)),
|
||||
context=context, **KW)
|
||||
|
||||
|
||||
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_child, run._gather_halo
|
||||
def record_feed(buffer, **kw):
|
||||
self.assertIs(buffer, (f.current, f.spare)[len(feeds)])
|
||||
feeds.append(buffer)
|
||||
advance(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_child", 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_child", side_effect=lambda buffer, **kw:
|
||||
record_feed(f.current, **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(IndexError):
|
||||
run.set_jet_amplitude(0, -.002) # default stays L1
|
||||
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, ())
|
||||
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.child_boundary.flags.reshape(run.child_patch.fine_shape) & SOLID != 0
|
||||
self.assertTrue(solid.any())
|
||||
self.assertFalse(run._child_leaf[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.child_layout.to_dense(sent)
|
||||
self.assertTrue(np.isnan(dense[:, ~run._child_leaf]).all())
|
||||
run.close()
|
||||
|
||||
|
||||
class ThreeLevelGPU(unittest.TestCase):
|
||||
def test_equilibrium_is_kept_on_every_level(self):
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
@@ -85,5 +194,67 @@ class ThreeLevelGPU(unittest.TestCase):
|
||||
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, L1, context=ctx, coarse_boundary=mode, child_objects=[child_body(spec)],
|
||||
child_jets=jets, **dict(KW, child_block=block)) 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.patch.covered, run._fine_leaf, run._child_leaf]
|
||||
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.patch.covered.copy(), run._middle_kind == OUTER_GHOST,
|
||||
(run.child_patch.fine_kind == OUTER_GHOST) |
|
||||
((run.child_boundary.flags.reshape(run.child_patch.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()
|
||||
|
||||
Reference in New Issue
Block a user