Files
CelerisLab/research/j3-reservoir-2026-10-12/incremental.diff
T

450 lines
22 KiB
Diff

--- a/src/CelerisLab/common/checkpoint.py
+++ b/src/CelerisLab/common/checkpoint.py
@@ -220,6 +220,10 @@
raise ValueError(f"Checkpoint on-site boundary mismatch on '{key}'")
if saved_cfg.get("wall_slot_width", 0) != lbm_cfg.wall_slot_width:
raise ValueError("Checkpoint on-site boundary mismatch on 'wall_slot_width'")
+ if saved_cfg.get("top_wall_bc", "velocity") != lbm_cfg.top_wall_bc:
+ raise ValueError("Checkpoint on-site boundary mismatch on 'top_wall_bc'")
+ if lbm_cfg.top_wall_bc == "reservoir" and saved_cfg.get("rho") != lbm_cfg.rho:
+ raise ValueError("Checkpoint reservoir boundary mismatch on 'rho'")
for key in ("lattice_model", "nx", "ny", "nz", "store_precision",
"streaming", "ddf_shifting"):
saved_val = saved_cfg.get(key)
--- a/src/CelerisLab/config.py
+++ b/src/CelerisLab/config.py
@@ -35,6 +35,7 @@
}
OUTLET_MAP = {"neq_extrap": 0, "zero_gradient": 1, "blended": 2}
Y_WALL_BC_MAP = {"bounce_back": 0, "free_slip": 1, "on_site_velocity": 2}
+TOP_WALL_BC_MAP = {"velocity": 0, "reservoir": 1}
DTYPE_MAP = {"FP32": "float", "FP64": "double"}
@@ -88,6 +89,7 @@
y_wall_bc: str = "bounce_back"
wall_normal_velocity: float = 0.0
wall_slot_width: int = 0
+ top_wall_bc: str = "velocity"
omega_min: float = 0.01
omega_max: float = 1.99
@@ -112,6 +114,7 @@
for name, allowed in (
("lattice_model", LATTICE_MODEL_MAP), ("collision", COLLISION_MAP),
("streaming", STREAMING_MAP), ("y_wall_bc", Y_WALL_BC_MAP),
+ ("top_wall_bc", TOP_WALL_BC_MAP),
("data_type", DTYPE_MAP), ("store_precision", PRECISION_MAP),
("inlet_profile", INLET_MAP), ("inlet_scheme", INLET_SCHEME_MAP),
("outlet_mode", OUTLET_MAP),
@@ -157,6 +160,8 @@
raise ValueError("wall_slot_width must be 0 (disabled) or an integer in [2, nx]")
if self.wall_slot_width and (self.y_wall_bc != "on_site_velocity" or self.velocity != 0):
raise ValueError("wall_slot_width requires on_site_velocity and velocity=0")
+ if self.top_wall_bc == "reservoir" and (self.y_wall_bc != "on_site_velocity" or self.velocity != 0):
+ raise ValueError("reservoir requires on_site_velocity and velocity=0")
if self.y_wall_bc == "on_site_velocity":
if (not self.is_d2q9 or self.collision != "SRT"
or self.streaming != "double_buffer" or self.store_precision != "FP32"
@@ -202,12 +207,13 @@
self.validate()
return (self.lattice_model, self.nx, self.ny, self.nz, self.data_type,
self.store_precision, bool(self.ddf_shifting), self.streaming,
- (self.velocity, self.wall_normal_velocity, self.wall_slot_width)
+ (self.velocity, self.wall_normal_velocity, self.wall_slot_width,
+ self.top_wall_bc, self.rho if self.top_wall_bc == "reservoir" else None)
if self.y_wall_bc == "on_site_velocity" else None)
@property
def top_wall_normal_velocity(self) -> float:
- """Uniform top target balancing the bottom's discrete requested volume."""
+ """Volume-balanced top velocity target, or reservoir initialization guess."""
if self.wall_slot_width in (0, self.nx):
return self.wall_normal_velocity
weights = (max(0.0, 1.0-((2*x-(self.nx-1))/self.wall_slot_width)**2)**2
@@ -244,6 +250,7 @@
"WALL_NORMAL_VELOCITY": _float_literal(self.wall_normal_velocity),
"WALL_SLOT_WIDTH": self.wall_slot_width,
"TOP_WALL_NORMAL_VELOCITY": _float_literal(self.top_wall_normal_velocity),
+ "TOP_WALL_BC": TOP_WALL_BC_MAP[self.top_wall_bc],
"OMEGA_COLLISION_MIN": _float_literal(self.omega_min),
"OMEGA_COLLISION_MAX": _float_literal(self.omega_max),
"TRT_MAGIC_PARAM": _float_literal(self.trt_magic_param),
@@ -314,6 +321,7 @@
y_wall_bc=m.get("y_wall_bc", "bounce_back"),
wall_normal_velocity=m.get("wall_normal_velocity", 0.0),
wall_slot_width=m.get("wall_slot_width", 0),
+ top_wall_bc=m.get("top_wall_bc", "velocity"),
omega_min=m["omega_guard"]["min"],
omega_max=m["omega_guard"]["max"],
threads_per_block=c["threads_per_block"],
--- a/src/CelerisLab/configs/CONFIG.md
+++ b/src/CelerisLab/configs/CONFIG.md
@@ -143,3 +143,23 @@
The finite-slot mode is experimental: the bounded W=16, A=±0.002 checks in
`research/j2-slot-2026-10-12/REVIEW.md` exceeded the fixed late-time mass-drift
tolerance. It is not yet validated for steady mass balance or long runs.
+
+### Opt-in top reservoir
+
+Set `method.top_wall_bc="reservoir"` with `on_site_velocity` and
+`physics.velocity=0` to fix the top density to `physics.rho` and tangential
+velocity to zero. The top Cartesian normal velocity is solved locally from
+known poststream populations; positive means outflow, negative means inflow.
+It may vary along x and is **not** constrained to the bottom requested Q/NX.
+The bottom slot/flanks and the supported lattice/collision/storage scope stay
+the same. This changes the physical top boundary condition.
+
+The initializer still uses uniform Q/NX at the top as an initial guess, then
+the density reconstruction applies from the first step. Mode, reservoir density
+and tangential velocity are fixed at construction and checked on restart.
+Missing `top_wall_bc` in older configs/checkpoints means `"velocity"`, which
+remains the default and preserves the previous two-velocity-boundary path.
+This reservoir mode is experimental. The bounded positive-slot test in
+`research/j3-reservoir-2026-10-12/REVIEW.md` passed its mass thresholds but failed
+the frozen full-field velocity convergence criterion; negative/zero cases and
+GPU restart validation were therefore not run. Long-run stability is unverified.
--- a/src/CelerisLab/cuda/compiler_v2.py
+++ b/src/CelerisLab/cuda/compiler_v2.py
@@ -178,6 +178,7 @@
#define WALL_NORMAL_VELOCITY {m['WALL_NORMAL_VELOCITY']}
#define WALL_SLOT_WIDTH {m['WALL_SLOT_WIDTH']}
#define TOP_WALL_NORMAL_VELOCITY {m['TOP_WALL_NORMAL_VELOCITY']}
+#define TOP_WALL_BC {m['TOP_WALL_BC']}
#define OMEGA_COLLISION_MIN {m['OMEGA_COLLISION_MIN']}
#define OMEGA_COLLISION_MAX {m['OMEGA_COLLISION_MAX']}
--- a/src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh
+++ b/src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh
@@ -31,4 +31,14 @@
f[6] = f[5] + 0.5f*(f[1]-f[2]) - 0.5f*rho*u - (1.0f/6.0f)*rho*v;
f[7] = f[8] + 0.5f*(f[2]-f[1]) + 0.5f*rho*u - (1.0f/6.0f)*rho*v;
}
+
+// Fixed top density and tangential u=0; positive Cartesian v exits upward.
+// J_y=rho*v follows from known poststream populations (ordinary D2Q9).
+__device__ __forceinline__ void reconstruct_top_reservoir(float* f, float rho)
+{
+ const float jy = f[0]+f[1]+f[2]+2.0f*(f[3]+f[5]+f[8])-rho;
+ f[4] = f[3] - (2.0f/3.0f)*jy;
+ f[6] = f[5] + 0.5f*(f[1]-f[2]) - (1.0f/6.0f)*jy;
+ f[7] = f[8] + 0.5f*(f[2]-f[1]) - (1.0f/6.0f)*jy;
+}
#endif
--- a/src/CelerisLab/lbm/kernels/step/one_step_double.cu
+++ b/src/CelerisLab/lbm/kernels/step/one_step_double.cu
@@ -92,7 +92,11 @@
// Reconstruct f(t+dt) on the physical plane, then collide below.
// This replaces, rather than compounds, the halfway wall correction.
if (y == 1u) reconstruct_bottom_velocity(f, 0.0f, bottom_wall_normal_velocity(x));
+#if TOP_WALL_BC == 1
+ else reconstruct_top_reservoir(f, RHO);
+#else
else reconstruct_top_velocity(f, U0, TOP_WALL_NORMAL_VELOCITY);
+#endif
#elif Y_WALL_BC == 1
apply_wall_freeslip_d2q9_y_pull(y, f, fi_in, k);
#else
--- /dev/null
+++ b/tests/unit/cpp/reservoir.cpp
@@ -0,0 +1,26 @@
+#include <cstdio>
+#include <initializer_list>
+#define __device__
+#define __forceinline__ inline
+#define WALL_NORMAL_VELOCITY 0.0f
+#include "boundary/planar_velocity.cuh"
+
+int main() {
+ const int cy[9]={0,0,0,1,-1,1,-1,-1,1};
+ const float w[9]={4.f/9,1.f/9,1.f/9,1.f/9,1.f/9,1.f/36,1.f/36,1.f/36,1.f/36};
+ for (float rho : {1.f,1.04f}) for (float v : {-.01f,-.001f,0.f,.001f,.01f})
+ for (int neq : {0,1}) {
+ float f[9];
+ printf("%.9g %.9g %d\n",rho,v,neq);
+ for (int i=0;i<9;++i) {
+ float cu=cy[i]*v;
+ f[i]=w[i]*rho*(1+3*cu+4.5f*cu*cu-1.5f*v*v)+neq*.0001f*(i-4);
+ if (cy[i]==-1) f[i]=-7.f-i;
+ printf("%.9g ",f[i]);
+ }
+ puts("");
+ reconstruct_top_reservoir(f,rho);
+ for (float value : f) printf("%.9g ",value);
+ puts("");
+ }
+}
--- /dev/null
+++ b/tests/unit/test_reservoir.py
@@ -0,0 +1,90 @@
+"""Actual reservoir helper moments and static boundary restart contracts."""
+import json
+from types import SimpleNamespace
+
+import h5py
+import numpy as np
+import pytest
+
+from CelerisLab.config import LBMConfig, load_lbm_config
+from CelerisLab.common.checkpoint import save_checkpoint, load_checkpoint
+from .test_numerical_operators import compile_run, ROOT
+from .test_state_regressions import device, make_field, manager
+
+
+def test_reservoir_moments_and_known_populations(tmp_path):
+ lines = compile_run(tmp_path, 'reservoir.cpp', {})
+ cx = np.array([0,1,-1,0,0,1,-1,1,-1])
+ cy = np.array([0,0,0,1,-1,1,-1,-1,1])
+ weights = np.array([4/9]+[1/9]*4+[1/36]*4)
+ for start in range(0, len(lines), 3):
+ rho, v, neq = np.fromstring(lines[start], sep=' ')
+ before, after = [np.fromstring(s, sep=' ') for s in lines[start+1:start+3]]
+ known = cy >= 0
+ np.testing.assert_array_equal(after[known], before[known])
+ # The normal momentum reference uses ONLY prescribed rho and original known values.
+ jy = before[cy == 0].sum()+2*before[cy == 1].sum()-rho
+ np.testing.assert_allclose([after.sum(), after@cx, after@cy], [rho, 0., jy], atol=3e-7, rtol=0)
+ if not neq:
+ cu = cy*v
+ expected = weights*rho*(1+3*cu+4.5*cu**2-1.5*v*v)
+ np.testing.assert_allclose(after, expected, atol=3e-7, rtol=0)
+ assert abs(after@cy/rho-v) < 3e-7
+
+
+@pytest.mark.parametrize('override', [dict(top_wall_bc='unknown'), dict(y_wall_bc='bounce_back'),
+ dict(velocity=.01), dict(rho=0.)])
+def test_reservoir_invalid_configuration(override):
+ cfg = dict(y_wall_bc='on_site_velocity', top_wall_bc='reservoir', velocity=0.)
+ cfg.update(override)
+ with pytest.raises(ValueError):
+ LBMConfig(**cfg).validate_runtime()
+
+
+def test_reservoir_json_and_static_signature(tmp_path):
+ data = json.loads((ROOT/'src/CelerisLab/configs/config_lbm.json').read_text())
+ data['physics']['velocity'] = 0.
+ data['method'].update(y_wall_bc='on_site_velocity', top_wall_bc='reservoir',
+ collision='SRT', streaming='double_buffer', store_precision='FP32', ddf_shifting=False)
+ data['method']['les']['enabled'] = False
+ path = tmp_path/'config.json'
+ path.write_text(json.dumps(data))
+ cfg = load_lbm_config(path)
+ assert cfg.to_macros()['TOP_WALL_BC'] == 1
+ before = cfg.allocation_signature()
+ cfg.rho *= 1.01
+ assert before != cfg.allocation_signature()
+ cfg.rho /= 1.01
+ cfg.top_wall_bc = 'velocity'
+ assert before != cfg.allocation_signature()
+
+
+@pytest.mark.parametrize('mode, damage, accepted', [
+ ('reservoir', 'mode', False), ('reservoir', 'missing', False),
+ ('reservoir', 'rho', False), ('reservoir', 'u', False),
+ ('reservoir', None, True), ('velocity', 'missing', True), ('velocity', 'mode', False),
+])
+def test_reservoir_checkpoint_contract(device, tmp_path, mode, damage, accepted):
+ field = make_field(device, velocity=0., y_wall_bc='on_site_velocity', top_wall_bc=mode)
+ bodies = manager(field)
+ bodies.sync_to_gpu(field)
+ path = save_checkpoint(field, SimpleNamespace(step_count=3), field.cfg, bodies,
+ str(tmp_path/'reservoir.h5'))
+ with h5py.File(path, 'a') as hf:
+ assert hf.attrs['ckpt_version'] == 3
+ cfg = json.loads(hf.attrs['config_json'])
+ if damage == 'missing': cfg.pop('top_wall_bc')
+ elif damage == 'mode': cfg['top_wall_bc'] = 'velocity' if mode == 'reservoir' else 'reservoir'
+ elif damage == 'rho': cfg['rho'] = 1.01
+ elif damage == 'u': cfg['velocity'] = .01
+ hf.attrs['config_json'] = json.dumps(cfg)
+ writes, before = device.writes, field.ddf.copy()
+ stepper = SimpleNamespace(_step_count=0)
+ if accepted:
+ load_checkpoint(path, field, stepper, field.cfg, bodies)
+ np.testing.assert_array_equal(field.ddf, before)
+ assert stepper._step_count == 3
+ else:
+ with pytest.raises(ValueError, match='boundary mismatch'):
+ load_checkpoint(path, field, stepper, field.cfg, bodies)
+ assert device.writes == writes and stepper._step_count == 0
--- a/tests/validation/run_j1.py
+++ b/tests/validation/run_j1.py
@@ -28,13 +28,14 @@
u_change=2e-5, v_change=2e-7, budget_relative_residual=2e-7)
-def make_sim(v, mode='on_site_velocity', nx=16, top_u=.02, slot_width=0):
+def make_sim(v, mode='on_site_velocity', nx=16, top_u=.02, slot_width=0, top_mode='velocity'):
config = json.loads((ROOT/'src/CelerisLab/configs/config_lbm.json').read_text())
config['grid'].update(nx=nx, ny=35, nz=1, lattice_model='D2Q9')
config['physics'].update(viscosity=.1, velocity=top_u, rho=1.)
config['method'].update(collision='SRT', streaming='double_buffer',
store_precision='FP32', ddf_shifting=False,
- y_wall_bc=mode, wall_normal_velocity=v, wall_slot_width=slot_width)
+ y_wall_bc=mode, wall_normal_velocity=v, wall_slot_width=slot_width,
+ top_wall_bc=top_mode)
config['method']['les']['enabled'] = False
with TemporaryDirectory(prefix='celeris-j1-config-') as folder:
lbm, body = Path(folder)/'lbm.json', Path(folder)/'body.json'
--- a/tests/validation/run_j2_slot.py
+++ b/tests/validation/run_j2_slot.py
@@ -38,12 +38,12 @@
return bool(all(np.array_equal(a, b) for a, b in zip(*snapshots)))
-def restart_check(sim, amplitude):
+def restart_check(sim, amplitude, top_mode='velocity'):
with TemporaryDirectory(prefix='celeris-j2-checkpoint-') as folder:
path = sim.save_checkpoint(str(Path(folder)/'slot.h5'))
sim.run(7)
expected, flags, count = sim.get_ddf().copy(), sim.get_flags().copy(), sim.stepper.step_count
- with make_sim(amplitude, nx=NX, top_u=0., slot_width=WIDTH) as target:
+ with make_sim(amplitude, nx=NX, top_u=0., slot_width=WIDTH, top_mode=top_mode) as target:
target.load_checkpoint(path)
target.run(7)
equal = (np.array_equal(expected, target.get_ddf())
@@ -52,10 +52,10 @@
return bool(equal)
-def run_case(amplitude):
+def run_case(amplitude, top_mode='velocity', check_restart=True):
bottom, top, slot = targets(amplitude)
scale = max(abs(amplitude), .002)
- with make_sim(amplitude, nx=NX, top_u=0., slot_width=WIDTH) as sim:
+ with make_sim(amplitude, nx=NX, top_u=0., slot_width=WIDTH, top_mode=top_mode) as sim:
sim.initialize()
rho, ux, uy = observables(populations(sim))
initial_mass = float(rho.sum())
@@ -90,13 +90,15 @@
symmetry = dict(rho=float(np.max(np.abs(rho-rho[:, ::-1]))),
u=float(np.max(np.abs(ux+ux[:, ::-1]))/scale),
v=float(np.max(np.abs(uy-uy[:, ::-1]))/scale))
- boundary_error = float(max(np.abs(uy[0]-bottom).max(), np.abs(uy[-1]-top).max(),
+ boundary_error = float(max(np.abs(uy[0]-bottom).max(),
+ np.abs(uy[-1]-top).max() if top_mode == 'velocity' else 0.,
np.abs(ux[[0, -1]]).max()))
+ top_density_error = float(np.abs(rho[-1]-sim.lbm_cfg.rho).max()/sim.lbm_cfg.rho)
offset = abs(mass-initial_mass)/initial_mass
speed = float(np.hypot(ux, uy).max())
density_error = float(np.abs(rho-1).max())
nonuniform = bool(uy[8, NX//2]*amplitude > 0 and np.abs(ux).max() > 1e-5) if amplitude else True
- restart = restart_check(sim, amplitude) if amplitude > 0 else None
+ restart = restart_check(sim, amplitude, top_mode) if check_restart and amplitude > 0 else None
budget = population_budget(sim, bottom_slot=slot)
checks = dict(steady=steady, initialization=initial_error <= GATES['boundary_error'],
boundary=boundary_error <= GATES['boundary_error'],
@@ -105,14 +107,25 @@
flux=flux_error <= GATES['flux_error'] and flux_span <= GATES['flux_span'],
symmetry=symmetry['rho'] <= GATES['rho_symmetry'] and
max(symmetry['u'], symmetry['v']) <= GATES['velocity_symmetry'],
- zero=bool(amplitude or speed <= GATES['zero_speed']), nonuniform=nonuniform,
- restart=restart is not False, audit=budget['instrumented_matches_plain'] and
+ audit=budget['instrumented_matches_plain'] and
budget['max_relative_residual'] <= GATES['budget_residual'])
+ if amplitude:
+ checks['nonuniform'] = nonuniform
+ else:
+ checks.pop('flux') # Relative flux error has no nonzero reference here.
+ checks['zero_speed'] = speed <= GATES['zero_speed']
+ if restart is not None:
+ checks['restart'] = restart
+ if top_mode == 'reservoir':
+ checks['top_density'] = top_density_error <= 5e-7
checks = {name: bool(passed) for name, passed in checks.items()}
return dict(amplitude=amplitude, width=WIDTH, nx=NX, height=32, viscosity=.1, top_u=0.,
steps=sim.stepper.step_count-16, max_steps=40*CHUNK, checks=checks, passed=all(checks.values()),
- requested_volume=request_q, top_target=top, bottom_targets=bottom.tolist(),
+ requested_volume=request_q, top_initial_guess=top,
+ top_target=top if top_mode == 'velocity' else None, bottom_targets=bottom.tolist(),
velocity_scale=scale, rho_scale=1., initial_boundary_error=initial_error,
+ top_mode=top_mode, top_density_error=top_density_error,
+ top_velocity_range=[float(uy[-1].min()), float(uy[-1].max())],
boundary_error=boundary_error, mass_offset=offset, mass_window_span=span,
flux_error=flux_error, flux_span=flux_span, section_y=sections,
mass_flux_by_y=actual_mass_flux.tolist(), volume_flux_by_y=actual_volume_flux.tolist(),
--- /dev/null
+++ b/tests/validation/run_j3_reservoir.py
@@ -0,0 +1,81 @@
+"""Sequential reservoir experiment: stop after a failed positive case."""
+import argparse
+import json
+import os
+from pathlib import Path
+import sys
+from unittest.mock import patch
+
+import numpy as np
+import CelerisLab
+from CelerisLab.cuda import compiler_v2 as compiler
+from run_j1 import ROOT, make_sim, off_path_check
+from run_j2_slot import NX, WIDTH, GATES, run_case, restart_check
+
+
+def old_velocity_check(baseline, width, top_u):
+ states = []
+ for source in (baseline, Path(compiler.kernel_path(''))):
+ with patch.object(compiler, '_KERNEL_DIR', str(source)):
+ with make_sim(.002, nx=NX, slot_width=width, top_u=top_u) as sim:
+ sim.initialize()
+ initial = sim.get_ddf().copy()
+ sim.run(64)
+ states.append((initial, sim.get_ddf().copy(), sim.get_flags().copy()))
+ return bool(all(np.array_equal(a, b) for a, b in zip(*states)))
+
+
+def checkpoint_check():
+ with make_sim(.002, nx=NX, top_u=0., slot_width=WIDTH, top_mode='reservoir') as sim:
+ sim.initialize()
+ sim.run(11)
+ return restart_check(sim, .002, top_mode='reservoir')
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--output', type=Path, required=True)
+ parser.add_argument('--baseline-kernels', 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')
+ report = dict(python=sys.executable, package=CelerisLab.__file__,
+ cuda_source=compiler.kernel_path('kernel_v2.cu'), physical_gpu=os.environ['CUDA_VISIBLE_DEVICES'],
+ gates=dict(GATES, top_density_error=5e-7), regressions={}, cases=[],
+ status=dict(positive='not_tested', negative='not_tested', zero='not_tested',
+ checkpoint='not_tested'))
+
+ def save():
+ args.output.write_text(json.dumps(report, indent=2)+'\n')
+
+ for name, result in (
+ ('j1_velocity_top', old_velocity_check(args.baseline_kernels, 0, .02)),
+ ('j2_velocity_top', old_velocity_check(args.baseline_kernels, WIDTH, 0.)),
+ ('default_off', off_path_check(args.baseline_kernels)),
+ ):
+ report['regressions'][name] = 'passed' if result else 'failed'
+ save()
+ print(json.dumps(report['regressions']), flush=True)
+ if 'failed' in report['regressions'].values():
+ return 1
+ for name, amplitude in (('positive', .002), ('negative', -.002), ('zero', 0.)):
+ case = run_case(amplitude, top_mode='reservoir', check_restart=False)
+ report['cases'].append(case)
+ report['status'][name] = 'passed' if case['passed'] else 'failed'
+ save()
+ print(json.dumps({k: case[k] for k in ('amplitude', 'steps', 'checks', 'mass_offset',
+ 'mass_window_span', 'flux_error', 'top_density_error',
+ 'top_velocity_range', 'symmetry')}), flush=True)
+ if not case['passed']:
+ return 1
+ report['status']['checkpoint'] = 'passed' if checkpoint_check() else 'failed'
+ save()
+ print(json.dumps(report['status']), flush=True)
+ return 0 if all(status == 'passed' for status in report['status'].values()) else 1
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())