Files
CelerisLab/tests/unit/test_raw_exchange.py
T
ClaudeandClaude Opus 5.5 2b10b01c49 test: retire dense-route tests, require tiles everywhere (D2)
Retired with the dense refinement route (results recorded at 1573942 and
2469af7 or named as accepted losses):
- test_refinement_blocked_base: BlockedBaseGPU exact parity (3 tests),
  DenseBlockedRouteParity (6 tests incl. MRT and two bodies),
  DenseBlockedRouteCPU (capability matrix, ledger comparator);
- test_refinement_blocked_fine: BlockedFineGPU dense-vs-blocked (3 tests);
- test_refinement_mrt_gpu: test_dense_versus_blocked_mrt_two_level;
- test_raw_exchange_gpu: test_dense_fine_storage_reference (private
  link_exchange cut-link oracle, accepted loss);
- test_refinement_gpu: test_bounded_equivalence_and_ownership (dense
  CELERIS_REFINEMENT_AUDIT ownership counters, accepted loss); its physics
  stays in test_bounded_equivalence_blocked; eight_steps loses the audit
  branch and the misleading 'ownership/counts PASS' label;
- test_fine_surface_gpu: test_fine_surface_four_hops (dense per-hop flux
  ledger, CurvedLinkSoA reflection oracle, audit counts: accepted losses;
  raw exchange covers the boundary part only, not full flux closure) and the
  dense SoA fault tests (missing surface buffer, close frees SoA); blocked
  equivalents remain in test_refinement_blocked_fine/_base;
- test_raw_exchange.test_dense_observation_rejected_before_device_access.
New TileContractCPU: level_blocks omitted/None/wrong length/None entry/
wrong arity/non-integer/bool/non-positive/non-tuple/not dividing all raise
before CUDA; whole-level tiles reach CUDA. Rejection tests and fixtures pass
tiles; launch-order and kernel-text tests follow the blocked root;
examples/fixed_periodic_patch uses whole-level tiles (+ host tile test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:12:20 +00:00

163 lines
8.1 KiB
Python

"""CPU reduction and real host scheduling with CUDA doubles, not GPU physics."""
from contextlib import nullcontext
from dataclasses import replace
from unittest.mock import Mock
import numpy as np
import unittest
from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.objects import SimObject, SurfaceActuation
from CelerisLab.config import LBMConfig
from CelerisLab.lbm.blocks import BlockLayout2D
from CelerisLab.lbm.refinement import FixedPatch2D, prepare_fine_boundary
from CelerisLab.lbm.refinement_tables import _prepare_blocked_boundary
from CelerisLab.lbm.raw_exchange import summarize_exchange
from tests.unit.test_refinement_three_level_multipatch import host_module
def body(center=(16., 16.), radius=.5, speed=0.):
return SimObject(91, geometry=CircleGeometry(*center, radius), center=center, radius=radius,
surface_actuation=SurfaceActuation(.7, .85, speed))
def backend(observe=True, obj=None):
host = host_module()
calls = []
module = Mock()
module.get_global.return_value = (Mock(), 20)
module.get_function.side_effect = lambda name: Mock(side_effect=lambda *a, **k: calls.append((name, a, k)))
host.compiler.build_module.return_value = module
host.cuda.mem_alloc.side_effect = lambda n: Mock(size=n)
host.cuda.memcpy_dtoh.side_effect = lambda array, pointer: array.fill(0)
ctx = Mock(activate=nullcontext, sm_arch='sm_70')
run = host.FixedPatchStepper(LBMConfig(nx=48, ny=48, viscosity=.02), context=ctx,
refinements=[[(6, 6, 26, 26)], [(20, 20, 44, 44)], [(56, 56, 72, 72)]],
level_blocks=[(4, 4)]*4, level_objects=[(), (), (), (body() if obj is None else obj,)],
observe_raw_exchange=observe)
arrays = [np.full(l.shape, 1/9, np.float32) for l in run._levels]
run.initialize(*arrays)
return run, host, calls, arrays
class RawExchange(unittest.TestCase):
def _check_weights_split_and_window(self, level):
h = 2. ** -level
rows = np.array([[2, -3, 5, 7], [-1, 4, -2, -6], [99, 99, 99, 99]], np.float32)
kw = dict(body_id=0, level=level, h=h, time_end=3, reference_center=(7, 8))
last = summarize_exchange(rows, [0, 0, 1], [False, True, False], window='last_substep', **kw)
integral = rows.astype(float)*[h*h, h*h, h**3, h*h]/h
full = summarize_exchange(integral, [0, 0, 1], [False, True, False], window='last_base_step', **kw)
np.testing.assert_array_equal(last['total']['force'], np.array([1, 1])*h)
self.assertEqual(last['total']['torque'], 3 * h * h)
self.assertEqual(last['total']['mass_into_fluid'], h * h)
for name in ('force', 'torque', 'mass_rate'):
np.testing.assert_array_equal(last['total'][name], full['total'][name])
for name in ('force', 'impulse', 'torque', 'angular_impulse', 'mass_into_fluid'):
np.testing.assert_array_equal(last['total'][name],
np.asarray(last['wall'][name])+last['aperture'][name])
self.assertEqual(last['time_start'], 3 - h)
self.assertEqual(full['time_start'], 2)
self.assertEqual(last['sample_count'], 1)
self.assertEqual(full['sample_count'], 2 ** level)
self.assertEqual(full['reference_center'], (7, 8))
def test_final_substep_is_not_interval_mean(self):
kw = dict(body_id=0, level=1, h=.5, time_end=1, reference_center=(0, 0))
last = summarize_exchange([[4, 0, 0, 0]], [0], [False], window='last_substep', **kw)
full = summarize_exchange([[1.5, 0, 0, 0]], [0], [False], window='last_base_step', **kw)
self.assertEqual(last['total']['force'][0], 2)
self.assertEqual(full['total']['force'][0], 1.5)
def test_host_metadata_zero_jet_and_rows(self):
cfg = LBMConfig(nx=32, ny=32, viscosity=.1)
pair = FixedPatch2D.from_config(cfg, (4, 4, 28, 28))
boundary = prepare_fine_boundary(cfg, pair, [body(radius=2.15)])
local = replace(cfg, nx=pair.fine_shape[1], ny=pair.fine_shape[0], viscosity=.2)
local.validate()
layout = BlockLayout2D.build(*pair.fine_shape, 8, 8, periodic=(False, False))
b = _prepare_blocked_boundary(boundary, layout, local, ())
for key, idx in [('body_id', 3), ('rx', 4), ('ry', 5)]:
np.testing.assert_array_equal(b.tables[key], boundary.curved_host[idx])
self.assertTrue(b.tables['aperture'].any())
self.assertTrue((~b.tables['aperture']).any())
self.assertFalse(b.tables['surface_velocity'].any())
# Classification does not disappear when prescribed velocity is zero.
self.assertEqual(len(b.tables['cell']), len(boundary.curved_host[0]))
def test_four_level_schedule_publication_and_fixed_storage(self):
run, host, calls, arrays = backend()
b = run._level_boundaries[3]
rows = len(b.tables['cell'])
self.assertEqual(b.last_raw.size, rows * 16)
self.assertEqual(b.interval_integral.size, rows * 32)
with self.assertRaisesRegex(RuntimeError, 'completed'):
run.read_raw_exchange(0, level=3)
run.step()
names = [name for name, _, _ in calls]
self.assertEqual(names.count('BlockAccumulateExchange'), 8)
for i, (name, args, kw) in enumerate(calls):
if name == 'BlockAccumulateExchange':
self.assertEqual(names[i - 1], 'BlockCurvedBoundary')
self.assertEqual(names[i + 1], 'BlockStep')
self.assertIs(args[0], b.last_raw)
self.assertIs(args[1], b.interval_integral)
self.assertEqual(args[3:], (0.125, 0.125))
self.assertIs(kw['stream'], run._stream)
self.assertEqual(host.cuda.memset_d8_async.call_count, 1)
value = run.read_raw_exchange(0, level=3)
self.assertEqual(value['sample_count'], 8)
self.assertEqual(value['time_start'], 0)
run.step(0)
self.assertEqual(run.read_raw_exchange(0, level=3)['time_end'], 1)
self.assertEqual(run.read_raw_exchange(0, level=3, window='last_substep')['time_start'], 0.875)
run.step(2)
self.assertEqual(run.read_raw_exchange(0, level=3)['time_start'], 2)
run.initialize(*arrays)
with self.assertRaisesRegex(RuntimeError, 'completed'):
run.read_raw_exchange(0, level=3)
run.close()
def test_torque_reference_uses_geometry_center_not_optional_object_center(self):
for center in (None, (3., 4.)):
obj = SimObject(91, geometry=CircleGeometry(16., 16., .5), center=center, radius=.5)
run, _, _, _ = backend(obj=obj)
run.step()
result = run.read_raw_exchange(0, level=3)
self.assertEqual(result['reference_center'], (16.0, 16.0))
self.assertNotEqual(result['reference_center'], tuple(obj.center))
run.close()
def test_disabled_has_nulls_and_no_observation_allocation_or_launch(self):
run, host, calls, _ = backend(False)
b = run._level_boundaries[3]
self.assertIs(b.last_raw, b.interval_integral)
self.assertIs(b.interval_integral, None)
run.step()
for name, args, _ in calls:
if name == 'BlockCurvedBoundary':
self.assertEqual(tuple(args[8:]), (0, 0, 0))
self.assertTrue(all((n != 'BlockAccumulateExchange' for n, _, _ in calls)))
host.cuda.memset_d8_async.assert_not_called()
with self.assertRaisesRegex(RuntimeError, 'disabled'):
run.read_raw_exchange(0, level=3)
run.close()
def test_failed_sync_never_publishes_partial_interval(self):
run, host, calls, _ = backend()
run.step()
run._stream.synchronize.side_effect = RuntimeError('device failure')
with self.assertRaisesRegex(RuntimeError, 'device failure'):
run.step()
self.assertFalse(run._exchange_valid)
with self.assertRaisesRegex(RuntimeError, 'failed'):
run.read_raw_exchange(0, level=3)
run._stream.synchronize.side_effect = None
run.close()
def test_all_depth_weights(self):
for level in (1, 2, 3, 7):
self._check_weights_split_and_window(level)
if __name__ == "__main__":
unittest.main()