Generalize synchronized remap to multiple pinned body rectangles

This commit is contained in:
TRON committed 2026-10-16 11:32:42 -07:00
1 parent 864a220dbe
commit 60ad6b8b0d
6 files changed
+375 -32

No files matched your search

@@ -0,0 +1,175 @@
"""Manual GPU gates for two pinned bodies and several fluid-only L2 regions.
Run explicitly on an approved idle GPU. The CPU helper checks immediate
whole-population transfer at the same physical time, not independent evolution
accuracy. Continuation checks cover finite leaves and no-op bitwise identity;
they do not establish an independent BFL replay or a numerical error bound.
"""
from contextlib import ExitStack
import unittest
import numpy as np
from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.jets import Jet, Parallel, Tube
from CelerisLab.body.objects import SimObject
from CelerisLab.cuda.context import CudaContext
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
from CelerisLab.lbm.synchronized_remap import (
RemapTopology, remap_synchronized, same_physical_boundary,
)
from test_refinement_blocked_base import exact
from test_refinement_three_level_multipatch_gpu import CFG, PARENTS
from test_runtime_child_remap_gpu import RuntimeChildRemapGPU, initial
# All bounds use the allocated L1 frame, with support aligned to 4x4 blocks.
PINS = ((16, 16, 32, 24), (56, 16, 72, 24))
FLUID = ((16, 32, 32, 40), (56, 32, 72, 40))
EXTRA = ((16, 44, 32, 48),)
MOVED = ((20, 32, 36, 40), (60, 32, 76, 40))
START = PINS + FLUID
CHANGES = (
('add', START + EXTRA, (.017, -.011)),
('move', PINS + MOVED + EXTRA, (-.009, .005)),
('remove', PINS + MOVED, (.013, -.007)),
)
def make(context):
centers = ((12., 10.), (32., 10.))
objects = tuple(
SimObject(71 + index, geometry=CircleGeometry(*center, .8),
center=center, radius=.8)
for index, center in enumerate(centers)
)
jets = tuple(
Jet(Tube((x - 1.5, y), 0., 3., .3), Parallel(0.), 'parabolic', amplitude)
for (x, y), amplitude in zip(centers, (.003, -.002))
)
run = FixedPatchStepper(
CFG, PARENTS, context=context, base_block=(4, 4), fine_block=(4, 4),
child_block=(4, 4), child_rectangles_in_parent=START,
child_objects=objects, child_jets=jets,
)
return run, objects
def topology(run, rectangles, objects):
return RemapTopology.from_config(
CFG, run.patch, rectangles, child_objects=objects,
child_jets=run._boundaries[2].jets,
)
def population_integrals(mesh, arrays):
"""Nine separate area integrals in the root frame, accumulated in FP64."""
return sum(array[:, leaf].sum(axis=1, dtype=np.float64) / 4**level
for level, (array, leaf) in enumerate(zip(arrays, mesh.leaf_masks)))
class MultiplePinnedRemapGPU(unittest.TestCase):
check_public = RuntimeChildRemapGPU.check_public
check_reset = RuntimeChildRemapGPU.check_reset
def change_amplitudes(self, run, amplitudes):
# Each jet selects a different body. Updating one must preserve the
# other body's current per-link velocities, including its driven links.
boundary = run._boundaries[2]
body_ids = run.child_boundary.curved_host[3]
for index, amplitude in enumerate(amplitudes):
before = boundary.tables['surface_velocity'].copy()
run.set_jet_amplitude(index, amplitude, level=2)
after = boundary.tables['surface_velocity']
exact(self, after[body_ids != index], before[body_ids != index],
'other body velocity unchanged')
self.assertTrue(np.any(after[body_ids == index] != before[body_ids == index]))
self.assertEqual(tuple(jet.amplitude for jet in boundary.jets), amplitudes)
def check_boundary(self, old, new, mesh, amplitudes):
self.assertEqual(mesh.pinned_rectangles, PINS)
self.assertTrue(same_physical_boundary(old.child_boundary, new.child_boundary))
self.assertTrue(same_physical_boundary(new.child_boundary, mesh.boundary))
# Compare physical cut-link rows, never layout-dependent blocked addresses.
for run in (old, new):
boundary = run._boundaries[2]
self.assertEqual(tuple(jet.amplitude for jet in boundary.jets), amplitudes)
exact(self, boundary.tables['surface_velocity'], mesh.surface_velocity,
'latest physical per-link velocities')
def test_noop_next_step_bitwise(self):
with CudaContext(0) as context, ExitStack() as owned:
old, objects = make(context)
owned.enter_context(old)
old.initialize(*initial(old, False))
old.step(2)
amplitudes = (.017, -.011)
self.change_amplitudes(old, amplitudes)
before, count = old.download(), old.step_count
mesh = topology(old, START, objects)
# Input order is irrelevant even with several pins and fluid regions.
new = owned.enter_context(old.remap_child(START[::-1], child_objects=objects))
self.assertIsNot(old, new)
self.assertEqual((old.step_count, new.step_count), (count, count))
self.check_boundary(old, new, mesh, amplitudes)
self.check_reset(new, before)
for got, want in zip(old.download(), before):
exact(self, got, want, 'no-op source unchanged')
old.step()
new.step()
self.assertEqual((old.step_count, new.step_count), (count + 1, count + 1))
for got, want in zip(new.download(), old.download()):
exact(self, got, want, 'no-op next step bitwise')
self.check_public(old)
self.check_public(new)
def test_add_move_remove_immediate_transfer_and_source_independence(self):
with CudaContext(0) as context, ExitStack() as owned:
current, objects = make(context)
owned.enter_context(current)
current.initialize(*initial(current, False))
current.step(2)
for action, rectangles, amplitudes in CHANGES:
with self.subTest(action=action):
previous = current
self.change_amplitudes(previous, amplitudes)
before, count = previous.download(), previous.step_count
old_mesh = topology(previous, previous.child_patch.bounds, objects)
new_mesh = topology(previous, rectangles, objects)
expected = remap_synchronized(
old_mesh, new_mesh, before, base_count=count,
level_steps=(count, 2 * count, 4 * count),
phase='postcollision_g',
).g
current = owned.enter_context(
previous.remap_child(rectangles, child_objects=objects))
self.assertIsNot(previous, current)
self.assertEqual((previous.step_count, current.step_count), (count, count))
self.check_boundary(previous, current, new_mesh, amplitudes)
self.check_reset(current, expected)
actual = current.download()
for got, want, source, old_leaf, new_leaf in zip(
actual, expected, before, old_mesh.leaf_masks, new_mesh.leaf_masks):
exact(self, got, want, 'immediate CPU transfer parity')
exact(self, got[:, old_leaf & new_leaf], source[:, old_leaf & new_leaf],
'same-level physical fluid overlap')
self.assertTrue(np.isfinite(got[:, new_leaf]).all())
self.assertTrue(np.isnan(got[:, ~new_leaf]).all())
# Conservation is asserted only across remap, not collision.
np.testing.assert_allclose(
population_integrals(new_mesh, actual),
population_integrals(old_mesh, before),
rtol=2 * np.finfo(np.float32).eps, atol=0,
err_msg='all nine area-weighted population integrals',
)
self.check_public(current)
current.step()
self.assertEqual((previous.step_count, current.step_count), (count, count + 1))
for got, want in zip(previous.download(), before):
exact(self, got, want, 'source unchanged after candidate advances')
self.check_boundary(previous, current, new_mesh, amplitudes)
self.check_public(current)
if __name__ == '__main__':
unittest.main()