Jets: one jet may drive links of only one body

surface_link_velocity (the shared host check used by CurvedLinkSoA,
ObjectManager sync, prepare_fine_boundary and FixedPatchStepper) now
rejects a jet whose selected cut links carry more than one body id, at
any amplitude, before uploads. Identity is the scene-wide body id of the
physical cut-link list, so one body spanning storage blocks stays valid.
No body selector; empty-selection semantics unchanged.

Tests: two bodies under one jet rejected (amplitude 2e-3 and 0) on the
Simulation/manager path and on the fine path; separate jets on separate
bodies accepted on both; one fine body crossing 2x2 block seams accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-16 02:39:58 +00:00
1 parent f1e03423f3
commit 64d54ab23e
3 files changed
+54 -1

No files matched your search

+36
View File
@@ -293,6 +293,16 @@ def test_fine_boundary_keeps_edge_frame_hits_and_checks_jets():
refinement.prepare_fine_boundary(cfg, patch, [body], jets=[jet, jet])
with pytest.raises(ValueError, match="need fine boundary objects"):
refinement.prepare_fine_boundary(cfg, patch, [], jets=[jet])
# One fine jet over two physical bodies is rejected; one per body is accepted.
A, B = (8.75, 12.25), (15.75, 12.25)
pair = [SimObject(i, geometry=CircleGeometry(*c, 1.5), center=c, radius=1.5)
for i, c in enumerate((A, B))]
span = Jet(Tube((4.5, 12.25), 0., 15., .5), Parallel(0.))
with pytest.raises(ValueError, match="one jet may drive only one body"):
refinement.prepare_fine_boundary(cfg, patch, pair, jets=[span])
refinement.prepare_fine_boundary(cfg, patch, pair, jets=[
Jet(Sector(A, 0., .5, 2.), Radial(A, inward=False)),
Jet(Sector(B, math.pi, .5, 2.), Radial(B, inward=False))])
# Jet vs legacy SurfaceActuation overlap on the fine level (root's case).
legacy = SimObject(0, geometry=CircleGeometry(*C, R), center=C, radius=R,
surface_actuation=SurfaceActuation(0., .5, .002))
@@ -310,3 +320,29 @@ def test_action_upload_keeps_cfg_none_fallback(device):
bodies._upload_action_async(stream)
bodies.add(circle(radius=2.))
bodies._upload_action_async(stream)
def two_body_case(device, jets):
field = make_field(device)
bodies = manager(field, [circle(center=(3., 5.), radius=1.6),
circle(center=(8.5, 5.), radius=1.6)])
bodies.jets.extend(jets)
bodies.sync_to_gpu(field)
return field, bodies
@pytest.mark.parametrize("amplitude", [2e-3, 0.0]) # geometry checked even at zero amplitude
def test_one_jet_spanning_two_bodies_rejected(device, amplitude):
both = Jet(Tube((0., 5.), 0., 12., .5), Parallel(0.), amplitude=amplitude)
with pytest.raises(ValueError, match="one jet may drive only one body"):
two_body_case(device, [both])
def test_separate_jets_on_separate_bodies_accepted(device):
left = Jet(Sector((3., 5.), 0., .6, 2.), Radial((3., 5.), inward=False), amplitude=1e-3)
right = Jet(Sector((8.5, 5.), math.pi, .6, 2.), Radial((8.5, 5.), inward=False), amplitude=-1e-3)
field, _ = two_body_case(device, [left, right])
cl = field.curved
for j in (0, 1):
assert len(np.unique(cl.body_id[cl.jet_id == j])) == 1
assert cl.body_id[cl.jet_id == 0][0] != cl.body_id[cl.jet_id == 1][0]