refinement: FixedPatchStepper(refinements=[]) owns a single blocked level (S1)

A zero-refinement FixedPatchStepper is the PeriodicBlockStepper capability:
one doubly periodic blocked SRT level (no LES, Jets or exchange
observation) whose level_objects[0] are stationary passive circles. The
circle tables come from the same checks, now blocks.static_circle_tables
(moved from block_stepper, pycuda-free); BFL, halo and BlockStep launches
are the shared helpers, with an all-ACTIVE (null) kind. Base-level bodies
stay rejected for depth >= 2. PeriodicBlockStepper and its callers are
unchanged (deleted only after S1's GPU result).

Tests: host contract rejections before CUDA for the single level; GPU
bitwise comparison against PeriodicBlockStepper for every split, with and
without the circle (the S2 gate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-10-26 22:34:52 +00:00
co-authored by Claude Opus 5.5
parent a0700ab370
commit f764d68c81
4 changed files with 130 additions and 66 deletions
@@ -25,6 +25,7 @@ from CelerisLab.cuda import compiler
from CelerisLab.lbm import block_stepper as implementation
from CelerisLab.lbm.blocks import NO_DONOR, BlockLayout2D
from CelerisLab.lbm.block_stepper import PeriodicBlockStepper
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
ROOT = Path(__file__).resolve().parents[3]
@@ -118,6 +119,23 @@ class CpuContracts(unittest.TestCase):
def forbidden_context(self):
return Mock(activate=Mock(side_effect=AssertionError("CUDA reached")), sm_arch="sm_70")
def test_single_level_fixed_patch_contract_before_cuda(self):
def single(cfg=None, **kw):
options = dict(context=self.forbidden_context(), refinements=[], level_blocks=[(12, 16)])
options.update(kw)
return FixedPatchStepper(cfg or config(), **options)
for cfg, kw in ((None, dict(level_blocks=[None])),
(None, dict(coarse_boundary="channel")),
(None, dict(coarse_boundary="slot_reservoir")),
(config(collision="MRT"), {}),
(None, dict(observe_raw_exchange=True)),
(None, dict(level_jets=[(object(),)])),
(None, dict(level_objects=[(circle((15.5, 4.0), 4.3),)]))): # touches edge
with self.subTest(kw=kw), self.assertRaises(ValueError):
single(cfg, **kw)
with self.assertRaisesRegex(AssertionError, "CUDA reached"):
single(level_objects=[(circle(),)])
def test_unsupported_inputs_rejected_before_cuda(self):
layout = BlockLayout2D.build(NY, NX, 12, 16)
cases = ((config(), BlockLayout2D.build(NY, NX, 12, 16, periodic=(True, False))),
@@ -139,7 +157,7 @@ class CpuContracts(unittest.TestCase):
with self.subTest(center=center, radius=radius), self.assertRaises(ValueError):
PeriodicBlockStepper(config(), layout, context=self.forbidden_context(),
objects=[circle(center, radius)])
with patch.object(implementation, "_static_circles") as prepare:
with patch.object(implementation, "static_circle_tables") as prepare:
with self.assertRaisesRegex(AssertionError, "CUDA reached"): # body-free path
PeriodicBlockStepper(config(), layout, context=self.forbidden_context(), objects=iter(()))
prepare.assert_not_called()
@@ -177,7 +195,7 @@ class CpuContracts(unittest.TestCase):
def test_static_circle_tables(self):
layout = BlockLayout2D.build(NY, NX, 12, 16)
tables = implementation._static_circles(layout, (circle(),))
tables = implementation.static_circle_tables(layout, (circle(),))
links = CircleGeometry(*CENTER, RADIUS).build_cut_links(NX, NY, domain_flags=dense_flags())
self.assertEqual(len(tables["cell"]), len(links))
np.testing.assert_array_equal(tables["q"], np.array([l.q for l in links], np.float32))
@@ -331,6 +349,29 @@ class PeriodicBlockGPU(unittest.TestCase):
*(a - b for a, b in zip(after, before))), flush=True)
self.assertLessEqual(error, ORACLE_ATOL)
def test_single_level_fixed_patch_is_bit_identical(self):
"""FixedPatchStepper(refinements=[]) reproduces PeriodicBlockStepper bit for bit."""
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)):
layout = BlockLayout2D.build(NY, NX, by, bx)
with PeriodicBlockStepper(config(), layout, context=context,
objects=objects) as backend:
backend.initialize(g0)
backend.step(STEPS)
expected = backend.download()
with FixedPatchStepper(config(), context=context, refinements=[],
level_blocks=[(by, bx)],
level_objects=[objects]) as backend:
backend.initialize(g0)
backend.step(STEPS)
self.assertEqual(backend.step_count, STEPS)
(actual,) = backend.download()
np.testing.assert_array_equal(actual, expected)
if __name__ == "__main__":
unittest.main()