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:
1 parent
2bbf82121e
commit
7bf6101460
5 files changed
+181
-50
No files matched your search
@@ -16,6 +16,7 @@ from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
from CelerisLab.body.objects import SimObject, SurfaceActuation
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.cuda import compiler_v2 as compiler
|
||||
from CelerisLab.lbm import curved_links
|
||||
from CelerisLab.lbm.blocks import NO_DONOR, BlockLayout2D, blocked_link_tables
|
||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||
from CelerisLab.lbm.descriptors import SOLID
|
||||
@@ -47,16 +48,31 @@ def state(ny, nx, seed):
|
||||
return (feq + noise).astype(np.float32)
|
||||
|
||||
|
||||
def circle(actuation=None):
|
||||
def circle(spec=None):
|
||||
"""Astra fixture: coarse cell-edge C=(16,12), R=2.15 -> fine C=(19.5,15.5), R=4.3."""
|
||||
return SimObject(0, geometry=CircleGeometry(16.0, 12.0, 2.15), center=(16.0, 12.0),
|
||||
radius=2.15, surface_actuation=actuation)
|
||||
radius=2.15, surface_actuation=spec)
|
||||
|
||||
|
||||
def fine_circle(mode):
|
||||
def actuation(speed):
|
||||
"""Aperture centred on +x: hits links straddling the horizontal y 15|16 seam."""
|
||||
return SurfaceActuation(0.0, 0.6, speed)
|
||||
|
||||
|
||||
def fine_circle(mode, spec=None):
|
||||
cfg = config(mode)
|
||||
patch = FixedPatch2D.from_config(cfg, BOUNDS)
|
||||
return patch, prepare_fine_boundary(cfg, patch, (circle(),), wall_aware=mode == "slot_reservoir")
|
||||
return patch, prepare_fine_boundary(cfg, patch, (circle(spec),),
|
||||
wall_aware=mode == "slot_reservoir")
|
||||
|
||||
|
||||
def fine_cfg(mode, patch):
|
||||
from dataclasses import replace
|
||||
cfg = replace(config(mode), nx=patch.fine_shape[1], ny=patch.fine_shape[0], viscosity=.2)
|
||||
if mode == "slot_reservoir":
|
||||
cfg = replace(cfg, y_wall_bc="bounce_back", top_wall_bc="velocity",
|
||||
wall_slot_width=0, wall_normal_velocity=0.0, velocity=0.0)
|
||||
return cfg
|
||||
|
||||
|
||||
def seam_cells(layout, mapped):
|
||||
@@ -86,9 +102,7 @@ class BlockedFineCPU(unittest.TestCase):
|
||||
return Mock(activate=Mock(side_effect=AssertionError("CUDA reached")), sm_arch="sm_70")
|
||||
|
||||
def test_rejections_before_cuda(self):
|
||||
actuated = circle(SurfaceActuation(0.0, 0.5, 0.002))
|
||||
for kw in ({"fine_block": (7, 7)}, {"fine_block": (16,)},
|
||||
{"fine_block": (16, 20), "fine_objects": [actuated]}):
|
||||
for kw in ({"fine_block": (7, 7)}, {"fine_block": (16,)}):
|
||||
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
||||
FixedPatchStepper(config("periodic"), BOUNDS, context=self.forbidden_context(), **kw)
|
||||
|
||||
@@ -123,10 +137,13 @@ class BlockedFineCPU(unittest.TestCase):
|
||||
fluid_idx, dirs, q, _, _, _, _, fallback = boundary.curved_host
|
||||
self.assertEqual(len(fluid_idx), 76, mode)
|
||||
self.assertEqual(int(np.count_nonzero(q < .5)), 68, mode)
|
||||
for by, bx in FINE_BLOCKS[1:]:
|
||||
self.assertTrue((fallback[q < .5] == 0).all(), mode) # every q<.5 link uses its donor
|
||||
for by, bx in FINE_BLOCKS:
|
||||
layout = BlockLayout2D.build(*patch.fine_shape, by, bx, periodic=(False, False))
|
||||
_, mapped = blocked_link_tables(layout, boundary.flags, fluid_idx, dirs, q, fallback)
|
||||
self.assertEqual(seam_cells(layout, mapped), (8, 8), (mode, by, bx))
|
||||
single = layout.nblocks == 1
|
||||
self.assertEqual(seam_cells(layout, mapped), (0, 0) if single else (8, 8), (mode, by, bx))
|
||||
self.assertEqual(len(set(mapped.block.tolist())), 1 if single else 4, (mode, by, bx))
|
||||
|
||||
def test_storage_adapter_flags_sizing(self):
|
||||
patch, boundary = fine_circle("periodic")
|
||||
@@ -142,7 +159,8 @@ class BlockedFineCPU(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
blocked_link_tables(layout, bad, *columns)
|
||||
|
||||
def test_same_buffer_halo_bfl_step_launch_order(self):
|
||||
def mocked_backend(self, objects):
|
||||
"""Real constructor/initialize with driver doubles; kernels record (name, args)."""
|
||||
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
||||
calls, functions = [], {}
|
||||
|
||||
@@ -154,26 +172,77 @@ class BlockedFineCPU(unittest.TestCase):
|
||||
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
||||
patch.object(compiler, "load_module", return_value=module), \
|
||||
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
|
||||
patch.object(cuda, "memcpy_htod"):
|
||||
patch.object(cuda, "memcpy_htod"), \
|
||||
patch.object(curved_links, "CurvedLinkSoA", side_effect=AssertionError("dense SoA built")):
|
||||
backend = FixedPatchStepper(config("periodic"), BOUNDS, context=context,
|
||||
fine_objects=[circle()], fine_block=FINE_BLOCKS[1])
|
||||
g_fine = state(*backend.patch.fine_shape, 2)
|
||||
backend.initialize(state(NY, NX, 1), g_fine)
|
||||
fine_objects=objects, fine_block=FINE_BLOCKS[1])
|
||||
backend.initialize(state(NY, NX, 1), state(*backend.patch.fine_shape, 2))
|
||||
return backend, calls
|
||||
|
||||
def test_same_buffer_halo_bfl_step_launch_order(self):
|
||||
for spec in (None, actuation(.002)):
|
||||
backend, calls = self.mocked_backend([circle(spec)])
|
||||
c, f = backend._levels
|
||||
coarse, cur, spare = c.current, f.current, f.spare
|
||||
flags, kind = backend._fine_flags, backend._kind
|
||||
velocity = backend._link_tables.get("surface_velocity")
|
||||
self.assertEqual(velocity is None, spec is None)
|
||||
backend.step(1)
|
||||
backend.close()
|
||||
order = [(name, a[0]) for name, a in calls]
|
||||
self.assertEqual(order, [("BlockExplode", coarse),
|
||||
("BlockHaloCopy", cur), ("BlockCurvedBoundary", cur), ("BlockStep", cur),
|
||||
("BlockHaloCopy", spare), ("BlockCurvedBoundary", spare),
|
||||
("BlockStep", spare), ("PatchCoarseStep", coarse)])
|
||||
steps = [a for name, a in calls if name == "BlockStep"]
|
||||
self.assertEqual([a[1] for a in steps], [spare, cur])
|
||||
for a in steps:
|
||||
self.assertIs(a[3], flags) # blocked flags, not null
|
||||
self.assertIs(a[4], kind)
|
||||
order = [(name, a[0]) for name, a in calls]
|
||||
self.assertEqual(order, [("BlockExplode", coarse),
|
||||
("BlockHaloCopy", cur), ("BlockCurvedBoundary", cur), ("BlockStep", cur),
|
||||
("BlockHaloCopy", spare), ("BlockCurvedBoundary", spare),
|
||||
("BlockStep", spare), ("PatchCoarseStep", coarse)])
|
||||
steps = [a for name, a in calls if name == "BlockStep"]
|
||||
self.assertEqual([a[1] for a in steps], [spare, cur])
|
||||
for a in steps:
|
||||
self.assertIs(a[3], flags) # blocked flags, not null
|
||||
self.assertIs(a[4], kind)
|
||||
for a in (a for name, a in calls if name == "BlockCurvedBoundary"):
|
||||
if spec is None: # explicit null at the raw launch ABI
|
||||
self.assertIsInstance(a[7], np.uintp)
|
||||
self.assertEqual(int(a[7]), 0)
|
||||
else:
|
||||
self.assertIs(a[7], velocity)
|
||||
|
||||
def test_actuation_without_velocity_buffer_fails_before_bfl(self):
|
||||
for speed in (0.0, .002): # any spec, even zero amplitude
|
||||
backend, calls = self.mocked_backend([circle(actuation(speed))])
|
||||
self.assertIsNotNone(backend._fine_tables["surface_velocity"])
|
||||
del backend._link_tables["surface_velocity"]
|
||||
with self.assertRaisesRegex(RuntimeError, "Surface velocity device buffer is missing"):
|
||||
backend.step(1)
|
||||
names = [name for name, _ in calls]
|
||||
self.assertNotIn("BlockCurvedBoundary", names)
|
||||
self.assertNotIn("BlockStep", names)
|
||||
self.assertEqual(backend._status, "failed")
|
||||
backend.close()
|
||||
|
||||
def test_surface_velocity_helper_matches_dense_soa(self):
|
||||
for mode in ("periodic", "slot_reservoir"):
|
||||
patch, boundary = fine_circle(mode, actuation(-.002))
|
||||
cfg = fine_cfg(mode, patch)
|
||||
soa = curved_links.CurvedLinkSoA()
|
||||
soa.assign_host(*boundary.curved_host)
|
||||
soa.assign_surface(boundary.objects, cfg, boundary.flags)
|
||||
fluid_idx, dirs, _, body_id, rx, ry, _, _ = boundary.curved_host
|
||||
velocity, aperture = curved_links.surface_link_velocity(
|
||||
boundary.objects, cfg, boundary.flags, fluid_idx, dirs, body_id, rx, ry)
|
||||
np.testing.assert_array_equal(velocity, soa.surface_velocity)
|
||||
np.testing.assert_array_equal(aperture, soa.aperture)
|
||||
self.assertEqual(curved_links.surface_link_velocity(
|
||||
(circle(),), cfg, boundary.flags, fluid_idx, dirs, body_id, rx, ry), (None, None))
|
||||
# The aperture must actually cross a block seam in both decomposed layouts.
|
||||
for by, bx in FINE_BLOCKS[1:]:
|
||||
layout = BlockLayout2D.build(*patch.fine_shape, by, bx, periodic=(False, False))
|
||||
_, mapped = blocked_link_tables(layout, boundary.flags, fluid_idx, dirs,
|
||||
boundary.curved_host[2], boundary.curved_host[7])
|
||||
L = int(np.prod(layout.local_shape))
|
||||
core = set(layout.owner.ravel().tolist())
|
||||
seam = [int(w) // (9 * L) * L + int(w) % L not in core for w in mapped.write]
|
||||
self.assertTrue(np.any(np.array(seam) & aperture & np.any(velocity != 0, axis=1)),
|
||||
(mode, by, bx))
|
||||
|
||||
def test_tables_map_back_to_dense_slots_in_order(self):
|
||||
patch = FixedPatch2D.from_config(config("periodic"), BOUNDS)
|
||||
@@ -259,6 +328,35 @@ class BlockedFineGPU(unittest.TestCase):
|
||||
print(f"circle {mode}: dense vs {len(blocked)} fine-block layouts bit-identical "
|
||||
f"after {STEPS} coarse steps", flush=True)
|
||||
|
||||
def test_actuated_circle_dense_and_blocked_fine_storage_are_bit_identical(self):
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
for mode in ("periodic", "slot_reservoir"):
|
||||
for speed in (.002, -.002):
|
||||
cfg = config(mode)
|
||||
patch, boundary = fine_circle(mode, actuation(speed))
|
||||
solid = (boundary.flags.reshape(patch.fine_shape) & SOLID) != 0
|
||||
g_coarse, g_fine = state(NY, NX, 1), state(*patch.fine_shape, 2)
|
||||
results = []
|
||||
with CudaContext(0) as context:
|
||||
for fine_block in (None, *FINE_BLOCKS):
|
||||
with FixedPatchStepper(cfg, BOUNDS, context=context, coarse_boundary=mode,
|
||||
fine_objects=[circle(actuation(speed))],
|
||||
fine_block=fine_block) as backend:
|
||||
backend.initialize(g_coarse, g_fine)
|
||||
backend.step(STEPS)
|
||||
results.append((backend.download(), raw(backend)))
|
||||
(dense_public, dense_raw), blocked = results[0], results[1:]
|
||||
fine = dense_raw[1]
|
||||
self.assertTrue(np.isfinite(fine[:, (patch.fine_kind == ACTIVE) & ~solid]).all())
|
||||
self.assertTrue(np.isnan(fine[:, solid]).all())
|
||||
self.assertTrue(np.isnan(fine[:, patch.fine_kind == OUTER_GHOST]).all())
|
||||
for fine_block, (public, raw_arrays) in zip(FINE_BLOCKS, blocked):
|
||||
msg = f"actuated {speed} {mode} fine_block={fine_block}"
|
||||
for a, b in zip(public + tuple(raw_arrays), dense_public + tuple(dense_raw)):
|
||||
np.testing.assert_array_equal(a, b, err_msg=msg) # values and NaN masks
|
||||
print(f"actuated {speed:+.3f} {mode}: dense vs {len(blocked)} fine-block layouts "
|
||||
f"bit-identical after {STEPS} coarse steps", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user