"""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))',CX,delta)-count*.001)',CY,delta)+count*.0007)