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:
1 parent
f764d68c81
commit
985db14249
3 files changed
+80
-8
No files matched your search
@@ -204,9 +204,10 @@ class FixedPatchStepper:
|
|||||||
if self._ddf_shifting and any(b is None for b in level_blocks):
|
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")
|
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
|
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):
|
or observe_raw_exchange or observe_window_exchange):
|
||||||
raise ValueError("a single level requires a periodic SRT root without LES or exchange "
|
raise ValueError("a single level requires a periodic SRT root without LES, projection "
|
||||||
"observation")
|
"or exchange observation")
|
||||||
self._channel = coarse_boundary == "channel"
|
self._channel = coarse_boundary == "channel"
|
||||||
if self._channel:
|
if self._channel:
|
||||||
validate_channel_config(cfg, level_blocks)
|
validate_channel_config(cfg, level_blocks)
|
||||||
@@ -533,7 +534,9 @@ class FixedPatchStepper:
|
|||||||
try:
|
try:
|
||||||
for level, array in zip(self._levels, arrays):
|
for level, array in zip(self._levels, arrays):
|
||||||
cuda.memcpy_htod(level.current, array)
|
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:
|
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)
|
cuda.memset_d8(self._fine_curved.link_exchange_gpu, 0, self._fine_curved.count * 12)
|
||||||
self._step_count = 0
|
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"), {}),
|
(config(collision="MRT"), {}),
|
||||||
(None, dict(observe_raw_exchange=True)),
|
(None, dict(observe_raw_exchange=True)),
|
||||||
(None, dict(level_jets=[(object(),)])),
|
(None, dict(level_jets=[(object(),)])),
|
||||||
|
(None, dict(explosion="projected_linear")),
|
||||||
(None, dict(level_objects=[(circle((15.5, 4.0), 4.3),)]))): # touches edge
|
(None, dict(level_objects=[(circle((15.5, 4.0), 4.3),)]))): # touches edge
|
||||||
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
||||||
single(cfg, **kw)
|
single(cfg, **kw)
|
||||||
@@ -354,16 +355,17 @@ class PeriodicBlockGPU(unittest.TestCase):
|
|||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
g0 = initial_state()
|
g0 = initial_state()
|
||||||
with CudaContext(0) as context:
|
with CudaContext(0) as context:
|
||||||
for (by, bx), objects in [(split, objects) for split in SPLITS
|
for (by, bx), objects, shifted in [(split, objects, shifted) for split in SPLITS
|
||||||
for objects in ((), (circle(),))]:
|
for objects in ((), (circle(),))
|
||||||
with self.subTest(split=(by, bx), bodies=len(objects)):
|
for shifted in (False, True)]:
|
||||||
|
with self.subTest(split=(by, bx), bodies=len(objects), shifted=shifted):
|
||||||
layout = BlockLayout2D.build(NY, NX, by, bx)
|
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:
|
objects=objects) as backend:
|
||||||
backend.initialize(g0)
|
backend.initialize(g0)
|
||||||
backend.step(STEPS)
|
backend.step(STEPS)
|
||||||
expected = backend.download()
|
expected = backend.download()
|
||||||
with FixedPatchStepper(config(), context=context, refinements=[],
|
with FixedPatchStepper(config(ddf_shifting=shifted), context=context, refinements=[],
|
||||||
level_blocks=[(by, bx)],
|
level_blocks=[(by, bx)],
|
||||||
level_objects=[objects]) as backend:
|
level_objects=[objects]) as backend:
|
||||||
backend.initialize(g0)
|
backend.initialize(g0)
|
||||||
|
|||||||
Reference in new issue
Block a user