From e1ed27b438a409c9a6f9394ea6285498aaf17cef Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 06:57:02 +0000 Subject: [PATCH] simulation: revert latch-only refresh (e682d16, 36eac713) No reachable path leaves a stale latch (Grok review): initialize and _recompile recompute it, and post-init removal of a translating circle cannot commit. simulation.py is back to b448bc3f; the latch-only test is dropped. The recompile, close-retry and tightened after-close tests stay. Co-Authored-By: Claude Opus 5.5 --- src/CelerisLab/simulation.py | 17 ++++------------- tests/unit/test_moving_bodies.py | 10 ---------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/CelerisLab/simulation.py b/src/CelerisLab/simulation.py index 7fbdc8a..fc8f7d1 100644 --- a/src/CelerisLab/simulation.py +++ b/src/CelerisLab/simulation.py @@ -148,6 +148,7 @@ class Simulation: from .body.motion import MovingCircle obj.motion = MovingCircle((cx, cy), radius, translation, on_failure=self._fail_moving) + self.stepper.blocked = MOVING_BLOCKED elif type_lower == "sensor": from .body.geometry.circle import CircleGeometry cx, cy = float(center[0]), float(center[1]) @@ -171,10 +172,7 @@ class Simulation: if self._initialized: self.bodies.stage_add(obj) return -1 - body_id = self.bodies.add(obj) - if translation is not None: - self._refresh_stepper_block() - return body_id + return self.bodies.add(obj) def add_jet(self, jet) -> int: """Register a body-independent Jet before ``initialize()``; returns its index. @@ -214,10 +212,6 @@ class Simulation: # Device geometry is unknown after a failed moving step: same lifecycle as Jets. self._failed = f"moving circle step failed: {exc!r}" - def _refresh_stepper_block(self) -> None: - # Derived latch: direct stepper calls must not step after static-link exclusion. - self.stepper.blocked = MOVING_BLOCKED if self._moving_bodies() else None - def _moving_bodies(self): return [obj for obj in self.bodies.objects if obj.motion is not None] @@ -274,10 +268,7 @@ class Simulation: if self._initialized: self.bodies.stage_remove(id) else: - moving = self.bodies.get(id).motion is not None self.bodies.remove(id) - if moving: - self._refresh_stepper_block() # -- Legacy convenience (keep for backward compat) ----------------------- def add_cylinder(self, center: Tuple[float, ...], @@ -525,7 +516,7 @@ class Simulation: self.field, self._module, self.lbm_cfg, ) self.stepper._step_count = _prev_step_count - self._refresh_stepper_block() + self.stepper.blocked = MOVING_BLOCKED if self._moving_bodies() else None self._assert_object_count_contract(expected_count=expected_n_objects) @@ -552,7 +543,7 @@ class Simulation: # change. Compact-list construction publishes no outputs on failure. next_flags = self.bodies.build_flags(self.field.build_channel_flags()) self._validate_moving_bodies() - self._refresh_stepper_block() + self.stepper.blocked = MOVING_BLOCKED if self._moving_bodies() else None if self.bodies.jets and self.bodies.count == 0: raise ValueError("Jets need bodies with cut links") self.bodies.build_compact_lists_for(self.bodies.objects, next_flags) diff --git a/tests/unit/test_moving_bodies.py b/tests/unit/test_moving_bodies.py index 86fa9c0..f90c725 100644 --- a/tests/unit/test_moving_bodies.py +++ b/tests/unit/test_moving_bodies.py @@ -258,16 +258,6 @@ def test_recompile_installs_a_guarded_stepper(monkeypatch): assert sim.stepper.step_count == 7 -def test_stepper_latch_follows_moving_bodies(monkeypatch): - sim = routed_simulation(monkeypatch) - still = sim.add_body("circle", (100., 40.), 5.) - assert sim.stepper.blocked is None - moving = sim.add_body("circle", (50., 40.), 10., translation=Translation((0., 0., -1., 1.), .1)) - assert sim.stepper.blocked is not None - sim.remove_body(moving) # removing the last translating circle clears the latch - assert sim.stepper.blocked is None and sim.bodies.get(still).motion is None - - def validate(monkeypatch, objects, flags=None, **cfg): sim = routed_simulation(monkeypatch) for obj in objects: