Wire Jets into FixedPatchStepper fine level (dense and blocked)

FixedPatchStepper(..., jets=()) passes jets (coarse cell-edge frame, as
fine_objects) to prepare_fine_boundary, which runs the shared assignment
checks before CUDA. Dense fine path: assign_surface with jets and the
boundary's true hits. Blocked path: surface_link_velocity with jets/hits;
unit-amplitude shapes cached. set_jet_amplitude (new/ready only) validates
first, then rewrites the dense SoA column or the blocked jet rows and
re-uploads only the velocity buffer after a stream sync. Scheduling,
kernels and launch ABI unchanged.

tests/validation/multigrid/test_refinement_jets.py: mocked-driver CPU
checks and frozen GPU gates G1-G3 (layout identity, jet vs legacy <=1e-6,
amplitude switch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-15 08:42:51 +00:00
1 parent f26410f94a
commit aebef52345
2 files changed
+170 -7

No files matched your search

+36 -7
View File
@@ -16,6 +16,7 @@ import pycuda.driver as cuda
from ..cuda import compiler_v2 as compiler
from .blocks import BlockLayout2D, blocked_link_tables
from .curved_links import surface_link_velocity
from ..body.jets import jet_link_shapes, jet_link_velocity
from .block_stepper import MAX_GRID_DIM
from .refinement import ACTIVE, HALO, OUTER_GHOST, FixedPatch2D, prepare_fine_boundary
from .descriptors import SOLID, D2Q9_EX, D2Q9_EY
@@ -106,9 +107,12 @@ class FixedPatchStepper:
covered cells are explicitly poisoned on every step. Public shapes stay dense.
Blocked mode applies prescribed surface velocity only; its per-link
exchange diagnostic is unavailable (rows keep physical cut-link order).
``jets`` (body.jets.Jet) use the same edge frame as ``fine_objects`` and
drive fine cut links selected at their true hits; ``set_jet_amplitude``
re-uploads the per-link velocity column only.
"""
def __init__(self, cfg, bounds, *, context, coarse_boundary="periodic", fine_objects=(),
fine_block=None, base_block=None):
fine_block=None, base_block=None, jets=()):
if HALO != 4:
raise RuntimeError("refinement.cu requires a four-cell fine halo")
if coarse_boundary not in ("periodic", "slot_reservoir"):
@@ -143,9 +147,10 @@ class FixedPatchStepper:
if (self.fine_layout.nblocks > MAX_GRID_DIM
or self.fine_layout.local_shape[0] > MAX_GRID_DIM):
raise ValueError("fine blocks exceed launch limits")
self._jets = tuple(jets)
self.fine_boundary = prepare_fine_boundary(
cfg, self.patch, fine_objects, wall_aware=self._wall_aware)
self._fine_tables, self._actuated = None, False
cfg, self.patch, fine_objects, wall_aware=self._wall_aware, jets=self._jets)
self._fine_tables, self._actuated, self._jet_cache = None, False, None
if self.fine_layout is not None and self.fine_boundary is not None:
fluid_idx, dirs, q, _, _, _, _, fallback = self.fine_boundary.curved_host
self._fine_tables, _ = blocked_link_tables(
@@ -172,12 +177,16 @@ class FixedPatchStepper:
if self._fine_tables is not None:
# Any actuation spec (even zero amplitude) requires the velocity buffer.
b = self.fine_boundary
self._actuated = any(obj.surface_actuation is not None for obj in b.objects)
self._actuated = bool(self._jets) or any(
obj.surface_actuation is not None for obj in b.objects)
if self._actuated:
fine_cfg.validate() # derives nq etc.; the unique-writer key needs them
fluid_idx, dirs, _, body_id, rx, ry, _, _ = b.curved_host
self._fine_tables["surface_velocity"], _ = surface_link_velocity(
b.objects, fine_cfg, b.flags, fluid_idx, dirs, body_id, rx, ry)
b.objects, fine_cfg, b.flags, fluid_idx, dirs, body_id, rx, ry,
jets=self._jets, hits=b.curved_hits)
if self._jets:
self._jet_cache = jet_link_shapes(self._jets, b.curved_hits)
with self.ctx.activate():
try:
self._stream = cuda.Stream()
@@ -257,7 +266,8 @@ class FixedPatchStepper:
# SoA owns its allocations; never also register them in _allocations.
self._fine_curved = CurvedLinkSoA()
self._fine_curved.assign_host(*boundary.curved_host)
self._fine_curved.assign_surface(boundary.objects, fine_cfg, boundary.flags)
self._fine_curved.assign_surface(boundary.objects, fine_cfg, boundary.flags,
jets=self._jets, hits=boundary.curved_hits)
self._fine_curved.upload()
self._curved = self._levels[1].module.get_function("CurvedBoundaryKernel")
except BaseException:
@@ -333,7 +343,8 @@ class FixedPatchStepper:
return
if self._curved is None or cl.fluid_idx_gpu is None:
raise RuntimeError("Fine boundary device buffers are missing")
driven = any(obj.surface_actuation is not None for obj in self.fine_boundary.objects)
driven = bool(self._jets) or any(
obj.surface_actuation is not None for obj in self.fine_boundary.objects)
if driven and (cl.surface_velocity_gpu is None or cl.link_exchange_gpu is None):
raise RuntimeError("Surface boundary device buffers are missing")
self._curved(buffer, cl.fluid_idx_gpu, cl.dir_gpu, cl.q_gpu,
@@ -343,6 +354,24 @@ class FixedPatchStepper:
cl.link_exchange_gpu if cl.link_exchange_gpu is not None else np.uintp(0),
grid=((cl.count + self._block[0] - 1) // self._block[0], 1), **kw)
def set_jet_amplitude(self, index, amplitude):
"""Replace one jet's amplitude between steps; no geometry/table rebuild."""
with self._lock, self.ctx.activate():
self._require("new", "ready")
jets = list(self._jets)
jets[index] = jets[index].with_amplitude(amplitude) # validates first
if self._fine_curved is not None:
self._fine_curved.set_jet_amplitude(index, amplitude, self._stream)
elif self._jet_cache is not None:
shape, jet_id = self._jet_cache
velocity = self._fine_tables["surface_velocity"].copy()
driven = jet_id >= 0
velocity[driven] = jet_link_velocity(jets, shape, jet_id)[driven]
self._stream.synchronize() # no in-flight step reads the column
cuda.memcpy_htod(self._link_tables["surface_velocity"], velocity)
self._fine_tables["surface_velocity"] = velocity
self._jets = tuple(jets)
def _blocked_fine_boundary(self, buffer, **kw):
if self._fine_tables is None:
return