Integrate reviewed test changes from 7c1517de819741907fb945ea0ab7864734fa3856 and 340ba4e1dc6386581839c0b28c8fe8a1f4aeb70b unchanged. Document bounded 2D implementation, evidence and open gates; production source and historical manifests are unchanged.
187 lines
7.9 KiB
Python
187 lines
7.9 KiB
Python
"""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)
|