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

630 lines
29 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--- 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 <cstdio>
+#include <initializer_list>
+#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<NQ;++i) before+=(double)f[i];
+ if(y==1) j1_incoming[x]=(double)f[3]+f[5]+f[8];
+ if(y==NY-2) j1_incoming[NX+x]=(double)f[4]+f[6]+f[7];
+ collide_dispatch(f,rho,ux,uy);
+ double after=0.0;
+ for(int i=0;i<NQ;++i) after+=(double)f[i];
+ j1_collision[y*NX+x]=after-before;
+}
+'''
+
+
+def population_budget(sim):
+ """16 real steps, separate incoming/outgoing/collision records, no targets."""
+ saved = sim.get_ddf().copy()
+ count = sim.stepper.step_count
+ sim.run(16)
+ plain = sim.get_ddf().copy()
+ sim.field.ddf[:] = saved
+ sim.field.upload_ddf()
+ sim.stepper._step_count = count
+ 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().replace('collide_dispatch(f, rho_n, ux, uy);',
+ 'j1_collide_audit(f, rho_n, ux, uy);')
+ marker = '__global__ __launch_bounds__(NT, 2)'
+ assert code.count(marker) == 1
+ source.write_text(code.replace(marker, AUDIT+'\n'+marker))
+ audit_hash = hashlib.sha256(source.read_bytes()).hexdigest()
+ ptx = compiler.compile_kernel(build_dir=build)
+ rows = []
+ with sim.ctx.activate():
+ 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*sim.field.nx, dtype=np.float64)
+ collision = np.empty(sim.field.n, dtype=np.float64)
+ sim.stepper.step_fn = module.get_function('OneStep')
+ for _ in range(16):
+ before = populations(sim)
+ mass = before[:, 1:-1].sum()
+ outgoing_bottom = before[[4,6,7], 1, :].sum()
+ outgoing_top = before[[3,5,8], -2, :].sum()
+ sim.run(1)
+ after = populations(sim)
+ cuda.memcpy_dtoh(incoming, incoming_ptr)
+ cuda.memcpy_dtoh(collision, collision_ptr)
+ delta = after[:, 1:-1].sum()-mass
+ bottom = incoming[:sim.field.nx].sum()-outgoing_bottom
+ top = incoming[sim.field.nx:].sum()-outgoing_top
+ physical_residual = delta-bottom-top
+ residual = physical_residual-collision.sum()
+ rows.append(dict(step=sim.stepper.step_count, mass=mass, delta_mass=delta,
+ bottom_net_in=bottom, top_net_in=top,
+ collision_roundoff=float(collision.sum()),
+ physical_residual=physical_residual,
+ budget_residual=residual))
+ identical = np.array_equal(sim.get_ddf(), plain)
+ return dict(steps=rows, instrumented_matches_plain=identical,
+ audit_source_sha256=audit_hash,
+ max_relative_residual=max(abs(r['budget_residual'])/r['mass'] for r in rows),
+ max_physical_relative_residual=max(abs(r['physical_residual'])/r['mass'] for r in rows))
+ finally:
+ sim.stepper.step_fn = original_step
+ compiler.cleanup_build_dir(build)
+
+
+def run_case(v):
+ height, nu, speed, nx = 32, .1, .02, 16
+ chunk = int(np.ceil(.5*height**2/(np.pi**2*nu)))
+ with make_sim(v) as sim:
+ sim.initialize()
+ # Reject unsupported entrypoints before observing the reference flow.
+ try:
+ sim.add_body('circle', center=(8,16), radius=2)
+ except ValueError:
+ pass
+ else:
+ raise AssertionError('J1 accepted an object')
+ rho, ux, uy = observables(populations(sim))
+ initial_mass = rho.sum()
+ previous_u, previous_v, previous_mass = ux.mean(axis=1), uy.mean(axis=1), initial_mass
+ history = [dict(step=0, mass=float(initial_mass))]
+ steady = False
+ for sample in range(1, 41):
+ sim.run(chunk)
+ f = populations(sim)
+ if not np.isfinite(f).all():
+ raise AssertionError('Nonfinite J1 populations')
+ rho, ux, uy = observables(f)
+ u, normal, mass = ux.mean(axis=1), uy.mean(axis=1), rho.sum()
+ item = dict(step=sim.stepper.step_count, mass=float(mass),
+ u_change=float(np.sqrt(np.mean((u-previous_u)**2))/speed),
+ v_change=float(np.sqrt(np.mean((normal-previous_v)**2))),
+ mass_change=float(abs(mass-previous_mass)/initial_mass))
+ history.append(item)
+ previous_u, previous_v, previous_mass = u.copy(), normal.copy(), mass
+ tail = history[-4:]
+ steady = sample >= 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())