#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use crate::compute::{WgpuBackend, WgpuBufferHandle};
#[derive(Debug, Clone)]
pub struct SphConfig {
pub n_particles: usize,
pub smoothing_h: f64,
pub rest_density: f64,
pub pressure_k: f64,
pub viscosity: f64,
pub gravity: f64,
pub particle_mass: f64,
pub domain_min: [f64; 3],
pub domain_max: [f64; 3],
pub boundary_restitution: f64,
}
impl Default for SphConfig {
fn default() -> Self {
let h = 0.05;
Self {
n_particles: 256,
smoothing_h: h,
rest_density: 1000.0,
pressure_k: 100.0,
viscosity: 0.01,
gravity: 9.81,
particle_mass: 0.0, domain_min: [-1.0, 0.0, -1.0],
domain_max: [1.0, 2.0, 1.0],
boundary_restitution: 0.3,
}
}
}
#[derive(Debug)]
pub struct SphParticleState {
pub n: usize,
pub pos_x: Vec<f64>,
pub pos_y: Vec<f64>,
pub pos_z: Vec<f64>,
pub vel_x: Vec<f64>,
pub vel_y: Vec<f64>,
pub vel_z: Vec<f64>,
pub density: Vec<f64>,
pub pressure: Vec<f64>,
}
impl SphParticleState {
pub fn new(n: usize) -> Self {
Self {
n,
pos_x: vec![0.0; n],
pos_y: vec![0.0; n],
pos_z: vec![0.0; n],
vel_x: vec![0.0; n],
vel_y: vec![0.0; n],
vel_z: vec![0.0; n],
density: vec![0.0; n],
pressure: vec![0.0; n],
}
}
pub fn zero_velocities(&mut self) {
self.vel_x.fill(0.0);
self.vel_y.fill(0.0);
self.vel_z.fill(0.0);
}
}
#[inline]
pub fn cubic_spline_w3(r: f64, h: f64) -> f64 {
let sigma = 8.0 / std::f64::consts::PI;
let q = r / h;
let coeff = sigma / (h * h * h);
if q >= 1.0 {
0.0
} else if q >= 0.5 {
let t = 1.0 - q;
coeff * 2.0 * t * t * t
} else {
coeff * (6.0 * q * q * (q - 1.0) + 1.0)
}
}
#[inline]
pub fn cubic_spline_dw_dr(r: f64, h: f64) -> f64 {
let sigma = 8.0 / std::f64::consts::PI;
let q = r / h;
let coeff = sigma / (h * h * h * h);
if r < 1e-12 || q >= 1.0 {
0.0
} else if q >= 0.5 {
let t = 1.0 - q;
coeff * (-6.0 * t * t)
} else {
coeff * (6.0 * q * (3.0 * q - 2.0))
}
}
pub struct SphSimulation {
pub config: SphConfig,
pub state: SphParticleState,
backend: Option<WgpuBackend>,
buf_pos_x: Option<WgpuBufferHandle>,
buf_pos_y: Option<WgpuBufferHandle>,
buf_pos_z: Option<WgpuBufferHandle>,
buf_vel_x: Option<WgpuBufferHandle>,
buf_vel_y: Option<WgpuBufferHandle>,
buf_vel_z: Option<WgpuBufferHandle>,
buf_density: Option<WgpuBufferHandle>,
buf_pressure: Option<WgpuBufferHandle>,
pub time: f64,
}
impl SphSimulation {
pub fn new(mut config: SphConfig) -> Self {
if config.particle_mass == 0.0 {
let vol = (2.0 * config.smoothing_h).powi(3);
config.particle_mass = config.rest_density * vol;
}
let n = config.n_particles;
let (backend, bufs) = Self::init_gpu(n);
let state = SphParticleState::new(n);
let (bx, by, bz, bvx, bvy, bvz, bd, bp) = bufs;
Self {
config,
state,
backend,
buf_pos_x: bx,
buf_pos_y: by,
buf_pos_z: bz,
buf_vel_x: bvx,
buf_vel_y: bvy,
buf_vel_z: bvz,
buf_density: bd,
buf_pressure: bp,
time: 0.0,
}
}
#[allow(clippy::type_complexity)]
fn init_gpu(
n: usize,
) -> (
Option<WgpuBackend>,
(
Option<WgpuBufferHandle>,
Option<WgpuBufferHandle>,
Option<WgpuBufferHandle>,
Option<WgpuBufferHandle>,
Option<WgpuBufferHandle>,
Option<WgpuBufferHandle>,
Option<WgpuBufferHandle>,
Option<WgpuBufferHandle>,
),
) {
match WgpuBackend::try_new() {
Ok(mut b) => {
b.register_shader(
"sph_density",
crate::compute::wgpu_backend::WGSL_SPH_DENSITY,
);
let bx = Some(b.create_buffer(n));
let by = Some(b.create_buffer(n));
let bz = Some(b.create_buffer(n));
let bvx = Some(b.create_buffer(n));
let bvy = Some(b.create_buffer(n));
let bvz = Some(b.create_buffer(n));
let bd = Some(b.create_buffer(n));
let bp = Some(b.create_buffer(n));
(Some(b), (bx, by, bz, bvx, bvy, bvz, bd, bp))
}
Err(_) => (None, (None, None, None, None, None, None, None, None)),
}
}
pub fn has_gpu(&self) -> bool {
self.backend.is_some()
}
pub fn step(&mut self, dt: f64) {
let n = self.config.n_particles;
if self.backend.is_some() {
self.step_gpu(dt);
} else {
self.step_cpu(dt, n);
}
self.time += dt;
}
fn step_gpu(&mut self, dt: f64) {
let n = self.config.n_particles;
let bx = self.buf_pos_x.expect("buf_pos_x allocated in new_gpu");
let by = self.buf_pos_y.expect("buf_pos_y allocated in new_gpu");
let bz = self.buf_pos_z.expect("buf_pos_z allocated in new_gpu");
let bvx = self.buf_vel_x.expect("buf_vel_x allocated in new_gpu");
let bvy = self.buf_vel_y.expect("buf_vel_y allocated in new_gpu");
let bvz = self.buf_vel_z.expect("buf_vel_z allocated in new_gpu");
let bd = self.buf_density.expect("buf_density allocated in new_gpu");
{
let b = self
.backend
.as_mut()
.expect("step_gpu called only when backend is Some");
b.write_buffer(bx, &self.state.pos_x);
b.write_buffer(by, &self.state.pos_y);
b.write_buffer(bz, &self.state.pos_z);
b.write_buffer(bvx, &self.state.vel_x);
b.write_buffer(bvy, &self.state.vel_y);
b.write_buffer(bvz, &self.state.vel_z);
let wg = (n as u32).div_ceil(64);
b.dispatch("sph_density", &[bx, by, bz, bd], wg);
let density = b.read_buffer(bd);
for (i, &d) in density.iter().enumerate() {
self.state.density[i] = d;
}
}
self.pressure_and_integrate(dt, n);
{
let b = self
.backend
.as_mut()
.expect("step_gpu called only when backend is Some");
b.write_buffer(bvx, &self.state.vel_x);
b.write_buffer(bvy, &self.state.vel_y);
b.write_buffer(bvz, &self.state.vel_z);
b.write_buffer(bx, &self.state.pos_x);
b.write_buffer(by, &self.state.pos_y);
b.write_buffer(bz, &self.state.pos_z);
}
}
fn step_cpu(&mut self, dt: f64, n: usize) {
let h = self.config.smoothing_h;
let m = self.config.particle_mass;
let h2 = (2.0 * h) * (2.0 * h);
for i in 0..n {
let mut rho = 0.0;
for j in 0..n {
let dx = self.state.pos_x[i] - self.state.pos_x[j];
let dy = self.state.pos_y[i] - self.state.pos_y[j];
let dz = self.state.pos_z[i] - self.state.pos_z[j];
let r2 = dx * dx + dy * dy + dz * dz;
if r2 < h2 {
rho += m * cubic_spline_w3(r2.sqrt(), h);
}
}
self.state.density[i] = rho.max(1e-6);
}
self.pressure_and_integrate(dt, n);
}
fn pressure_and_integrate(&mut self, dt: f64, n: usize) {
let rho0 = self.config.rest_density;
let k = self.config.pressure_k;
let nu = self.config.viscosity;
let m = self.config.particle_mass;
let h = self.config.smoothing_h;
let g = self.config.gravity;
let h2 = (2.0 * h) * (2.0 * h);
for i in 0..n {
self.state.pressure[i] = k * (self.state.density[i] / rho0 - 1.0);
}
let mut ax = vec![0.0_f64; n];
let mut ay = vec![-g; n]; let mut az = vec![0.0_f64; n];
for i in 0..n {
let pi = self.state.pressure[i];
let rhi = self.state.density[i];
for j in 0..n {
if i == j {
continue;
}
let dx = self.state.pos_x[i] - self.state.pos_x[j];
let dy = self.state.pos_y[i] - self.state.pos_y[j];
let dz = self.state.pos_z[i] - self.state.pos_z[j];
let r2 = dx * dx + dy * dy + dz * dz;
if r2 < h2 && r2 > 1e-12 {
let r = r2.sqrt();
let pj = self.state.pressure[j];
let rhj = self.state.density[j];
let dw = cubic_spline_dw_dr(r, h);
let pf = -m * (pi / (rhi * rhi) + pj / (rhj * rhj)) * dw;
ax[i] += pf * dx / r;
ay[i] += pf * dy / r;
az[i] += pf * dz / r;
let vdotr = (self.state.vel_x[i] - self.state.vel_x[j]) * dx
+ (self.state.vel_y[i] - self.state.vel_y[j]) * dy
+ (self.state.vel_z[i] - self.state.vel_z[j]) * dz;
if vdotr < 0.0 {
let vf = nu * m / rhj * vdotr / (r2 + 0.01 * h * h) * dw / r;
ax[i] += vf * dx;
ay[i] += vf * dy;
az[i] += vf * dz;
}
}
}
}
for i in 0..n {
self.state.vel_x[i] += ax[i] * dt;
self.state.vel_y[i] += ay[i] * dt;
self.state.vel_z[i] += az[i] * dt;
self.state.pos_x[i] += self.state.vel_x[i] * dt;
self.state.pos_y[i] += self.state.vel_y[i] * dt;
self.state.pos_z[i] += self.state.vel_z[i] * dt;
}
let [xmin, ymin, zmin] = self.config.domain_min;
let [xmax, ymax, zmax] = self.config.domain_max;
let e = self.config.boundary_restitution;
macro_rules! reflect {
($pos:expr, $vel:expr, $min:expr, $max:expr) => {
if $pos < $min {
$pos = $min;
$vel = $vel.abs() * e;
}
if $pos > $max {
$pos = $max;
$vel = -$vel.abs() * e;
}
};
}
for i in 0..n {
reflect!(self.state.pos_x[i], self.state.vel_x[i], xmin, xmax);
reflect!(self.state.pos_y[i], self.state.vel_y[i], ymin, ymax);
reflect!(self.state.pos_z[i], self.state.vel_z[i], zmin, zmax);
}
}
pub fn kinetic_energy(&self) -> f64 {
let m = self.config.particle_mass;
let n = self.config.n_particles;
(0..n)
.map(|i| {
let v2 = self.state.vel_x[i].powi(2)
+ self.state.vel_y[i].powi(2)
+ self.state.vel_z[i].powi(2);
0.5 * m * v2
})
.sum()
}
pub fn mean_density(&self) -> f64 {
self.state.density.iter().sum::<f64>() / self.config.n_particles as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cubic_spline_w3_normalisation() {
let h = 0.1;
assert!(cubic_spline_w3(0.0, h) > 0.0);
assert_eq!(cubic_spline_w3(2.0 * h, h), 0.0);
assert_eq!(cubic_spline_w3(2.1 * h, h), 0.0);
}
#[test]
fn test_cubic_spline_dw_dr() {
let h = 0.1;
assert_eq!(cubic_spline_dw_dr(0.0, h), 0.0);
assert_eq!(cubic_spline_dw_dr(3.0 * h, h), 0.0);
}
#[test]
fn test_sph_construction() {
let sim = SphSimulation::new(SphConfig {
n_particles: 8,
..SphConfig::default()
});
assert_eq!(sim.state.n, 8);
assert!(sim.config.particle_mass > 0.0);
}
#[test]
fn test_sph_step_falls_under_gravity() {
let mut sim = SphSimulation::new(SphConfig {
n_particles: 4,
smoothing_h: 0.2,
gravity: 9.81,
domain_min: [-5., 0., -5.],
domain_max: [5., 10., 5.],
..SphConfig::default()
});
for i in 0..4 {
sim.state.pos_y[i] = 5.0;
}
let dt = 1.0 / 60.0;
for _ in 0..10 {
sim.step(dt);
}
for i in 0..4 {
assert!(
sim.state.pos_y[i] < 5.0,
"particle {} should fall, y={}",
i,
sim.state.pos_y[i]
);
}
}
#[test]
fn test_sph_boundary_reflection() {
let mut sim = SphSimulation::new(SphConfig {
n_particles: 1,
smoothing_h: 0.2,
gravity: 0.0, domain_min: [0., 0., 0.],
domain_max: [1., 1., 1.],
boundary_restitution: 1.0,
..SphConfig::default()
});
sim.state.pos_y[0] = 0.5;
sim.state.vel_y[0] = -10.0;
for _ in 0..10 {
sim.step(0.01);
}
assert!(sim.state.pos_y[0] >= 0.0);
assert!(sim.state.pos_y[0] <= 1.0);
}
#[test]
fn test_sph_kinetic_energy() {
let mut sim = SphSimulation::new(SphConfig {
n_particles: 4,
..SphConfig::default()
});
for i in 0..4 {
sim.state.vel_y[i] = 1.0;
}
let ke = sim.kinetic_energy();
assert!(ke > 0.0, "KE should be positive");
}
}