Add jet, surface and fixed-patch tests and runnable examples
This commit is contained in:
1 parent
ed1a6ee54e
commit
cdbb9c42f6
48 files changed
+9267
-210
No files matched your search
@@ -0,0 +1,24 @@
|
||||
"""Explicit, isolated low-Mach GPU test configuration."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from CelerisLab.simulation import Simulation
|
||||
|
||||
|
||||
def make_sim(nx=128, ny=64, *, streaming="double_buffer", precision="FP32",
|
||||
shifted=False, collision="SRT", y_wall="bounce_back", **physics):
|
||||
config_path = Path(__file__).resolve().parents[2] / "src/CelerisLab/configs/config_lbm.json"
|
||||
cfg = json.loads(config_path.read_text())
|
||||
cfg["grid"].update(nx=nx, ny=ny, nz=1, lattice_model="D2Q9")
|
||||
cfg["physics"].update(viscosity=0.05, velocity=0.02, rho=1.0)
|
||||
cfg["physics"].update(physics)
|
||||
cfg["method"].update(collision=collision, streaming=streaming,
|
||||
store_precision=precision, ddf_shifting=shifted, y_wall_bc=y_wall)
|
||||
cfg["method"]["les"]["enabled"] = False
|
||||
with TemporaryDirectory(prefix="celeris-test-config-") as temp:
|
||||
lbm = Path(temp) / "lbm.json"
|
||||
bodies = Path(temp) / "bodies.json"
|
||||
lbm.write_text(json.dumps(cfg))
|
||||
bodies.write_text('{"objects": []}')
|
||||
return Simulation(lbm_config_path=str(lbm), body_config_path=str(bodies))
|
||||
@@ -7,9 +7,9 @@ import unittest
|
||||
import tempfile
|
||||
|
||||
import numpy as np
|
||||
import pycuda.autoinit
|
||||
|
||||
from CelerisLab.simulation import Simulation
|
||||
from ._cases import make_sim
|
||||
from CelerisLab.lbm.descriptors import OBSTACLE, FLUID
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class TestBodySyncE2E(unittest.TestCase):
|
||||
|
||||
def test_full_lifecycle(self):
|
||||
"""Create, init, run, add body, remove body, checkpoint, load."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
cx, cy = nx // 4, ny // 2
|
||||
@@ -61,7 +61,7 @@ class TestBodySyncE2E(unittest.TestCase):
|
||||
self.assertTrue(os.path.exists(saved_path))
|
||||
|
||||
# 7. Load checkpoint in a new simulation
|
||||
sim2 = Simulation(device_id=0)
|
||||
sim2 = make_sim()
|
||||
sim2.initialize()
|
||||
sim2.run(1)
|
||||
|
||||
@@ -71,9 +71,10 @@ class TestBodySyncE2E(unittest.TestCase):
|
||||
|
||||
# 8. Continue running in restored sim
|
||||
sim2.run(50)
|
||||
# Verify step count advances (DDF may have NaN from pre-existing body)
|
||||
# Verify the restored field remains finite and stepping advances
|
||||
self.assertEqual(sim2.stepper.step_count,
|
||||
sim.stepper.step_count + 50)
|
||||
self.assertTrue(np.all(np.isfinite(sim2.get_ddf())))
|
||||
sim2.close()
|
||||
finally:
|
||||
os.unlink(ckpt_path)
|
||||
@@ -82,7 +83,7 @@ class TestBodySyncE2E(unittest.TestCase):
|
||||
|
||||
def test_add_remove_add_cycle(self):
|
||||
"""Add → run → remove → run → add → run cycle with finite checks."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
cx, cy = nx // 4, ny // 2
|
||||
|
||||
@@ -5,9 +5,9 @@ Requires GPU."""
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
import pycuda.autoinit
|
||||
|
||||
from CelerisLab.simulation import Simulation
|
||||
from ._cases import make_sim
|
||||
from CelerisLab.lbm.descriptors import FLUID, OBSTACLE
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestDDFPatchAddBody(unittest.TestCase):
|
||||
def test_add_body_runs_stably(self):
|
||||
"""After adding a body, the simulation runs with finite forces
|
||||
for a moderate number of steps."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
cx, cy = nx // 4, ny // 2
|
||||
@@ -44,7 +44,7 @@ class TestDDFPatchRemoveBody(unittest.TestCase):
|
||||
|
||||
def test_remove_body_released_region_is_fluid(self):
|
||||
"""After removal, the former body center should be a fluid cell."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
cx, cy = nx // 4, ny // 2
|
||||
@@ -63,7 +63,7 @@ class TestDDFPatchRemoveBody(unittest.TestCase):
|
||||
|
||||
def test_remove_body_finite_field(self):
|
||||
"""After removal, the macroscopic field is finite."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
cx, cy = nx // 4, ny // 2
|
||||
@@ -91,7 +91,7 @@ class TestDDFPatchAddAndRemove(unittest.TestCase):
|
||||
|
||||
def test_add_one_remove_another(self):
|
||||
"""Add a body and remove a different one in the same sync."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
cx, cy = nx // 4, ny // 2
|
||||
@@ -119,8 +119,8 @@ class TestDDFPatchNoChange(unittest.TestCase):
|
||||
|
||||
def test_no_mask_no_change(self):
|
||||
"""When neither mask has any True entries, DDF should be unchanged."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim.add_body("circle", center=(128, 128), radius=8)
|
||||
sim = make_sim()
|
||||
sim.add_body("circle", center=(sim.lbm_cfg.nx // 4, sim.lbm_cfg.ny // 2), radius=8)
|
||||
sim.initialize()
|
||||
sim.run(100)
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""GPU ownership, isolated builds and immutable allocation contracts."""
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
import pytest
|
||||
|
||||
import CelerisLab
|
||||
from CelerisLab.cuda import compiler_v2
|
||||
from ._cases import make_sim
|
||||
|
||||
|
||||
def test_source_provenance():
|
||||
root = Path(__file__).resolve().parents[2] / "src/CelerisLab"
|
||||
assert Path(CelerisLab.__file__).resolve().parent == root
|
||||
assert Path(compiler_v2.kernel_path("kernel_v2.cu")).resolve() == root / "lbm/kernels/kernel_v2.cu"
|
||||
|
||||
|
||||
def test_two_contexts_interleaved_and_non_lifo_close():
|
||||
before = cuda.Context.get_current()
|
||||
first = make_sim(nx=32, ny=16)
|
||||
second = make_sim(nx=48, ny=24, streaming="esopull", shifted=True)
|
||||
try:
|
||||
assert first._build_dir != second._build_dir
|
||||
assert cuda.Context.get_current() == before
|
||||
first.initialize()
|
||||
second.initialize()
|
||||
for _ in range(3):
|
||||
first.run(1)
|
||||
second.run(1)
|
||||
assert first.get_macroscopic()["rho"].shape == (16, 32)
|
||||
assert second.get_macroscopic()["rho"].shape == (24, 48)
|
||||
first.close()
|
||||
second.run(1)
|
||||
assert np.isfinite(second.get_macroscopic()["rho"]).all()
|
||||
assert cuda.Context.get_current() == before
|
||||
finally:
|
||||
second.close()
|
||||
first.close()
|
||||
|
||||
|
||||
def test_structural_change_rejected_before_running():
|
||||
with make_sim(nx=32, ny=16) as sim:
|
||||
sim.initialize()
|
||||
before = sim.get_ddf().copy()
|
||||
sim.lbm_cfg.nx = 64
|
||||
with pytest.raises(ValueError, match="allocation|structural"):
|
||||
sim.run(1)
|
||||
with pytest.raises(ValueError, match="allocation|structural"):
|
||||
sim._recompile()
|
||||
sim.lbm_cfg.nx = 32
|
||||
np.testing.assert_array_equal(sim.get_ddf(), before)
|
||||
assert sim.stepper.step_count == 0
|
||||
sim.run(1)
|
||||
|
||||
|
||||
def test_viscosity_recompile_updates_device_omega():
|
||||
import struct
|
||||
with make_sim(nx=32, ny=16) as sim:
|
||||
sim.lbm_cfg.viscosity = 0.08
|
||||
sim.recompile()
|
||||
assert sim.stepper.step_count == 0
|
||||
sim.initialize()
|
||||
with sim.ctx.activate():
|
||||
ptr, _ = sim._module.get_global("d_params")
|
||||
buf = np.empty(20, dtype=np.uint8)
|
||||
cuda.memcpy_dtoh(buf, ptr)
|
||||
omega = struct.unpack("=ffffI", buf.tobytes())[0]
|
||||
assert omega == pytest.approx(sim.lbm_cfg.omega, rel=1e-7)
|
||||
sim.run(1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("streaming",["double_buffer","esopull"])
|
||||
@pytest.mark.parametrize("prior_steps",[3,4])
|
||||
def test_reinitialize_matches_fresh_state_and_observation_window(streaming,prior_steps):
|
||||
with make_sim(nx=32,ny=16,streaming=streaming,shifted=True) as fresh, \
|
||||
make_sim(nx=32,ny=16,streaming=streaming,shifted=True) as reused:
|
||||
for sim in (fresh,reused):
|
||||
sim.add_body("sensor",center=(16,8),radius=2)
|
||||
sim.initialize()
|
||||
reused.run(prior_steps)
|
||||
assert reused.stepper.step_count==prior_steps
|
||||
assert reused.bodies._obs_accum_steps==prior_steps
|
||||
reused.initialize()
|
||||
assert reused.stepper.step_count==0
|
||||
assert reused.bodies._obs_accum_steps==0
|
||||
np.testing.assert_array_equal(reused.bodies.obs_pinned,0)
|
||||
with reused.ctx.activate():
|
||||
obs=np.empty(reused.bodies.obs_total_floats,np.float32)
|
||||
cuda.memcpy_dtoh(obs,reused.bodies.obs_gpu)
|
||||
np.testing.assert_array_equal(obs,0)
|
||||
np.testing.assert_array_equal(reused.get_ddf(),fresh.get_ddf())
|
||||
for key,value in fresh.get_macroscopic().items():
|
||||
np.testing.assert_array_equal(reused.get_macroscopic()[key],value)
|
||||
# Equal next step proves restored parity, not only equal initialization bytes.
|
||||
fresh.run(1);reused.run(1)
|
||||
np.testing.assert_array_equal(reused.get_ddf(),fresh.get_ddf())
|
||||
@@ -0,0 +1,181 @@
|
||||
"""Bounded GPU regressions for repaired layout/state contracts (D2Q9, low Ma)."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from ._cases import make_sim
|
||||
from CelerisLab.lbm.descriptors import FLUID
|
||||
|
||||
|
||||
def physical_esopull(sim):
|
||||
"""Decode physical next-pull state without changing the solver clock."""
|
||||
with sim.ctx.activate():
|
||||
indices = np.arange(sim.field.n,dtype=np.uint32)
|
||||
scratch, values, idx_gpu = sim.field.esopull_sync_decode(
|
||||
indices,sim.stepper.step_count,sim.stream)
|
||||
result = values.copy()
|
||||
scratch.free()
|
||||
idx_gpu.free()
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.parametrize('precision',['FP32','FP16S'])
|
||||
@pytest.mark.parametrize('shifted',[False,True])
|
||||
def test_esopull_field_snapshot_across_one_step(precision,shifted):
|
||||
with make_sim(nx=48,ny=24,streaming='esopull',precision=precision,shifted=shifted) as sim:
|
||||
sim.add_body('circle',center=(16,12),radius=3)
|
||||
sim.initialize()
|
||||
sim.run(3)
|
||||
saved = physical_esopull(sim)
|
||||
sim.snapshot()
|
||||
sim.set_body(0,omega=.0003)
|
||||
sim.run(1)
|
||||
current_step = sim.stepper.step_count
|
||||
action = sim.bodies.action.copy()
|
||||
sim.restore()
|
||||
assert sim.stepper.step_count == current_step == 4
|
||||
np.testing.assert_array_equal(sim.bodies.action,action)
|
||||
# Encoding roundtrip can lose a float32 addition/subtraction ulp when
|
||||
# adding lattice weights around shifted FP16 storage; it cannot stream.
|
||||
np.testing.assert_allclose(physical_esopull(sim),saved,rtol=0,atol=1e-7)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('streaming',['double_buffer','esopull'])
|
||||
@pytest.mark.parametrize('precision',['FP32','FP16S'])
|
||||
def test_shifted_topology_add_remove_preserves_cache_and_finite_flow(streaming,precision):
|
||||
with make_sim(nx=48,ny=24,streaming=streaming,precision=precision,shifted=True) as sim:
|
||||
sim.initialize()
|
||||
sim.run(3)
|
||||
sim.get_ddf() # Fill the same-count host cache before a geometry change.
|
||||
sim.add_body('circle',center=(18,12),radius=3)
|
||||
sim.sync_bodies()
|
||||
assert sim.stepper.step_count == 3
|
||||
assert sim.field._host_ddf_step is None
|
||||
sim.run(3)
|
||||
assert np.all(np.isfinite(sim.read_force(0)))
|
||||
sim.get_ddf()
|
||||
sim.remove_body(0)
|
||||
sim.sync_bodies()
|
||||
assert sim.stepper.step_count == 6
|
||||
assert sim.field._host_ddf_step is None
|
||||
sim.run(3)
|
||||
macro = sim.get_macroscopic()
|
||||
fluid = (sim.get_flags().reshape(24,48) & FLUID) != 0
|
||||
assert all(np.isfinite(array[fluid]).all() for array in macro.values())
|
||||
assert np.min(macro['rho'][fluid]) > .5
|
||||
assert np.max(macro['rho'][fluid]) < 1.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize('streaming',['double_buffer','esopull'])
|
||||
def test_checkpoint_exact_continuation_and_controls(streaming,tmp_path):
|
||||
with make_sim(nx=48,ny=24,streaming=streaming,shifted=True) as sim:
|
||||
sim.add_body('circle',center=(14,12),radius=3)
|
||||
sim.add_body('force_region',center=(30,12),radius=2)
|
||||
sim.initialize()
|
||||
sim.set_force(1,fx=1e-5,fy=-2e-6)
|
||||
sim.set_body(0,omega=.0002)
|
||||
sim.run(3)
|
||||
saved = sim.get_ddf()
|
||||
path = sim.save_checkpoint(str(tmp_path/'restart.h5'))
|
||||
sim.run(4)
|
||||
expected = sim.get_ddf()
|
||||
sim.set_force(1,fx=0,fy=0)
|
||||
sim.set_body(0,omega=0)
|
||||
# Force the exact count cache collision which formerly skipped D2H.
|
||||
sim.field._host_ddf_step = 3
|
||||
sim.field.ddf.fill(99)
|
||||
sim.load_checkpoint(path)
|
||||
assert sim.stepper.step_count == 3
|
||||
np.testing.assert_array_equal(sim.get_ddf(),saved)
|
||||
np.testing.assert_allclose(sim.bodies.action[[5,6,7]],[.0002,1e-5,-2e-6])
|
||||
sim.run(4)
|
||||
np.testing.assert_array_equal(sim.get_ddf(),expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('streaming', ['double_buffer', 'esopull'])
|
||||
@pytest.mark.parametrize('periodic_x', [False, True])
|
||||
def test_checkpoint_domain_exact_continuation(streaming, periodic_x, tmp_path):
|
||||
# Same low-Mach forced channel as the Poiseuille runner, bounded to 7 steps.
|
||||
kwargs = dict(nx=16, ny=18, streaming=streaming, collision='MRT',
|
||||
shifted=True, viscosity=.1, velocity=0.)
|
||||
with make_sim(**kwargs) as source:
|
||||
source.initialize()
|
||||
if periodic_x:
|
||||
source.field.flag.reshape(18, 16)[1:-1, [0, -1]] = FLUID
|
||||
source.field.upload_flags()
|
||||
source.update_runtime_params(fx=8 * .1 * .03 / 16**2)
|
||||
source.run(3)
|
||||
flags = source.get_flags().copy()
|
||||
saved = source.get_ddf().copy()
|
||||
path = source.save_checkpoint(str(tmp_path / 'domain.h5'))
|
||||
source.run(4)
|
||||
expected = source.get_ddf().copy()
|
||||
# Restore into this instance and also a fresh default-domain instance.
|
||||
source.load_checkpoint(path)
|
||||
np.testing.assert_array_equal(source.get_flags(), flags)
|
||||
np.testing.assert_array_equal(source.get_ddf(), saved)
|
||||
source.run(4)
|
||||
np.testing.assert_array_equal(source.get_ddf(), expected)
|
||||
with make_sim(**kwargs) as target:
|
||||
target.initialize()
|
||||
target.load_checkpoint(path)
|
||||
assert target.stepper.step_count == 3
|
||||
np.testing.assert_array_equal(target.get_flags(), flags)
|
||||
np.testing.assert_array_equal(target.get_ddf(), saved)
|
||||
target.run(4)
|
||||
np.testing.assert_array_equal(target.get_ddf(), expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('periodic_x', [False, True])
|
||||
def test_checkpoint_geometry_mismatch_leaves_device_unchanged(tmp_path, periodic_x):
|
||||
with make_sim(nx=48,ny=24) as source:
|
||||
source.add_body('circle',center=(14,12),radius=3)
|
||||
source.initialize()
|
||||
if periodic_x:
|
||||
source.field.flag.reshape(24, 48)[1:-1, [0, -1]] = FLUID
|
||||
source.field.upload_flags()
|
||||
source.run(2)
|
||||
path = source.save_checkpoint(str(tmp_path/'geometry.h5'))
|
||||
with make_sim(nx=48,ny=24) as target:
|
||||
target.add_body('circle',center=(28,12),radius=3)
|
||||
target.initialize()
|
||||
target.run(3)
|
||||
before = target.get_ddf()
|
||||
flags = target.get_flags()
|
||||
with pytest.raises(ValueError,match='geometry'):
|
||||
target.load_checkpoint(path)
|
||||
assert target.stepper.step_count == 3
|
||||
target.field.invalidate_host_ddf_cache()
|
||||
np.testing.assert_array_equal(target.get_ddf(),before)
|
||||
np.testing.assert_array_equal(target.get_flags(),flags)
|
||||
|
||||
|
||||
def test_periodic_checkpoint_restores_observation_interval(tmp_path,monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
with make_sim(nx=48,ny=24) as sim:
|
||||
sim.add_body('sensor',center=(24,12),radius=2)
|
||||
sim.initialize()
|
||||
sim.run(6,checkpoint_interval=2)
|
||||
sim.load_checkpoint(str(tmp_path/'output/checkpoint_00000002.h5'))
|
||||
assert sim.stepper.step_count == 2
|
||||
assert sim.bodies._obs_accum_steps == 2
|
||||
raw = sim.read_sensor(0,normalize=False)
|
||||
assert np.linalg.norm(raw) > 0
|
||||
np.testing.assert_allclose(sim.read_sensor(0),raw/2,rtol=1e-6)
|
||||
|
||||
|
||||
def test_checkpoint_preserves_pending_and_applied_controls(tmp_path):
|
||||
with make_sim(nx=48,ny=24,shifted=True) as sim:
|
||||
sim.add_body('force_region',center=(24,12),radius=3)
|
||||
sim.initialize()
|
||||
sim.set_force(0,fx=1e-5,fy=-2e-6)
|
||||
sim.run(3)
|
||||
sim.set_force(0,fx=3e-5,fy=4e-6) # Host target has not been uploaded.
|
||||
pending = sim.bodies.action.copy()
|
||||
path = sim.save_checkpoint(str(tmp_path/'pending.h5'))
|
||||
sim.run(4,upload_act=False)
|
||||
expected = sim.get_ddf()
|
||||
sim.set_force(0,fx=0,fy=0)
|
||||
sim.load_checkpoint(path)
|
||||
np.testing.assert_array_equal(sim.bodies.action,pending)
|
||||
sim.run(4,upload_act=False)
|
||||
np.testing.assert_array_equal(sim.get_ddf(),expected)
|
||||
@@ -0,0 +1,249 @@
|
||||
"""Fixed surface actuation: discrete operator/ledger checks, not flux accuracy."""
|
||||
from dataclasses import asdict
|
||||
import json
|
||||
|
||||
import h5py
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
import pytest
|
||||
|
||||
from CelerisLab.lbm.descriptors import FLUID, SOLID
|
||||
from ._cases import make_sim
|
||||
from .test_numerical_gpu import CX, CY, install_spatial_state, spatial_state
|
||||
|
||||
|
||||
WEIGHTS = np.array([4/9] + [1/9]*4 + [1/36]*4)
|
||||
OPPOSITE = np.array([0, 2, 1, 4, 3, 6, 5, 8, 7])
|
||||
GEOMETRY = ("fluid_idx", "dir", "q", "body_id", "rx", "ry", "rz", "fallback_class")
|
||||
|
||||
|
||||
def feq(rho, ux, uy):
|
||||
"""Independent FP64 polynomial; round only the uploaded populations."""
|
||||
cu = CX[:, None, None]*ux + CY[:, None, None]*uy
|
||||
return WEIGHTS[:, None, None]*rho*(1 + 3*cu + 4.5*cu**2 - 1.5*(ux**2 + uy**2))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("speed", [-.01, .01])
|
||||
def test_actual_surface_kernel_equilibrium_branches(speed):
|
||||
"""Both q branches, q=.5 and donor fallback; non-unit rho and signed normal."""
|
||||
sim = make_sim(nx=24, ny=16, velocity=0)
|
||||
allocations = []
|
||||
try:
|
||||
sim.initialize()
|
||||
normal = np.array([-.6, .8])
|
||||
expected = feq(1.17, *(speed*normal))[:, 0, 0]
|
||||
initial = np.broadcast_to(expected[:, None, None], (9, 16, 24)).astype(np.float32).copy()
|
||||
# Distinct source/destination neighborhoods; no synthetic link race.
|
||||
cells = np.array([4*24+12, 6*24+12, 8*24+12, 10*24+12], np.uint32)
|
||||
directions = np.ones(4, np.uint8)
|
||||
radii = np.tile(5*normal, (4, 1)).astype(np.float32)
|
||||
columns = [cells, directions, np.array([.2, .5, .8, .2], np.float32),
|
||||
radii[:, 0].copy(), radii[:, 1].copy(), np.zeros(4, np.float32),
|
||||
np.array([0, 0, 0, 1], np.uint8), np.zeros(4, np.int32)]
|
||||
with sim.ctx.activate():
|
||||
for array in columns + [np.tile(speed*normal, (4, 1)).astype(np.float32),
|
||||
np.zeros((4, 3), np.float32)]:
|
||||
allocations.append(cuda.to_device(array))
|
||||
cuda.memcpy_htod(sim.field.ddf_gpu, initial)
|
||||
sim.stepper.curved_fn(sim.field.ddf_gpu, *allocations[:8],
|
||||
np.uintp(0), np.uintp(0), np.uint32(4),
|
||||
*allocations[8:], block=(32, 1, 1), grid=(1, 1))
|
||||
actual = np.empty_like(initial)
|
||||
exchange = np.empty((4, 3), np.float32)
|
||||
cuda.memcpy_dtoh(actual, sim.field.ddf_gpu)
|
||||
cuda.memcpy_dtoh(exchange, allocations[-1])
|
||||
reflected = actual.reshape(9, -1)[2, cells+1]
|
||||
np.testing.assert_allclose(reflected, expected[2], atol=7e-8, rtol=0)
|
||||
# Equilibrium identity is independent of interpolation implementation.
|
||||
fx = expected[1] + expected[2]
|
||||
wanted = np.column_stack((np.full(4, fx), np.zeros(4), -radii[:, 1]*fx))
|
||||
np.testing.assert_allclose(exchange, wanted, atol=4e-7, rtol=0)
|
||||
finally:
|
||||
with sim.ctx.activate():
|
||||
for allocation in allocations:
|
||||
allocation.free()
|
||||
sim.close()
|
||||
|
||||
|
||||
def circle_sim(speed):
|
||||
sim = make_sim(nx=32, ny=24, velocity=0)
|
||||
spec = None if speed is None else dict(angle=.7, half_width=.85, normal_velocity=speed)
|
||||
try:
|
||||
bid = sim.add_body("circle", center=(16, 12), radius=4.3, surface_actuation=spec)
|
||||
sim.initialize()
|
||||
flags = sim.field.flag.copy()
|
||||
flags.reshape(24, 32)[1:-1, [0, -1]] = FLUID # x periodic, y halfway walls
|
||||
y, x = np.mgrid[:24, :32]
|
||||
initial = feq(1.17+.001*np.cos(.2*x+.3*y),
|
||||
.003*np.sin(.3*y), .002*np.cos(.4*x)).astype(np.float32)
|
||||
install_spatial_state(sim, initial)
|
||||
sim.field.flag[:] = flags # installer intentionally resets all flags
|
||||
sim.field.upload_flags()
|
||||
return sim, bid, initial
|
||||
except BaseException:
|
||||
sim.close()
|
||||
raise
|
||||
|
||||
|
||||
@pytest.mark.parametrize("speed", [-.01, .01])
|
||||
def test_surface_population_ledger_and_public_diagnostics(speed):
|
||||
sim, bid, initial = circle_sim(speed)
|
||||
try:
|
||||
cl = sim.field.curved
|
||||
frozen = {name: getattr(cl, name).copy() for name in GEOMETRY}
|
||||
flags = sim.field.flag.copy()
|
||||
body_state = vars(sim.bodies.get(bid).state).copy()
|
||||
physical = initial.astype(np.float64)
|
||||
flat = physical.reshape(9, -1)
|
||||
cell, direction = cl.fluid_idx.astype(int), cl.dir.astype(int)
|
||||
ex, ey, q = CX[direction], CY[direction], cl.q.astype(float)
|
||||
# Independently project the prescribed arc at true intersections.
|
||||
angle = np.arctan2(cl.ry.astype(float), cl.rx.astype(float))
|
||||
delta = (angle-.7+np.pi) % (2*np.pi)-np.pi
|
||||
aperture = np.abs(delta) < .85
|
||||
radial = np.column_stack((cl.rx, cl.ry)).astype(float)
|
||||
normal = radial / np.linalg.norm(radial, axis=1)[:, None]
|
||||
velocity = speed*np.maximum(0, 1-(delta/.85)**2)[:, None]**2*normal
|
||||
np.testing.assert_array_equal(cl.aperture, aperture)
|
||||
np.testing.assert_allclose(cl.surface_velocity, velocity, atol=1e-8, rtol=0)
|
||||
assert aperture.any() and (~aperture).any()
|
||||
assert (q < .5).any() and (q >= .5).any()
|
||||
a = flat[direction, cell]
|
||||
b = np.empty(len(cell))
|
||||
passive = np.empty(len(cell))
|
||||
for i, (k, d) in enumerate(zip(cell, direction)):
|
||||
rho = flat[:, k].sum()
|
||||
dot = CX[d]*velocity[i, 0] + CY[d]*velocity[i, 1]
|
||||
if cl.fallback_class[i]:
|
||||
passive[i] = a[i]
|
||||
factor = 6
|
||||
elif q[i] < .5:
|
||||
passive[i] = 2*q[i]*a[i] + (1-2*q[i])*flat[d, k-CX[d]-32*CY[d]]
|
||||
factor = 6
|
||||
else:
|
||||
passive[i] = a[i]/(2*q[i]) + (1-1/(2*q[i]))*flat[OPPOSITE[d], k]
|
||||
factor = 3/q[i]
|
||||
b[i] = passive[i]-factor*WEIGHTS[d]*rho*dot
|
||||
force = np.column_stack((ex, ey))*(a+b)[:, None]
|
||||
torque = cl.rx*force[:, 1]-cl.ry*force[:, 0]
|
||||
wanted = np.column_stack((force, torque))
|
||||
# Fluid-node and intersection lever arms differ by q*c: same link torque.
|
||||
fluid_r = radial-q[:, None]*np.column_stack((ex, ey))
|
||||
np.testing.assert_allclose(torque, fluid_r[:, 0]*force[:, 1]-fluid_r[:, 1]*force[:, 0],
|
||||
atol=2e-15, rtol=0)
|
||||
with sim.ctx.activate():
|
||||
sim.stepper.curved_fn(sim.field.ddf_gpu,
|
||||
*(getattr(cl, name+"_gpu") for name in GEOMETRY[:3]),
|
||||
cl.rx_gpu, cl.ry_gpu, cl.rz_gpu, cl.fallback_class_gpu, cl.body_id_gpu,
|
||||
np.uintp(0), np.uintp(0), np.uint32(cl.count),
|
||||
cl.surface_velocity_gpu, cl.link_exchange_gpu,
|
||||
block=(64, 1, 1), grid=((cl.count+63)//64, 1))
|
||||
staged = np.empty_like(initial)
|
||||
cuda.memcpy_dtoh(staged, sim.field.ddf_gpu)
|
||||
incoming = staged.reshape(9, -1)[OPPOSITE[direction], cell+ex+32*ey].astype(float)
|
||||
np.testing.assert_allclose(incoming, b, atol=8e-8, rtol=0)
|
||||
# Incoming populations alone respond with the expected signed normal drive.
|
||||
assert np.all((-np.sum(normal[aperture]*np.column_stack((ex, ey))[aperture], axis=1)) > 0)
|
||||
core = aperture & (np.abs(delta) < .5*.85)
|
||||
assert core.any()
|
||||
assert np.all(speed*(incoming-passive)[core] > 0)
|
||||
|
||||
# Independent pull ledger, including all planar-wall links. Use actual
|
||||
# rounded incoming values to isolate bookkeeping from CUDA arithmetic.
|
||||
arrived = np.stack([np.roll(physical[d], (CY[d], CX[d]), (0, 1)) for d in range(9)])
|
||||
arrived.reshape(9, -1)[OPPOSITE[direction], cell] = incoming
|
||||
mass_exchange = float(np.sum(incoming-a))
|
||||
momentum_exchange = -np.sum(np.column_stack((ex, ey))*(a+incoming)[:, None], axis=0)
|
||||
for y, outgoing in ((1, (4, 6, 7)), (22, (3, 5, 8))):
|
||||
for d in outgoing:
|
||||
values = physical[d, y, :]
|
||||
arrived[OPPOSITE[d], y, :] = values
|
||||
momentum_exchange -= 2*values.sum()*np.array([CX[d], CY[d]])
|
||||
fluid = ((flags & FLUID) != 0) & ((flags & SOLID) == 0)
|
||||
change = (arrived-physical).reshape(9, -1)[:, fluid].sum(axis=1)
|
||||
np.testing.assert_allclose([change.sum(), change@CX, change@CY],
|
||||
[mass_exchange, *momentum_exchange], atol=3e-13, rtol=0)
|
||||
# Start public run from the original state, not a previously staged field.
|
||||
with sim.ctx.activate():
|
||||
cuda.memcpy_htod(sim.field.ddf_gpu, initial)
|
||||
sim.run(1)
|
||||
actual = spatial_state(sim).astype(float).reshape(9, -1)[:, fluid]
|
||||
expected = arrived.reshape(9, -1)[:, fluid]
|
||||
moments = np.stack((np.ones(9), CX, CY))
|
||||
np.testing.assert_allclose(moments@actual, moments@expected, atol=5e-7, rtol=0)
|
||||
# FP32 collision conserves moments up to rounding, not bitwise globally.
|
||||
change = (actual-physical.reshape(9, -1)[:, fluid]).sum(axis=1)
|
||||
budget = 8*np.finfo(np.float32).eps*np.abs(expected).sum()
|
||||
np.testing.assert_allclose([change.sum(), change@CX, change@CY],
|
||||
[mass_exchange, *momentum_exchange], atol=budget, rtol=0)
|
||||
diagnostic = sim.read_surface_exchange(bid)
|
||||
for name, mask in (("wall", ~aperture), ("aperture", aperture)):
|
||||
np.testing.assert_allclose(diagnostic[name], wanted[mask].sum(axis=0),
|
||||
atol=4e-6, rtol=0)
|
||||
total = diagnostic["wall"]+diagnostic["aperture"]
|
||||
np.testing.assert_allclose(sim.read_force(bid, normalize=False), total[:2], atol=4e-6, rtol=0)
|
||||
np.testing.assert_allclose(sim.read_torque(bid, normalize=False), total[2:], atol=4e-6, rtol=0)
|
||||
for name, value in frozen.items():
|
||||
np.testing.assert_array_equal(getattr(cl, name), value)
|
||||
np.testing.assert_array_equal(sim.field.flag, flags)
|
||||
assert vars(sim.bodies.get(bid).state) == body_state
|
||||
finally:
|
||||
sim.close()
|
||||
|
||||
|
||||
def test_zero_surface_drive_preserves_legacy_populations_and_geometry():
|
||||
records = []
|
||||
for speed in (None, 0.0):
|
||||
sim, bid, _ = circle_sim(speed)
|
||||
try:
|
||||
sim.run(2)
|
||||
records.append((spatial_state(sim), sim.field.flag.copy(),
|
||||
{name: getattr(sim.field.curved, name).copy() for name in GEOMETRY}))
|
||||
finally:
|
||||
sim.close()
|
||||
np.testing.assert_array_equal(records[0][0], records[1][0])
|
||||
np.testing.assert_array_equal(records[0][1], records[1][1])
|
||||
for name in GEOMETRY:
|
||||
np.testing.assert_array_equal(records[0][2][name], records[1][2][name])
|
||||
|
||||
|
||||
def test_surface_checkpoint_continues_and_resets_latest_exchange(tmp_path):
|
||||
sim, bid, _ = circle_sim(.01)
|
||||
try:
|
||||
sim.run(1)
|
||||
saved = spatial_state(sim)
|
||||
count = sim.stepper.step_count
|
||||
spec = asdict(sim.bodies.get(bid).surface_actuation)
|
||||
action = sim.bodies.action.copy()
|
||||
path = sim.save_checkpoint(str(tmp_path / "surface.h5"))
|
||||
with h5py.File(path, "r") as checkpoint:
|
||||
assert checkpoint.attrs["ckpt_version"] == 4
|
||||
geometry = json.loads(checkpoint.attrs["geometry_json"])
|
||||
assert len(geometry[0]["projection_sha256"]) == 64
|
||||
assert json.loads(checkpoint.attrs["surface_actuation_json"]) == [spec]
|
||||
applied = checkpoint["applied_action"][:]
|
||||
sim.run(2)
|
||||
expected = spatial_state(sim)
|
||||
expected_exchange = sim.read_surface_exchange(bid)
|
||||
assert np.linalg.norm(expected_exchange["aperture"]) > 0
|
||||
sim.load_checkpoint(path)
|
||||
assert sim.stepper.step_count == count
|
||||
np.testing.assert_array_equal(spatial_state(sim), saved)
|
||||
np.testing.assert_array_equal(sim.bodies.action, action)
|
||||
actual_action = np.empty_like(applied)
|
||||
with sim.ctx.activate():
|
||||
cuda.memcpy_dtoh(actual_action, sim.bodies.action_gpu)
|
||||
np.testing.assert_array_equal(actual_action, applied)
|
||||
assert asdict(sim.bodies.get(bid).surface_actuation) == spec
|
||||
for values in sim.read_surface_exchange(bid).values():
|
||||
np.testing.assert_array_equal(values, np.zeros(3))
|
||||
restored = sim.save_checkpoint(str(tmp_path / "restored.h5"))
|
||||
with h5py.File(restored, "r") as checkpoint:
|
||||
assert json.loads(checkpoint.attrs["geometry_json"]) == geometry
|
||||
assert json.loads(checkpoint.attrs["surface_actuation_json"]) == [spec]
|
||||
sim.run(2)
|
||||
np.testing.assert_array_equal(spatial_state(sim), expected)
|
||||
for name, values in sim.read_surface_exchange(bid).items():
|
||||
np.testing.assert_array_equal(values, expected_exchange[name])
|
||||
finally:
|
||||
sim.close()
|
||||
@@ -9,9 +9,9 @@ import json
|
||||
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
import pycuda.autoinit
|
||||
|
||||
from CelerisLab.simulation import Simulation, _scatter_to_field, _gather_from_field
|
||||
from ._cases import make_sim
|
||||
|
||||
|
||||
# Use a small grid for fast compilation and test execution
|
||||
@@ -23,7 +23,7 @@ class TestSyncBodiesSkeleton(unittest.TestCase):
|
||||
|
||||
def _make_sim(self) -> Simulation:
|
||||
"""Create a Simulation with a small double_buffer D2Q9 grid."""
|
||||
return Simulation(device_id=0)
|
||||
return make_sim()
|
||||
|
||||
def test_remove_body_sync_and_run(self):
|
||||
"""Remove a body, sync, and continue running without crash."""
|
||||
@@ -134,28 +134,7 @@ class TestSyncBodiesEsoPull(unittest.TestCase):
|
||||
|
||||
def _make_esopull_sim(self, nx=128, ny=64) -> Simulation:
|
||||
"""Create a Simulation with a small esopull D2Q9 grid."""
|
||||
cfg = {
|
||||
"grid": {"lattice_model": "D2Q9", "nx": nx, "ny": ny, "nz": 1},
|
||||
"physics": {"data_type": "FP32", "viscosity": 0.05,
|
||||
"velocity": 0.03, "rho": 1.0},
|
||||
"method": {
|
||||
"collision": "SRT", "streaming": "esopull",
|
||||
"store_precision": "FP32", "ddf_shifting": False,
|
||||
"les": {"enabled": False, "cs": 0.16, "closed_form": True},
|
||||
"trt": {"magic_param": 0.1875},
|
||||
"inlet": {"profile": "parabolic", "scheme": "zou_he_local"},
|
||||
"outlet": {"mode": "neq_extrap", "backflow_clamp": True,
|
||||
"blend_alpha": 0.7},
|
||||
"y_wall_bc": "bounce_back",
|
||||
"omega_guard": {"min": 0.01, "max": 1.99},
|
||||
},
|
||||
"cuda": {"threads_per_block": 256, "compute_capability": "auto"},
|
||||
}
|
||||
tmpd = tempfile.mkdtemp(prefix="esopull_sync_")
|
||||
lbm_path = os.path.join(tmpd, "config_lbm.json")
|
||||
with open(lbm_path, "w") as f:
|
||||
json.dump(cfg, f)
|
||||
return Simulation(lbm_config_path=lbm_path)
|
||||
return make_sim(nx=nx, ny=ny, streaming="esopull")
|
||||
|
||||
def test_esopull_add_body(self):
|
||||
"""EsoPull: add body, sync, run, read finite force."""
|
||||
@@ -234,7 +213,8 @@ class TestSyncBodiesEsoPull(unittest.TestCase):
|
||||
_gather_from_field(sim.field, s_host, all_cells)
|
||||
sim.field.esopull_sync_encode(
|
||||
s_gpu, s_host, idx_gpu, len(all_cells), step, stream)
|
||||
stream.synchronize()
|
||||
with sim.ctx.activate():
|
||||
stream.synchronize()
|
||||
|
||||
sim.run(100)
|
||||
macro = sim.get_macroscopic()
|
||||
|
||||
@@ -6,9 +6,9 @@ import unittest
|
||||
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
import pycuda.autoinit
|
||||
|
||||
from CelerisLab.simulation import Simulation
|
||||
from ._cases import make_sim
|
||||
|
||||
|
||||
class TestUnifiedObs(unittest.TestCase):
|
||||
@@ -16,7 +16,7 @@ class TestUnifiedObs(unittest.TestCase):
|
||||
|
||||
def test_set_body_then_run_read_body(self):
|
||||
"""set_body (host-only), run, read_body returns finite force."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
|
||||
@@ -35,7 +35,7 @@ class TestUnifiedObs(unittest.TestCase):
|
||||
|
||||
def test_skip_transfer(self):
|
||||
"""run(upload_act=False, sync_obs=False) should not crash."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
|
||||
@@ -47,12 +47,13 @@ class TestUnifiedObs(unittest.TestCase):
|
||||
|
||||
def test_external_stream(self):
|
||||
"""Providing an external CUDA stream should not crash."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
|
||||
sim.initialize()
|
||||
s = cuda.Stream()
|
||||
with sim.ctx.activate():
|
||||
s = cuda.Stream()
|
||||
sim.run(50, stream=s)
|
||||
force = sim.read_force(0)
|
||||
self.assertTrue(np.all(np.isfinite(force)),
|
||||
@@ -62,8 +63,8 @@ class TestUnifiedObs(unittest.TestCase):
|
||||
def test_read_body_before_run_returns_zeros(self):
|
||||
"""read_body before any run() should return zero force (buffer is
|
||||
initialized to zero during sync_to_gpu)."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim.add_body("circle", center=(128, 128), radius=8)
|
||||
sim = make_sim()
|
||||
sim.add_body("circle", center=(sim.lbm_cfg.nx // 4, sim.lbm_cfg.ny // 2), radius=8)
|
||||
sim.initialize()
|
||||
force = sim.read_force(0)
|
||||
np.testing.assert_array_equal(force, np.zeros(2, dtype=np.float32))
|
||||
@@ -71,7 +72,7 @@ class TestUnifiedObs(unittest.TestCase):
|
||||
|
||||
def test_drl_pattern(self):
|
||||
"""DRL-style loop: run → read → set → run → read."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
|
||||
@@ -95,7 +96,7 @@ class TestObsZeroingAndNormalize(unittest.TestCase):
|
||||
def test_zero_obs_true_resets_force(self):
|
||||
"""run(zero_obs=True) resets the step counter; force per-step
|
||||
should be the same order of magnitude across blocks."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
|
||||
@@ -115,7 +116,7 @@ class TestObsZeroingAndNormalize(unittest.TestCase):
|
||||
|
||||
def test_zero_obs_false_accumulates_force(self):
|
||||
"""run(zero_obs=False) twice → step counter accumulates."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
|
||||
@@ -136,7 +137,7 @@ class TestObsZeroingAndNormalize(unittest.TestCase):
|
||||
|
||||
def test_zero_obs_true_resets_sensor(self):
|
||||
"""Sensor values should not spill across run() calls with zero_obs."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("sensor", center=(nx // 2, ny // 2), radius=8)
|
||||
@@ -157,7 +158,7 @@ class TestObsZeroingAndNormalize(unittest.TestCase):
|
||||
|
||||
def test_normalize_divides_by_steps(self):
|
||||
"""read_force(normalize=True) should give per-step force."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
|
||||
@@ -175,7 +176,7 @@ class TestObsZeroingAndNormalize(unittest.TestCase):
|
||||
def test_read_sensor_normalize_false(self):
|
||||
"""read_sensor(normalize=False) returns area-averaged but not
|
||||
time-averaged value."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("sensor", center=(nx // 2, ny // 2), radius=8)
|
||||
@@ -193,7 +194,7 @@ class TestObsZeroingAndNormalize(unittest.TestCase):
|
||||
|
||||
def test_read_body_normalize(self):
|
||||
"""read_body(normalize=True) divides all fields by step count."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
|
||||
@@ -212,8 +213,8 @@ class TestObsZeroingAndNormalize(unittest.TestCase):
|
||||
|
||||
def test_normalize_returns_zero_before_run(self):
|
||||
"""read(..., normalize=True) before any run() returns zeros."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim.add_body("circle", center=(128, 128), radius=8)
|
||||
sim = make_sim()
|
||||
sim.add_body("circle", center=(sim.lbm_cfg.nx // 4, sim.lbm_cfg.ny // 2), radius=8)
|
||||
sim.initialize()
|
||||
|
||||
force = sim.read_force(0, normalize=True)
|
||||
@@ -227,7 +228,7 @@ class TestObsZeroingAndNormalize(unittest.TestCase):
|
||||
def test_sensor_area_always_normalized(self):
|
||||
"""Sensor always does area-normalisation internally.
|
||||
normalize=False should NOT equal GPU raw (should be smaller by cell_count)."""
|
||||
sim = Simulation(device_id=0)
|
||||
sim = make_sim()
|
||||
nx = sim.lbm_cfg.nx
|
||||
ny = sim.lbm_cfg.ny
|
||||
sim.add_body("sensor", center=(nx // 2, ny // 2), radius=8)
|
||||
|
||||
Reference in new issue
Block a user