305 lines
10 KiB
Rust
305 lines
10 KiB
Rust
//! Tests for GPU kernels module
|
|
//!
|
|
//! This module tests the real CUDA implementations of CFD kernels including:
|
|
//! - Advection schemes (upwind, central differencing)
|
|
//! - Diffusion schemes (implicit/explicit)
|
|
//! - Pressure Poisson solver
|
|
//! - Matrix operations for CFD
|
|
|
|
use approx::assert_relative_eq;
|
|
use rtx_cfd::{CfdConfig, CfdResult};
|
|
|
|
#[cfg(feature = "cuda")]
|
|
mod cuda_tests {
|
|
use super::*;
|
|
use rtx_cfd::kernels::{
|
|
AdvectionKernel, AdvectionScheme, CudaKernelManager, DiffusionKernel, DiffusionScheme,
|
|
MatrixOpsKernel, PoissonKernel,
|
|
};
|
|
|
|
#[tokio::test]
|
|
async fn test_advection_kernel_upwind() -> CfdResult<()> {
|
|
let config = CfdConfig::new().with_gpu(true);
|
|
let kernel_manager = CudaKernelManager::new(&config)?;
|
|
let advection_kernel = AdvectionKernel::new(&kernel_manager, AdvectionScheme::Upwind)?;
|
|
|
|
// Test data: 1D advection with known analytical solution
|
|
let nx = 128;
|
|
let dx = 1.0 / (nx as f64);
|
|
let dt = 0.001;
|
|
let velocity = 1.0;
|
|
|
|
// Initial condition: Gaussian pulse
|
|
let mut phi = vec![0.0f32; nx];
|
|
let center = nx / 4;
|
|
let sigma = 5.0;
|
|
for i in 0..nx {
|
|
let x = (i as f64 - center as f64) * dx;
|
|
phi[i] = (-(x * x) / (2.0 * sigma * sigma)).exp() as f32;
|
|
}
|
|
|
|
// Allocate GPU memory
|
|
let mut d_phi = kernel_manager.allocate_f32(nx)?;
|
|
let d_phi_new = kernel_manager.allocate_f32(nx)?;
|
|
|
|
// Copy to GPU
|
|
kernel_manager.copy_to_device(&phi, &mut d_phi)?;
|
|
|
|
// Run advection kernel
|
|
advection_kernel.apply(&d_phi, &d_phi_new, velocity as f32, dt as f32, dx as f32)?;
|
|
|
|
// Copy result back
|
|
let mut result = vec![0.0f32; nx];
|
|
kernel_manager.copy_from_device(&d_phi_new, &mut result)?;
|
|
|
|
// Verify that the pulse has moved (mass conservation)
|
|
let initial_mass: f32 = phi.iter().sum();
|
|
let final_mass: f32 = result.iter().sum();
|
|
assert_relative_eq!(initial_mass, final_mass, epsilon = 1e-6);
|
|
|
|
// Verify that maximum has shifted in the correct direction
|
|
let initial_max_idx = phi
|
|
.iter()
|
|
.enumerate()
|
|
.max_by(|a, b| a.1.total_cmp(b.1))
|
|
.unwrap()
|
|
.0;
|
|
let final_max_idx = result
|
|
.iter()
|
|
.enumerate()
|
|
.max_by(|a, b| a.1.total_cmp(b.1))
|
|
.unwrap()
|
|
.0;
|
|
|
|
assert!(
|
|
final_max_idx > initial_max_idx,
|
|
"Pulse should move in positive direction"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_advection_kernel_central() -> CfdResult<()> {
|
|
let config = CfdConfig::new().with_gpu(true);
|
|
let kernel_manager = CudaKernelManager::new(&config)?;
|
|
let advection_kernel = AdvectionKernel::new(&kernel_manager, AdvectionScheme::Central)?;
|
|
|
|
// Test data: smooth sinusoidal wave
|
|
let nx = 256;
|
|
let dx = 2.0 * std::f64::consts::PI / (nx as f64);
|
|
let dt = 0.001;
|
|
let velocity = 1.0;
|
|
|
|
let mut phi = vec![0.0f32; nx];
|
|
for i in 0..nx {
|
|
let x = i as f64 * dx;
|
|
phi[i] = (2.0 * x).sin() as f32;
|
|
}
|
|
|
|
let mut d_phi = kernel_manager.allocate_f32(nx)?;
|
|
let d_phi_new = kernel_manager.allocate_f32(nx)?;
|
|
|
|
kernel_manager.copy_to_device(&phi, &mut d_phi)?;
|
|
advection_kernel.apply(&d_phi, &d_phi_new, velocity as f32, dt as f32, dx as f32)?;
|
|
|
|
let mut result = vec![0.0f32; nx];
|
|
kernel_manager.copy_from_device(&d_phi_new, &mut result)?;
|
|
|
|
// For central scheme, verify mass conservation and smoothness
|
|
let initial_mass: f32 = phi.iter().sum();
|
|
let final_mass: f32 = result.iter().sum();
|
|
assert_relative_eq!(initial_mass, final_mass, epsilon = 1e-5);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_diffusion_kernel_explicit() -> CfdResult<()> {
|
|
let config = CfdConfig::new().with_gpu(true);
|
|
let kernel_manager = CudaKernelManager::new(&config)?;
|
|
let diffusion_kernel = DiffusionKernel::new(&kernel_manager, DiffusionScheme::Explicit)?;
|
|
|
|
// Test 1D heat equation with analytical solution
|
|
let nx = 128;
|
|
let dx = 1.0 / (nx as f64);
|
|
let dt = 0.0001; // Small dt for stability
|
|
let alpha = 0.1; // Thermal diffusivity
|
|
|
|
// Initial condition: step function
|
|
let mut temperature = vec![0.0f32; nx];
|
|
for i in nx / 4..3 * nx / 4 {
|
|
temperature[i] = 1.0;
|
|
}
|
|
|
|
let mut d_temp = kernel_manager.allocate_f32(nx)?;
|
|
let d_temp_new = kernel_manager.allocate_f32(nx)?;
|
|
|
|
kernel_manager.copy_to_device(&temperature, &mut d_temp)?;
|
|
diffusion_kernel.apply(&d_temp, &d_temp_new, alpha as f32, dt as f32, dx as f32)?;
|
|
|
|
let mut result = vec![0.0f32; nx];
|
|
kernel_manager.copy_from_device(&d_temp_new, &mut result)?;
|
|
|
|
// Verify diffusion: edges should be smoother, total heat conserved
|
|
let initial_total: f32 = temperature.iter().sum();
|
|
let final_total: f32 = result.iter().sum();
|
|
assert_relative_eq!(initial_total, final_total, epsilon = 1e-6);
|
|
|
|
// Verify smoothing: gradient at edges should be reduced
|
|
let initial_gradient = (temperature[nx / 4] - temperature[nx / 4 - 1]).abs();
|
|
let final_gradient = (result[nx / 4] - result[nx / 4 - 1]).abs();
|
|
assert!(
|
|
final_gradient < initial_gradient,
|
|
"Diffusion should smooth gradients"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_poisson_kernel_2d() -> CfdResult<()> {
|
|
let config = CfdConfig::new().with_gpu(true);
|
|
let kernel_manager = CudaKernelManager::new(&config)?;
|
|
let poisson_kernel = PoissonKernel::new(&kernel_manager)?;
|
|
|
|
// Test 2D Poisson equation: ∇²φ = f
|
|
// Analytical solution: φ(x,y) = sin(πx)sin(πy), f = -2π²sin(πx)sin(πy)
|
|
let nx = 64;
|
|
let ny = 64;
|
|
let dx = 1.0 / (nx as f64);
|
|
let dy = 1.0 / (ny as f64);
|
|
|
|
// Setup source term f
|
|
let mut source = vec![0.0f32; nx * ny];
|
|
for j in 0..ny {
|
|
for i in 0..nx {
|
|
let x = i as f64 * dx;
|
|
let y = j as f64 * dy;
|
|
let idx = j * nx + i;
|
|
source[idx] = (-2.0
|
|
* std::f64::consts::PI.powi(2)
|
|
* (std::f64::consts::PI * x).sin()
|
|
* (std::f64::consts::PI * y).sin()) as f32;
|
|
}
|
|
}
|
|
|
|
// Initial guess (zeros)
|
|
let phi = vec![0.0f32; nx * ny];
|
|
|
|
let mut d_phi = kernel_manager.allocate_f32(nx * ny)?;
|
|
let mut d_source = kernel_manager.allocate_f32(nx * ny)?;
|
|
|
|
kernel_manager.copy_to_device(&phi, &mut d_phi)?;
|
|
kernel_manager.copy_to_device(&source, &mut d_source)?;
|
|
|
|
// Solve Poisson equation
|
|
let max_iterations = 1000;
|
|
let tolerance = 1e-6;
|
|
let iterations = poisson_kernel.solve_2d(
|
|
&d_phi,
|
|
&d_source,
|
|
nx,
|
|
ny,
|
|
dx as f32,
|
|
dy as f32,
|
|
max_iterations,
|
|
tolerance,
|
|
)?;
|
|
|
|
let mut result = vec![0.0f32; nx * ny];
|
|
kernel_manager.copy_from_device(&d_phi, &mut result)?;
|
|
|
|
// Verify against analytical solution
|
|
let mut max_error = 0.0f32;
|
|
for j in 1..ny - 1 {
|
|
for i in 1..nx - 1 {
|
|
let x = i as f64 * dx;
|
|
let y = j as f64 * dy;
|
|
let idx = j * nx + i;
|
|
let analytical =
|
|
(std::f64::consts::PI * x).sin() * (std::f64::consts::PI * y).sin();
|
|
let error = (result[idx] - analytical as f32).abs();
|
|
max_error = max_error.max(error);
|
|
}
|
|
}
|
|
|
|
assert!(iterations < max_iterations, "Solver should converge");
|
|
assert!(max_error < 0.1, "Solution should be reasonably accurate");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_matrix_ops_kernel() -> CfdResult<()> {
|
|
let config = CfdConfig::new().with_gpu(true);
|
|
let kernel_manager = CudaKernelManager::new(&config)?;
|
|
let matrix_ops = MatrixOpsKernel::new(&kernel_manager)?;
|
|
|
|
// Test sparse matrix-vector multiplication (typical in CFD)
|
|
let n = 1000;
|
|
let mut diagonal = vec![2.0f32; n];
|
|
let mut off_diagonal = vec![-1.0f32; n - 1];
|
|
let mut x = vec![1.0f32; n];
|
|
|
|
// Create tridiagonal matrix (common in CFD discretizations)
|
|
let mut d_diag = kernel_manager.allocate_f32(n)?;
|
|
let mut d_off_diag = kernel_manager.allocate_f32(n - 1)?;
|
|
let mut d_x = kernel_manager.allocate_f32(n)?;
|
|
let d_y = kernel_manager.allocate_f32(n)?;
|
|
|
|
kernel_manager.copy_to_device(&diagonal, &mut d_diag)?;
|
|
kernel_manager.copy_to_device(&off_diagonal, &mut d_off_diag)?;
|
|
kernel_manager.copy_to_device(&x, &mut d_x)?;
|
|
|
|
// Perform A*x = y operation
|
|
matrix_ops.tridiagonal_matvec(&d_diag, &d_off_diag, &d_x, &d_y, n)?;
|
|
|
|
let mut result = vec![0.0f32; n];
|
|
kernel_manager.copy_from_device(&d_y, &mut result)?;
|
|
|
|
// Verify result manually for first few elements
|
|
assert_relative_eq!(result[0], 2.0 * x[0] - x[1], epsilon = 1e-6);
|
|
assert_relative_eq!(result[1], -x[0] + 2.0 * x[1] - x[2], epsilon = 1e-6);
|
|
assert_relative_eq!(result[n - 1], -x[n - 2] + 2.0 * x[n - 1], epsilon = 1e-6);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "cuda"))]
|
|
mod cpu_fallback_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_cpu_fallback_advection() {
|
|
// Basic test to ensure CPU fallback works
|
|
let config = CfdConfig::new().with_gpu(false);
|
|
// Implementation will provide CPU fallback
|
|
assert!(config.validate().is_ok());
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_kernel_manager_initialization() -> CfdResult<()> {
|
|
let config = CfdConfig::new();
|
|
|
|
#[cfg(feature = "cuda")]
|
|
{
|
|
// Should successfully create kernel manager
|
|
let result = rtx_cfd::kernels::CudaKernelManager::new(&config);
|
|
// May fail if no CUDA device available - that's expected in CI
|
|
match result {
|
|
Ok(_) => println!("CUDA kernel manager created successfully"),
|
|
Err(e) => println!("CUDA not available (expected in CI): {}", e),
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "cuda"))]
|
|
{
|
|
println!("CUDA feature not enabled - using CPU fallback");
|
|
}
|
|
|
|
Ok(())
|
|
}
|