1use crate::compute::{WgpuBackend, WgpuBufferHandle};
49
50type SphGpuBuffers = (
52 Option<WgpuBufferHandle>,
53 Option<WgpuBufferHandle>,
54 Option<WgpuBufferHandle>,
55 Option<WgpuBufferHandle>,
56 Option<WgpuBufferHandle>,
57 Option<WgpuBufferHandle>,
58 Option<WgpuBufferHandle>,
59);
60
61#[derive(Debug, Clone)]
65pub struct SphConfig {
66 pub n_particles: usize,
68 pub smoothing_h: f64,
70 pub rest_density: f64,
72 pub pressure_k: f64,
74 pub viscosity: f64,
76 pub gravity: f64,
78 pub particle_mass: f64,
80 pub domain_min: [f64; 3],
82 pub domain_max: [f64; 3],
84 pub boundary_restitution: f64,
86}
87
88impl Default for SphConfig {
89 fn default() -> Self {
90 let h = 0.05;
91 Self {
92 n_particles: 256,
93 smoothing_h: h,
94 rest_density: 1000.0,
95 pressure_k: 100.0,
96 viscosity: 0.01,
97 gravity: 9.81,
98 particle_mass: 0.0, domain_min: [-1.0, 0.0, -1.0],
100 domain_max: [1.0, 2.0, 1.0],
101 boundary_restitution: 0.3,
102 }
103 }
104}
105
106#[derive(Debug)]
110pub struct SphParticleState {
111 pub n: usize,
113 pub pos_x: Vec<f64>,
115 pub pos_y: Vec<f64>,
117 pub pos_z: Vec<f64>,
119 pub vel_x: Vec<f64>,
121 pub vel_y: Vec<f64>,
123 pub vel_z: Vec<f64>,
125 pub density: Vec<f64>,
127 pub pressure: Vec<f64>,
129}
130
131impl SphParticleState {
132 pub fn new(n: usize) -> Self {
134 Self {
135 n,
136 pos_x: vec![0.0; n],
137 pos_y: vec![0.0; n],
138 pos_z: vec![0.0; n],
139 vel_x: vec![0.0; n],
140 vel_y: vec![0.0; n],
141 vel_z: vec![0.0; n],
142 density: vec![0.0; n],
143 pressure: vec![0.0; n],
144 }
145 }
146
147 pub fn zero_velocities(&mut self) {
149 self.vel_x.fill(0.0);
150 self.vel_y.fill(0.0);
151 self.vel_z.fill(0.0);
152 }
153}
154
155#[inline]
161pub fn cubic_spline_w3(r: f64, h: f64) -> f64 {
162 let sigma = 8.0 / std::f64::consts::PI;
163 let q = r / h;
164 let coeff = sigma / (h * h * h);
165 if q >= 1.0 {
166 0.0
167 } else if q >= 0.5 {
168 let t = 1.0 - q;
169 coeff * 2.0 * t * t * t
170 } else {
171 coeff * (6.0 * q * q * (q - 1.0) + 1.0)
172 }
173}
174
175#[inline]
177pub fn cubic_spline_dw_dr(r: f64, h: f64) -> f64 {
178 let sigma = 8.0 / std::f64::consts::PI;
179 let q = r / h;
180 let coeff = sigma / (h * h * h * h);
181 if r < 1e-12 || q >= 1.0 {
182 0.0
183 } else if q >= 0.5 {
184 let t = 1.0 - q;
185 coeff * (-6.0 * t * t)
186 } else {
187 coeff * (6.0 * q * (3.0 * q - 2.0))
188 }
189}
190
191pub struct SphSimulation {
195 pub config: SphConfig,
197 pub state: SphParticleState,
199 backend: Option<WgpuBackend>,
201 buf_pos_x: Option<WgpuBufferHandle>,
203 buf_pos_y: Option<WgpuBufferHandle>,
204 buf_pos_z: Option<WgpuBufferHandle>,
205 buf_vel_x: Option<WgpuBufferHandle>,
206 buf_vel_y: Option<WgpuBufferHandle>,
207 buf_vel_z: Option<WgpuBufferHandle>,
208 buf_density: Option<WgpuBufferHandle>,
209 pub time: f64,
211}
212
213impl SphSimulation {
214 pub fn new(mut config: SphConfig) -> Self {
216 if config.particle_mass == 0.0 {
217 let vol = (2.0 * config.smoothing_h).powi(3);
218 config.particle_mass = config.rest_density * vol;
219 }
220 let n = config.n_particles;
221 let (backend, bufs) = Self::init_gpu(n);
222
223 let state = SphParticleState::new(n);
224 let (bx, by, bz, bvx, bvy, bvz, bd) = bufs;
225
226 Self {
227 config,
228 state,
229 backend,
230 buf_pos_x: bx,
231 buf_pos_y: by,
232 buf_pos_z: bz,
233 buf_vel_x: bvx,
234 buf_vel_y: bvy,
235 buf_vel_z: bvz,
236 buf_density: bd,
237 time: 0.0,
238 }
239 }
240
241 fn init_gpu(n: usize) -> (Option<WgpuBackend>, SphGpuBuffers) {
242 match WgpuBackend::try_new() {
243 Ok(mut b) => {
244 b.register_shader(
245 "sph_density",
246 crate::compute::wgpu_backend::WGSL_SPH_DENSITY,
247 );
248 let bx = Some(b.create_buffer(n));
249 let by = Some(b.create_buffer(n));
250 let bz = Some(b.create_buffer(n));
251 let bvx = Some(b.create_buffer(n));
252 let bvy = Some(b.create_buffer(n));
253 let bvz = Some(b.create_buffer(n));
254 let bd = Some(b.create_buffer(n));
255 (Some(b), (bx, by, bz, bvx, bvy, bvz, bd))
256 }
257 Err(_) => (None, (None, None, None, None, None, None, None)),
258 }
259 }
260
261 pub fn has_gpu(&self) -> bool {
263 self.backend.is_some()
264 }
265
266 pub fn step(&mut self, dt: f64) {
275 let n = self.config.n_particles;
276
277 if self.backend.is_some() {
278 self.step_gpu(dt);
279 } else {
280 self.step_cpu(dt, n);
281 }
282
283 self.time += dt;
284 }
285
286 fn step_gpu(&mut self, dt: f64) {
289 let n = self.config.n_particles;
290 let bx = self.buf_pos_x.expect("buf_pos_x allocated in new_gpu");
291 let by = self.buf_pos_y.expect("buf_pos_y allocated in new_gpu");
292 let bz = self.buf_pos_z.expect("buf_pos_z allocated in new_gpu");
293 let bvx = self.buf_vel_x.expect("buf_vel_x allocated in new_gpu");
294 let bvy = self.buf_vel_y.expect("buf_vel_y allocated in new_gpu");
295 let bvz = self.buf_vel_z.expect("buf_vel_z allocated in new_gpu");
296 let bd = self.buf_density.expect("buf_density allocated in new_gpu");
297
298 {
300 let b = self
301 .backend
302 .as_mut()
303 .expect("step_gpu called only when backend is Some");
304 b.write_buffer(bx, &self.state.pos_x);
305 b.write_buffer(by, &self.state.pos_y);
306 b.write_buffer(bz, &self.state.pos_z);
307 b.write_buffer(bvx, &self.state.vel_x);
308 b.write_buffer(bvy, &self.state.vel_y);
309 b.write_buffer(bvz, &self.state.vel_z);
310 let wg = (n as u32).div_ceil(64);
311 b.dispatch("sph_density", &[bx, by, bz, bd], wg);
312 let density = b.read_buffer(bd);
313 for (i, &d) in density.iter().enumerate() {
314 self.state.density[i] = d;
315 }
316 } self.pressure_and_integrate(dt, n);
320
321 {
323 let b = self
324 .backend
325 .as_mut()
326 .expect("step_gpu called only when backend is Some");
327 b.write_buffer(bvx, &self.state.vel_x);
328 b.write_buffer(bvy, &self.state.vel_y);
329 b.write_buffer(bvz, &self.state.vel_z);
330 b.write_buffer(bx, &self.state.pos_x);
331 b.write_buffer(by, &self.state.pos_y);
332 b.write_buffer(bz, &self.state.pos_z);
333 }
334 }
335
336 fn step_cpu(&mut self, dt: f64, n: usize) {
339 let h = self.config.smoothing_h;
341 let m = self.config.particle_mass;
342 let h2 = (2.0 * h) * (2.0 * h);
343
344 for i in 0..n {
345 let mut rho = 0.0;
346 for j in 0..n {
347 let dx = self.state.pos_x[i] - self.state.pos_x[j];
348 let dy = self.state.pos_y[i] - self.state.pos_y[j];
349 let dz = self.state.pos_z[i] - self.state.pos_z[j];
350 let r2 = dx * dx + dy * dy + dz * dz;
351 if r2 < h2 {
352 rho += m * cubic_spline_w3(r2.sqrt(), h);
353 }
354 }
355 self.state.density[i] = rho.max(1e-6);
356 }
357
358 self.pressure_and_integrate(dt, n);
359 }
360
361 fn pressure_and_integrate(&mut self, dt: f64, n: usize) {
362 let rho0 = self.config.rest_density;
363 let k = self.config.pressure_k;
364 let nu = self.config.viscosity;
365 let m = self.config.particle_mass;
366 let h = self.config.smoothing_h;
367 let g = self.config.gravity;
368 let h2 = (2.0 * h) * (2.0 * h);
369
370 for i in 0..n {
372 self.state.pressure[i] = k * (self.state.density[i] / rho0 - 1.0);
373 }
374
375 let mut ax = vec![0.0_f64; n];
377 let mut ay = vec![-g; n]; let mut az = vec![0.0_f64; n];
379
380 for i in 0..n {
381 let pi = self.state.pressure[i];
382 let rhi = self.state.density[i];
383
384 for j in 0..n {
385 if i == j {
386 continue;
387 }
388 let dx = self.state.pos_x[i] - self.state.pos_x[j];
389 let dy = self.state.pos_y[i] - self.state.pos_y[j];
390 let dz = self.state.pos_z[i] - self.state.pos_z[j];
391 let r2 = dx * dx + dy * dy + dz * dz;
392 if r2 < h2 && r2 > 1e-12 {
393 let r = r2.sqrt();
394 let pj = self.state.pressure[j];
395 let rhj = self.state.density[j];
396
397 let dw = cubic_spline_dw_dr(r, h);
399 let pf = -m * (pi / (rhi * rhi) + pj / (rhj * rhj)) * dw;
400 ax[i] += pf * dx / r;
401 ay[i] += pf * dy / r;
402 az[i] += pf * dz / r;
403
404 let vdotr = (self.state.vel_x[i] - self.state.vel_x[j]) * dx
406 + (self.state.vel_y[i] - self.state.vel_y[j]) * dy
407 + (self.state.vel_z[i] - self.state.vel_z[j]) * dz;
408 if vdotr < 0.0 {
409 let vf = nu * m / rhj * vdotr / (r2 + 0.01 * h * h) * dw / r;
410 ax[i] += vf * dx;
411 ay[i] += vf * dy;
412 az[i] += vf * dz;
413 }
414 }
415 }
416 }
417
418 for i in 0..n {
420 self.state.vel_x[i] += ax[i] * dt;
421 self.state.vel_y[i] += ay[i] * dt;
422 self.state.vel_z[i] += az[i] * dt;
423 self.state.pos_x[i] += self.state.vel_x[i] * dt;
424 self.state.pos_y[i] += self.state.vel_y[i] * dt;
425 self.state.pos_z[i] += self.state.vel_z[i] * dt;
426 }
427
428 let [xmin, ymin, zmin] = self.config.domain_min;
430 let [xmax, ymax, zmax] = self.config.domain_max;
431 let e = self.config.boundary_restitution;
432 macro_rules! reflect {
433 ($pos:expr, $vel:expr, $min:expr, $max:expr) => {
434 if $pos < $min {
435 $pos = $min;
436 $vel = $vel.abs() * e;
437 }
438 if $pos > $max {
439 $pos = $max;
440 $vel = -$vel.abs() * e;
441 }
442 };
443 }
444 for i in 0..n {
445 reflect!(self.state.pos_x[i], self.state.vel_x[i], xmin, xmax);
446 reflect!(self.state.pos_y[i], self.state.vel_y[i], ymin, ymax);
447 reflect!(self.state.pos_z[i], self.state.vel_z[i], zmin, zmax);
448 }
449 }
450
451 pub fn kinetic_energy(&self) -> f64 {
453 let m = self.config.particle_mass;
454 let n = self.config.n_particles;
455 (0..n)
456 .map(|i| {
457 let v2 = self.state.vel_x[i].powi(2)
458 + self.state.vel_y[i].powi(2)
459 + self.state.vel_z[i].powi(2);
460 0.5 * m * v2
461 })
462 .sum()
463 }
464
465 pub fn mean_density(&self) -> f64 {
467 self.state.density.iter().sum::<f64>() / self.config.n_particles as f64
468 }
469}
470
471#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_cubic_spline_w3_normalisation() {
479 let h = 0.1;
481 assert!(cubic_spline_w3(0.0, h) > 0.0);
482 assert_eq!(cubic_spline_w3(2.0 * h, h), 0.0);
483 assert_eq!(cubic_spline_w3(2.1 * h, h), 0.0);
484 }
485
486 #[test]
487 fn test_cubic_spline_dw_dr() {
488 let h = 0.1;
489 assert_eq!(cubic_spline_dw_dr(0.0, h), 0.0);
491 assert_eq!(cubic_spline_dw_dr(3.0 * h, h), 0.0);
493 }
494
495 #[test]
496 fn test_sph_construction() {
497 let sim = SphSimulation::new(SphConfig {
498 n_particles: 8,
499 ..SphConfig::default()
500 });
501 assert_eq!(sim.state.n, 8);
502 assert!(sim.config.particle_mass > 0.0);
503 }
504
505 #[test]
506 fn test_sph_step_falls_under_gravity() {
507 let mut sim = SphSimulation::new(SphConfig {
508 n_particles: 4,
509 smoothing_h: 0.2,
510 gravity: 9.81,
511 domain_min: [-5., 0., -5.],
512 domain_max: [5., 10., 5.],
513 ..SphConfig::default()
514 });
515 for i in 0..4 {
517 sim.state.pos_y[i] = 5.0;
518 }
519
520 let dt = 1.0 / 60.0;
521 for _ in 0..10 {
522 sim.step(dt);
523 }
524
525 for i in 0..4 {
527 assert!(
528 sim.state.pos_y[i] < 5.0,
529 "particle {} should fall, y={}",
530 i,
531 sim.state.pos_y[i]
532 );
533 }
534 }
535
536 #[test]
537 fn test_sph_boundary_reflection() {
538 let mut sim = SphSimulation::new(SphConfig {
539 n_particles: 1,
540 smoothing_h: 0.2,
541 gravity: 0.0, domain_min: [0., 0., 0.],
543 domain_max: [1., 1., 1.],
544 boundary_restitution: 1.0,
545 ..SphConfig::default()
546 });
547 sim.state.pos_y[0] = 0.5;
548 sim.state.vel_y[0] = -10.0; for _ in 0..10 {
551 sim.step(0.01);
552 }
553
554 assert!(sim.state.pos_y[0] >= 0.0);
556 assert!(sim.state.pos_y[0] <= 1.0);
557 }
558
559 #[test]
560 fn test_sph_kinetic_energy() {
561 let mut sim = SphSimulation::new(SphConfig {
562 n_particles: 4,
563 ..SphConfig::default()
564 });
565 for i in 0..4 {
566 sim.state.vel_y[i] = 1.0;
567 }
568 let ke = sim.kinetic_energy();
569 assert!(ke > 0.0, "KE should be positive");
570 }
571}