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