--- a/src/CelerisLab/common/checkpoint.py +++ b/src/CelerisLab/common/checkpoint.py @@ -214,6 +214,10 @@ # Config compatibility check saved_cfg = json.loads(hf.attrs["config_json"]) + if "on_site_velocity" in (saved_cfg.get("y_wall_bc"), lbm_cfg.y_wall_bc): + for key in ("y_wall_bc", "velocity", "wall_normal_velocity"): + if saved_cfg.get(key) != getattr(lbm_cfg, key): + raise ValueError(f"Checkpoint on-site boundary mismatch on '{key}'") for key in ("lattice_model", "nx", "ny", "nz", "store_precision", "streaming", "ddf_shifting"): saved_val = saved_cfg.get(key) @@ -279,6 +283,9 @@ if key in raw_params} if not all(np.isfinite(value) for value in params.values()): raise ValueError("Checkpoint runtime parameters must be finite") + if lbm_cfg.y_wall_bc == "on_site_velocity" and ( + bodies.count or any(params.get(k, 0.0) != 0 for k in ("fx", "fy", "fz"))): + raise ValueError("on_site_velocity does not support body forces or objects") obs_accum_steps = int(hf.attrs.get("obs_accum_steps", 0)) if obs_accum_steps < 0: raise ValueError("Checkpoint obs_accum_steps must be nonnegative") --- a/src/CelerisLab/config.py +++ b/src/CelerisLab/config.py @@ -34,7 +34,7 @@ "regularized": 3, } OUTLET_MAP = {"neq_extrap": 0, "zero_gradient": 1, "blended": 2} -Y_WALL_BC_MAP = {"bounce_back": 0, "free_slip": 1} +Y_WALL_BC_MAP = {"bounce_back": 0, "free_slip": 1, "on_site_velocity": 2} DTYPE_MAP = {"FP32": "float", "FP64": "double"} @@ -86,6 +86,7 @@ inlet_regularized_neq_damp: float = 0.5 outlet_srt_neq_damp: float = 0.5 y_wall_bc: str = "bounce_back" + wall_normal_velocity: float = 0.0 omega_min: float = 0.01 omega_max: float = 1.99 @@ -127,7 +128,7 @@ for name in ("viscosity", "velocity", "rho", "les_cs", "trt_magic_param", "outlet_blend_alpha", "inlet_trt_neq_damp", "inlet_regularized_neq_damp", "outlet_srt_neq_damp", - "omega_min", "omega_max"): + "omega_min", "omega_max", "wall_normal_velocity"): if not math.isfinite(getattr(self, name)): raise ValueError(f"{name} must be finite.") if self.viscosity <= 0 or self.rho <= 0: @@ -150,6 +151,13 @@ ) if self.dim == 2 and self.nz != 1: raise ValueError("nz must be 1 for 2D") + 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" + or self.ddf_shifting or self.les_enabled): + raise ValueError("on_site_velocity requires D2Q9/SRT/FP32/unshifted/double_buffer without LES") + if abs(self.wall_normal_velocity) >= 1: + raise ValueError("on_site_velocity requires abs(wall_normal_velocity) < 1") if not (0.0 <= self.inlet_trt_neq_damp <= 1.0): raise ValueError("method.inlet.trt_neq_damp must lie in [0, 1].") if not (0.0 <= self.inlet_regularized_neq_damp <= 1.0): @@ -187,7 +195,9 @@ """Parameters that cannot change while field allocations/state are retained.""" 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.store_precision, bool(self.ddf_shifting), self.streaming, + (self.velocity, self.wall_normal_velocity) + if self.y_wall_bc == "on_site_velocity" else None) def to_macros(self) -> Dict[str, Any]: """Return flat dict of macro_name → value for header generation.""" @@ -216,6 +226,7 @@ "OUTLET_BLEND_ALPHA": _float_literal(self.outlet_blend_alpha), "OUTLET_BACKFLOW_CLAMP": int(self.outlet_backflow_clamp), "Y_WALL_BC": Y_WALL_BC_MAP[self.y_wall_bc], + "WALL_NORMAL_VELOCITY": _float_literal(self.wall_normal_velocity), "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), @@ -284,6 +295,7 @@ inlet_regularized_neq_damp=m["inlet"].get("regularized_neq_damp", 0.5), outlet_srt_neq_damp=m["outlet"].get("srt_neq_damp", 0.5), y_wall_bc=m.get("y_wall_bc", "bounce_back"), + wall_normal_velocity=m.get("wall_normal_velocity", 0.0), 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 @@ -101,3 +101,18 @@ D2Q9/D3Q19 的 flag 扩展位 `0x0800` 定义为 `FRC_REGION`,用于标记 force_region 类型物体的格子。 - ForceRegionKernel 不依赖 flag 位执行,flag 只用于后处理可视化。 - `merge_flag_masks` 会正确合并 force_region 的 `FLUID | FRC_REGION` 标记。 +# J1 on-site velocity planes (limited reference case) + +Set `method.y_wall_bc` to `"on_site_velocity"` and +`method.wall_normal_velocity` to V (default 0). This selects an x-periodic +domain with physical planes through fluid rows y=1 and y=NY−2, H=NY−3. +The bottom velocity is (0,V); the top is (`physics.velocity`,V). +These are constant **Cartesian** velocities: positive V injects through the +bottom and leaves through the top. The selected rows do not also receive +halfway reflection. Initialization uses a linear tangential profile and V. + +Supported only for D2Q9/SRT/FP32/unshifted/double_buffer without LES, objects +or body forces. Boundary targets and the mode are fixed at construction; +checkpoint restart requires matching targets. This is an entire-plane +Couette-throughflow reference, not a finite slot or curved jet API. The default +`bounce_back` mode and existing configurations retain their previous path. --- a/src/CelerisLab/cuda/compiler_v2.py +++ b/src/CelerisLab/cuda/compiler_v2.py @@ -65,6 +65,8 @@ macros = cfg.to_macros() if isinstance(n_objects, bool) or not isinstance(n_objects, int) or n_objects < 0: raise ValueError("n_objects must be a nonnegative integer") + if cfg.y_wall_bc == "on_site_velocity" and n_objects: + raise ValueError("on_site_velocity currently requires an object-free domain") build_dir = tempfile.mkdtemp(prefix="celeris-build-", dir=build_root) try: shutil.copytree(_KERNEL_DIR, build_dir, dirs_exist_ok=True, @@ -173,6 +175,7 @@ #define OUTLET_BLEND_ALPHA {m['OUTLET_BLEND_ALPHA']} #define OUTLET_BACKFLOW_CLAMP {m['OUTLET_BACKFLOW_CLAMP']} #define Y_WALL_BC {m['Y_WALL_BC']} +#define WALL_NORMAL_VELOCITY {m['WALL_NORMAL_VELOCITY']} #define OMEGA_COLLISION_MIN {m['OMEGA_COLLISION_MIN']} #define OMEGA_COLLISION_MAX {m['OMEGA_COLLISION_MAX']} --- a/src/CelerisLab/lbm/field.py +++ b/src/CelerisLab/lbm/field.py @@ -123,6 +123,8 @@ if self.cfg.is_d2q9: flag[0:self.nx] = SOLID | BC_WALL flag[(self.ny - 1) * self.nx:self.ny * self.nx] = SOLID | BC_WALL + if self.cfg.y_wall_bc == "on_site_velocity": + return flag # x periodic; physical y planes are the adjacent fluid rows. flag[::self.nx] = SOLID | BC_INLET flag[self.nx - 1::self.nx] = SOLID | BC_OUTLET flag[0] = SOLID | BC_WALL @@ -231,6 +233,10 @@ Accepted keys: omega, fx, fy, fz, n_objects. """ + if self.cfg.y_wall_bc == "on_site_velocity" and ( + any(float(kwargs.get(k, 0.0)) != 0 for k in ("fx", "fy", "fz")) + or int(kwargs.get("n_objects", 0)) != 0): + raise ValueError("on_site_velocity does not support body forces or objects") self._runtime_params.update({ "omega": float(kwargs.get("omega", self._runtime_params["omega"])), "fx": float(kwargs.get("fx", self._runtime_params["fx"])), --- /dev/null +++ b/src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh @@ -0,0 +1,22 @@ +// On-site D2Q9 Zou-He velocity closure, paired lattice ordering. +// Input is poststream f at the arrival time. Only unknown populations change; +// the caller owns collision/store. No force, shifting or off-lattice walls. +#ifndef CELERIS_BOUNDARY_PLANAR_VELOCITY_CUH +#define CELERIS_BOUNDARY_PLANAR_VELOCITY_CUH + +__device__ __forceinline__ void reconstruct_bottom_velocity(float* f, float u, float v) +{ + const float rho = (f[0] + f[1] + f[2] + 2.0f*(f[4] + f[6] + f[7])) / (1.0f-v); + f[3] = f[4] + (2.0f/3.0f)*rho*v; + f[5] = f[6] + 0.5f*(f[2]-f[1]) + 0.5f*rho*u + (1.0f/6.0f)*rho*v; + f[8] = f[7] + 0.5f*(f[1]-f[2]) - 0.5f*rho*u + (1.0f/6.0f)*rho*v; +} + +__device__ __forceinline__ void reconstruct_top_velocity(float* f, float u, float v) +{ + const float rho = (f[0] + f[1] + f[2] + 2.0f*(f[3] + f[5] + f[8])) / (1.0f+v); + f[4] = f[3] - (2.0f/3.0f)*rho*v; + 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; +} +#endif --- a/src/CelerisLab/lbm/kernels/kernel_v2.cu +++ b/src/CelerisLab/lbm/kernels/kernel_v2.cu @@ -56,6 +56,9 @@ #include "boundary/bounce_back.cuh" #include "boundary/curved_boundary.cuh" #include "boundary/inlet_outlet.cuh" +#if Y_WALL_BC == 2 +#include "boundary/planar_velocity.cuh" +#endif #include "boundary/ibm_kernels.cuh" // ============================================================================ --- a/src/CelerisLab/lbm/kernels/step/init_flow.cu +++ b/src/CelerisLab/lbm/kernels/step/init_flow.cu @@ -49,10 +49,12 @@ { if (y == 0u || y == (unsigned int)(NY - 1)) return (uint16_t)(FLAG_SOLID | FLAG_BC_WALL); +#if Y_WALL_BC != 2 if (x == 0u) return (uint16_t)(FLAG_SOLID | FLAG_BC_INLET); if (x == (unsigned int)(NX - 1)) return (uint16_t)(FLAG_SOLID | FLAG_BC_OUTLET); +#endif return FLAG_FLUID; } @@ -85,7 +87,15 @@ if (is_solid(fl)) { write_rest_equilibrium(fi, k); } else { +#if Y_WALL_BC == 2 + // Initial g(0)=feq: both boundary velocities are already satisfied. + // Start from linear Couette, not the sought exponential steady profile. + float f[NQ]; + compute_feq(RHO, U0*(float)(y-1u)/(float)(NY-3), WALL_NORMAL_VELOCITY, f); + stream_pull_store(k, f, fi); +#else write_equilibrium(fi, k, inlet_target_u((float)y)); +#endif } #elif DIM == 3 unsigned int x, y, z; unsigned long k; @@ -138,4 +148,4 @@ #endif } -#endif // CELERIS_STEP_INIT_FLOW_CU \ No newline at end of file +#endif // CELERIS_STEP_INIT_FLOW_CU --- a/src/CelerisLab/lbm/kernels/step/one_step_double.cu +++ b/src/CelerisLab/lbm/kernels/step/one_step_double.cu @@ -88,7 +88,12 @@ bounce_back_swap(f); if (is_fluid(fl) && (y == 1u || y == (unsigned int)(NY - 2))) { -#if Y_WALL_BC == 1 +#if Y_WALL_BC == 2 + // 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, WALL_NORMAL_VELOCITY); + else reconstruct_top_velocity(f, U0, WALL_NORMAL_VELOCITY); +#elif Y_WALL_BC == 1 apply_wall_freeslip_d2q9_y_pull(y, f, fi_in, k); #else apply_wall_bb_d2q9_y_pull(y, f, fi_in, k); --- a/src/CelerisLab/simulation.py +++ b/src/CelerisLab/simulation.py @@ -115,6 +115,8 @@ Returns: int body_id (formal id before initialize; -1 if staged). """ + if self.lbm_cfg.y_wall_bc == "on_site_velocity": + raise ValueError("on_site_velocity currently requires an object-free domain") type_lower = str(type).strip().lower() if type_lower == "circle": --- /dev/null +++ b/tests/unit/cpp/planar_velocity.cpp @@ -0,0 +1,30 @@ +#include +#include +#define __device__ +#define __forceinline__ inline +#include "boundary/planar_velocity.cuh" + +int main() { + const int cx[9]={0,1,-1,0,0,1,-1,1,-1}; + 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 (int top : {0,1}) for (float rho : {1.f,1.04f}) + for (float u : {-.02f,0.f,.02f}) for (float v : {-.01f,-.001f,0.f,.001f,.01f}) + for (int neq : {0,1}) { + float f[9]; + printf("%d %.9g %.9g %.9g %d\n",top,rho,u,v,neq); + for (int i=0;i<9;++i) { + float cu=cx[i]*u+cy[i]*v; + f[i]=w[i]*rho*(1+3*cu+4.5f*cu*cu-1.5f*(u*u+v*v)); + f[i]+=neq*.0001f*(i-4); + // Unknown inputs must be completely overwritten, never consumed. + if (cy[i]==(top?-1:1)) f[i]=-7.f-i; + printf("%.9g ",f[i]); + } + puts(""); + if (top) reconstruct_top_velocity(f,u,v); + else reconstruct_bottom_velocity(f,u,v); + for (float value : f) printf("%.9g ",value); + puts(""); + } +} --- /dev/null +++ b/tests/unit/test_planar_velocity.py @@ -0,0 +1,95 @@ +"""J1 actual-header moments and bounded host capability contracts.""" +import json +from pathlib import Path +from types import SimpleNamespace + +import h5py +import numpy as np +import pytest + +import CelerisLab +from CelerisLab.config import LBMConfig, load_lbm_config +from CelerisLab.cuda import compiler_v2 as compiler +from CelerisLab.common.checkpoint import save_checkpoint, load_checkpoint +from .test_numerical_operators import compile_run +from .test_state_regressions import device, make_field, manager + + +def test_planar_helper_moments_and_known_populations(tmp_path): + root = Path(__file__).resolve().parents[2] + 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' + lines = compile_run(tmp_path, 'planar_velocity.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]) + w = np.array([4/9]+[1/9]*4+[1/36]*4) + for start in range(0, len(lines), 3): + top, rho, u, v, neq = np.fromstring(lines[start], sep=' ') + before, after = [np.fromstring(s, sep=' ') for s in lines[start+1:start+3]] + known = cy != (-1 if top else 1) + np.testing.assert_array_equal(after[known], before[known]) + density = after.sum() + np.testing.assert_allclose([after@cx, after@cy], density*np.array([u,v]), atol=3e-7, rtol=0) + if not neq: + cu = cx*u+cy*v + expected = w*rho*(1+3*cu+4.5*cu**2-1.5*(u*u+v*v)) + np.testing.assert_allclose(after, expected, atol=3e-7, rtol=0) + assert abs(density-rho) <= 3e-7 + + +@pytest.mark.parametrize('override', [ + dict(collision='TRT'), dict(collision='MRT'), dict(streaming='esopull'), + dict(store_precision='FP16S'), dict(ddf_shifting=True), dict(les_enabled=True), + dict(lattice_model='D3Q19'), dict(wall_normal_velocity=1.), + dict(wall_normal_velocity=float('nan')), +]) +def test_planar_unsupported_configuration(override): + with pytest.raises(ValueError): + LBMConfig(y_wall_bc='on_site_velocity', **override).validate_runtime() + + +def test_planar_config_roundtrip_and_fixed_targets(tmp_path): + path = Path(__file__).resolve().parents[2]/'src/CelerisLab/configs/config_lbm.json' + data = json.loads(path.read_text()) + data['method'].update(y_wall_bc='on_site_velocity', wall_normal_velocity=-.01, + 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(str(path)) + assert float(cfg.to_macros()['WALL_NORMAL_VELOCITY'][:-1]) == -.01 + with pytest.raises(ValueError, match='object-free'): + compiler.generate_config(cfg, n_objects=1, build_root=tmp_path) + before = cfg.allocation_signature() + cfg.wall_normal_velocity = .01 + assert cfg.allocation_signature() != before + + +def test_planar_force_rejected_before_state_change(device): + field = make_field(device, y_wall_bc='on_site_velocity') + before, writes = field._runtime_params.copy(), device.writes + with pytest.raises(ValueError, match='body forces'): + field.update_params(fx=.001) + assert field._runtime_params == before and device.writes == writes + + +@pytest.mark.parametrize('damage', ['target', 'mode', 'force']) +def test_planar_checkpoint_contract_before_writes(device, tmp_path, damage): + field = make_field(device, y_wall_bc='on_site_velocity') + bodies = manager(field) + bodies.sync_to_gpu(field) + path = save_checkpoint(field, SimpleNamespace(step_count=3), field.cfg, bodies, + str(tmp_path/'plane.h5')) + with h5py.File(path, 'a') as hf: + if damage == 'force': + hf.attrs['runtime_params_json'] = '{"fx":0.001}' + else: + cfg = json.loads(hf.attrs['config_json']) + cfg['wall_normal_velocity' if damage == 'target' else 'y_wall_bc'] = ( + .01 if damage == 'target' else 'bounce_back') + hf.attrs['config_json'] = json.dumps(cfg) + writes = device.writes + with pytest.raises(ValueError): + load_checkpoint(path, field, SimpleNamespace(_step_count=0), field.cfg, bodies) + assert device.writes == writes --- /dev/null +++ b/tests/validation/run_j1.py @@ -0,0 +1,239 @@ +"""Bounded J1 Couette-throughflow validation; gates fixed before first run. + +See research/j1-2026-10-12/REVIEW.md. No production audit kernel or extra launch +is added: a temporary test build records actual incoming populations and the +roundoff of the real collision, then is compared with uninstrumented stepping. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import sys +from tempfile import TemporaryDirectory +from unittest.mock import patch + +import numpy as np +import pycuda.driver as cuda + +import CelerisLab +from CelerisLab.cuda import compiler_v2 as compiler +from CelerisLab.simulation import Simulation + +ROOT = Path(__file__).resolve().parents[2] +CX = np.array([0,1,-1,0,0,1,-1,1,-1]) +CY = np.array([0,0,0,1,-1,1,-1,-1,1]) +GATES = dict(u_relative_l2=1e-3, v_max_error=2e-6, flux_relative_error=1e-3, + mass_offset=1e-3, mass_window_span=6e-6, mass_chunk_change=2e-6, + u_change=2e-5, v_change=2e-7, budget_relative_residual=2e-7) + + +def make_sim(v, mode='on_site_velocity'): + config = json.loads((ROOT/'src/CelerisLab/configs/config_lbm.json').read_text()) + config['grid'].update(nx=16, ny=35, nz=1, lattice_model='D2Q9') + config['physics'].update(viscosity=.1, velocity=.02, rho=1.) + config['method'].update(collision='SRT', streaming='double_buffer', + store_precision='FP32', ddf_shifting=False, + y_wall_bc=mode, wall_normal_velocity=v) + config['method']['les']['enabled'] = False + with TemporaryDirectory(prefix='celeris-j1-config-') as folder: + lbm, body = Path(folder)/'lbm.json', Path(folder)/'body.json' + lbm.write_text(json.dumps(config)) + body.write_text('{"objects":[]}') + return Simulation(str(lbm), str(body), device_id=0) + + +def populations(sim): + return sim.get_ddf().reshape(9, sim.field.ny, sim.field.nx).astype(np.float64) + + +def observables(f): + interior = f[:, 1:-1, :] + rho = interior.sum(axis=0) + ux = np.einsum('q,qyx->yx', CX, interior)/rho + uy = np.einsum('q,qyx->yx', CY, interior)/rho + return rho, ux, uy + + +AUDIT = r''' +__device__ double j1_incoming[2*NX]; +__device__ double j1_collision[NX*NY]; +__device__ void j1_collide_audit(float* f, float rho, float ux, float uy) { + const unsigned x=threadIdx.x+blockIdx.x*blockDim.x, y=blockIdx.y; + double before=0.0; + for(int i=0;i= 8 and all( + r['u_change'] <= GATES['u_change'] and r['v_change'] <= GATES['v_change'] + and r['mass_change'] <= GATES['mass_chunk_change'] for r in tail) + steady = steady and np.ptp([r['mass'] for r in history[-5:]])/initial_mass <= GATES['mass_window_span'] + if steady: + break + y = np.arange(height+1) + exact = speed*y/height if v == 0 else speed*np.expm1(v*y/nu)/np.expm1(v*height/nu) + relative_l2 = float(np.linalg.norm(u[1:-1]-exact[1:-1])/np.linalg.norm(exact[1:-1])) + v_error = float(np.max(np.abs(uy-v))) + sections = [8,16,24] # Distances above physical bottom; none is a BC row. + flux = [float(np.sum(rho[s]*uy[s])) for s in sections] + target_flux = float(rho.mean()*v*nx) + flux_error = max(abs(q-target_flux)/abs(target_flux) for q in flux) if v else max(abs(q)/nx for q in flux) + mass_span = float(np.ptp([r['mass'] for r in history[-5:]])/initial_mass) + offset = float(abs(mass-initial_mass)/initial_mass) + result = dict(v=v, height=height, nx=nx, nu=nu, top_u=speed, + pe=v*height/nu, re=speed*height/nu, + ma=float(np.hypot(speed,v)*np.sqrt(3)), + steps=sim.stepper.step_count, max_steps=40*chunk, steady=bool(steady), + relative_l2=relative_l2, v_max_error=v_error, + mass_offset=offset, mass_window_span=mass_span, + rho_range=[float(rho.min()),float(rho.max())], + section_y=sections, measured_flux=flux, reference_flux=target_flux, + flux_error=flux_error, profile_u=u.tolist(), profile_v=normal.tolist(), + analytic_u=exact.tolist(), history=history) + budget = population_budget(sim) + result['population_budget'] = budget + result['passed'] = bool(steady and relative_l2 <= GATES['u_relative_l2'] + and v_error <= GATES['v_max_error'] + and flux_error <= (GATES['flux_relative_error'] if v else GATES['v_max_error']) + and offset <= GATES['mass_offset'] and mass_span <= GATES['mass_window_span'] + and budget['instrumented_matches_plain'] + and budget['max_relative_residual'] <= GATES['budget_relative_residual']) + return result + + +def off_path_check(baseline): + outcomes = [] + for kernel_root in (baseline, Path(compiler.kernel_path(''))): + with patch.object(compiler, '_KERNEL_DIR', str(kernel_root)): + with make_sim(0., mode='bounce_back') as sim: + sim.initialize() + sim.run(7) + outcomes.append((sim.get_ddf().copy(), sim.get_flags().copy())) + return bool(all(np.array_equal(a,b) for a,b in zip(*outcomes))) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--baseline-kernels', type=Path, + help='Optional pre-change CUDA source for the off-mode comparison') + args = parser.parse_args() + assert Path.cwd() == ROOT == Path('/home/frank14f/CelerisLab') + 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=GATES, cases=[]) + report['off_path_bitwise_equal'] = (off_path_check(args.baseline_kernels) + if args.baseline_kernels else None) + for v in (0., .001, -.001, .01, -.01): + case = run_case(v) + report['cases'].append(case) + args.output.write_text(json.dumps(report, indent=2)+'\n') + print(json.dumps({k:case[k] for k in ('v','steps','steady','relative_l2','v_max_error', + 'mass_offset','mass_window_span','flux_error','passed')}), flush=True) + return 0 if report['off_path_bitwise_equal'] is not False and all(c['passed'] for c in report['cases']) else 1 + + +if __name__ == '__main__': + raise SystemExit(main())