Wire Jets through ObjectManager and Simulation

ObjectManager.jets holds scene-level Jets. build_compact_lists_for
collects true FP64 hits (SimObject.get_curved_list hits_out) row-aligned
with the cut-link columns; BodySyncPlan carries them as curved_hits, and
sync_to_gpu / apply_sync_plan / the pre-sync check pass jets+hits to
assign_surface. Jets require D2Q9/FP32/unshifted/double_buffer and may
only drive links of stationary bodies. Simulation.add_jet (before
initialize) and set_jet_amplitude (velocity column re-upload only);
save_checkpoint refuses scenes with jets. Fine-path wiring not included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-15 08:15:22 +00:00
1 parent 611a64c54a
commit 932702f779
6 files changed
+112 -11

No files matched your search

+45
View File
@@ -1,5 +1,6 @@
"""Body-independent Jet: true-hit spatial selection, fields, profiles, overlap, amplitude."""
import math
from types import SimpleNamespace
import numpy as np
import pytest
@@ -8,6 +9,7 @@ from CelerisLab.body.jets import (Jet, Parallel, Radial, Sector, Tube,
fine_hits_to_physical, jet_link_shapes)
from CelerisLab.body.objects import SurfaceActuation
from CelerisLab.lbm.curved_links import CurvedLinkSoA, surface_link_velocity
from .test_state_regressions import device, make_field, manager
from .test_surface_actuation import circle, projected
CENTER, RADIUS = (6., 5.), 2.999
@@ -132,3 +134,46 @@ def test_legacy_adapter_measured_equivalence(spec):
diff = np.abs(cl.surface_velocity - legacy.surface_velocity).max()
print(f"legacy adapter max |dU| = {diff:.3e} (exact={diff == 0})")
assert diff <= 4*np.finfo(np.float32).eps*abs(spec.normal_velocity)
def jet_case(device, jets):
field = make_field(device)
bodies = manager(field, [circle(radius=2.)])
bodies.jets.extend(jets)
bodies.sync_to_gpu(field)
return field, bodies
def test_manager_passes_true_hits_and_reuploads_only_velocity(device):
# CPU memory double only: checks host/device bookkeeping, not GPU numerics.
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
field, bodies = jet_case(device, [jet])
cl = field.curved
assert cl.jet_id is not None and (cl.jet_id == 0).sum() >= 2
velocity_gpu = cl.surface_velocity_gpu
np.testing.assert_array_equal(np.frombuffer(velocity_gpu.data, np.float32).reshape(-1, 2),
cl.surface_velocity)
geometry = {name: bytes(getattr(cl, name + "_gpu").data)
for name in ("fluid_idx", "dir", "q", "body_id", "rx", "ry")}
writes, before = device.writes, cl.surface_velocity.copy()
cl.set_jet_amplitude(0, -1e-3, SimpleNamespace(synchronize=lambda: None))
assert device.writes == writes + 1 and cl.surface_velocity_gpu is velocity_gpu
np.testing.assert_array_equal(np.frombuffer(velocity_gpu.data, np.float32).reshape(-1, 2),
cl.surface_velocity)
assert all(bytes(getattr(cl, name + "_gpu").data) == data for name, data in geometry.items())
np.testing.assert_allclose(cl.surface_velocity, -.5*before, rtol=1e-6, atol=0)
np.testing.assert_array_equal(cl.surface_velocity[cl.jet_id < 0], 0.)
def test_manager_rejects_overlapping_jets_and_moving_bodies(device):
a = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER))
b = Jet(Tube(CENTER, 0., 3., .5), Parallel(0.))
with pytest.raises(ValueError, match="overlaps"):
jet_case(device, [a, b])
field = make_field(device)
moving = circle(radius=2.)
moving.state.omega = 1e-3
bodies = manager(field, [moving])
bodies.jets.append(a)
with pytest.raises(ValueError, match="stationary"):
bodies.sync_to_gpu(field)