body: moving-step failure fails the Simulation; S1 acceptance tests

A failed MovingCircle.advance now calls the owner hook, so the Simulation
is marked failed (Jet lifecycle) and refuses run/step/sync until close().
Adds host tests for set_body routing on real objects, initialize band and
mode rejections before mutation, exclusion from static link packing, and
the Simulation failure lifecycle. Contract status line updated for S1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-30 06:43:16 +00:00
1 parent 305ab2409b
commit 8a8e8a03ff
4 files changed
+145 -5

No files matched your search

+125
View File
@@ -129,3 +129,128 @@ def test_moving_body_rejects_topology_and_checkpoint(monkeypatch):
for call in (sim.sync_bodies, sim.save_checkpoint, lambda: sim.load_checkpoint("x.h5")):
with pytest.raises(ValueError, match="translating circles"):
call()
# -- Real-object routing, initialize rejections, packing, lifecycle -------------
import numpy as np # noqa: E402
from CelerisLab.body.geometry.circle import CircleGeometry # noqa: E402
from CelerisLab.body.manager import ObjectManager # noqa: E402
from CelerisLab.body.objects import SimObject # noqa: E402
from CelerisLab.lbm.descriptors import FLUID, SOLID # noqa: E402
class Cfg:
dim, is_d3q19, is_d2q9 = 2, False, True
streaming, ddf_shifting, store_precision, y_wall_bc = "double_buffer", False, "FP32", "bounce_back"
def body(x, y, r, translation=None, **kw):
obj = SimObject(obj_id=-1, geometry=CircleGeometry(x, y, r), center=(x, y), radius=r, **kw)
if translation is not None:
obj.motion = MovingCircle((x, y), r, translation)
return obj
def routed_simulation(monkeypatch):
sim = make_simulation(monkeypatch)
sim._failed, sim._initialized, sim.lbm_cfg = None, False, Cfg()
sim.bodies = ObjectManager(nx=120, ny=80, nz=1, nq=9, cfg=Cfg())
return sim
def test_add_body_and_set_body_route_targets(monkeypatch):
sim = routed_simulation(monkeypatch)
bid = sim.add_body("circle", (50., 40.), 10., translation=Translation((-2., 2., -3., 3.), .1))
still = sim.add_body("circle", (100., 40.), 5.)
motion = sim.bodies.get(bid).motion
sim.set_body(bid, offset=(0., .3), vy=.05)
sim.set_body(bid, offset=(0., .4), vy=.05) # last wins
for kwargs in (dict(offset=(0., .6)), dict(vy=.05), dict(vx=.05),
dict(offset=(0., .1), vy=.2), dict(offset=(0., 0.), vy=.01)):
with pytest.raises(ValueError):
sim.set_body(bid, **kwargs)
assert motion.pending == ((0., .4), (0., .05)) # invalid input keeps the target
sim.set_body(bid, omega=.01) # omega-only keeps the pending target
obj = sim.bodies.get(bid)
assert obj.state.omega == .01 and (obj.state.vx, obj.state.vy) == (0., 0.)
assert motion.pending == ((0., .4), (0., .05)) and motion.applied == (0., 0.)
with pytest.raises(ValueError, match="not declared"):
sim.set_body(still, offset=(0., .1))
sim.set_body(still, vx=.01) # stationary API unchanged
assert sim.bodies.get(still).state.vx == .01 and sim.bodies.get(still).motion is None
for kwargs in (dict(type="sensor"), dict(surface_actuation=dict(angle=0., half_width=.2,
normal_velocity=.01))):
kind = kwargs.pop("type", "circle")
with pytest.raises(ValueError, match="translation"):
sim.add_body(kind, (20., 20.), 5., translation=Translation((0., 0., 0., 0.), .1),
**kwargs)
def validate(monkeypatch, objects, flags=None, **cfg):
sim = routed_simulation(monkeypatch)
for obj in objects:
sim.bodies.add(obj)
for name, value in cfg.items():
setattr(sim.lbm_cfg, name, value)
base = np.full(120 * 80, FLUID, np.uint16) if flags is None else flags
sim.field = SimpleNamespace(nx=120, ny=80, build_channel_flags=lambda: base.copy())
sim._validate_moving_bodies()
def test_initialize_rejects_band_and_modes(monkeypatch):
t = Translation((-1., 1., -3., 3.), .1)
validate(monkeypatch, [body(50., 40., 10., t), body(100., 40., 5.)]) # accepted
walls = np.full(120 * 80, FLUID, np.uint16)
walls[60 + 30 * 120] = SOLID
cases = [([body(10., 40., 10., t)], None, {}, "domain edge"),
([body(50., 40., 10., t)], walls, {}, "non-fluid"),
([body(50., 40., 10., t), body(68., 40., 4.)], None, {}, "overlaps"),
([body(50., 40., 10., t), body(50., 60., 3., is_sensor=True)], None, {}, "overlaps"),
([body(50., 40., 10., t), body(64., 40., 3., is_force_region=True)], None, {}, "overlaps"),
([body(50., 40., 10., t)], None, {"streaming": "esopull"}, "double_buffer"),
([body(50., 40., 10., t)], None, {"ddf_shifting": True}, "unshifted"),
([body(50., 40., 10., t)], None, {"is_d2q9": False}, "D2Q9")]
for objects, flags, cfg, match in cases:
with pytest.raises(ValueError, match=match):
validate(monkeypatch, objects, flags, **cfg)
def test_initialize_validates_before_mutation(monkeypatch):
sim = routed_simulation(monkeypatch)
sim.bodies.add(body(10., 40., 10., Translation((0., 0., 0., 0.), .1)))
uploads = []
sim.field = SimpleNamespace(nx=120, ny=80, build_channel_flags=lambda: np.full(9600, FLUID, np.uint16),
upload_flags=lambda: uploads.append("flags"))
sim._assert_allocation_contract = lambda: None
with pytest.raises(ValueError, match="domain edge"):
sim.initialize()
assert uploads == [] and not sim._initialized
def test_moving_circle_excluded_from_static_links():
still = [body(100., 40., 5.)]
mgr = ObjectManager(nx=120, ny=80, nz=1, nq=9, cfg=Cfg())
reference = mgr.build_compact_lists_for(still)
with_moving = mgr.build_compact_lists_for(
still + [body(50., 40., 10., Translation((0., 0., -1., 1.), .1))])
for a, b in zip(reference[:8], with_moving[:8]):
assert np.asarray(a).tobytes() == np.asarray(b).tobytes()
assert len(reference[0]) > 0
def test_failed_moving_step_marks_simulation_failed(monkeypatch):
sim = routed_simulation(monkeypatch)
bid = sim.add_body("circle", (50., 40.), 10., translation=Translation((0., 0., -1., 1.), .1))
motion = sim.bodies.get(bid).motion
motion.set_target((0., .2), (0., .05))
def fail(*_):
raise RuntimeError("sync failed")
with pytest.raises(RuntimeError, match="sync failed"):
motion.advance(lambda *_: None, lambda: None, fail)
assert sim._failed is not None and motion.applied == (0., 0.)
for call in (lambda: sim.run(0), lambda: sim.run(1), sim.step, sim.sync_bodies):
with pytest.raises(RuntimeError, match="Simulation failed"):
call()
sim.close() # close still works; no re-entry afterwards