From 353a7b03c4d491ced1e658cf9e44e5acfd3ec52a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 01:23:54 +0000 Subject: [PATCH] stepper: bind moving-circle kernels only for D2Q9 double buffer Both kernels are compiled under DIM == 2, but LBMStepper requested them for every double-buffer module, so stationary D3Q19 double-buffer construction would fail at get_function. Moving circles already reject D3Q19 and EsoPull at initialize. Stale latch comment updated. Parametrized CPU test with a module double that lacks the symbols. Co-Authored-By: Claude Opus 5.5 --- src/CelerisLab/lbm/stepper.py | 8 +++++--- tests/unit/test_moving_bodies.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/CelerisLab/lbm/stepper.py b/src/CelerisLab/lbm/stepper.py index 3b794ed..2a764f0 100644 --- a/src/CelerisLab/lbm/stepper.py +++ b/src/CelerisLab/lbm/stepper.py @@ -21,7 +21,7 @@ class LBMStepper: self._esopull = (cfg.streaming == "esopull") # Set by the owner when a step would be wrong (translating circles - # excluded from the packed links before the moving kernels exist). + # without attached moving tables, a pending target, or a failed step). self.blocked: str | None = None self.moving = None # MovingTables attached by the owner (translating circles) @@ -39,8 +39,10 @@ class LBMStepper: self.apply_esopull_curved_fn = module.get_function("ApplyEsoPullCurvedKernel") self.sensor_fn = module.get_function("SensorKernel") self.force_region_fn = module.get_function("ForceRegionKernel") - self.moving_fn = None if self._esopull else module.get_function("MovingCircleLinks") - self.moving_cells_fn = None if self._esopull else module.get_function("MovingCircleCells") + # Moving-circle kernels exist only in D2Q9 double-buffer modules. + moving = not self._esopull and cfg.is_d2q9 + self.moving_fn = module.get_function("MovingCircleLinks") if moving else None + self.moving_cells_fn = module.get_function("MovingCircleCells") if moving else None tpb = cfg.threads_per_block self.block = (tpb, 1, 1) diff --git a/tests/unit/test_moving_bodies.py b/tests/unit/test_moving_bodies.py index 89d6ff3..1c4f418 100644 --- a/tests/unit/test_moving_bodies.py +++ b/tests/unit/test_moving_bodies.py @@ -310,6 +310,24 @@ def test_zero_motion_target_is_consumed_by_run_one(monkeypatch): assert ("step", 3, False) in writes +@pytest.mark.parametrize("streaming,d2q9,bound", [("double_buffer", True, True), + ("double_buffer", False, False), + ("esopull", True, False)]) +def test_moving_kernels_bound_only_for_d2q9_double_buffer(monkeypatch, streaming, d2q9, bound): + from CelerisLab.lbm import stepper as stepper_module + monkeypatch.setattr(stepper_module.cuda, "mem_alloc", lambda n: object()) + moving = {"MovingCircleLinks", "MovingCircleCells"} + + def get_function(name): + if name in moving and not (d2q9 and streaming == "double_buffer"): + raise KeyError(name) # absent from D3Q19 / EsoPull modules + return lambda *a, **k: None + cfg = SimpleNamespace(streaming=streaming, threads_per_block=128, is_d2q9=d2q9, + nx=16, ny=8, nz=4, nq=9 if d2q9 else 19) + stepper = stepper_module.LBMStepper(SimpleNamespace(), SimpleNamespace(get_function=get_function), cfg) + assert (stepper.moving_fn is not None) == bound and (stepper.moving_cells_fn is not None) == bound + + def test_moving_diagnostics_empty_shape_matches_layout(monkeypatch): from CelerisLab.body.motion import MovingTables sim = make_simulation(monkeypatch)