1const TAIT_GAMMA: f64 = 7.0;
14
15#[derive(Debug, Clone)]
21pub struct GpuSphGrid {
22 pub positions: Vec<f64>,
24 pub masses: Vec<f64>,
26 pub smoothing_lengths: Vec<f64>,
28 pub densities: Vec<f64>,
30 pub pressures: Vec<f64>,
32 pub velocities: Vec<f64>,
34 pub forces: Vec<f64>,
36 pub rho0: f64,
38 pub c0: f64,
40}
41
42impl GpuSphGrid {
43 pub fn new(n: usize) -> Self {
48 Self {
49 positions: vec![0.0; n * 3],
50 masses: vec![1.0; n],
51 smoothing_lengths: vec![1.0; n],
52 densities: vec![0.0; n],
53 pressures: vec![0.0; n],
54 velocities: vec![0.0; n * 3],
55 forces: vec![0.0; n * 3],
56 rho0: 1000.0,
57 c0: 1500.0,
58 }
59 }
60
61 pub fn particle_count(&self) -> usize {
63 self.masses.len()
64 }
65}
66
67pub fn cubic_spline_kernel(r: f64, h: f64) -> f64 {
73 if h <= 0.0 {
74 return 0.0;
75 }
76 let q = r / h;
77 let sigma = 1.0 / (std::f64::consts::PI * h * h * h);
78 if q < 1.0 {
79 sigma * (1.0 - 1.5 * q * q + 0.75 * q * q * q)
80 } else if q < 2.0 {
81 let t = 2.0 - q;
82 sigma * 0.25 * t * t * t
83 } else {
84 0.0
85 }
86}
87
88pub fn cubic_spline_kernel_grad(dx: f64, dy: f64, dz: f64, h: f64) -> [f64; 3] {
92 let r = (dx * dx + dy * dy + dz * dz).sqrt();
93 if h <= 0.0 || r < 1e-15 {
94 return [0.0; 3];
95 }
96 let q = r / h;
97 let sigma = 1.0 / (std::f64::consts::PI * h * h * h);
98 let dw_dr = if q < 1.0 {
99 sigma * (-3.0 * q + 2.25 * q * q) / h
100 } else if q < 2.0 {
101 let t = 2.0 - q;
102 -sigma * 0.75 * t * t / h
103 } else {
104 0.0
105 };
106 let inv_r = 1.0 / r;
107 [dw_dr * dx * inv_r, dw_dr * dy * inv_r, dw_dr * dz * inv_r]
108}
109
110pub fn gpu_density_kernel(grid: &mut GpuSphGrid) {
119 let n = grid.particle_count();
120 for i in 0..n {
121 let mut rho = 0.0f64;
122 let xi = grid.positions[i * 3];
123 let yi = grid.positions[i * 3 + 1];
124 let zi = grid.positions[i * 3 + 2];
125 let hi = grid.smoothing_lengths[i];
126 for j in 0..n {
127 let xj = grid.positions[j * 3];
128 let yj = grid.positions[j * 3 + 1];
129 let zj = grid.positions[j * 3 + 2];
130 let r = ((xi - xj).powi(2) + (yi - yj).powi(2) + (zi - zj).powi(2)).sqrt();
131 rho += grid.masses[j] * cubic_spline_kernel(r, hi);
132 }
133 grid.densities[i] = rho;
134 }
135}
136
137pub fn gpu_pressure_tait(grid: &mut GpuSphGrid) {
144 let n = grid.particle_count();
145 let prefactor = grid.rho0 * grid.c0 * grid.c0 / TAIT_GAMMA;
146 for i in 0..n {
147 let ratio = grid.densities[i] / grid.rho0;
148 grid.pressures[i] = prefactor * (ratio.powf(TAIT_GAMMA) - 1.0);
149 }
150}
151
152pub fn gpu_force_kernel(grid: &mut GpuSphGrid, nu: f64) {
158 let n = grid.particle_count();
159 for f in grid.forces.iter_mut() {
161 *f = 0.0;
162 }
163 for i in 0..n {
164 let xi = grid.positions[i * 3];
165 let yi = grid.positions[i * 3 + 1];
166 let zi = grid.positions[i * 3 + 2];
167 let hi = grid.smoothing_lengths[i];
168 let pi = grid.pressures[i];
169 let rhoi = grid.densities[i];
170 if rhoi < 1e-15 {
171 continue;
172 }
173 let vxi = grid.velocities[i * 3];
174 let vyi = grid.velocities[i * 3 + 1];
175 let vzi = grid.velocities[i * 3 + 2];
176
177 for j in 0..n {
178 if i == j {
179 continue;
180 }
181 let dx = xi - grid.positions[j * 3];
182 let dy = yi - grid.positions[j * 3 + 1];
183 let dz = zi - grid.positions[j * 3 + 2];
184 let rhoj = grid.densities[j];
185 if rhoj < 1e-15 {
186 continue;
187 }
188 let pj = grid.pressures[j];
189 let mj = grid.masses[j];
190 let grad = cubic_spline_kernel_grad(dx, dy, dz, hi);
191 let coeff = -mj * (pi / (rhoi * rhoi) + pj / (rhoj * rhoj));
193 let dvx = vxi - grid.velocities[j * 3];
195 let dvy = vyi - grid.velocities[j * 3 + 1];
196 let dvz = vzi - grid.velocities[j * 3 + 2];
197 let r2 = dx * dx + dy * dy + dz * dz + 1e-15;
198 let vdotr = dvx * dx + dvy * dy + dvz * dz;
199 let visc = -nu * mj * vdotr / (rhoj * r2);
200
201 grid.forces[i * 3] += (coeff + visc) * grad[0];
202 grid.forces[i * 3 + 1] += (coeff + visc) * grad[1];
203 grid.forces[i * 3 + 2] += (coeff + visc) * grad[2];
204 }
205 }
206}
207
208pub fn gpu_neighbor_list(grid: &GpuSphGrid, cell_size: f64) -> Vec<Vec<usize>> {
217 let n = grid.particle_count();
218 let mut neighbors: Vec<Vec<usize>> = vec![Vec::new(); n];
219 let cutoff2 = (2.0 * cell_size) * (2.0 * cell_size);
220 for (i, nb) in neighbors.iter_mut().enumerate() {
221 let xi = grid.positions[i * 3];
222 let yi = grid.positions[i * 3 + 1];
223 let zi = grid.positions[i * 3 + 2];
224 for j in 0..n {
225 if i == j {
226 continue;
227 }
228 let dx = xi - grid.positions[j * 3];
229 let dy = yi - grid.positions[j * 3 + 1];
230 let dz = zi - grid.positions[j * 3 + 2];
231 if dx * dx + dy * dy + dz * dz <= cutoff2 {
232 nb.push(j);
233 }
234 }
235 }
236 neighbors
237}
238
239pub fn launch_density_pass(grid: &mut GpuSphGrid, nu: f64) {
246 gpu_density_kernel(grid);
247 gpu_pressure_tait(grid);
248 gpu_force_kernel(grid, nu);
249}
250
251#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn make_two_particle_grid() -> GpuSphGrid {
258 let mut g = GpuSphGrid::new(2);
259 g.positions = vec![0.0, 0.0, 0.0, 0.5, 0.0, 0.0];
261 g.masses = vec![1.0, 1.0];
262 g.smoothing_lengths = vec![1.0, 1.0];
263 g.rho0 = 1000.0;
264 g.c0 = 100.0;
265 g
266 }
267
268 #[test]
269 fn test_new_grid_particle_count() {
270 let g = GpuSphGrid::new(10);
271 assert_eq!(g.particle_count(), 10);
272 }
273
274 #[test]
275 fn test_new_grid_default_rho0() {
276 let g = GpuSphGrid::new(1);
277 assert!((g.rho0 - 1000.0).abs() < 1e-10);
278 }
279
280 #[test]
281 fn test_new_grid_default_c0() {
282 let g = GpuSphGrid::new(1);
283 assert!((g.c0 - 1500.0).abs() < 1e-10);
284 }
285
286 #[test]
287 fn test_cubic_kernel_zero_distance() {
288 let w = cubic_spline_kernel(0.0, 1.0);
290 assert!(w > 0.0, "kernel at r=0 should be positive, got {w}");
291 }
292
293 #[test]
294 fn test_cubic_kernel_beyond_support() {
295 let w = cubic_spline_kernel(3.0, 1.0);
296 assert!(
297 (w).abs() < 1e-15,
298 "kernel beyond 2h should be zero, got {w}"
299 );
300 }
301
302 #[test]
303 fn test_cubic_kernel_positive_within_support() {
304 let w = cubic_spline_kernel(1.5, 1.0);
305 assert!(w >= 0.0);
306 }
307
308 #[test]
309 fn test_cubic_kernel_zero_h() {
310 let w = cubic_spline_kernel(1.0, 0.0);
311 assert_eq!(w, 0.0);
312 }
313
314 #[test]
315 fn test_cubic_kernel_grad_zero_displacement() {
316 let g = cubic_spline_kernel_grad(0.0, 0.0, 0.0, 1.0);
317 assert_eq!(g, [0.0; 3]);
318 }
319
320 #[test]
321 fn test_cubic_kernel_grad_symmetry() {
322 let g1 = cubic_spline_kernel_grad(0.3, 0.0, 0.0, 1.0);
324 let g2 = cubic_spline_kernel_grad(-0.3, 0.0, 0.0, 1.0);
325 assert!((g1[0] + g2[0]).abs() < 1e-12);
326 }
327
328 #[test]
329 fn test_density_kernel_single_particle() {
330 let mut g = GpuSphGrid::new(1);
331 g.positions = vec![0.0, 0.0, 0.0];
332 g.masses = vec![1.0];
333 g.smoothing_lengths = vec![1.0];
334 gpu_density_kernel(&mut g);
335 assert!(g.densities[0] > 0.0);
337 }
338
339 #[test]
340 fn test_density_kernel_two_particles() {
341 let mut g = make_two_particle_grid();
342 gpu_density_kernel(&mut g);
343 assert!(g.densities[0] > 0.0);
344 assert!(g.densities[1] > 0.0);
345 }
346
347 #[test]
348 fn test_density_kernel_symmetric() {
349 let mut g = make_two_particle_grid();
350 gpu_density_kernel(&mut g);
351 assert!((g.densities[0] - g.densities[1]).abs() < 1e-12);
353 }
354
355 #[test]
356 fn test_pressure_tait_zero_density() {
357 let mut g = GpuSphGrid::new(1);
358 g.densities = vec![0.0];
359 g.rho0 = 1000.0;
360 g.c0 = 100.0;
361 gpu_pressure_tait(&mut g);
362 assert!(g.pressures[0] < 0.0);
364 }
365
366 #[test]
367 fn test_pressure_tait_at_reference_density() {
368 let mut g = GpuSphGrid::new(1);
369 g.rho0 = 1000.0;
370 g.c0 = 100.0;
371 g.densities = vec![g.rho0];
372 gpu_pressure_tait(&mut g);
373 assert!(g.pressures[0].abs() < 1e-6);
375 }
376
377 #[test]
378 fn test_pressure_tait_above_reference() {
379 let mut g = GpuSphGrid::new(1);
380 g.rho0 = 1000.0;
381 g.c0 = 100.0;
382 g.densities = vec![1100.0];
383 gpu_pressure_tait(&mut g);
384 assert!(g.pressures[0] > 0.0);
385 }
386
387 #[test]
388 fn test_force_kernel_self_zero() {
389 let mut g = GpuSphGrid::new(1);
391 g.positions = vec![0.0, 0.0, 0.0];
392 g.masses = vec![1.0];
393 g.densities = vec![1000.0];
394 g.pressures = vec![0.0];
395 gpu_force_kernel(&mut g, 0.0);
396 assert!((g.forces[0]).abs() < 1e-15);
397 }
398
399 #[test]
400 fn test_force_kernel_repulsion() {
401 let mut g = make_two_particle_grid();
403 gpu_density_kernel(&mut g);
404 gpu_pressure_tait(&mut g);
405 gpu_force_kernel(&mut g, 0.0);
406 let fx0 = g.forces[0];
409 let fx1 = g.forces[3];
410 assert!(fx0 * fx1 < 0.0 || (fx0.abs() < 1e-12 && fx1.abs() < 1e-12));
412 }
413
414 #[test]
415 fn test_force_kernel_zeros_forces_first() {
416 let mut g = GpuSphGrid::new(2);
417 g.forces = vec![9.9, 9.9, 9.9, 9.9, 9.9, 9.9];
418 gpu_force_kernel(&mut g, 0.0);
419 for &f in &g.forces {
421 assert!(f.abs() < 1e-15);
422 }
423 }
424
425 #[test]
426 fn test_neighbor_list_all_close() {
427 let g = make_two_particle_grid();
428 let nl = gpu_neighbor_list(&g, 1.0);
429 assert!(nl[0].contains(&1));
431 assert!(nl[1].contains(&0));
432 }
433
434 #[test]
435 fn test_neighbor_list_too_far() {
436 let mut g = GpuSphGrid::new(2);
437 g.positions = vec![0.0, 0.0, 0.0, 100.0, 0.0, 0.0];
438 let nl = gpu_neighbor_list(&g, 1.0);
439 assert!(nl[0].is_empty());
440 assert!(nl[1].is_empty());
441 }
442
443 #[test]
444 fn test_neighbor_list_no_self() {
445 let g = make_two_particle_grid();
446 let nl = gpu_neighbor_list(&g, 1.0);
447 assert!(!nl[0].contains(&0));
448 assert!(!nl[1].contains(&1));
449 }
450
451 #[test]
452 fn test_launch_density_pass_updates_all() {
453 let mut g = make_two_particle_grid();
454 launch_density_pass(&mut g, 0.01);
455 assert!(g.densities[0] > 0.0);
458 assert!(g.densities[1] > 0.0);
459 }
460
461 #[test]
462 fn test_launch_density_pass_idempotent() {
463 let mut g1 = make_two_particle_grid();
464 let mut g2 = make_two_particle_grid();
465 launch_density_pass(&mut g1, 0.0);
466 launch_density_pass(&mut g2, 0.0);
467 for i in 0..2 {
468 assert!((g1.densities[i] - g2.densities[i]).abs() < 1e-12);
469 assert!((g1.pressures[i] - g2.pressures[i]).abs() < 1e-12);
470 }
471 }
472
473 #[test]
474 fn test_force_magnitude_finite() {
475 let mut g = make_two_particle_grid();
476 launch_density_pass(&mut g, 0.01);
477 for &f in &g.forces {
478 assert!(f.is_finite(), "force is not finite: {f}");
479 }
480 }
481
482 #[test]
483 fn test_density_five_particles() {
484 let n = 5;
485 let mut g = GpuSphGrid::new(n);
486 for i in 0..n {
488 g.positions[i * 3] = i as f64 * 0.4;
489 }
490 gpu_density_kernel(&mut g);
491 assert!(g.densities[2] >= g.densities[0]);
493 }
494
495 #[test]
496 fn test_pressure_increases_with_density() {
497 let mut g = GpuSphGrid::new(2);
498 g.rho0 = 1000.0;
499 g.c0 = 100.0;
500 g.densities = vec![1000.0, 1200.0];
501 gpu_pressure_tait(&mut g);
502 assert!(g.pressures[1] > g.pressures[0]);
503 }
504
505 #[test]
506 fn test_gpu_sph_grid_clone() {
507 let g = GpuSphGrid::new(3);
508 let g2 = g.clone();
509 assert_eq!(g2.particle_count(), 3);
510 }
511
512 #[test]
513 fn test_gpu_sph_grid_debug() {
514 let g = GpuSphGrid::new(1);
515 let s = format!("{g:?}");
516 assert!(s.contains("GpuSphGrid"));
517 }
518}