1use std::f64::consts::PI;
11
12#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub struct GpuSphPressureSolver {
18 pub n_particles: usize,
20 pub pressure: Vec<f64>,
22 pub density: Vec<f64>,
24 pub positions: Vec<[f64; 3]>,
26 pub masses: Vec<f64>,
28 pub smoothing_h: f64,
30 pub rest_density: f64,
32 pub stiffness: f64,
34}
35
36impl GpuSphPressureSolver {
37 pub fn new(n: usize, h: f64, rho0: f64, k: f64) -> Self {
40 Self {
41 n_particles: n,
42 pressure: vec![0.0; n],
43 density: vec![0.0; n],
44 positions: vec![[0.0; 3]; n],
45 masses: vec![1.0; n],
46 smoothing_h: h,
47 rest_density: rho0,
48 stiffness: k,
49 }
50 }
51
52 pub fn particle_count(&self) -> usize {
54 self.n_particles
55 }
56
57 pub fn set_position(&mut self, i: usize, pos: [f64; 3]) {
59 self.positions[i] = pos;
60 }
61
62 pub fn set_mass(&mut self, i: usize, mass: f64) {
64 self.masses[i] = mass;
65 }
66
67 pub fn total_mass(&self) -> f64 {
69 self.masses.iter().sum()
70 }
71
72 pub fn density_error(&self) -> f64 {
74 if self.rest_density <= 0.0 {
75 return 0.0;
76 }
77 self.density
78 .iter()
79 .map(|&rho| (rho - self.rest_density).abs() / self.rest_density)
80 .fold(0.0_f64, f64::max)
81 }
82
83 pub fn compute_stats(&self) -> GpuSphStats {
85 let max_density = self
86 .density
87 .iter()
88 .cloned()
89 .fold(f64::NEG_INFINITY, f64::max);
90 let min_density = self.density.iter().cloned().fold(f64::INFINITY, f64::min);
91 let mean_pressure = if self.n_particles == 0 {
92 0.0
93 } else {
94 self.pressure.iter().sum::<f64>() / self.n_particles as f64
95 };
96 let compression_error = self.density_error();
97 GpuSphStats {
98 max_density,
99 min_density,
100 mean_pressure,
101 compression_error,
102 }
103 }
104
105 pub fn gpu_compute_density(&mut self) {
107 let n = self.n_particles;
108 let h = self.smoothing_h;
109 for i in 0..n {
110 let mut rho = 0.0f64;
111 for j in 0..n {
112 let dx = self.positions[i][0] - self.positions[j][0];
113 let dy = self.positions[i][1] - self.positions[j][1];
114 let dz = self.positions[i][2] - self.positions[j][2];
115 let r = (dx * dx + dy * dy + dz * dz).sqrt();
116 rho += self.masses[j] * kernel_poly6(r, h);
117 }
118 self.density[i] = rho;
119 }
120 }
121
122 pub fn gpu_compute_pressure(&mut self) {
124 let k = self.stiffness;
125 let rho0 = self.rest_density;
126 for i in 0..self.n_particles {
127 self.pressure[i] = k * (self.density[i] - rho0);
128 }
129 }
130
131 pub fn gpu_pressure_force(&self, i: usize) -> [f64; 3] {
134 let h = self.smoothing_h;
135 let rhoi = self.density[i];
136 let pi = self.pressure[i];
137 let mut force = [0.0f64; 3];
138 if rhoi < 1e-15 {
139 return force;
140 }
141 for j in 0..self.n_particles {
142 if i == j {
143 continue;
144 }
145 let rhoj = self.density[j];
146 if rhoj < 1e-15 {
147 continue;
148 }
149 let r_vec = [
150 self.positions[i][0] - self.positions[j][0],
151 self.positions[i][1] - self.positions[j][1],
152 self.positions[i][2] - self.positions[j][2],
153 ];
154 let r = (r_vec[0] * r_vec[0] + r_vec[1] * r_vec[1] + r_vec[2] * r_vec[2]).sqrt();
155 let grad = kernel_spiky_grad(r_vec, r, h);
156 let coeff = -self.masses[j] * (pi / (rhoi * rhoi) + self.pressure[j] / (rhoj * rhoj));
157 force[0] += coeff * grad[0];
158 force[1] += coeff * grad[1];
159 force[2] += coeff * grad[2];
160 }
161 force
162 }
163
164 #[allow(clippy::too_many_arguments)]
167 pub fn gpu_viscosity_force(&self, i: usize, velocities: &[[f64; 3]], mu: f64) -> [f64; 3] {
168 let h = self.smoothing_h;
169 let mut force = [0.0f64; 3];
170 for j in 0..self.n_particles {
171 if i == j {
172 continue;
173 }
174 let rhoj = self.density[j];
175 if rhoj < 1e-15 {
176 continue;
177 }
178 let r_vec = [
179 self.positions[i][0] - self.positions[j][0],
180 self.positions[i][1] - self.positions[j][1],
181 self.positions[i][2] - self.positions[j][2],
182 ];
183 let r = (r_vec[0] * r_vec[0] + r_vec[1] * r_vec[1] + r_vec[2] * r_vec[2]).sqrt();
184 let lap = kernel_viscosity_laplacian(r, h);
185 let dv = [
186 velocities[j][0] - velocities[i][0],
187 velocities[j][1] - velocities[i][1],
188 velocities[j][2] - velocities[i][2],
189 ];
190 let coeff = mu * self.masses[j] / rhoj * lap;
191 force[0] += coeff * dv[0];
192 force[1] += coeff * dv[1];
193 force[2] += coeff * dv[2];
194 }
195 force
196 }
197}
198
199#[allow(dead_code)]
201#[derive(Debug, Clone)]
202pub struct GpuSphStats {
203 pub max_density: f64,
205 pub min_density: f64,
207 pub mean_pressure: f64,
209 pub compression_error: f64,
211}
212
213pub fn kernel_poly6(r: f64, h: f64) -> f64 {
219 if h <= 0.0 || r >= h {
220 return 0.0;
221 }
222 let h2 = h * h;
223 let diff = h2 - r * r;
224 315.0 / (64.0 * PI * h.powi(9)) * diff.powi(3)
225}
226
227pub fn kernel_spiky_grad(r_vec: [f64; 3], r: f64, h: f64) -> [f64; 3] {
231 if h <= 0.0 || r >= h || r < 1e-15 {
232 return [0.0; 3];
233 }
234 let coeff = -45.0 / (PI * h.powi(6)) * (h - r).powi(2) / r;
235 [coeff * r_vec[0], coeff * r_vec[1], coeff * r_vec[2]]
236}
237
238pub fn kernel_viscosity_laplacian(r: f64, h: f64) -> f64 {
242 if h <= 0.0 || r >= h {
243 return 0.0;
244 }
245 45.0 / (PI * h.powi(6)) * (h - r)
246}
247
248pub fn pcisph_gpu_correction(
255 solver: &mut GpuSphPressureSolver,
256 max_iter: usize,
257 tol: f64,
258) -> usize {
259 for iter in 0..max_iter {
260 solver.gpu_compute_density();
261 solver.gpu_compute_pressure();
262 if solver.density_error() < tol {
263 return iter + 1;
264 }
265 }
266 max_iter
267}
268
269pub fn wcsph_tait_eos(rho: f64, rho0: f64, cs: f64, gamma: f64) -> f64 {
273 if rho0 <= 0.0 || gamma <= 0.0 {
274 return 0.0;
275 }
276 rho0 * cs * cs / gamma * ((rho / rho0).powf(gamma) - 1.0)
277}
278
279#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
288 fn test_poly6_positive_within_h() {
289 let w = kernel_poly6(0.5, 1.0);
290 assert!(w > 0.0, "poly6 should be positive for r < h, got {w}");
291 }
292
293 #[test]
294 fn test_poly6_zero_at_h() {
295 let w = kernel_poly6(1.0, 1.0);
296 assert_eq!(w, 0.0, "poly6 should be zero at r == h");
297 }
298
299 #[test]
300 fn test_poly6_zero_beyond_h() {
301 let w = kernel_poly6(1.5, 1.0);
302 assert_eq!(w, 0.0, "poly6 should be zero for r > h");
303 }
304
305 #[test]
306 fn test_poly6_at_origin_positive() {
307 let w = kernel_poly6(0.0, 1.0);
308 assert!(w > 0.0, "poly6 at r=0 should be positive");
309 }
310
311 #[test]
312 fn test_poly6_decreasing() {
313 let w0 = kernel_poly6(0.0, 1.0);
314 let w1 = kernel_poly6(0.4, 1.0);
315 let w2 = kernel_poly6(0.8, 1.0);
316 assert!(w0 > w1 && w1 > w2);
317 }
318
319 #[test]
320 fn test_poly6_zero_h() {
321 assert_eq!(kernel_poly6(0.0, 0.0), 0.0);
322 }
323
324 #[test]
325 fn test_spiky_grad_zero_outside_h() {
326 let g = kernel_spiky_grad([1.0, 0.0, 0.0], 1.0, 0.5);
327 assert_eq!(g, [0.0; 3]);
328 }
329
330 #[test]
331 fn test_spiky_grad_nonzero_within_h() {
332 let r_vec = [0.3, 0.0, 0.0];
333 let r = 0.3_f64;
334 let g = kernel_spiky_grad(r_vec, r, 1.0);
335 let mag = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
336 assert!(mag > 0.0, "spiky grad should be nonzero within h");
337 }
338
339 #[test]
340 fn test_spiky_grad_points_radially() {
341 let r_vec = [0.4, 0.0, 0.0];
343 let g = kernel_spiky_grad(r_vec, 0.4, 1.0);
344 assert!(g[1].abs() < 1e-15 && g[2].abs() < 1e-15);
345 }
346
347 #[test]
348 fn test_viscosity_laplacian_positive_within_h() {
349 let lap = kernel_viscosity_laplacian(0.5, 1.0);
350 assert!(lap > 0.0);
351 }
352
353 #[test]
354 fn test_viscosity_laplacian_zero_at_h() {
355 let lap = kernel_viscosity_laplacian(1.0, 1.0);
356 assert_eq!(lap, 0.0);
357 }
358
359 #[test]
360 fn test_viscosity_laplacian_zero_beyond_h() {
361 let lap = kernel_viscosity_laplacian(1.5, 1.0);
362 assert_eq!(lap, 0.0);
363 }
364
365 #[test]
368 fn test_wcsph_zero_at_rest_density() {
369 let p = wcsph_tait_eos(1000.0, 1000.0, 100.0, 7.0);
370 assert!(
371 p.abs() < 1e-6,
372 "WCSPH pressure at rest density should be zero, got {p}"
373 );
374 }
375
376 #[test]
377 fn test_wcsph_positive_above_rest() {
378 let p = wcsph_tait_eos(1100.0, 1000.0, 100.0, 7.0);
379 assert!(p > 0.0, "WCSPH pressure should be positive for rho > rho0");
380 }
381
382 #[test]
383 fn test_wcsph_negative_below_rest() {
384 let p = wcsph_tait_eos(900.0, 1000.0, 100.0, 7.0);
385 assert!(p < 0.0, "WCSPH pressure should be negative for rho < rho0");
386 }
387
388 #[test]
389 fn test_wcsph_zero_rho0() {
390 assert_eq!(wcsph_tait_eos(1000.0, 0.0, 100.0, 7.0), 0.0);
391 }
392
393 #[test]
396 fn test_solver_new_particle_count() {
397 let s = GpuSphPressureSolver::new(5, 1.0, 1000.0, 1.0);
398 assert_eq!(s.particle_count(), 5);
399 }
400
401 #[test]
402 fn test_solver_new_initial_pressure_zero() {
403 let s = GpuSphPressureSolver::new(3, 1.0, 1000.0, 1.0);
404 assert!(s.pressure.iter().all(|&p| p == 0.0));
405 }
406
407 #[test]
408 fn test_solver_total_mass() {
409 let mut s = GpuSphPressureSolver::new(3, 1.0, 1000.0, 1.0);
410 s.masses = vec![1.0, 2.0, 3.0];
411 assert!((s.total_mass() - 6.0).abs() < 1e-12);
412 }
413
414 #[test]
415 fn test_solver_set_position() {
416 let mut s = GpuSphPressureSolver::new(2, 1.0, 1000.0, 1.0);
417 s.set_position(0, [1.0, 2.0, 3.0]);
418 assert_eq!(s.positions[0], [1.0, 2.0, 3.0]);
419 }
420
421 #[test]
422 fn test_solver_set_mass() {
423 let mut s = GpuSphPressureSolver::new(2, 1.0, 1000.0, 1.0);
424 s.set_mass(1, 5.0);
425 assert!((s.masses[1] - 5.0).abs() < 1e-12);
426 }
427
428 #[test]
431 fn test_gpu_compute_density_positive() {
432 let mut s = GpuSphPressureSolver::new(2, 1.0, 1000.0, 1.0);
433 s.set_position(0, [0.0, 0.0, 0.0]);
434 s.set_position(1, [0.3, 0.0, 0.0]);
435 s.gpu_compute_density();
436 assert!(s.density[0] > 0.0 && s.density[1] > 0.0);
437 }
438
439 #[test]
440 fn test_gpu_compute_density_self_contribution() {
441 let mut s = GpuSphPressureSolver::new(1, 1.0, 1000.0, 1.0);
443 s.gpu_compute_density();
444 assert!(s.density[0] > 0.0);
445 }
446
447 #[test]
448 fn test_gpu_compute_pressure_nonneg_above_rho0() {
449 let mut s = GpuSphPressureSolver::new(1, 1.0, 1000.0, 200.0);
450 s.density[0] = 1100.0; s.gpu_compute_pressure();
452 assert!(s.pressure[0] > 0.0);
453 }
454
455 #[test]
456 fn test_gpu_compute_pressure_zero_at_rest() {
457 let mut s = GpuSphPressureSolver::new(1, 1.0, 1000.0, 200.0);
458 s.density[0] = 1000.0; s.gpu_compute_pressure();
460 assert!(s.pressure[0].abs() < 1e-10);
461 }
462
463 #[test]
466 fn test_stats_max_density_ge_min() {
467 let mut s = GpuSphPressureSolver::new(3, 1.0, 1000.0, 1.0);
468 s.density = vec![900.0, 1000.0, 1100.0];
469 let stats = s.compute_stats();
470 assert!(stats.max_density >= stats.min_density);
471 }
472
473 #[test]
474 fn test_stats_mean_pressure() {
475 let mut s = GpuSphPressureSolver::new(2, 1.0, 1000.0, 1.0);
476 s.pressure = vec![10.0, 20.0];
477 let stats = s.compute_stats();
478 assert!((stats.mean_pressure - 15.0).abs() < 1e-10);
479 }
480
481 #[test]
482 fn test_density_error_zero_at_rest() {
483 let mut s = GpuSphPressureSolver::new(2, 1.0, 1000.0, 1.0);
484 s.density = vec![1000.0, 1000.0];
485 assert!(s.density_error().abs() < 1e-12);
486 }
487
488 #[test]
491 fn test_pcisph_returns_iterations_le_max() {
492 let mut s = GpuSphPressureSolver::new(2, 1.0, 1000.0, 1.0);
493 s.set_position(0, [0.0, 0.0, 0.0]);
494 s.set_position(1, [0.3, 0.0, 0.0]);
495 let iters = pcisph_gpu_correction(&mut s, 10, 1e-3);
496 assert!(iters <= 10);
497 }
498
499 #[test]
500 fn test_pcisph_updates_density() {
501 let mut s = GpuSphPressureSolver::new(1, 1.0, 1000.0, 1.0);
502 pcisph_gpu_correction(&mut s, 1, 1.0);
503 assert!(s.density[0] > 0.0);
504 }
505
506 #[test]
509 fn test_pressure_force_finite() {
510 let mut s = GpuSphPressureSolver::new(2, 1.0, 1000.0, 200.0);
511 s.set_position(0, [0.0, 0.0, 0.0]);
512 s.set_position(1, [0.3, 0.0, 0.0]);
513 s.gpu_compute_density();
514 s.gpu_compute_pressure();
515 let f = s.gpu_pressure_force(0);
516 assert!(f.iter().all(|v| v.is_finite()));
517 }
518
519 #[test]
520 fn test_viscosity_force_finite() {
521 let mut s = GpuSphPressureSolver::new(2, 1.0, 1000.0, 200.0);
522 s.set_position(0, [0.0, 0.0, 0.0]);
523 s.set_position(1, [0.3, 0.0, 0.0]);
524 s.gpu_compute_density();
525 let vels = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
526 let f = s.gpu_viscosity_force(0, &vels, 0.001);
527 assert!(f.iter().all(|v| v.is_finite()));
528 }
529
530 #[test]
531 fn test_total_mass_empty() {
532 let s = GpuSphPressureSolver::new(0, 1.0, 1000.0, 1.0);
533 assert_eq!(s.total_mass(), 0.0);
534 }
535
536 #[test]
537 fn test_solver_clone() {
538 let s = GpuSphPressureSolver::new(3, 1.0, 1000.0, 1.0);
539 let s2 = s.clone();
540 assert_eq!(s2.particle_count(), 3);
541 }
542
543 #[test]
544 fn test_stats_compression_error_nonzero() {
545 let mut s = GpuSphPressureSolver::new(1, 1.0, 1000.0, 1.0);
546 s.density[0] = 1100.0;
547 let stats = s.compute_stats();
548 assert!(stats.compression_error > 0.0);
549 }
550}