Blocked fine storage: velocity-only surface actuation on fixed circles

- curved_links.surface_link_velocity: pure host helper extracted from
  CurvedLinkSoA.assign_surface (which now calls it; dense path unchanged).
  Rows follow physical cut-link order.
- blocks.cu BlockCurvedBoundary: one nullable trailing surface_velocity
  argument (2 floats/link), same convention as CurvedBoundaryKernel; no
  exchange output. Both call sites pass it explicitly (PeriodicBlockStepper
  passes null; single-level behaviour unchanged).
- FixedPatchStepper blocked mode accepts actuated stationary circles,
  validated against the physical fine_cfg; uploads the velocity table only
  when any actuation spec exists and fails before BFL if that buffer is
  missing (even at zero amplitude). Blocked exchange diagnostics are
  unavailable; dense exchange behaviour unchanged. No force aggregation,
  no motion, no new numerical formula.
Tests: 1x1 seam (0,0), receivers in 4 blocks, all q<.5 links fallback 0;
dense SoA never built in blocked mode; launch order incl. null vs velocity
pointer; missing-buffer guard at 0 and 0.002; helper == assign_surface;
aperture crosses a seam; GPU: periodic and slot_reservoir, +/-0.002,
8 coarse steps, dense vs 1x1/2x2/4x2 exact values + NaN masks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-15 04:45:17 +00:00
1 parent 2bbf82121e
commit 7bf6101460
5 files changed
+181 -50

No files matched your search

+2 -1
View File
@@ -210,7 +210,8 @@ class PeriodicBlockStepper:
if self._curved is not None:
t = self._tables
self._curved(self._current, t["block"], t["cell"], t["dir"], t["q"],
t["fallback"], np.uint32(self._links), grid=self._link_grid, **kw)
t["fallback"], np.uint32(self._links), np.uintp(0), # no actuation
grid=self._link_grid, **kw)
self._step(self._current, self._spare, np.uint32(self.layout.halo),
self._tables.get("flags", np.uintp(0)), np.uintp(0), # no kind
grid=self._grid, **kw)
+29 -18
View File
@@ -42,6 +42,33 @@ def _upload_columns(columns, stream=None):
stream.synchronize()
def surface_link_velocity(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry):
"""Per-link prescribed surface velocity (n, 2) FP32 and aperture mask.
Host-only and storage-neutral: rows follow the given physical cut-link
order. Returns (None, None) when no object carries surface actuation.
"""
enabled = [obj for obj in objects if obj.surface_actuation is not None]
if not enabled:
return None, None
from .descriptors import FLUID, SOLID
if np.any((flags[fluid_idx] & FLUID) == 0) or np.any(flags[fluid_idx] & SOLID):
raise ValueError("Surface cut links must have active fluid receivers")
if len(np.unique(fluid_idx.astype(np.uint64)*cfg.nq+dir)) != len(dir):
raise ValueError("Surface boundary requires unique cut-link writers")
surface_velocity = np.zeros((len(dir), 2), np.float32)
surface_aperture = np.zeros(len(dir), bool)
for obj in enabled:
obj.validate_surface_actuation(cfg)
selected = body_id == obj.obj_id
velocity, aperture = obj.surface_actuation.project(rx[selected], ry[selected])
if not aperture.any():
raise ValueError("Surface aperture hits no resolved cut links")
surface_velocity[selected] = velocity
surface_aperture[selected] = aperture
return surface_velocity, surface_aperture
@dataclass
class CurvedLinkSoA:
"""Cut-link columns on host with matching ``*_gpu`` device buffers."""
@@ -83,25 +110,9 @@ class CurvedLinkSoA:
def assign_surface(self, objects, cfg, flags):
"""Keep the original eight-column geometry/checkpoint contract unchanged."""
enabled = [obj for obj in objects if obj.surface_actuation is not None]
self.surface_velocity = self.aperture = None
if not enabled:
return
from .descriptors import FLUID, SOLID
if np.any((flags[self.fluid_idx] & FLUID) == 0) or np.any(flags[self.fluid_idx] & SOLID):
raise ValueError("Surface cut links must have active fluid receivers")
if len(np.unique(self.fluid_idx.astype(np.uint64)*cfg.nq+self.dir)) != len(self.dir):
raise ValueError("Surface boundary requires unique cut-link writers")
self.surface_velocity = np.zeros((len(self.dir), 2), np.float32)
self.aperture = np.zeros(len(self.dir), bool)
for obj in enabled:
obj.validate_surface_actuation(cfg)
selected = self.body_id == obj.obj_id
velocity, aperture = obj.surface_actuation.project(self.rx[selected], self.ry[selected])
if not aperture.any():
raise ValueError("Surface aperture hits no resolved cut links")
self.surface_velocity[selected] = velocity
self.aperture[selected] = aperture
self.surface_velocity, self.aperture = surface_link_velocity(
objects, cfg, flags, self.fluid_idx, self.dir, self.body_id, self.rx, self.ry)
def surface_exchange(self, body_id):
"""Latest boundary step's (Fx,Fy,Tz), split by aperture; not nozzle thrust.
+10 -2
View File
@@ -54,14 +54,22 @@ extern "C" __global__ void BlockExplode(
// Static cut links, receiver-local staging: cell is the receiver's local core
// cell, so the existing BFL writes the solid-source window image that this
// block's next pull reads (no halo copy between this kernel and BlockStep).
// surface_velocity (nullable, 2 floats per link): prescribed fluid velocity on
// fixed geometry, same convention as CurvedBoundaryKernel. No exchange output.
extern "C" __global__ void BlockCurvedBoundary(
fpxx* f, const uint32_t* block, const uint32_t* cell, const unsigned char* dir,
const float* q, const unsigned char* fallback, unsigned int count)
const float* q, const unsigned char* fallback, unsigned int count,
const float* surface_velocity)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
float Uw = 0.0f, Vw = 0.0f;
if (surface_velocity != nullptr) {
const float u = surface_velocity[2 * row], v = surface_velocity[2 * row + 1];
if (u != 0.0f || v != 0.0f) { Uw = u; Vw = v; }
}
apply_bouzidi_link(dir[row], q[row], cell[row], f + (unsigned long)block[row] * NQ * TOTAL_CELLS,
0.0f, 0.0f, 0.0f, 0.0f, fallback[row], nullptr, -1);
Uw, Vw, 0.0f, 0.0f, fallback[row], nullptr, -1);
}
// Pull/collide/store core cells only; halo cells of out are left untouched.
+18 -5
View File
@@ -15,6 +15,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 .block_stepper import MAX_GRID_DIM
from .refinement import ACTIVE, HALO, OUTER_GHOST, FixedPatch2D, prepare_fine_boundary
from .descriptors import SOLID
@@ -71,9 +72,11 @@ class FixedPatchStepper:
same-level halos. Patch kinds stay per cell (OUTER invalid, INNER stream
only, ACTIVE collide); halos are gathered before each fine hop. Public
arrays keep the dense (9, ny, nx) fine shape; only the allocation differs.
Blocked fine_objects must be passive stationary circles (no actuation):
Blocked fine_objects are stationary circles, optionally surface-actuated:
prepare_fine_boundary decides legality on the dense fine grid, then cut
links are staged receiver-local per hop: halo -> BFL -> step, same buffer.
Blocked mode applies prescribed surface velocity only; its per-link
exchange diagnostic is unavailable (rows keep physical cut-link order).
"""
def __init__(self, cfg, bounds, *, context, coarse_boundary="periodic", fine_objects=(),
fine_block=None):
@@ -93,8 +96,6 @@ class FixedPatchStepper:
fine_objects = tuple(fine_objects)
self.fine_layout = None
if fine_block is not None:
if any(getattr(obj, "surface_actuation", None) is not None for obj in fine_objects):
raise ValueError("fine_block supports passive circles only; actuation needs the dense fine level")
if len(fine_block) != 2:
raise ValueError("fine_block must be (by, bx)")
fy, fx = self.patch.fine_shape
@@ -104,7 +105,7 @@ class FixedPatchStepper:
raise ValueError("fine blocks exceed launch limits")
self.fine_boundary = prepare_fine_boundary(
cfg, self.patch, fine_objects, wall_aware=self._wall_aware)
self._fine_tables = None
self._fine_tables, self._actuated = None, False
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(
@@ -128,6 +129,14 @@ class FixedPatchStepper:
if self._wall_aware:
fine_cfg = replace(fine_cfg, y_wall_bc="bounce_back", top_wall_bc="velocity",
wall_slot_width=0, wall_normal_velocity=0.0, velocity=0.0)
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)
if self._actuated:
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)
with self.ctx.activate():
try:
self._stream = cuda.Stream()
@@ -286,8 +295,12 @@ class FixedPatchStepper:
if self._curved is None or self._fine_flags is None:
raise RuntimeError("Fine boundary device buffers are missing")
t, count = self._link_tables, len(self._fine_tables["cell"])
velocity = t.get("surface_velocity")
if self._actuated and velocity is None:
raise RuntimeError("Surface velocity device buffer is missing")
self._curved(buffer, t["block"], t["cell"], t["dir"], t["q"], t["fallback"],
np.uint32(count), grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
np.uint32(count), velocity if velocity is not None else np.uintp(0),
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
def _fine_halo(self, buffer, **kw):
pairs = len(self.fine_layout.halo_dst)