refinement: single level matches PeriodicBlockStepper lifecycle; host contract tests
Review follow-ups for S1 (f764d68):
- a single level starts its spare buffer NaN, as PeriodicBlockStepper does,
so stale halos cannot hide (multilevel initialization unchanged);
- projected explosion is rejected for a single level;
- new tests/unit/test_single_level.py: NaN spare after initialize, per-step
launch order halo -> BFL (circles only) -> BlockStep with a null kind and
one wait per step(n), circle solids excluded from the state mask;
- the GPU bitwise comparison also covers shifted storage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f764d68c81
commit
985db14249
@@ -204,9 +204,10 @@ class FixedPatchStepper:
|
||||
if self._ddf_shifting and any(b is None for b in level_blocks):
|
||||
raise ValueError("shifted populations require all-blocked levels; dense/mixed is unsupported")
|
||||
if depth == 1 and (coarse_boundary != "periodic" or cfg.collision != "SRT" or cfg.les_enabled
|
||||
or explosion != "constant"
|
||||
or observe_raw_exchange or observe_window_exchange):
|
||||
raise ValueError("a single level requires a periodic SRT root without LES or exchange "
|
||||
"observation")
|
||||
raise ValueError("a single level requires a periodic SRT root without LES, projection "
|
||||
"or exchange observation")
|
||||
self._channel = coarse_boundary == "channel"
|
||||
if self._channel:
|
||||
validate_channel_config(cfg, level_blocks)
|
||||
@@ -533,7 +534,9 @@ class FixedPatchStepper:
|
||||
try:
|
||||
for level, array in zip(self._levels, arrays):
|
||||
cuda.memcpy_htod(level.current, array)
|
||||
cuda.memcpy_htod(level.spare, array)
|
||||
# A single level starts its spare NaN so stale halos cannot hide.
|
||||
cuda.memcpy_htod(level.spare, np.full_like(array, np.nan)
|
||||
if len(self._levels) == 1 else array)
|
||||
if self._fine_curved is not None and self._fine_curved.link_exchange_gpu is not None:
|
||||
cuda.memset_d8(self._fine_curved.link_exchange_gpu, 0, self._fine_curved.count * 12)
|
||||
self._step_count = 0
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Host contract of FixedPatchStepper(refinements=[]): one blocked level (mocked driver).
|
||||
|
||||
Per step: halo refresh -> static BFL (circles only) -> BlockStep with a null
|
||||
kind, one stream wait per step(n) call, NaN spare after initialize.
|
||||
"""
|
||||
from contextlib import nullcontext
|
||||
from unittest.mock import Mock
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
from CelerisLab.body.objects import SimObject
|
||||
from CelerisLab.config import LBMConfig
|
||||
from tests.unit.test_refinement_three_level_multipatch import host_module
|
||||
|
||||
|
||||
def circle(center=(15.5, 11.5), radius=4.3):
|
||||
return SimObject(0, geometry=CircleGeometry(*center, radius), center=center, radius=radius)
|
||||
|
||||
|
||||
def backend(objects=()):
|
||||
host, log = host_module(), []
|
||||
module = Mock()
|
||||
module.get_global.return_value = (Mock(), 20)
|
||||
module.get_function.side_effect = lambda name: Mock(
|
||||
side_effect=lambda *a, **k: log.append((name, a)))
|
||||
host.compiler.build_module.return_value = module
|
||||
host.cuda.mem_alloc.side_effect = lambda size: Mock(size=size)
|
||||
host.cuda.memcpy_htod.side_effect = lambda pointer, array: log.append(("upload", (pointer, array)))
|
||||
run = host.FixedPatchStepper(LBMConfig(nx=32, ny=24, viscosity=.1),
|
||||
context=Mock(activate=nullcontext, sm_arch="sm_70"),
|
||||
refinements=[], level_blocks=[(12, 16)], level_objects=[objects])
|
||||
run._stream.synchronize.side_effect = lambda: log.append(("sync", ()))
|
||||
log.clear()
|
||||
run.initialize(np.full(run._levels[0].shape, 1 / 9, np.float32))
|
||||
return run, log
|
||||
|
||||
|
||||
class SingleLevel(unittest.TestCase):
|
||||
def test_spare_starts_nan_and_current_holds_state(self):
|
||||
run, log = backend()
|
||||
level = run._levels[0]
|
||||
uploads = {id(a[0]): a[1] for name, a in log if name == "upload"}
|
||||
self.assertTrue(np.isnan(uploads[id(level.spare)]).all())
|
||||
self.assertTrue(np.isfinite(level.layout.to_dense(uploads[id(level.current)])).all())
|
||||
|
||||
def test_launch_order_per_step(self):
|
||||
for objects, order in (((), ["BlockHaloCopy", "BlockStep"]),
|
||||
((circle(),), ["BlockHaloCopy", "BlockCurvedBoundary", "BlockStep"])):
|
||||
with self.subTest(bodies=len(objects)):
|
||||
run, log = backend(objects)
|
||||
log.clear()
|
||||
run.step(2)
|
||||
self.assertEqual([name for name, _ in log], order * 2 + ["sync"])
|
||||
step = [a for name, a in log if name == "BlockStep"][0]
|
||||
self.assertEqual(int(step[4]), 0) # null kind: every core cell ACTIVE
|
||||
|
||||
def test_circle_solids_are_not_state(self):
|
||||
run, _ = backend((circle(),))
|
||||
solid = run._level_boundaries[0].tables["solid"]
|
||||
self.assertTrue(solid.any())
|
||||
np.testing.assert_array_equal(run._state[0], ~solid)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -130,6 +130,7 @@ class CpuContracts(unittest.TestCase):
|
||||
(config(collision="MRT"), {}),
|
||||
(None, dict(observe_raw_exchange=True)),
|
||||
(None, dict(level_jets=[(object(),)])),
|
||||
(None, dict(explosion="projected_linear")),
|
||||
(None, dict(level_objects=[(circle((15.5, 4.0), 4.3),)]))): # touches edge
|
||||
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
||||
single(cfg, **kw)
|
||||
@@ -354,16 +355,17 @@ class PeriodicBlockGPU(unittest.TestCase):
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
g0 = initial_state()
|
||||
with CudaContext(0) as context:
|
||||
for (by, bx), objects in [(split, objects) for split in SPLITS
|
||||
for objects in ((), (circle(),))]:
|
||||
with self.subTest(split=(by, bx), bodies=len(objects)):
|
||||
for (by, bx), objects, shifted in [(split, objects, shifted) for split in SPLITS
|
||||
for objects in ((), (circle(),))
|
||||
for shifted in (False, True)]:
|
||||
with self.subTest(split=(by, bx), bodies=len(objects), shifted=shifted):
|
||||
layout = BlockLayout2D.build(NY, NX, by, bx)
|
||||
with PeriodicBlockStepper(config(), layout, context=context,
|
||||
with PeriodicBlockStepper(config(ddf_shifting=shifted), layout, context=context,
|
||||
objects=objects) as backend:
|
||||
backend.initialize(g0)
|
||||
backend.step(STEPS)
|
||||
expected = backend.download()
|
||||
with FixedPatchStepper(config(), context=context, refinements=[],
|
||||
with FixedPatchStepper(config(ddf_shifting=shifted), context=context, refinements=[],
|
||||
level_blocks=[(by, bx)],
|
||||
level_objects=[objects]) as backend:
|
||||
backend.initialize(g0)
|
||||
|
||||
Reference in New Issue
Block a user