Files
CelerisLab/tests/integration/test_numerical_gpu.py

220 lines
9.2 KiB
Python

"""Bounded GPU invariants. No high-Re/benchmark-performance claim.
Compare spatial postcollision populations, not DB macro fields against EsoPull's
next-pull diagnostics. Closed-channel evolution starts at completed count 1 with
an explicitly identical spatial state. The distinct t=0 init contract is tested
separately through the actual initialization and wall staging kernels.
"""
import numpy as np
import pytest
import pycuda.driver as cuda
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY, FLUID, SOLID, BC_WALL
from ._cases import make_sim
CX = np.asarray(D2Q9_EX)
CY = np.asarray(D2Q9_EY)
W = np.asarray([4/9]+[1/9]*4+[1/36]*4, dtype=np.float32)
def equilibrium(rho, ux, uy):
cu = CX[:, None, None]*ux + CY[:, None, None]*uy
return (W[:, None, None]*rho*(1+3*cu+4.5*cu*cu-1.5*(ux*ux+uy*uy))).astype(np.float32)
def postcollision_to_backing(values, completed_count):
"""Independent spatial encoder for the previous advancing-store contract."""
previous = completed_count-1
result = np.empty_like(values)
result[0] = values[0]
for i in range(1,9,2):
result[i+1 if previous % 2 else i] = np.roll(values[i], (CY[i],CX[i]), (0,1))
result[i if previous % 2 else i+1] = values[i+1]
return result
def backing_to_postcollision(values, completed_count):
previous = completed_count-1
result = np.empty_like(values)
result[0] = values[0]
for i in range(1,9,2):
result[i] = np.roll(values[i+1 if previous % 2 else i], (-CY[i],-CX[i]), (0,1))
result[i+1] = values[i if previous % 2 else i+1]
return result
def install_spatial_state(sim, physical, count=1):
field = sim.field
data = physical-W[:,None,None] if sim.lbm_cfg.ddf_shifting else physical.copy()
if sim.lbm_cfg.streaming == "esopull":
data = postcollision_to_backing(data,count)
with sim.ctx.activate():
cuda.memcpy_htod(field.ddf_gpu,np.ascontiguousarray(data,dtype=np.float32))
cuda.memcpy_htod(field.temp_gpu,np.ascontiguousarray(data,dtype=np.float32))
sim.stepper._step_count=count
field.invalidate_host_ddf_cache()
# x is periodic; no ghost inlet/outlet donor algorithm is under test.
field.flag[:]=FLUID
field.flag[:field.nx]=SOLID|BC_WALL
field.flag[-field.nx:]=SOLID|BC_WALL
field.upload_flags()
def spatial_state(sim):
field=sim.field
data=np.empty((9,field.ny,field.nx),dtype=np.float32)
with sim.ctx.activate():
cuda.memcpy_dtoh(data,field.ddf_gpu)
if sim.lbm_cfg.streaming == "esopull":
data=backing_to_postcollision(data,sim.stepper.step_count)
if sim.lbm_cfg.ddf_shifting:
data=data+W[:,None,None]
return data
@pytest.mark.parametrize("collision,shifted,wall", [
("SRT",False,"bounce_back"), ("TRT",True,"bounce_back"),
("MRT",False,"free_slip"), ("MRT",True,"bounce_back"),
])
def test_nonuniform_closed_channel_matches_between_layouts(collision,shifted,wall):
traces=[]
for streaming in ["double_buffer","esopull"]:
sim=make_sim(nx=32,ny=16,streaming=streaming,shifted=shifted,
collision=collision,y_wall=wall)
try:
sim.initialize()
y,x=np.mgrid[:16,:32]
physical=equilibrium(1+.001*np.cos(.3*x+.2*y),
.01*np.sin(.2*x+.3*y),.005*np.cos(.3*x-.2*y))
install_spatial_state(sim,physical,count=1)
trace=[]
for _ in range(4): # first launch, both parities, both adjacent rows
sim.run(1)
trace.append(spatial_state(sim)[:,1:-1,:])
traces.append(trace)
finally:
sim.close()
# FP32 local arithmetic/roundoff; old donor-stage errors are much larger.
np.testing.assert_allclose(traces[0],traces[1],atol=2e-6,rtol=0)
@pytest.mark.parametrize("shifted",[False,True])
@pytest.mark.parametrize("wall",["bounce_back","free_slip"])
def test_actual_initialization_wall_staging(shifted,wall):
sim=make_sim(nx=16,ny=32,streaming="esopull",shifted=shifted,y_wall=wall)
try:
sim.initialize()
field=sim.field
cells=np.arange(field.n,dtype=np.uint32)
scratch, physical, indices=field.esopull_sync_decode(cells,0)
physical=physical.reshape(field.n,9).T.reshape(9,32,16).copy()
with sim.ctx.activate():
scratch.free();indices.free()
sim.stepper.prepare_boundaries_fn(field.ddf_gpu,sim.stepper.boundary_values_gpu,
np.uint64(0),block=sim.stepper.block,grid=sim.stepper.boundary_grid)
prepared=np.empty(6*16+2*32*9,dtype=np.float32)
cuda.memcpy_dtoh(prepared,sim.stepper.boundary_values_gpu)
staged=prepared[:6*16].reshape(2,16,3)
for side,row in [(0,1),(1,30)]:
donors=[4,6,7] if side==0 else [3,5,8]
if wall=="free_slip": donors[1:]=donors[1:][::-1]
expected=physical[donors,row,:].T
if shifted: expected=expected-W[donors]
np.testing.assert_allclose(staged[side],expected,atol=1e-7,rtol=0)
donors=prepared[6*16:].reshape(2,32,9)
for side,x in [(0,1),(1,14)]:
expected=physical[:,:,x].T
if shifted: expected=expected-W
np.testing.assert_allclose(donors[side],expected,atol=1e-7,rtol=0)
sim.run(1) # actual first step consumes the staged values without a race
assert np.isfinite(spatial_state(sim)).all()
finally:
sim.close()
@pytest.mark.parametrize("collision",["SRT","TRT","MRT"])
@pytest.mark.parametrize("streaming",["double_buffer","esopull"])
def test_uniform_global_force_momentum(collision,streaming):
sim=make_sim(nx=32,ny=16,collision=collision,streaming=streaming,
shifted=collision!="SRT")
try:
sim.initialize()
initial=equilibrium(np.full((16,32),1.17),np.full((16,32),.03),np.full((16,32),-.02))
install_spatial_state(sim,initial)
sim.field.update_params(fx=.001,fy=-.0007)
sim.run(1)
# Stay outside the wall influence region. Global force is uniform.
delta=(spatial_state(sim)-initial)[:,3:-3,3:-3]
np.testing.assert_allclose(delta.sum(axis=0),0,atol=5e-7,rtol=0)
np.testing.assert_allclose(np.einsum('q,qyx->yx',CX,delta),.001,atol=2e-7,rtol=0)
np.testing.assert_allclose(np.einsum('q,qyx->yx',CY,delta),-.0007,atol=2e-7,rtol=0)
finally:
sim.close()
@pytest.mark.parametrize("streaming",["double_buffer","esopull"])
@pytest.mark.parametrize("shifted",[False,True])
def test_force_region_impulse_budget(streaming,shifted):
sim=make_sim(nx=32,ny=16,streaming=streaming,shifted=shifted,collision="MRT")
try:
sim.add_body("force_region",center=(16,8),radius=2)
sim.initialize()
initial=equilibrium(np.full((16,32),1.17),np.full((16,32),.03),np.full((16,32),-.02))
install_spatial_state(sim,initial)
sim.set_force(0,fx=.001,fy=-.0007)
sim.run(1)
# The region postpass uses the public layout-specific current-state
# convention. Sum global momentum so transport cannot move the target
# footprint outside a diagnostic slice. Subtract an identical zero-force
# run to cancel wall impulse and baseline FP32 collision roundoff.
forced=spatial_state(sim)
install_spatial_state(sim,initial)
sim.set_force(0,fx=0,fy=0)
sim.run(1)
delta=forced-spatial_state(sim)
count=sim.field.n_force_region
assert count>0
# Budget scales with touched cells and FP32 subtraction error.
tol=3e-7*count
assert abs(delta.sum(dtype=np.float64))<tol
assert abs(np.einsum('q,qyx->',CX,delta)-count*.001)<tol
assert abs(np.einsum('q,qyx->',CY,delta)+count*.0007)<tol
finally:
sim.close()
@pytest.mark.parametrize("omega",[0.,.002])
@pytest.mark.parametrize("periodic",[True,False])
def test_curved_layout_equivalence_isolates_open_boundary(omega,periodic):
"""Both periodic and open cases must agree at the same spatial phase."""
traces=[]
for streaming in ["double_buffer","esopull"]:
sim=make_sim(nx=64,ny=32,streaming=streaming,collision="MRT",y_wall="free_slip")
try:
# Match the boundary family of the rotating-cylinder K2 benchmark.
sim.lbm_cfg.inlet_scheme="regularized"
sim.lbm_cfg.inlet_profile="uniform"
sim.lbm_cfg.inlet_regularized_neq_damp=1.
sim.add_body("circle",center=(32,16),radius=4)
sim.recompile()
sim.initialize()
flags=sim.field.flag.copy()
y,x=np.mgrid[:32,:64]
initial=equilibrium(1+.0001*np.cos(.2*x+.1*y),
.02+.001*np.sin(.3*x+.2*y),.001*np.cos(.2*x-.1*y))
install_spatial_state(sim,initial,count=1)
sim.field.flag[:]=flags # preserve obstacle and curved flags
if periodic:
grid=sim.field.flag.reshape(32,64)
grid[1:-1,0]=FLUID;grid[1:-1,-1]=FLUID
sim.field.upload_flags()
sim.set_body(0,omega=omega)
trace=[]
for step in range(20):
sim.run(1)
if step in [0,1,4,19]: trace.append(spatial_state(sim))
traces.append(trace)
finally:
sim.close()
np.testing.assert_allclose(traces[0],traces[1],atol=2e-6,rtol=0)