Files
CelerisLab/tests/unit/test_raw_exchange_window.py
ClaudeandClaude Opus 5.5 d930323479 feat: optional observation-window raw exchange ledger
FixedPatchStepper(observe_window_exchange=True), which requires
observe_raw_exchange, allocates a second per-link FP64 ledger per body
level. While a window is open, the unchanged BlockAccumulateExchange
kernel also accumulates into it after each fine hop. Small API:
start_exchange_window() zeroes and opens at the current completed step;
read_exchange_window(body_id, level=) is non-destructive and reports
impulse with rates over the integer window duration. initialize/
_initialize_raw and step failure invalidate; remap builds a new instance
with no open window; Jet amplitude updates may stay in-window. Default
disabled: no allocation and no extra launch; last_substep/last_base_step
unchanged. summarize_exchange gains window='window' with duration.
Host contract tests (mocked driver) and a GPU gate against summed
per-base reads with a reordering-bound FP64 tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:54:44 +00:00

46 lines
2.0 KiB
Python

"""Host reduction contract for the optional observation-window exchange ledger."""
import unittest
import numpy as np
from CelerisLab.lbm.raw_exchange import summarize_exchange
def reduce(rows, window, duration=None, h=.5, time_end=10):
return summarize_exchange(rows, np.array([0, 0, 1]), np.array([False, True, False]),
body_id=0, level=1, h=h, time_end=time_end, window=window,
reference_center=(0., 0.), duration=duration)
class ExchangeWindowReduction(unittest.TestCase):
def test_window_duration_scaling(self):
rows = np.array([[1., 2., 3., 4.], [.5, -1., 0., 1.], [9., 9., 9., 9.]])
out = reduce(rows, "window", duration=4)
np.testing.assert_array_equal(out["total"]["impulse"], [1.5, 1.])
np.testing.assert_array_equal(out["total"]["force"], [1.5 / 4, 1. / 4])
self.assertEqual(out["total"]["torque"], 3. / 4)
self.assertEqual(out["total"]["mass_rate"], 5. / 4)
np.testing.assert_array_equal(out["aperture"]["impulse"], [.5, -1.])
self.assertEqual((out["time_start"], out["time_end"]), (6., 10))
self.assertEqual(out["sample_count"], 8) # duration / h fine substeps
self.assertEqual(out["window"], "window")
def test_existing_windows_unchanged(self):
rows = np.array([[1., 2., 3., 4.], [0., 0., 0., 0.], [0., 0., 0., 0.]])
base = reduce(rows, "last_base_step")
self.assertEqual((base["time_start"], base["sample_count"]), (9., 2))
np.testing.assert_array_equal(base["total"]["force"], [1., 2.])
sub = reduce(rows, "last_substep")
self.assertEqual(sub["sample_count"], 1)
def test_duration_validation(self):
rows = np.zeros((3, 4))
for bad in (None, 0, -1, 1.5, True):
with self.subTest(duration=bad), self.assertRaises(ValueError):
reduce(rows, "window", duration=bad)
with self.assertRaises(ValueError):
reduce(rows, "last_base_step", duration=2)
if __name__ == "__main__": unittest.main()