Generalize synchronized remap to multiple pinned body rectangles
This commit is contained in:
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()
|
||||
Reference in new issue
Block a user