"""Bounded GPU regressions for repaired layout/state contracts (D2Q9, low Ma).""" import numpy as np import pytest from ._cases import make_sim from CelerisLab.lbm.descriptors import FLUID def physical_esopull(sim): """Decode physical next-pull state without changing the solver clock.""" with sim.ctx.activate(): indices = np.arange(sim.field.n,dtype=np.uint32) scratch, values, idx_gpu = sim.field.esopull_sync_decode( indices,sim.stepper.step_count,sim.stream) result = values.copy() scratch.free() idx_gpu.free() return result @pytest.mark.parametrize('precision',['FP32','FP16S']) @pytest.mark.parametrize('shifted',[False,True]) def test_esopull_field_snapshot_across_one_step(precision,shifted): with make_sim(nx=48,ny=24,streaming='esopull',precision=precision,shifted=shifted) as sim: sim.add_body('circle',center=(16,12),radius=3) sim.initialize() sim.run(3) saved = physical_esopull(sim) sim.snapshot() sim.set_body(0,omega=.0003) sim.run(1) current_step = sim.stepper.step_count action = sim.bodies.action.copy() sim.restore() assert sim.stepper.step_count == current_step == 4 np.testing.assert_array_equal(sim.bodies.action,action) # Encoding roundtrip can lose a float32 addition/subtraction ulp when # adding lattice weights around shifted FP16 storage; it cannot stream. np.testing.assert_allclose(physical_esopull(sim),saved,rtol=0,atol=1e-7) @pytest.mark.parametrize('streaming',['double_buffer','esopull']) @pytest.mark.parametrize('precision',['FP32','FP16S']) def test_shifted_topology_add_remove_preserves_cache_and_finite_flow(streaming,precision): with make_sim(nx=48,ny=24,streaming=streaming,precision=precision,shifted=True) as sim: sim.initialize() sim.run(3) sim.get_ddf() # Fill the same-count host cache before a geometry change. sim.add_body('circle',center=(18,12),radius=3) sim.sync_bodies() assert sim.stepper.step_count == 3 assert sim.field._host_ddf_step is None sim.run(3) assert np.all(np.isfinite(sim.read_force(0))) sim.get_ddf() sim.remove_body(0) sim.sync_bodies() assert sim.stepper.step_count == 6 assert sim.field._host_ddf_step is None sim.run(3) macro = sim.get_macroscopic() fluid = (sim.get_flags().reshape(24,48) & FLUID) != 0 assert all(np.isfinite(array[fluid]).all() for array in macro.values()) assert np.min(macro['rho'][fluid]) > .5 assert np.max(macro['rho'][fluid]) < 1.5 @pytest.mark.parametrize('collision,streaming', [ pytest.param('SRT', 'double_buffer', id='double_buffer'), pytest.param('SRT', 'esopull', id='esopull'), pytest.param('TRT', 'double_buffer', id='trt-double_buffer'), ]) def test_checkpoint_exact_continuation_and_controls(collision,streaming,tmp_path): # Same collision throughout save/load/continue; this is not a method-switch restart. with make_sim(nx=48,ny=24,streaming=streaming,shifted=True,collision=collision) as sim: sim.add_body('circle',center=(14,12),radius=3) sim.add_body('force_region',center=(30,12),radius=2) sim.initialize() sim.set_force(1,fx=1e-5,fy=-2e-6) sim.set_body(0,omega=.0002) sim.run(3) saved = sim.get_ddf() path = sim.save_checkpoint(str(tmp_path/'restart.h5')) sim.run(4) expected = sim.get_ddf() sim.set_force(1,fx=0,fy=0) sim.set_body(0,omega=0) # Force the exact count cache collision which formerly skipped D2H. sim.field._host_ddf_step = 3 sim.field.ddf.fill(99) sim.load_checkpoint(path) assert sim.stepper.step_count == 3 np.testing.assert_array_equal(sim.get_ddf(),saved) np.testing.assert_allclose(sim.bodies.action[[5,6,7]],[.0002,1e-5,-2e-6]) sim.run(4) np.testing.assert_array_equal(sim.get_ddf(),expected) @pytest.mark.parametrize('streaming', ['double_buffer', 'esopull']) @pytest.mark.parametrize('periodic_x', [False, True]) def test_checkpoint_domain_exact_continuation(streaming, periodic_x, tmp_path): # Same low-Mach forced channel as the Poiseuille runner, bounded to 7 steps. kwargs = dict(nx=16, ny=18, streaming=streaming, collision='MRT', shifted=True, viscosity=.1, velocity=0.) with make_sim(**kwargs) as source: source.initialize() if periodic_x: source.field.flag.reshape(18, 16)[1:-1, [0, -1]] = FLUID source.field.upload_flags() source.update_runtime_params(fx=8 * .1 * .03 / 16**2) source.run(3) flags = source.get_flags().copy() saved = source.get_ddf().copy() path = source.save_checkpoint(str(tmp_path / 'domain.h5')) source.run(4) expected = source.get_ddf().copy() # Restore into this instance and also a fresh default-domain instance. source.load_checkpoint(path) np.testing.assert_array_equal(source.get_flags(), flags) np.testing.assert_array_equal(source.get_ddf(), saved) source.run(4) np.testing.assert_array_equal(source.get_ddf(), expected) with make_sim(**kwargs) as target: target.initialize() target.load_checkpoint(path) assert target.stepper.step_count == 3 np.testing.assert_array_equal(target.get_flags(), flags) np.testing.assert_array_equal(target.get_ddf(), saved) target.run(4) np.testing.assert_array_equal(target.get_ddf(), expected) @pytest.mark.parametrize('periodic_x', [False, True]) def test_checkpoint_geometry_mismatch_leaves_device_unchanged(tmp_path, periodic_x): with make_sim(nx=48,ny=24) as source: source.add_body('circle',center=(14,12),radius=3) source.initialize() if periodic_x: source.field.flag.reshape(24, 48)[1:-1, [0, -1]] = FLUID source.field.upload_flags() source.run(2) path = source.save_checkpoint(str(tmp_path/'geometry.h5')) with make_sim(nx=48,ny=24) as target: target.add_body('circle',center=(28,12),radius=3) target.initialize() target.run(3) before = target.get_ddf() flags = target.get_flags() with pytest.raises(ValueError,match='geometry'): target.load_checkpoint(path) assert target.stepper.step_count == 3 target.field.invalidate_host_ddf_cache() np.testing.assert_array_equal(target.get_ddf(),before) np.testing.assert_array_equal(target.get_flags(),flags) def test_periodic_checkpoint_restores_observation_interval(tmp_path,monkeypatch): monkeypatch.chdir(tmp_path) with make_sim(nx=48,ny=24) as sim: sim.add_body('sensor',center=(24,12),radius=2) sim.initialize() sim.run(6,checkpoint_interval=2) sim.load_checkpoint(str(tmp_path/'output/checkpoint_00000002.h5')) assert sim.stepper.step_count == 2 assert sim.bodies._obs_accum_steps == 2 raw = sim.read_sensor(0,normalize=False) assert np.linalg.norm(raw) > 0 np.testing.assert_allclose(sim.read_sensor(0),raw/2,rtol=1e-6) def test_checkpoint_preserves_pending_and_applied_controls(tmp_path): with make_sim(nx=48,ny=24,shifted=True) as sim: sim.add_body('force_region',center=(24,12),radius=3) sim.initialize() sim.set_force(0,fx=1e-5,fy=-2e-6) sim.run(3) sim.set_force(0,fx=3e-5,fy=4e-6) # Host target has not been uploaded. pending = sim.bodies.action.copy() path = sim.save_checkpoint(str(tmp_path/'pending.h5')) sim.run(4,upload_act=False) expected = sim.get_ddf() sim.set_force(0,fx=0,fy=0) sim.load_checkpoint(path) np.testing.assert_array_equal(sim.bodies.action,pending) sim.run(4,upload_act=False) np.testing.assert_array_equal(sim.get_ddf(),expected)