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