# CelerisLab/tests/validation/test_sensor_accuracy.py """Sensor accuracy validation: compare sensor readings to direct flow field averages. This script validates that the GPU sensor kernel accumulation matches a CPU-side manual average of the macroscopic field over the same cell footprint. Usage:: conda run -n pycuda_3_10 python tests/validation/test_sensor_accuracy.py """ from __future__ import annotations import json import os import sys import tempfile from pathlib import Path import numpy as np _REPO = Path(__file__).resolve().parents[2] sys.path.insert(0, str(_REPO / "src")) from CelerisLab import Simulation def test_sensor_accuracy() -> dict: """Run sensor accuracy validation with multiple sensor positions.""" cfg = json.loads( (Path(_REPO) / "src" / "CelerisLab" / "configs" / "config_lbm.json").read_text() ) cfg["grid"]["nx"] = 256 cfg["grid"]["ny"] = 128 cfg["grid"]["nz"] = 1 cfg["physics"]["viscosity"] = 0.009 cfg["physics"]["velocity"] = 0.03 cfg["method"]["collision"] = "MRT" cfg["method"]["inlet"]["scheme"] = "regularized" cfg["method"]["inlet"]["profile"] = "uniform" cfg["method"]["y_wall_bc"] = "free_slip" tmpd = tempfile.mkdtemp(prefix="sensor_test_") lbm_path = os.path.join(tmpd, "config_lbm.json") with open(lbm_path, "w") as f: json.dump(cfg, f) sim = Simulation(lbm_config_path=lbm_path) sim.add_body("circle", center=(80, 64), radius=15) positions = [(120, 50), (120, 64), (120, 78), (150, 64)] sensor_ids = [] for cx, cy in positions: sid = sim.add_body("sensor", center=(cx, cy), radius=10) sensor_ids.append(sid) sim.initialize() print(f"Initialized: nx={cfg['grid']['nx']} ny={cfg['grid']['ny']} " f"n_curved={sim.field.n_curved} n_sensor={sim.field.n_sensor}") # Step to develop wake for _ in range(50): sim.run(20) # Get macroscopic field after one more step (with sensor accumulation) import pycuda.driver as cuda stream = cuda.Stream() sim.bodies.zero_sensor_segment_async(stream) sim.stepper.step(1, action_gpu=sim.bodies.action_gpu, obs_gpu=sim.bodies.obs_gpu, stream=stream) stream.synchronize() macro = sim.get_macroscopic() ux = macro["ux"] uy = macro["uy"] results = {} all_pass = True for sid in sensor_ids: cells_arr, _ = sim.bodies.get(sid).get_sensor_list( sim.lbm_cfg.nx, sim.lbm_cfg.ny ) cell_idx = np.asarray(cells_arr, dtype=np.int64) ux_rav = ux.ravel().astype(np.float64) uy_rav = uy.ravel().astype(np.float64) sensor_ux_mean = float(np.mean(ux_rav[cell_idx])) sensor_uy_mean = float(np.mean(uy_rav[cell_idx])) sensor_reading = sim.read_sensor(sid) sensor_reading_x = float(sensor_reading[0]) sensor_reading_y = float(sensor_reading[1]) diff_ux = abs(sensor_reading_x - sensor_ux_mean) diff_uy = abs(sensor_reading_y - sensor_uy_mean) passed = diff_ux < 1e-4 and diff_uy < 1e-4 if not passed: all_pass = False results[f"sensor_{sid}_pos{positions[i]}"] = { "sensor_reading": [sensor_reading_x, sensor_reading_y], "manual_average": [sensor_ux_mean, sensor_uy_mean], "diff": [float(diff_ux), float(diff_uy)], "n_cells": int(len(cells_arr)), "pass": bool(passed), } status = "PASS" if passed else "FAIL" print( f" Sensor {sid} @ {positions[sid]}: " f"reading=({sensor_reading_x:.8f},{sensor_reading_y:.8f}) " f"manual=({sensor_ux_mean:.8f},{sensor_uy_mean:.8f}) " f"diff=({diff_ux:.2e},{diff_uy:.2e}) " f"cells={len(cells_arr)} [{status}]" ) sim.close() summary = {"all_pass": bool(all_pass), "results": results} print(f"\nSensor accuracy: {'ALL PASS' if all_pass else 'SOME FAILED'}") return summary if __name__ == "__main__": result = test_sensor_accuracy() sys.exit(0 if result["all_pass"] else 1)