Collect paired moving-pinball Fig2 research material
This commit is contained in:
1 parent
76873d911c
commit
63e5c3828b
8 files changed
+1284
No files matched your search
@@ -0,0 +1,36 @@
|
||||
"""Independent analytic data checks for offline estimands, no CFD context."""
|
||||
import unittest
|
||||
import numpy as np
|
||||
from research.fig2_collection.analyze import frequency, native_summary, compare_selected
|
||||
|
||||
|
||||
class AnalysisTests(unittest.TestCase):
|
||||
def test_frequency_units_and_no_dc(self):
|
||||
dt=.005/40;n=640000
|
||||
t=np.arange(n)*dt
|
||||
result=frequency(4+np.sin(2*np.pi*.2*t),dt)
|
||||
self.assertAlmostEqual(result['f_D_U'],.2)
|
||||
self.assertAlmostEqual(result['bin_width_D_U'],1/80)
|
||||
self.assertIsNone(frequency(np.ones(20),dt))
|
||||
|
||||
def test_motion_and_native_rhs_are_distinct_from_node_impulse(self):
|
||||
a=np.zeros((10,18));a[:,0]=np.arange(1,11);a[:,1]=40*np.tile([.5,-.5],5)
|
||||
a[:,3]=.001;a[:,6]=.002;a[:,9]=999
|
||||
result=native_summary(a,'taylor')
|
||||
self.assertEqual(result['motion']['centered_rms'],.5)
|
||||
self.assertEqual(result['motion']['half_range'],.5)
|
||||
self.assertAlmostEqual(result['structural_RHS_Cy_RMS'],6.)
|
||||
self.assertEqual(result['window_D_U'],[0.,10*.005/40])
|
||||
|
||||
def test_selected_vector_error_uses_common_roi_and_actual_flags(self):
|
||||
shape=(1,712,2162)
|
||||
target=dict(ux=np.zeros(shape,np.float32),uy=np.zeros(shape,np.float32),flags=np.ones(shape,np.uint16))
|
||||
current={k:v.copy() for k,v in target.items()}
|
||||
current['ux'][:,311:711,1360:2161]=.003
|
||||
current['uy'][:,311:711,1360:2161]=.004
|
||||
self.assertAlmostEqual(compare_selected(target,current)['aggregate_E'],1.,places=6)
|
||||
current['flags'][0,400,1500]=2
|
||||
with self.assertRaises(ValueError):compare_selected(target,current)
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,225 @@
|
||||
"""CPU contracts for Fig. 2 acquisition; no CUDA or simulation import required."""
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SPEC = importlib.util.spec_from_file_location('fig2_collection_tested', ROOT/'research/fig2_collection/run.py')
|
||||
driver = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(driver)
|
||||
METRIC_SPEC = importlib.util.spec_from_file_location('fig2_collection_metrics', ROOT/'research/fig2_materials/metrics.py')
|
||||
metrics = importlib.util.module_from_spec(METRIC_SPEC)
|
||||
METRIC_SPEC.loader.exec_module(metrics)
|
||||
|
||||
|
||||
class CollectionContracts(unittest.TestCase):
|
||||
def test_frozen_clocks_and_windows(self):
|
||||
for scene, release, count, first in [('karman', 4576000, 400, 240),
|
||||
('steady', 2048000, 400, 240),
|
||||
('taylor', 1024000, 150, 0)]:
|
||||
plan = driver.schedule(scene)
|
||||
self.assertEqual((plan['release_clock'], plan['intervals'], plan['field_start']),
|
||||
(release, count, first))
|
||||
self.assertEqual(plan['si'], 3200)
|
||||
self.assertEqual((400-240)*3200*driver.U/driver.D, 64.)
|
||||
self.assertEqual(400*3200*driver.U/driver.D, 160.)
|
||||
self.assertEqual(150*3200*driver.U/driver.D, 60.)
|
||||
|
||||
def test_smoke_has_no_physical_timing(self):
|
||||
for scene in ('karman', 'steady', 'taylor'):
|
||||
plan = driver.schedule(scene, True)
|
||||
self.assertEqual((plan['si'], plan['intervals'], plan['field_start']), (32, 3, 0))
|
||||
self.assertEqual(plan['release_clock'], 64 if scene == 'taylor' else 32)
|
||||
|
||||
def test_zero_is_exact_physical_zero_despite_bias(self):
|
||||
for scene in ('karman', 'steady', 'taylor'):
|
||||
np.testing.assert_array_equal(driver.wall_surface(scene, 'zero'), np.zeros(3))
|
||||
np.testing.assert_array_equal(driver.wall_surface(scene, 'target'), np.zeros(3))
|
||||
np.testing.assert_allclose(driver.wall_surface('karman', 'controlled', [.5, -.5, 1]),
|
||||
[.02, -.04, .06])
|
||||
np.testing.assert_allclose(driver.wall_surface('taylor', 'controlled', [.5, -.5, 1]),
|
||||
[.01, -.03, .04])
|
||||
np.testing.assert_allclose(driver.wall_surface('steady', 'controlled'), [0, -.0255, .0255])
|
||||
with self.assertRaises(ValueError):
|
||||
driver.wall_surface('karman', 'controlled', [0, 0, np.nan])
|
||||
|
||||
def test_structure_hard_stops(self):
|
||||
driver.check_structure(40., 0., 40.)
|
||||
for y, v, old in [(40.01, 0, 40), (0, .026, 0), (.026, 0, 0), (np.nan, 0, 0)]:
|
||||
with self.subTest(y=y, v=v), self.assertRaises(RuntimeError):
|
||||
driver.check_structure(y, v, old)
|
||||
|
||||
def test_fluid_nonfinite_fails_nonfluid_only_zero_filled(self):
|
||||
flags = np.array([[1, 257, 2]], np.uint16)
|
||||
fields = dict(rho=np.array([[1., 1., np.nan]]), ux=np.array([[0., .1, np.nan]]),
|
||||
uy=np.array([[0., -.1, np.inf]]))
|
||||
clean, retained, fluid = driver.clean_field(fields, flags)
|
||||
np.testing.assert_array_equal(fluid, [[True, True, False]])
|
||||
np.testing.assert_array_equal(retained, flags)
|
||||
self.assertTrue(np.isfinite(clean).all())
|
||||
self.assertEqual(clean[0, 0, 0], 0.) # zero flow remains fluid
|
||||
fields['ux'][0, 1] = np.nan
|
||||
with self.assertRaisesRegex(RuntimeError, 'FLUID'):
|
||||
driver.clean_field(fields, flags)
|
||||
|
||||
def test_memory_admission_uses_available_not_free(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory)/'meminfo'
|
||||
path.write_text('MemFree: 1 kB\nMemAvailable: 16777216 kB\n')
|
||||
self.assertEqual(driver.available_memory(path), driver.MIN_AVAILABLE)
|
||||
with patch.object(driver, 'available_memory', return_value=driver.MIN_AVAILABLE-1):
|
||||
with self.assertRaises(MemoryError):
|
||||
driver.admit_memory()
|
||||
self.assertIsNone(driver.admit_memory(True))
|
||||
|
||||
def test_roi_bounds_match_full_downstream_support(self):
|
||||
roi = metrics.roi_slices((1022, 2559), D=40, xs=1600, y0=510.5,
|
||||
downstream_solid_extent=1272.)
|
||||
self.assertEqual(roi, (slice(311, 711), slice(1360, 2161)))
|
||||
|
||||
def test_same_clock_error_and_mean_are_distinct(self):
|
||||
shape = (2, 2, 2, 3)
|
||||
target = np.zeros(shape, np.float32)
|
||||
changed = target.copy()
|
||||
changed[0, 0], changed[1, 0] = driver.U, -driver.U
|
||||
role = lambda fields: dict(roi=fields, mask=np.ones((2, 3), bool), times=np.array([3200, 6400]))
|
||||
roles = dict(target=role(target), zero=role(changed), controlled=role(target))
|
||||
result, mask = driver.score_roles(roles, metrics)
|
||||
self.assertAlmostEqual(result['zero']['mean_instantaneous_E'], 1., places=7)
|
||||
self.assertEqual(result['zero']['E_of_fixed_window_mean'], 0.)
|
||||
roles['zero']['times'] = np.array([6400, 9600])
|
||||
with self.assertRaisesRegex(ValueError, 'clocks'):
|
||||
driver.score_roles(roles, metrics)
|
||||
roles['zero']['times'] = roles['target']['times']
|
||||
roles['zero']['mask'][0, 0] = False
|
||||
with self.assertRaisesRegex(ValueError, 'entirely common fluid'):
|
||||
driver.score_roles(roles, metrics)
|
||||
|
||||
def test_native_archive_verified_raw_retained(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory)/'native.npy'
|
||||
rows = np.lib.format.open_memmap(path, mode='w+', dtype=np.float64,
|
||||
shape=(9, len(driver.NATIVE_COLUMNS)))
|
||||
rows[:] = np.arange(rows.size).reshape(rows.shape)
|
||||
report = driver.compress_native(path, rows, 7)
|
||||
self.assertTrue(report['verified'])
|
||||
self.assertTrue(path.exists())
|
||||
with np.load(path.with_suffix('.npz')) as data:
|
||||
np.testing.assert_array_equal(data['rows'], rows[:7])
|
||||
|
||||
def test_readonly_import_reuses_actual_offsets_and_functions(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root/'legacy_test/core').mkdir(parents=True)
|
||||
# Package __init__ must never be executed by the isolated source loader.
|
||||
for folder in (root, root/'legacy_test', root/'legacy_test/core'):
|
||||
(folder/'__init__.py').write_text("raise RuntimeError('application import forbidden')\n")
|
||||
(root/'legacy_test/core/dtw_metrics.py').write_text('calc_dtw_sim = None\ncalc_lag = None\n')
|
||||
(root/'acquisition.py').write_text(
|
||||
'from .legacy_test.core.dtw_metrics import calc_dtw_sim, calc_lag\n'
|
||||
'VORTEX_EVENT_OFFSETS = (-20,-15,-10,-5,0,5,10,15)\n')
|
||||
(root/'legacy_test/acquire.py').write_text(
|
||||
"raise RuntimeError('heavy imports forbidden')\n"
|
||||
'def _vortex_event_channel(case_name):\n return 3, "center_sensor_uy"\n'
|
||||
'def _select_vortex_event(sensors, case_name):\n'
|
||||
' return dict(offsets=VORTEX_EVENT_OFFSETS, channel=_vortex_event_channel(case_name))\n')
|
||||
helper = driver.load_helpers(root/'acquisition.py')
|
||||
result = helper.select_vortex_event(None, 'vortex_taylor')
|
||||
self.assertEqual(result['offsets'], (-20,-15,-10,-5,0,5,10,15))
|
||||
self.assertEqual(result['channel'], (3, 'center_sensor_uy'))
|
||||
self.assertFalse(list(root.rglob('*.pyc')))
|
||||
|
||||
def test_geometry_and_shared_translation_envelope(self):
|
||||
calls, translations = [], []
|
||||
def translation(*args, **kwargs):
|
||||
item = types.SimpleNamespace(args=args, kwargs=kwargs)
|
||||
translations.append(item)
|
||||
return item
|
||||
def add_body(*args, **kwargs):
|
||||
calls.append((args, kwargs))
|
||||
return len(calls)-1
|
||||
motion = types.ModuleType('CelerisLab.body.motion')
|
||||
motion.Translation = translation
|
||||
with patch.dict(sys.modules, {'CelerisLab.body.motion': motion}):
|
||||
probes, bodies = driver.add_scene(types.SimpleNamespace(add_body=add_body), 'karman', moving=True)
|
||||
self.assertEqual(calls[0][0], ('circle', (400.,510.5),40.))
|
||||
self.assertEqual(probes, [1,2,3])
|
||||
self.assertEqual(bodies, [4,5,6])
|
||||
self.assertEqual(translations[0].args, ((0.,0.,-40.,40.), .025))
|
||||
self.assertTrue(translations[0].kwargs['shared'])
|
||||
self.assertTrue(all(calls[i][1]['translation'] is translations[0] for i in bodies))
|
||||
calls.clear()
|
||||
with patch.dict(sys.modules, {'CelerisLab.body.motion': motion}):
|
||||
driver.add_scene(types.SimpleNamespace(add_body=add_body), 'steady', pinball=False)
|
||||
self.assertEqual(len(calls), 3)
|
||||
self.assertTrue(all(call[0][0] == 'sensor' for call in calls))
|
||||
|
||||
def test_taylor_operator_called_once_inside_context_at_response_zero(self):
|
||||
events = []
|
||||
class Context:
|
||||
def __enter__(self):
|
||||
events.append('enter')
|
||||
def __exit__(self, *args):
|
||||
events.append('exit')
|
||||
initializer = types.ModuleType('CelerisLab.lbm.initializers')
|
||||
initializer.add_vortex = lambda *args: events.append(args)
|
||||
field = object()
|
||||
sim = types.SimpleNamespace(field=field, stepper=types.SimpleNamespace(step_count=1024000),
|
||||
ctx=types.SimpleNamespace(activate=Context),
|
||||
stream=types.SimpleNamespace(synchronize=lambda: events.append('sync')))
|
||||
with patch.dict(sys.modules, {'CelerisLab.lbm.initializers': initializer}):
|
||||
receipt = driver.inject_taylor(sim)
|
||||
self.assertEqual(events, ['enter', 'sync', (field, (600.,510.5),80.,.000075,'taylor'), 'sync', 'exit'])
|
||||
self.assertEqual((receipt['response_clock'], receipt['absolute_clock'], receipt['count']),
|
||||
(0,1024000,1))
|
||||
|
||||
|
||||
class SelectionContracts(unittest.TestCase):
|
||||
@staticmethod
|
||||
def mean(ux, uy, indices):
|
||||
return dict(mean_ux=ux[indices].mean(axis=0), mean_uy=uy[indices].mean(axis=0))
|
||||
|
||||
def test_karman_missing_cycles_preserves_fixed_mean_and_last(self):
|
||||
helper = types.SimpleNamespace(accumulate_mean_fields=self.mean)
|
||||
def no_crossing(*args, **kwargs):
|
||||
raise ValueError('at least two accepted rising crossings are required')
|
||||
helper.assign_periodic_phase = no_crossing
|
||||
cache = np.arange(24, dtype=np.float32).reshape(3, 2, 2, 2)
|
||||
arrays, metadata, event = driver.select_fields('karman', dict(smoke=False), helper,
|
||||
np.array([3200, 6400, 9600]), np.zeros((3, 6)), cache)
|
||||
self.assertEqual(metadata['periodic_phase_status'], 'insufficient_crossings')
|
||||
self.assertNotIn('cycle_mean_ux', arrays)
|
||||
np.testing.assert_array_equal(arrays['ux'][0], cache[-1, 0])
|
||||
np.testing.assert_allclose(arrays['fixed_window_mean_ux'], cache[:, 0].mean(axis=0))
|
||||
|
||||
def test_taylor_common_controlled_event_not_reselected(self):
|
||||
def forbidden(*args):
|
||||
raise AssertionError('other roles must not select their own event')
|
||||
helper = types.SimpleNamespace(accumulate_mean_fields=self.mean, select_vortex_event=forbidden)
|
||||
cache = np.zeros((150, 2, 2, 2), np.float32)
|
||||
event = dict(field_indices=np.array([20,25,30,35,40,45,50,55]))
|
||||
arrays, meta, returned = driver.select_fields('taylor', dict(smoke=False), helper,
|
||||
np.arange(1,151)*3200, np.zeros((150,6)), cache, event)
|
||||
self.assertIs(returned, event)
|
||||
np.testing.assert_array_equal(arrays['selected_index'], event['field_indices'])
|
||||
self.assertEqual(len(arrays['ux']), 8)
|
||||
|
||||
def test_smoke_selection_has_no_event_or_phase_claim(self):
|
||||
helper = types.SimpleNamespace(accumulate_mean_fields=self.mean)
|
||||
for scene in ('karman', 'steady', 'taylor'):
|
||||
arrays, metadata, event = driver.select_fields(scene, dict(smoke=True), helper,
|
||||
np.array([32,64,96]), np.zeros((3,6)),
|
||||
np.zeros((3,2,2,2), np.float32))
|
||||
self.assertEqual(metadata['selection'], 'smoke_last_snapshot_not_physical')
|
||||
self.assertIsNone(event)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,49 @@
|
||||
"""CPU-only analytic checks, runnable with stdlib unittest (or pytest)."""
|
||||
import unittest
|
||||
import numpy as np
|
||||
from research.fig2_materials.metrics import (
|
||||
common_fluid_roi_mask, field_error, motion_summary, roi_slices,
|
||||
)
|
||||
|
||||
|
||||
class MetricsTests(unittest.TestCase):
|
||||
def test_u0_normalization_not_target_energy(self):
|
||||
zero = np.zeros((2, 2)); support = np.ones((2, 2), bool)
|
||||
self.assertEqual(field_error(zero + 3, zero + 4, zero, zero, 2, support), 2.5)
|
||||
self.assertEqual(field_error(zero + 13, zero + 4, zero + 10, zero, 2, support), 2.5)
|
||||
for u0 in (0, -1, np.nan):
|
||||
with self.assertRaises(ValueError):
|
||||
field_error(zero, zero, zero, zero, u0, support)
|
||||
|
||||
def test_common_mask_roi_finite_and_fluid(self):
|
||||
a = np.zeros((3, 4)); b = a.copy(); b[1, 2] = np.nan
|
||||
m = np.ones(a.shape, bool); n = m.copy(); n[1, 1] = False
|
||||
support = common_fluid_roi_mask([m, n], [a, b], (slice(1, 3), slice(1, 4)))
|
||||
self.assertEqual(int(support.sum()), 4)
|
||||
self.assertFalse(support[1, 1]); self.assertFalse(support[1, 2])
|
||||
self.assertTrue(support[2, 1]) # zero velocity is fluid
|
||||
self.assertEqual(field_error(a, a, b, a, 1, support), 0)
|
||||
with self.assertRaises(ValueError):
|
||||
common_fluid_roi_mask([m & False], [a], (slice(None), slice(None)))
|
||||
with self.assertRaises(ValueError):
|
||||
field_error(a, a, b, a, 1, m) # fixed support must be finite
|
||||
|
||||
def test_mean_field_differs_from_mean_instantaneous_error(self):
|
||||
snapshots = np.array([[[2.]], [[-2.]]]); z = np.zeros((1, 1)); mask = np.ones((1, 1), bool)
|
||||
self.assertEqual(field_error(snapshots.mean(0), z, z, z, 1, mask), 0)
|
||||
self.assertEqual(np.mean([field_error(s, z, z, z, 1, mask) for s in snapshots]), 2)
|
||||
|
||||
def test_actual_geometry_and_motion_bias(self):
|
||||
roi = roi_slices((1022, 2400), D=40, xs=1600, y0=510.5, downstream_solid_extent=1350)
|
||||
self.assertEqual(roi, (slice(311, 711), slice(1360, 2161)))
|
||||
m = motion_summary([8, 10, 12])
|
||||
self.assertEqual(m['mean'], 10)
|
||||
self.assertAlmostEqual(m['centered_rms'], np.sqrt(8 / 3))
|
||||
self.assertEqual(m['half_range'], 2)
|
||||
for a in ([], [np.nan]):
|
||||
with self.assertRaises(ValueError):
|
||||
motion_summary(a)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user