Add bounded pinned-circle manual L2 remapping

This commit is contained in:
TRON committed 2026-10-16 10:41:56 -07:00
1 parent 766658185f
commit 6a7e9226db
6 files changed
+361 -21

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
"""Bounded pinned-body transfer and host lifecycle; no CUDA numerical claim."""
import unittest
from unittest.mock import patch
import numpy as np
from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.objects import SimObject, SurfaceActuation
from CelerisLab.body.jets import Jet, Tube, Parallel
from CelerisLab.lbm.synchronized_remap import RemapTopology, same_physical_boundary
from tests.unit.test_synchronized_remap import CFG, PARENTS, fields, remap
from tests.unit.test_refinement_three_level_multipatch import mock_backend
from CelerisLab.lbm.refinement import FixedPatchSet2D
A = ((16,16,32,40), (56,16,72,40))
B = ((16,16,32,40), (60,16,76,40))
def body(spec=None, center=(12.,14.)):
return SimObject(71, geometry=CircleGeometry(*center,.8), center=center,
radius=.8, surface_actuation=spec)
def jet():
return Jet(Tube((10.5,14.),0.,3.,.3), Parallel(0.), 'parabolic', .003)
def mesh(rects, objects, jets=()):
return RemapTopology.from_config(CFG, FixedPatchSet2D.from_config(CFG,PARENTS),
rects, child_objects=objects, child_jets=jets)
def ready(objects=None, jets=None):
objects = (body(),) if objects is None else objects
jets = (jet(),) if jets is None else jets
with patch('tests.unit.test_refinement_three_level_multipatch.bodies_and_jets',
return_value=(objects,jets)):
run, module, allocations = mock_backend(children=A,bodies=True)
storage = {}
module.cuda.memcpy_htod.side_effect = lambda p,a: storage.__setitem__(id(p),np.array(a,copy=True))
module.cuda.memcpy_dtoh.side_effect = lambda a,p: np.copyto(a,storage[id(p)])
run.initialize(*fields(mesh(A,objects,jets),np.float32))
run._step_count = 7
return run,module,allocations,storage,objects
class PinnedBodyRemap(unittest.TestCase):
def test_fluid_only_transfer_poison_and_round_trip(self):
old,new = mesh(A,(body(),)),mesh(B,(body(),))
arrays=fields(old,np.float32)
for a,leaf in zip(arrays,old.leaf_masks): a[:,~leaf]=12345
out=remap(old,new,arrays)
for a,b,before,after in zip(arrays,out.g,old.leaf_masks,new.leaf_masks):
np.testing.assert_array_equal(a[:,before & after],b[:,before & after])
self.assertTrue(np.isnan(b[:,~after]).all())
self.assertTrue(same_physical_boundary(old.boundary,new.boundary))
back=remap(new,old,out.g)
self.assertTrue(np.isnan(back.g[2][:,~old.leaf_masks[2]]).all())
self.assertEqual(old.pinned_rectangle,A[0])
def test_current_jets_and_source_independence(self):
run,module,allocations,storage,objects=ready()
run.set_jet_amplitude(0,.017,level=2)
before=run.download()
clone=run.remap_child(B,child_objects=objects)
self.assertEqual(clone._boundaries[2].jets[0].amplitude,.017)
np.testing.assert_array_equal(clone._boundaries[2].tables['surface_velocity'],
run._boundaries[2].tables['surface_velocity'])
self.assertTrue(same_physical_boundary(run.child_boundary,clone.child_boundary))
clone.step()
self.assertEqual((run.step_count,clone.step_count),(7,8))
for got,want in zip(run.download(),before): np.testing.assert_array_equal(got,want)
clone.close();run.close()
def test_static_body_without_actuation(self):
run,module,allocations,storage,objects=ready(jets=())
clone=run.remap_child(A,child_objects=objects)
self.assertFalse(clone._boundaries[2].actuated)
self.assertNotIn('surface_velocity',clone._boundaries[2].tables)
for a,b in zip(run.download(),clone.download()): np.testing.assert_array_equal(a,b)
clone.close();run.close()
def test_reject_before_device_reads_or_allocation(self):
run,module,allocations,storage,objects=ready()
start=len(allocations)
cases=[(B,()),(B,run.child_boundary.objects),(B,(body(center=(12.1,14.)),)),
(((20,16,36,40),B[1]),objects),(((16,20,32,40),B[1]),objects),
((B[1],),objects)]
module.cuda.memcpy_dtoh.reset_mock()
for rects,objs in cases:
with self.subTest(rects=rects),self.assertRaises(ValueError):
run.remap_child(rects,child_objects=objs)
self.assertEqual(len(allocations),start)
module.cuda.memcpy_dtoh.assert_not_called()
self.assertEqual((run._status,run.step_count),('ready',7))
run.close()
def test_legacy_velocity_and_zero_velocity_ownership_rejected(self):
for speed in (.002,0.):
run,module,allocations,storage,objects=ready(
(body(SurfaceActuation(0.,.7,speed)),),())
for spec in (SurfaceActuation(0.,.7,.003),SurfaceActuation(2.,.7,speed)):
module.cuda.memcpy_dtoh.reset_mock()
with self.assertRaises(ValueError): run.remap_child(B,child_objects=(body(spec),))
module.cuda.memcpy_dtoh.assert_not_called()
clone=run.remap_child(B,child_objects=objects)
clone.close();run.close()
def test_allocation_failure_keeps_old_ready_and_count(self):
run,module,allocations,storage,objects=ready()
before=run.download(); saved=list(allocations)
allocate=module.cuda.mem_alloc.side_effect
calls=0
def fail(size):
nonlocal calls
calls+=1
if calls == 3: raise RuntimeError('candidate allocation')
return allocate(size)
with patch.object(module.cuda,'mem_alloc',side_effect=fail):
with self.assertRaisesRegex(RuntimeError,'allocation'):
run.remap_child(B,child_objects=objects)
self.assertEqual((run._status,run.step_count),('ready',7))
for got,want in zip(run.download(),before): np.testing.assert_array_equal(got,want)
for a in saved: a.free.assert_not_called()
for a in allocations[len(saved):]: a.free.assert_called_once()
run.close()
def test_direct_helper_rejects_changed_pinned_rectangle(self):
old=mesh(A,(body(),))
new=mesh(((12,16,32,40),A[1]),(body(),))
with self.assertRaisesRegex(ValueError,'pinned'): remap(old,new,fields(old))
+1 -1
View File
@@ -185,7 +185,7 @@ class SynchronizedRemapTests(unittest.TestCase):
with self.assertRaises(ValueError):
RemapTopology.from_config(replace(CFG, **changes), parent, A)
for kwargs in (dict(coarse_boundary='slot_reservoir'), dict(objects=(object(),))):
with self.assertRaisesRegex(ValueError, 'body-free periodic'):
with self.assertRaisesRegex(ValueError, 'periodic 2D'):
RemapTopology.from_config(CFG, parent, A, **kwargs)
different = topology(A, cfg=replace(CFG, viscosity=.12))
with self.assertRaisesRegex(ValueError, 'unchanged'):
@@ -0,0 +1,84 @@
"""Pinned-body GPU lifecycle gates. NOT executed by CPU test collection.
No evolution accuracy tolerance is asserted here: an independent BFL replay
reference is required before a 3e-6 FP32-vs-FP64 gate can be claimed. These
checks require exact physical boundary/velocity, remap overlap and no-op next
step identity; they never compare blocked boundary addresses across layouts.
"""
from contextlib import ExitStack
import unittest
import numpy as np
from CelerisLab.cuda.context import CudaContext
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
from CelerisLab.lbm.synchronized_remap import RemapTopology, remap_synchronized
from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.objects import SimObject
from CelerisLab.body.jets import Jet, Tube, Parallel
from test_runtime_child_remap_gpu import RuntimeChildRemapGPU, initial
from test_refinement_three_level_multipatch_gpu import CFG, PARENTS
from test_refinement_blocked_base import exact
A = ((16,16,32,40), (56,16,72,40))
B = ((16,16,32,40), (60,16,76,40))
def make(ctx):
body = SimObject(71, geometry=CircleGeometry(12.,14.,.8),center=(12.,14.),radius=.8)
jet = Jet(Tube((10.5,14.),0.,3.,.3),Parallel(0.),'parabolic',.003)
run = FixedPatchStepper(CFG,PARENTS,context=ctx,base_block=(4,4),fine_block=(4,4),
child_block=(4,4),child_rectangles_in_parent=A,
child_objects=(body,),child_jets=(jet,))
return run,(body,)
class PinnedBodyRemapGPU(unittest.TestCase):
check_public = RuntimeChildRemapGPU.check_public
check_reset = RuntimeChildRemapGPU.check_reset
def check_boundary(self, before, after):
for name in ('flags','boundary_cells','curved_hits'):
exact(self,getattr(after.child_boundary,name),getattr(before.child_boundary,name),name)
for a,b in zip(after.child_boundary.curved_host,before.child_boundary.curved_host):
exact(self,a,b,'physical cut links')
exact(self,after._boundaries[2].tables['surface_velocity'],
before._boundaries[2].tables['surface_velocity'],'current per-link velocities')
self.assertEqual(after._boundaries[2].jets[0].amplitude,.017)
def test_noop_next_step_bitwise(self):
with CudaContext(0) as ctx, ExitStack() as owned:
old,objects=make(ctx);owned.enter_context(old)
old.initialize(*initial(old,False));old.step(2)
old.set_jet_amplitude(0,.017,level=2)
before=old.download()
new=owned.enter_context(old.remap_child(A,child_objects=objects))
self.check_boundary(old,new);self.check_reset(new,before)
old.step();new.step()
for a,b in zip(new.download(),old.download()): exact(self,a,b,'no-op next step bitwise')
self.assertEqual((old.step_count,new.step_count),(3,3))
self.check_public(new)
def test_moved_fluid_return_and_source_independence(self):
with CudaContext(0) as ctx, ExitStack() as owned:
current,objects=make(ctx);owned.enter_context(current)
current.initialize(*initial(current,False));current.step(2)
current.set_jet_amplitude(0,.017,level=2)
for rects in (B,A):
previous=current;before=previous.download();count=previous.step_count
jets=previous._boundaries[2].jets
old=RemapTopology.from_config(CFG,previous.patch,previous.child_patch.bounds,
child_objects=objects,child_jets=jets)
new=RemapTopology.from_config(CFG,previous.patch,rects,
child_objects=objects,child_jets=jets)
expected=remap_synchronized(old,new,before,base_count=count,
level_steps=(count,2*count,4*count),phase='postcollision_g').g
current=owned.enter_context(previous.remap_child(rects,child_objects=objects))
self.check_boundary(previous,current);self.check_reset(current,expected)
for a,b,om,nm in zip(current.download(),before,old.leaf_masks,new.leaf_masks):
exact(self,a[:,om & nm],b[:,om & nm],'physical-fluid overlap')
current.step(2)
self.assertEqual((previous.step_count,current.step_count),(count,count+2))
for a,b in zip(previous.download(),before): exact(self,a,b,'source independent')
self.check_public(current)
if __name__ == '__main__': unittest.main()