450 lines
22 KiB
Diff
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())
|