141 lines
7.3 KiB
Python
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
|