#![allow(clippy::needless_range_loop)]
const TAIT_GAMMA: f64 = 7.0;
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct GpuSphGrid {
pub positions: Vec<f64>,
pub masses: Vec<f64>,
pub smoothing_lengths: Vec<f64>,
pub densities: Vec<f64>,
pub pressures: Vec<f64>,
pub velocities: Vec<f64>,
pub forces: Vec<f64>,
pub rho0: f64,
pub c0: f64,
}
impl GpuSphGrid {
pub fn new(n: usize) -> Self {
Self {
positions: vec![0.0; n * 3],
masses: vec![1.0; n],
smoothing_lengths: vec![1.0; n],
densities: vec![0.0; n],
pressures: vec![0.0; n],
velocities: vec![0.0; n * 3],
forces: vec![0.0; n * 3],
rho0: 1000.0,
c0: 1500.0,
}
}
pub fn particle_count(&self) -> usize {
self.masses.len()
}
}
#[allow(dead_code)]
pub fn cubic_spline_kernel(r: f64, h: f64) -> f64 {
if h <= 0.0 {
return 0.0;
}
let q = r / h;
let sigma = 1.0 / (std::f64::consts::PI * h * h * h);
if q < 1.0 {
sigma * (1.0 - 1.5 * q * q + 0.75 * q * q * q)
} else if q < 2.0 {
let t = 2.0 - q;
sigma * 0.25 * t * t * t
} else {
0.0
}
}
#[allow(dead_code)]
pub fn cubic_spline_kernel_grad(dx: f64, dy: f64, dz: f64, h: f64) -> [f64; 3] {
let r = (dx * dx + dy * dy + dz * dz).sqrt();
if h <= 0.0 || r < 1e-15 {
return [0.0; 3];
}
let q = r / h;
let sigma = 1.0 / (std::f64::consts::PI * h * h * h);
let dw_dr = if q < 1.0 {
sigma * (-3.0 * q + 2.25 * q * q) / h
} else if q < 2.0 {
let t = 2.0 - q;
-sigma * 0.75 * t * t / h
} else {
0.0
};
let inv_r = 1.0 / r;
[dw_dr * dx * inv_r, dw_dr * dy * inv_r, dw_dr * dz * inv_r]
}
pub fn gpu_density_kernel(grid: &mut GpuSphGrid) {
let n = grid.particle_count();
for i in 0..n {
let mut rho = 0.0f64;
let xi = grid.positions[i * 3];
let yi = grid.positions[i * 3 + 1];
let zi = grid.positions[i * 3 + 2];
let hi = grid.smoothing_lengths[i];
for j in 0..n {
let xj = grid.positions[j * 3];
let yj = grid.positions[j * 3 + 1];
let zj = grid.positions[j * 3 + 2];
let r = ((xi - xj).powi(2) + (yi - yj).powi(2) + (zi - zj).powi(2)).sqrt();
rho += grid.masses[j] * cubic_spline_kernel(r, hi);
}
grid.densities[i] = rho;
}
}
pub fn gpu_pressure_tait(grid: &mut GpuSphGrid) {
let n = grid.particle_count();
let prefactor = grid.rho0 * grid.c0 * grid.c0 / TAIT_GAMMA;
for i in 0..n {
let ratio = grid.densities[i] / grid.rho0;
grid.pressures[i] = prefactor * (ratio.powf(TAIT_GAMMA) - 1.0);
}
}
pub fn gpu_force_kernel(grid: &mut GpuSphGrid, nu: f64) {
let n = grid.particle_count();
for f in grid.forces.iter_mut() {
*f = 0.0;
}
for i in 0..n {
let xi = grid.positions[i * 3];
let yi = grid.positions[i * 3 + 1];
let zi = grid.positions[i * 3 + 2];
let hi = grid.smoothing_lengths[i];
let pi = grid.pressures[i];
let rhoi = grid.densities[i];
if rhoi < 1e-15 {
continue;
}
let vxi = grid.velocities[i * 3];
let vyi = grid.velocities[i * 3 + 1];
let vzi = grid.velocities[i * 3 + 2];
for j in 0..n {
if i == j {
continue;
}
let dx = xi - grid.positions[j * 3];
let dy = yi - grid.positions[j * 3 + 1];
let dz = zi - grid.positions[j * 3 + 2];
let rhoj = grid.densities[j];
if rhoj < 1e-15 {
continue;
}
let pj = grid.pressures[j];
let mj = grid.masses[j];
let grad = cubic_spline_kernel_grad(dx, dy, dz, hi);
let coeff = -mj * (pi / (rhoi * rhoi) + pj / (rhoj * rhoj));
let dvx = vxi - grid.velocities[j * 3];
let dvy = vyi - grid.velocities[j * 3 + 1];
let dvz = vzi - grid.velocities[j * 3 + 2];
let r2 = dx * dx + dy * dy + dz * dz + 1e-15;
let vdotr = dvx * dx + dvy * dy + dvz * dz;
let visc = -nu * mj * vdotr / (rhoj * r2);
grid.forces[i * 3] += (coeff + visc) * grad[0];
grid.forces[i * 3 + 1] += (coeff + visc) * grad[1];
grid.forces[i * 3 + 2] += (coeff + visc) * grad[2];
}
}
}
pub fn gpu_neighbor_list(grid: &GpuSphGrid, cell_size: f64) -> Vec<Vec<usize>> {
let n = grid.particle_count();
let mut neighbors: Vec<Vec<usize>> = vec![Vec::new(); n];
let cutoff2 = (2.0 * cell_size) * (2.0 * cell_size);
for i in 0..n {
let xi = grid.positions[i * 3];
let yi = grid.positions[i * 3 + 1];
let zi = grid.positions[i * 3 + 2];
for j in 0..n {
if i == j {
continue;
}
let dx = xi - grid.positions[j * 3];
let dy = yi - grid.positions[j * 3 + 1];
let dz = zi - grid.positions[j * 3 + 2];
if dx * dx + dy * dy + dz * dz <= cutoff2 {
neighbors[i].push(j);
}
}
}
neighbors
}
pub fn launch_density_pass(grid: &mut GpuSphGrid, nu: f64) {
gpu_density_kernel(grid);
gpu_pressure_tait(grid);
gpu_force_kernel(grid, nu);
}
#[cfg(test)]
mod tests {
use super::*;
fn make_two_particle_grid() -> GpuSphGrid {
let mut g = GpuSphGrid::new(2);
g.positions = vec![0.0, 0.0, 0.0, 0.5, 0.0, 0.0];
g.masses = vec![1.0, 1.0];
g.smoothing_lengths = vec![1.0, 1.0];
g.rho0 = 1000.0;
g.c0 = 100.0;
g
}
#[test]
fn test_new_grid_particle_count() {
let g = GpuSphGrid::new(10);
assert_eq!(g.particle_count(), 10);
}
#[test]
fn test_new_grid_default_rho0() {
let g = GpuSphGrid::new(1);
assert!((g.rho0 - 1000.0).abs() < 1e-10);
}
#[test]
fn test_new_grid_default_c0() {
let g = GpuSphGrid::new(1);
assert!((g.c0 - 1500.0).abs() < 1e-10);
}
#[test]
fn test_cubic_kernel_zero_distance() {
let w = cubic_spline_kernel(0.0, 1.0);
assert!(w > 0.0, "kernel at r=0 should be positive, got {w}");
}
#[test]
fn test_cubic_kernel_beyond_support() {
let w = cubic_spline_kernel(3.0, 1.0);
assert!(
(w).abs() < 1e-15,
"kernel beyond 2h should be zero, got {w}"
);
}
#[test]
fn test_cubic_kernel_positive_within_support() {
let w = cubic_spline_kernel(1.5, 1.0);
assert!(w >= 0.0);
}
#[test]
fn test_cubic_kernel_zero_h() {
let w = cubic_spline_kernel(1.0, 0.0);
assert_eq!(w, 0.0);
}
#[test]
fn test_cubic_kernel_grad_zero_displacement() {
let g = cubic_spline_kernel_grad(0.0, 0.0, 0.0, 1.0);
assert_eq!(g, [0.0; 3]);
}
#[test]
fn test_cubic_kernel_grad_symmetry() {
let g1 = cubic_spline_kernel_grad(0.3, 0.0, 0.0, 1.0);
let g2 = cubic_spline_kernel_grad(-0.3, 0.0, 0.0, 1.0);
assert!((g1[0] + g2[0]).abs() < 1e-12);
}
#[test]
fn test_density_kernel_single_particle() {
let mut g = GpuSphGrid::new(1);
g.positions = vec![0.0, 0.0, 0.0];
g.masses = vec![1.0];
g.smoothing_lengths = vec![1.0];
gpu_density_kernel(&mut g);
assert!(g.densities[0] > 0.0);
}
#[test]
fn test_density_kernel_two_particles() {
let mut g = make_two_particle_grid();
gpu_density_kernel(&mut g);
assert!(g.densities[0] > 0.0);
assert!(g.densities[1] > 0.0);
}
#[test]
fn test_density_kernel_symmetric() {
let mut g = make_two_particle_grid();
gpu_density_kernel(&mut g);
assert!((g.densities[0] - g.densities[1]).abs() < 1e-12);
}
#[test]
fn test_pressure_tait_zero_density() {
let mut g = GpuSphGrid::new(1);
g.densities = vec![0.0];
g.rho0 = 1000.0;
g.c0 = 100.0;
gpu_pressure_tait(&mut g);
assert!(g.pressures[0] < 0.0);
}
#[test]
fn test_pressure_tait_at_reference_density() {
let mut g = GpuSphGrid::new(1);
g.rho0 = 1000.0;
g.c0 = 100.0;
g.densities = vec![g.rho0];
gpu_pressure_tait(&mut g);
assert!(g.pressures[0].abs() < 1e-6);
}
#[test]
fn test_pressure_tait_above_reference() {
let mut g = GpuSphGrid::new(1);
g.rho0 = 1000.0;
g.c0 = 100.0;
g.densities = vec![1100.0];
gpu_pressure_tait(&mut g);
assert!(g.pressures[0] > 0.0);
}
#[test]
fn test_force_kernel_self_zero() {
let mut g = GpuSphGrid::new(1);
g.positions = vec![0.0, 0.0, 0.0];
g.masses = vec![1.0];
g.densities = vec![1000.0];
g.pressures = vec![0.0];
gpu_force_kernel(&mut g, 0.0);
assert!((g.forces[0]).abs() < 1e-15);
}
#[test]
fn test_force_kernel_repulsion() {
let mut g = make_two_particle_grid();
gpu_density_kernel(&mut g);
gpu_pressure_tait(&mut g);
gpu_force_kernel(&mut g, 0.0);
let fx0 = g.forces[0];
let fx1 = g.forces[3];
assert!(fx0 * fx1 < 0.0 || (fx0.abs() < 1e-12 && fx1.abs() < 1e-12));
}
#[test]
fn test_force_kernel_zeros_forces_first() {
let mut g = GpuSphGrid::new(2);
g.forces = vec![9.9, 9.9, 9.9, 9.9, 9.9, 9.9];
gpu_force_kernel(&mut g, 0.0);
for &f in &g.forces {
assert!(f.abs() < 1e-15);
}
}
#[test]
fn test_neighbor_list_all_close() {
let g = make_two_particle_grid();
let nl = gpu_neighbor_list(&g, 1.0);
assert!(nl[0].contains(&1));
assert!(nl[1].contains(&0));
}
#[test]
fn test_neighbor_list_too_far() {
let mut g = GpuSphGrid::new(2);
g.positions = vec![0.0, 0.0, 0.0, 100.0, 0.0, 0.0];
let nl = gpu_neighbor_list(&g, 1.0);
assert!(nl[0].is_empty());
assert!(nl[1].is_empty());
}
#[test]
fn test_neighbor_list_no_self() {
let g = make_two_particle_grid();
let nl = gpu_neighbor_list(&g, 1.0);
assert!(!nl[0].contains(&0));
assert!(!nl[1].contains(&1));
}
#[test]
fn test_launch_density_pass_updates_all() {
let mut g = make_two_particle_grid();
launch_density_pass(&mut g, 0.01);
assert!(g.densities[0] > 0.0);
assert!(g.densities[1] > 0.0);
}
#[test]
fn test_launch_density_pass_idempotent() {
let mut g1 = make_two_particle_grid();
let mut g2 = make_two_particle_grid();
launch_density_pass(&mut g1, 0.0);
launch_density_pass(&mut g2, 0.0);
for i in 0..2 {
assert!((g1.densities[i] - g2.densities[i]).abs() < 1e-12);
assert!((g1.pressures[i] - g2.pressures[i]).abs() < 1e-12);
}
}
#[test]
fn test_force_magnitude_finite() {
let mut g = make_two_particle_grid();
launch_density_pass(&mut g, 0.01);
for &f in &g.forces {
assert!(f.is_finite(), "force is not finite: {f}");
}
}
#[test]
fn test_density_five_particles() {
let n = 5;
let mut g = GpuSphGrid::new(n);
for i in 0..n {
g.positions[i * 3] = i as f64 * 0.4;
}
gpu_density_kernel(&mut g);
assert!(g.densities[2] >= g.densities[0]);
}
#[test]
fn test_pressure_increases_with_density() {
let mut g = GpuSphGrid::new(2);
g.rho0 = 1000.0;
g.c0 = 100.0;
g.densities = vec![1000.0, 1200.0];
gpu_pressure_tait(&mut g);
assert!(g.pressures[1] > g.pressures[0]);
}
#[test]
fn test_gpu_sph_grid_clone() {
let g = GpuSphGrid::new(3);
let g2 = g.clone();
assert_eq!(g2.particle_count(), 3);
}
#[test]
fn test_gpu_sph_grid_debug() {
let g = GpuSphGrid::new(1);
let s = format!("{g:?}");
assert!(s.contains("GpuSphGrid"));
}
}