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>
68 lines
2.8 KiB
Python
68 lines
2.8 KiB
Python
"""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()
|