Files
CelerisLab/tests/unit/test_numerical_operators.py
T

141 lines
7.3 KiB
Python

"""Actual CUDA operators compiled with a CPU shim; no GPU/import side effects."""
from pathlib import Path
import shutil
import subprocess
import numpy as np
import pytest
ROOT = Path(__file__).resolve().parents[2]
KERNELS = ROOT / "src/CelerisLab/lbm/kernels"
CPP = Path(__file__).with_name("cpp")
def compile_run(tmp_path, source, defines):
if not shutil.which("g++"):
pytest.skip("g++ required for actual-header CPU regression")
exe = tmp_path / "operator"
command = ["g++", "-std=c++17", "-O0", f"-I{KERNELS}"]
command += [f"-D{k}={v}" for k, v in defines.items()]
subprocess.run(command + [str(CPP / source), "-o", str(exe)], check=True, capture_output=True, text=True)
return subprocess.check_output([str(exe)], text=True).splitlines()
@pytest.mark.parametrize("dim", [2, 3])
@pytest.mark.parametrize("shift", [0, 1])
@pytest.mark.parametrize("model", [0, 1, 2])
def test_collision_moments_and_inlet_closure(tmp_path, dim, shift, model):
lines = compile_run(tmp_path, "collision_regression.cpp", dict(
DIM=dim, LATTICE_MODEL=dim-2, USE_DDF_SHIFTING=shift,
COLLISION_MODEL=model, NY=8, U0=".03f"))
cx = np.array([0,1,-1,0,0,1,-1,1,-1] if dim == 2 else
[0,1,-1,0,0,0,0,1,-1,1,-1,0,0,1,-1,1,-1,0,0])
cy = np.array([0,0,0,1,-1,1,-1,-1,1] if dim == 2 else
[0,0,0,1,-1,0,0,1,-1,0,0,1,-1,-1,1,0,0,1,-1])
cz = np.zeros(9) if dim == 2 else np.array([0,0,0,0,0,1,-1,0,0,1,-1,1,-1,0,0,-1,1,-1,1])
weights = np.array([4/9]+[1/9]*4+[1/36]*4 if dim == 2 else [1/3]+[1/18]*6+[1/36]*12)
# Independent D2 moment matrix (paired direction ordering).
M = np.array([[1]*9,[-4,-1,-1,-1,-1,2,2,2,2],
[4,-2,-2,-2,-2,1,1,1,1],[0,1,-1,0,0,1,-1,1,-1],
[0,-2,2,0,0,1,-1,1,-1],[0,0,0,1,-1,1,-1,-1,1],
[0,0,0,-2,2,1,-1,-1,1],[0,1,1,-1,-1,0,0,0,0],
[0,0,0,0,0,1,1,-1,-1]], dtype=float)
for a, omega in enumerate([.7,1.25,1.8]):
before, eq, source, after = [np.fromstring(s, sep=" ") for s in lines[4*a:4*a+4]]
delta = after-before
assert abs(delta.sum()) < 5e-7
np.testing.assert_allclose([delta@cx,delta@cy], [.001,-.0007], atol=1.5e-7, rtol=0)
if dim == 3:
assert abs(delta@cz-.0003) < 1.5e-7
if model == 0:
expected=before-omega*(before-eq)+(1-omega/2)*source
elif model == 1:
opp=np.array([0]+[i+1 if i%2 else i-1 for i in range(1,len(before))])
wm=1/(.1875/(1/omega-.5)+.5)
delta_s=(before-eq+(before-eq)[opp])/2
delta_a=(before-eq-(before-eq)[opp])/2
expected=before-omega*delta_s-wm*delta_a+(1-omega/2)*(source+source[opp])/2+(1-wm/2)*(source-source[opp])/2
elif dim == 2:
rates=np.array([0,1.2,1.2,0,1.2,0,1.2,omega,omega])
expected=before+np.linalg.solve(M,-rates*(M@(before-eq))+(1-rates/2)*(M@source))
else:
expected=None # D3 projection MRT: conservation/equilibrium only.
if expected is not None:
np.testing.assert_allclose(after,expected,atol=2e-7,rtol=0)
assert float(lines[12].split()[1]) == pytest.approx(1.17, abs=3e-7)
velocities=np.stack([cx,cy,cz],axis=1)
u=np.array([.03,-.02,.01 if dim==3 else 0])
cu=velocities@u
eq=weights*1.17*(1+3*cu+4.5*cu**2-1.5*(u@u))-shift*weights
np.testing.assert_allclose(np.fromstring(lines[13],sep=" "),eq,atol=2e-7,rtol=0)
def test_shifted_mrt_weak_force_full_vector(tmp_path):
"""Small populations must retain their precision through MRT relaxation."""
lines = compile_run(tmp_path, "collision_regression.cpp", dict(
DIM=2, LATTICE_MODEL=0, USE_DDF_SHIFTING=1, COLLISION_MODEL=2,
MRT_WEAK_FORCE_CHECK=1, NY=8, U0=".03f"))
M = np.array([[1]*9,[-4,-1,-1,-1,-1,2,2,2,2],
[4,-2,-2,-2,-2,1,1,1,1],[0,1,-1,0,0,1,-1,1,-1],
[0,-2,2,0,0,1,-1,1,-1],[0,0,0,1,-1,1,-1,-1,1],
[0,0,0,-2,2,1,-1,-1,1],[0,1,1,-1,-1,0,0,0,0],
[0,0,0,0,0,1,1,-1,-1]], dtype=float)
weights = np.array([4/9]+[1/9]*4+[1/36]*4)
np.testing.assert_allclose(M @ weights, [1,-2,1,0,0,0,0,0,0], atol=3e-16)
for start in range(0, len(lines), 4):
# Nine printed digits round-trip FP32; recover those exact binary inputs
# before doing FP64 arithmetic (especially rho-1 near unit density).
omega, rho, ux, uy, fx, fy = np.fromstring(lines[start], sep=" ", dtype=np.float32).astype(float)
before, source, after = [np.fromstring(s, sep=" ", dtype=np.float32).astype(float)
for s in lines[start+1:start+4]]
# Independent LL equilibrium moments, with the exact background removed.
drho, u2 = rho-1, ux*ux+uy*uy
meq = np.array([drho, -2*drho+3*rho*u2, drho-3*rho*u2,
rho*ux, -rho*ux, rho*uy, -rho*uy,
rho*(ux*ux-uy*uy), rho*ux*uy])
rates = np.array([0, np.float32(1.2), np.float32(1.2), 0,
np.float32(1.2), 0, np.float32(1.2), omega, omega])
expected = before + np.linalg.solve(
M, rates*(meq-M@before)+(1-rates/2)*(M@source))
# FP32 roundoff is relative to the stored small populations, not to w.
budget = 8*np.finfo(np.float32).eps * max(abs(before).max(), abs(source).max()) + 1e-13
np.testing.assert_allclose(after, expected, atol=budget, rtol=0)
np.testing.assert_allclose((after-before) @ M[[0,3,5]].T,
[0,fx,fy], atol=4*budget, rtol=0)
if not before.any() and fx == 0 and fy == 0:
np.testing.assert_array_equal(after, np.zeros(9))
@pytest.mark.parametrize("shift", [0, 1])
@pytest.mark.parametrize("wall", [0, 1])
@pytest.mark.parametrize("model", [0, 1, 2])
def test_actual_semantic_and_auxiliary_kernels(tmp_path, shift, wall, model):
lines = compile_run(tmp_path, "semantic_regression.cpp", dict(USE_DDF_SHIFTING=shift,Y_WALL_BC=wall,COLLISION_MODEL=model))
assert "passed" in lines[-1]
@pytest.mark.parametrize("model", [0, 1, 2])
@pytest.mark.parametrize("shift", [0, 1])
@pytest.mark.parametrize("storage", [0, 1])
def test_d2_entrypoint_compiles_in_isolation(tmp_path, model, shift, storage):
"""Both streaming entrypoints compile; generated repository headers untouched."""
import re
nvcc = shutil.which("nvcc") or "/usr/local/cuda/bin/nvcc"
if not Path(nvcc).is_file():
pytest.skip("CUDA toolkit required for compile-only regression")
copy = tmp_path / "kernels"
shutil.copytree(KERNELS, copy)
for file, defines in [
("config_grid.h", {"LATTICE_MODEL": 0, "NX": 16, "NY": 8, "NZ": 1}),
("config_method.h", {"COLLISION_MODEL": model, "USE_DDF_SHIFTING": shift,
"STORE_PRECISION": storage, "Y_WALL_BC": shift})
]:
path=copy / "config" / file
source=path.read_text()
for key,value in defines.items():
source=re.sub(r"(#define\s+"+key+r"\s+)\S+",lambda match: match[1]+str(value),source)
path.write_text(source)
result=subprocess.run([nvcc,"-ptx","-arch=sm_70","-std=c++17",
str(copy/"kernel_v2.cu"),"-o",str(tmp_path/"kernel.ptx")],
capture_output=True,text=True)
assert result.returncode == 0, result.stderr