280 lines
15 KiB
Diff
280 lines
15 KiB
Diff
--- /dev/null
|
|
+++ b/tests/validation/diagnose_j3_lags.py
|
|
@@ -0,0 +1,276 @@
|
|
+"""One exact J3 replay and one 520-step audit trajectory; no production edits."""
|
|
+import argparse
|
|
+from dataclasses import asdict
|
|
+import hashlib
|
|
+import json
|
|
+import os
|
|
+from pathlib import Path
|
|
+import sys
|
|
+
|
|
+import h5py
|
|
+import numpy as np
|
|
+import pycuda.driver as cuda
|
|
+import CelerisLab
|
|
+from CelerisLab.cuda import compiler_v2 as compiler
|
|
+from run_j1 import ROOT, AUDIT, make_sim, populations, observables
|
|
+from run_j2_slot import targets
|
|
+
|
|
+START, LENGTH, SCALE = 20760, 520, .002
|
|
+PHASE = 'postcollision g(t); D2Q9 paired ordering; unshifted FP32 double_buffer; q,y,x'
|
|
+
|
|
+
|
|
+def sha(path):
|
|
+ return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
+
|
|
+
|
|
+def buffers(sim):
|
|
+ arrays = dict(ddf=np.empty(sim.field.n*sim.field.nq, dtype=np.float32),
|
|
+ temp=np.empty(sim.field.n*sim.field.nq, dtype=np.float32),
|
|
+ flag=np.empty(sim.field.n, dtype=np.uint16))
|
|
+ with sim.ctx.activate():
|
|
+ for name, array in arrays.items():
|
|
+ cuda.memcpy_dtoh(array, getattr(sim.field, name+'_gpu'))
|
|
+ return arrays
|
|
+
|
|
+
|
|
+def save_verified(sim, path):
|
|
+ """Persist both real buffers, then restore into a new case without advancing."""
|
|
+ assert not path.exists(), f'Refusing to overwrite {path}'
|
|
+ before, config = buffers(sim), asdict(sim.lbm_cfg)
|
|
+ params, count = sim.field._runtime_params.copy(), sim.stepper.step_count
|
|
+ sim.save_checkpoint(str(path))
|
|
+ with h5py.File(path, 'a') as hf:
|
|
+ hf.attrs['distribution_phase'] = PHASE
|
|
+ hf.attrs['diagnostic_script_sha256'] = sha(__file__)
|
|
+ for name, array in before.items():
|
|
+ assert hf[name][:].tobytes() == array.tobytes(), name
|
|
+ assert json.loads(hf.attrs['config_json']) == config
|
|
+ assert json.loads(hf.attrs['runtime_params_json']) == params
|
|
+ assert int(hf.attrs['step_count']) == count
|
|
+ with make_sim(.002, nx=48, top_u=0., slot_width=16, top_mode='reservoir') as restored:
|
|
+ restored.load_checkpoint(str(path))
|
|
+ after = buffers(restored)
|
|
+ assert all(before[k].tobytes() == after[k].tobytes() for k in before)
|
|
+ assert asdict(restored.lbm_cfg) == config
|
|
+ assert restored.field._runtime_params == params
|
|
+ assert restored.stepper.step_count == count
|
|
+ return dict(path=str(path), sha256=sha(path), step=count, phase=PHASE,
|
|
+ no_advance_restore='passed', buffers=['ddf', 'temp', 'flag'])
|
|
+
|
|
+
|
|
+def replay(sim):
|
|
+ sim.initialize()
|
|
+ rho, old_u, old_v = observables(populations(sim))
|
|
+ initial_mass = old_mass = float(rho.sum())
|
|
+ history = [dict(step=0, mass=initial_mass)]
|
|
+ for _ in range(40):
|
|
+ sim.run(519)
|
|
+ rho, u, v = observables(populations(sim))
|
|
+ mass = float(rho.sum())
|
|
+ history.append(dict(step=sim.stepper.step_count, mass=mass,
|
|
+ u_change=float(np.sqrt(np.mean((u-old_u)**2))/SCALE),
|
|
+ v_change=float(np.sqrt(np.mean((v-old_v)**2))/SCALE),
|
|
+ mass_change=abs(mass-old_mass)/initial_mass))
|
|
+ old_u, old_v, old_mass = u, v, mass
|
|
+ assert sim.stepper.step_count == START
|
|
+ return history, (rho, u, v)
|
|
+
|
|
+
|
|
+def record_trajectory(sim, path, reference):
|
|
+ """Exactly LENGTH advances. Reuses J3's already validated audit source."""
|
|
+ build = compiler.generate_config(sim.lbm_cfg)
|
|
+ original_step = sim.stepper.step_fn
|
|
+ try:
|
|
+ source = Path(build)/'step/one_step_double.cu'
|
|
+ code = source.read_text()
|
|
+ call, marker = 'collide_dispatch(f, rho_n, ux, uy);', '__global__ __launch_bounds__(NT, 2)'
|
|
+ assert code.count(call) == 1 and code.count(marker) == 1
|
|
+ source.write_text(code.replace(call, 'j1_collide_audit(f, rho_n, ux, uy);')
|
|
+ .replace(marker, AUDIT+'\n'+marker))
|
|
+ audit_hash = sha(source)
|
|
+ assert audit_hash == reference['audit_source_sha256'], 'Audit source changed; stop'
|
|
+ ptx = compiler.compile_kernel(build_dir=build)
|
|
+ with sim.ctx.activate(), h5py.File(path, 'x') as hf:
|
|
+ module = compiler.load_module(ptx)
|
|
+ params, size = module.get_global('d_params')
|
|
+ cuda.memcpy_dtod(params, sim.field.module.get_global('d_params')[0], size)
|
|
+ incoming_ptr, _ = module.get_global('j1_incoming')
|
|
+ collision_ptr, size = module.get_global('j1_collision')
|
|
+ cuda.memset_d8(collision_ptr, 0, size)
|
|
+ incoming = np.empty((2, 48), dtype=np.float64)
|
|
+ collision = np.empty((35, 48), dtype=np.float64)
|
|
+ hf.attrs.update(phase=PHASE, first_step=START, last_step=START+LENGTH,
|
|
+ first_fluid_y=1, velocity_scale=SCALE, rho_scale=1.,
|
|
+ recorded_samples=0, audit_source_sha256=audit_hash)
|
|
+ hf.create_dataset('step', data=np.arange(START, START+LENGTH+1))
|
|
+ for name in ('rho', 'u', 'v'):
|
|
+ hf.create_dataset(name, (LENGTH+1, 33, 48), dtype='f8', chunks=(1, 33, 48),
|
|
+ compression='gzip')
|
|
+ for name in ('mass_flux_by_y', 'volume_flux_by_y'):
|
|
+ hf.create_dataset(name, (LENGTH+1, 33), dtype='f8')
|
|
+ hf.create_dataset('mass', (LENGTH+1,), dtype='f8')
|
|
+ for name in ('incoming', 'outgoing'):
|
|
+ hf.create_dataset(name, (LENGTH, 2, 48), dtype='f8', compression='gzip')
|
|
+ hf.create_dataset('collision', (LENGTH, 35, 48), dtype='f8', compression='gzip')
|
|
+ hf.attrs['boundary_order'] = 'bottom,top; all x including zero-target flanks'
|
|
+ bottom, top_guess, slot = targets(.002)
|
|
+ hf.create_dataset('bottom_slot_mask', data=slot)
|
|
+ hf.create_dataset('bottom_requested_velocity', data=bottom)
|
|
+ hf.attrs.update(requested_bottom_volume=float(bottom.sum()), top_initial_guess=float(top_guess))
|
|
+
|
|
+ def store_frame(n, f):
|
|
+ rho, u, v = observables(f)
|
|
+ for name, array in zip(('rho', 'u', 'v'), (rho, u, v)):
|
|
+ hf[name][n] = array
|
|
+ hf['mass'][n] = rho.sum()
|
|
+ hf['mass_flux_by_y'][n] = (rho*v).sum(axis=1)
|
|
+ hf['volume_flux_by_y'][n] = v.sum(axis=1)
|
|
+ hf.attrs['recorded_samples'] = n+1
|
|
+
|
|
+ before = populations(sim)
|
|
+ store_frame(0, before)
|
|
+ sim.stepper.step_fn = module.get_function('OneStep')
|
|
+ for n in range(LENGTH):
|
|
+ outgoing = np.stack((before[[4,6,7], 1].sum(axis=0), before[[3,5,8], -2].sum(axis=0)))
|
|
+ mass = before[:, 1:-1].sum()
|
|
+ sim.run(1)
|
|
+ after = populations(sim)
|
|
+ assert np.isfinite(after).all()
|
|
+ cuda.memcpy_dtoh(incoming, incoming_ptr)
|
|
+ cuda.memcpy_dtoh(collision, collision_ptr)
|
|
+ hf['incoming'][n], hf['outgoing'][n], hf['collision'][n] = incoming, outgoing, collision
|
|
+ store_frame(n+1, after)
|
|
+ if n < 16:
|
|
+ row = reference['steps'][n]
|
|
+ measured = dict(mass=mass, delta_mass=after[:, 1:-1].sum()-mass,
|
|
+ bottom_net_in=incoming[0].sum()-outgoing[0].sum(),
|
|
+ top_net_in=incoming[1].sum()-outgoing[1].sum(),
|
|
+ collision_roundoff=collision.sum())
|
|
+ assert all(measured[k] == row[k] for k in measured), 'Prior J3 audit not reproduced'
|
|
+ before = after
|
|
+ assert sim.stepper.step_count == START+LENGTH
|
|
+ hf.attrs['first_16_match_j3'] = True
|
|
+ return audit_hash
|
|
+ finally:
|
|
+ sim.stepper.step_fn = original_step
|
|
+ compiler.cleanup_build_dir(build)
|
|
+
|
|
+
|
|
+def variation(u, v):
|
|
+ """Per-time/per-origin spatial metrics; no assumption of independent samples."""
|
|
+ delta = np.stack((u, v), axis=1)
|
|
+ return dict(rms=np.sqrt(np.mean(delta**2, axis=(2, 3))),
|
|
+ max_abs=np.max(np.abs(delta), axis=(2, 3)),
|
|
+ signed_mean=np.mean(delta, axis=(2, 3)),
|
|
+ vector_rms=np.sqrt(np.mean(u*u+v*v, axis=(1, 2))),
|
|
+ vector_max=np.sqrt(np.max(u*u+v*v, axis=(1, 2))))
|
|
+
|
|
+
|
|
+def summarize(path):
|
|
+ summary = dict(lags={})
|
|
+ with h5py.File(path, 'a') as hf:
|
|
+ assert hf.attrs['recorded_samples'] == LENGTH+1
|
|
+ u, v, mass = hf['u'][:], hf['v'][:], hf['mass'][:]
|
|
+ for lag in (1, 16, 519):
|
|
+ group = hf.create_group(f'lag/{lag}')
|
|
+ group.create_dataset('origin_step', data=np.arange(START, START+LENGTH-lag+1))
|
|
+ values = variation(u[lag:]-u[:-lag], v[lag:]-v[:-lag])
|
|
+ for name, array in values.items(): group.create_dataset(name, data=array)
|
|
+ summary['lags'][str(lag)] = dict(origins=LENGTH+1-lag,
|
|
+ rms_mean=values['rms'].mean(axis=0).tolist(), rms_min=values['rms'].min(axis=0).tolist(),
|
|
+ rms_max=values['rms'].max(axis=0).tolist(), max_abs=values['max_abs'].max(axis=0).tolist(),
|
|
+ rms_mean_scaled=(values['rms'].mean(axis=0)/SCALE).tolist(),
|
|
+ signed_mean_change=values['signed_mean'].mean(axis=0).tolist(),
|
|
+ signed_mean_change_per_step=(values['signed_mean'].mean(axis=0)/lag).tolist(),
|
|
+ vector_rms_mean=float(values['vector_rms'].mean()),
|
|
+ vector_max=float(values['vector_max'].max()))
|
|
+ summary['lags'][str(lag)]['metrics'] = {
|
|
+ name: {label: func(array, axis=0).tolist()
|
|
+ for label, func in (('mean', np.mean), ('minimum', np.min), ('maximum', np.max))}
|
|
+ for name, array in values.items()}
|
|
+ summary['lags'][str(lag)]['metrics_scaled'] = {
|
|
+ name: {label: (np.asarray(value)/SCALE).tolist() for label, value in metrics.items()}
|
|
+ for name, metrics in summary['lags'][str(lag)]['metrics'].items()}
|
|
+ means = hf.create_group('time_mean')
|
|
+ for name in ('rho', 'u', 'v'): means.create_dataset(name, data=hf[name][:].mean(axis=0))
|
|
+ deviation = variation(u-u.mean(axis=0), v-v.mean(axis=0))
|
|
+ group = hf.create_group('deviation')
|
|
+ for name, array in deviation.items(): group.create_dataset(name, data=array)
|
|
+ summary['time_mean_deviation'] = dict(rms=np.sqrt(np.mean(deviation['rms']**2, axis=0)).tolist(),
|
|
+ max_abs=deviation['max_abs'].max(axis=0).tolist())
|
|
+ end_delta = np.stack((u[-1]-u[0], v[-1]-v[0]))
|
|
+ hf.create_dataset('end_minus_start_uv', data=end_delta)
|
|
+ hf.create_dataset('mean_drift_per_step_uv', data=end_delta/LENGTH)
|
|
+ summary['signed_end_change'] = dict(mean=end_delta.mean(axis=(1, 2)).tolist(),
|
|
+ minimum=end_delta.min(axis=(1, 2)).tolist(),
|
|
+ maximum=end_delta.max(axis=(1, 2)).tolist(),
|
|
+ mean_per_step=(end_delta.mean(axis=(1, 2))/LENGTH).tolist())
|
|
+ incoming, outgoing, collision = hf['incoming'][:], hf['outgoing'][:], hf['collision'][:]
|
|
+ slot = hf['bottom_slot_mask'][:]
|
|
+ groups = hf.create_group('budget')
|
|
+ net = incoming.sum(axis=(1, 2))-outgoing.sum(axis=(1, 2))
|
|
+ roundoff, delta = collision.sum(axis=(1, 2)), np.diff(mass)
|
|
+ residual = delta-net-roundoff
|
|
+ for name, array in dict(delta_mass=delta, net_boundary=net, collision_roundoff=roundoff,
|
|
+ physical_residual=delta-net, residual=residual).items():
|
|
+ groups.create_dataset(name, data=array)
|
|
+ summary['budget'] = dict(mean_delta_mass=float(delta.mean()), net_boundary=float(net.mean()),
|
|
+ collision_roundoff=float(roundoff.mean()), max_residual=float(np.abs(residual).max()),
|
|
+ max_relative_residual=float(np.max(np.abs(residual)/mass[:-1])),
|
|
+ max_physical_relative_residual=float(np.max(np.abs(delta-net)/mass[:-1])),
|
|
+ groups={name:dict(incoming=float(i.mean()), outgoing=float(o.mean()), net=float((i-o).mean()))
|
|
+ for name, i, o in (
|
|
+ ('slot', incoming[:, 0, slot].sum(axis=1), outgoing[:, 0, slot].sum(axis=1)),
|
|
+ ('flanks', incoming[:, 0, ~slot].sum(axis=1), outgoing[:, 0, ~slot].sum(axis=1)),
|
|
+ ('top', incoming[:, 1].sum(axis=1), outgoing[:, 1].sum(axis=1)))})
|
|
+ summary['mass'] = dict(start=float(mass[0]), end=float(mass[-1]), delta=float(mass[-1]-mass[0]),
|
|
+ relative_span=float(np.ptp(mass)/mass[0]),
|
|
+ relative_delta=float((mass[-1]-mass[0])/mass[0]))
|
|
+ return summary
|
|
+
|
|
+
|
|
+def main():
|
|
+ parser = argparse.ArgumentParser(description=__doc__)
|
|
+ parser.add_argument('--output-dir', type=Path, required=True)
|
|
+ args = parser.parse_args()
|
|
+ assert Path.cwd() == ROOT == Path('/home/frank14f/CelerisLab')
|
|
+ assert sys.executable == '/home/frank14f/anaconda3/envs/pycuda_3_10/bin/python'
|
|
+ assert Path(CelerisLab.__file__).resolve() == ROOT/'src/CelerisLab/__init__.py'
|
|
+ assert Path(compiler.kernel_path('kernel_v2.cu')).resolve() == ROOT/'src/CelerisLab/lbm/kernels/kernel_v2.cu'
|
|
+ assert os.environ.get('CUDA_VISIBLE_DEVICES') in ('0', '1')
|
|
+ out = args.output_dir.resolve()
|
|
+ assert out == ROOT/'research/j3-lag-2026-10-12'
|
|
+ baseline = json.loads((out/'baseline.json').read_text())
|
|
+ assert all(sha(ROOT/name) == digest for name, digest in baseline['hashes'].items())
|
|
+ for name in ('results.json', 'trajectory.h5', 'start-20760.h5', 'end-21280.h5'):
|
|
+ assert not (out/name).exists(), f'Refusing to overwrite {name}'
|
|
+ original = json.loads((ROOT/'research/j3-reservoir-2026-10-12/results.json').read_text())['cases'][0]
|
|
+ report = dict(python=sys.executable, package=CelerisLab.__file__,
|
|
+ cuda_source=compiler.kernel_path('kernel_v2.cu'), physical_gpu=os.environ['CUDA_VISIBLE_DEVICES'],
|
|
+ script_sha256=sha(__file__), phase=PHASE, velocity_scale=SCALE,
|
|
+ replay_steps=START, planned_diagnostic_steps=LENGTH, diagnostic_steps=0,
|
|
+ j3_acceptance='failed_unchanged')
|
|
+ def save_report(): (out/'results.json').write_text(json.dumps(report, indent=2)+'\n')
|
|
+ with make_sim(.002, nx=48, top_u=0., slot_width=16, top_mode='reservoir') as sim:
|
|
+ history, fields = replay(sim)
|
|
+ report['start_checkpoint'] = save_verified(sim, out/'start-20760.h5')
|
|
+ report['replay_history'] = history
|
|
+ report['replay_matches_j3'] = bool(history == original['history'] and all(
|
|
+ np.array_equal(array, original[name]) for array, name in zip(fields, ('rho', 'u', 'v'))))
|
|
+ save_report()
|
|
+ assert report['replay_matches_j3'], 'Replay differs from J3; saved real checkpoint, stopping'
|
|
+ print('20760 replay and no-advance checkpoint restore passed; matches J3.', flush=True)
|
|
+ try:
|
|
+ report['audit_source_sha256'] = record_trajectory(sim, out/'trajectory.h5', original['population_budget'])
|
|
+ finally:
|
|
+ report['diagnostic_steps'] = sim.stepper.step_count-START
|
|
+ save_report()
|
|
+ report['end_checkpoint'] = save_verified(sim, out/'end-21280.h5')
|
|
+ report.update(summarize(out/'trajectory.h5'))
|
|
+ report['trajectory_sha256'] = sha(out/'trajectory.h5')
|
|
+ save_report()
|
|
+ print(json.dumps({k:report[k] for k in ('lags','signed_end_change','mass','budget')}), flush=True)
|
|
+
|
|
+
|
|
+if __name__ == '__main__':
|
|
+ main()
|