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:
Claude
2026-10-26 22:36:05 +00:00
co-authored by Claude Opus 5.5
parent f764d68c81
commit 985db14249
3 changed files with 80 additions and 8 deletions
+6 -3
View File
@@ -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