simulation: derive the stepper latch from moving bodies on add/remove
One helper sets stepper.blocked from _moving_bodies() after add_body, pre-init remove_body, initialize and stepper recreation, so the latch never disagrees with motion ownership. Tests: latch follows add/remove; the after-close assertion checks the actual refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b448bc3f71
commit
e682d16c3a
@@ -148,7 +148,6 @@ 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])
|
||||
@@ -172,7 +171,9 @@ class Simulation:
|
||||
if self._initialized:
|
||||
self.bodies.stage_add(obj)
|
||||
return -1
|
||||
return self.bodies.add(obj)
|
||||
body_id = self.bodies.add(obj)
|
||||
self._refresh_stepper_block()
|
||||
return body_id
|
||||
|
||||
def add_jet(self, jet) -> int:
|
||||
"""Register a body-independent Jet before ``initialize()``; returns its index.
|
||||
@@ -212,6 +213,10 @@ 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]
|
||||
|
||||
@@ -269,6 +274,7 @@ class Simulation:
|
||||
self.bodies.stage_remove(id)
|
||||
else:
|
||||
self.bodies.remove(id)
|
||||
self._refresh_stepper_block()
|
||||
|
||||
# -- Legacy convenience (keep for backward compat) -----------------------
|
||||
def add_cylinder(self, center: Tuple[float, ...],
|
||||
@@ -516,7 +522,7 @@ class Simulation:
|
||||
self.field, self._module, self.lbm_cfg,
|
||||
)
|
||||
self.stepper._step_count = _prev_step_count
|
||||
self.stepper.blocked = MOVING_BLOCKED if self._moving_bodies() else None
|
||||
self._refresh_stepper_block()
|
||||
|
||||
self._assert_object_count_contract(expected_count=expected_n_objects)
|
||||
|
||||
@@ -543,7 +549,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.stepper.blocked = MOVING_BLOCKED if self._moving_bodies() else None
|
||||
self._refresh_stepper_block()
|
||||
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)
|
||||
|
||||
@@ -232,6 +232,16 @@ def test_add_body_and_set_body_route_targets(monkeypatch):
|
||||
**kwargs)
|
||||
|
||||
|
||||
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:
|
||||
@@ -306,5 +316,5 @@ def test_failed_moving_step_marks_simulation_failed(monkeypatch):
|
||||
with pytest.raises(RuntimeError, match="unknown"): # no retry on the failed circle
|
||||
motion.advance(lambda *_: None, lambda: None, lambda: None)
|
||||
sim.close() # close still works
|
||||
with pytest.raises(Exception): # no re-entry after close
|
||||
with pytest.raises(RuntimeError, match="Simulation failed"): # no re-entry after close
|
||||
sim.run(1)
|
||||
|
||||
Reference in New Issue
Block a user