1#![allow(dead_code, missing_docs)]
7
8use crate::compute::ComputeKernel;
9use std::f64::consts::PI;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum SphKernel {
18 CubicSpline,
20 Wendland,
22 Poly6,
24 Spiky,
26}
27
28#[derive(Debug, Clone, Copy)]
30pub struct SphKernelParams {
31 pub h: f64,
33 pub d_inv: f64,
35 pub d3_inv: f64,
37}
38
39impl SphKernelParams {
40 pub fn new(h: f64) -> Self {
42 Self {
43 h,
44 d_inv: 1.0 / h,
45 d3_inv: 1.0 / (h * h * h),
46 }
47 }
48}
49
50pub fn kernel_value(r: f64, params: &SphKernelParams, kernel: SphKernel) -> f64 {
54 let q = r * params.d_inv; match kernel {
56 SphKernel::CubicSpline => {
57 let alpha = 8.0 / (PI * params.h.powi(3));
59 if q >= 1.0 {
60 0.0
61 } else if q >= 0.5 {
62 let t = 1.0 - q;
63 alpha * 2.0 * t.powi(3)
64 } else {
65 alpha * (6.0 * q.powi(3) - 6.0 * q * q + 1.0)
66 }
67 }
68 SphKernel::Wendland => {
69 let alpha = 21.0 / (2.0 * PI * params.h.powi(3));
71 if q >= 1.0 {
72 0.0
73 } else {
74 let t = 1.0 - q;
75 alpha * t.powi(4) * (4.0 * q + 1.0)
76 }
77 }
78 SphKernel::Poly6 => {
79 let h2 = params.h * params.h;
80 let r2 = r * r;
81 if r2 >= h2 {
82 return 0.0;
83 }
84 let coeff = 315.0 / (64.0 * PI * params.h.powi(9));
85 coeff * (h2 - r2).powi(3)
86 }
87 SphKernel::Spiky => {
88 if q >= 1.0 {
89 return 0.0;
90 }
91 let coeff = 15.0 / (PI * params.h.powi(6));
92 coeff * (params.h - r).powi(3)
93 }
94 }
95}
96
97pub fn kernel_gradient(
102 r_vec: [f64; 3],
103 r: f64,
104 params: &SphKernelParams,
105 kernel: SphKernel,
106) -> [f64; 3] {
107 if r < 1e-12 || r * params.d_inv >= 1.0 {
108 return [0.0; 3];
109 }
110 let dw_dr = kernel_gradient_mag(r, params, kernel);
111 let scale = dw_dr / r;
112 [r_vec[0] * scale, r_vec[1] * scale, r_vec[2] * scale]
113}
114
115fn kernel_gradient_mag(r: f64, params: &SphKernelParams, kernel: SphKernel) -> f64 {
117 let q = r * params.d_inv;
118 match kernel {
119 SphKernel::CubicSpline => {
120 let alpha = 8.0 / (PI * params.h.powi(3));
121 if q >= 1.0 {
122 0.0
123 } else if q >= 0.5 {
124 let t = 1.0 - q;
125 alpha * (-6.0 * t * t) * params.d_inv
126 } else {
127 alpha * (18.0 * q * q - 12.0 * q) * params.d_inv
128 }
129 }
130 SphKernel::Wendland => {
131 let alpha = 21.0 / (2.0 * PI * params.h.powi(3));
132 if q >= 1.0 {
133 0.0
134 } else {
135 let t = 1.0 - q;
136 alpha * params.d_inv * t.powi(3) * (-4.0 * (4.0 * q + 1.0) + 4.0 * t)
139 }
140 }
141 SphKernel::Poly6 => {
142 let h2 = params.h * params.h;
143 let r2 = r * r;
144 if r2 >= h2 {
145 return 0.0;
146 }
147 let coeff = 315.0 / (64.0 * PI * params.h.powi(9));
148 coeff * (-6.0 * r) * (h2 - r2).powi(2)
150 }
151 SphKernel::Spiky => {
152 if q >= 1.0 {
153 return 0.0;
154 }
155 let coeff = 15.0 / (PI * params.h.powi(6));
156 coeff * (-3.0) * (params.h - r).powi(2)
158 }
159 }
160}
161
162pub fn density_summation(positions: &[[f64; 3]], masses: &[f64], h: f64) -> Vec<f64> {
166 let params = SphKernelParams::new(h);
167 let n = positions.len();
168 let mut densities = vec![0.0f64; n];
169 for i in 0..n {
170 let mut rho = 0.0;
171 for j in 0..n {
172 let dx = positions[i][0] - positions[j][0];
173 let dy = positions[i][1] - positions[j][1];
174 let dz = positions[i][2] - positions[j][2];
175 let r = (dx * dx + dy * dy + dz * dz).sqrt();
176 rho += masses[j] * kernel_value(r, ¶ms, SphKernel::CubicSpline);
177 }
178 densities[i] = rho;
179 }
180 densities
181}
182
183pub fn density_summation_kernel(
185 positions: &[[f64; 3]],
186 masses: &[f64],
187 h: f64,
188 kernel: SphKernel,
189) -> Vec<f64> {
190 let params = SphKernelParams::new(h);
191 let n = positions.len();
192 let mut densities = vec![0.0f64; n];
193 for i in 0..n {
194 let mut rho = 0.0;
195 for j in 0..n {
196 let dx = positions[i][0] - positions[j][0];
197 let dy = positions[i][1] - positions[j][1];
198 let dz = positions[i][2] - positions[j][2];
199 let r = (dx * dx + dy * dy + dz * dz).sqrt();
200 rho += masses[j] * kernel_value(r, ¶ms, kernel);
201 }
202 densities[i] = rho;
203 }
204 densities
205}
206
207pub fn pressure_force(
213 positions: &[[f64; 3]],
214 _velocities: &[[f64; 3]],
215 densities: &[f64],
216 pressures: &[f64],
217 masses: &[f64],
218 h: f64,
219) -> Vec<[f64; 3]> {
220 let params = SphKernelParams::new(h);
221 let n = positions.len();
222 let mut forces = vec![[0.0f64; 3]; n];
223 for i in 0..n {
224 let mut fx = 0.0f64;
225 let mut fy = 0.0f64;
226 let mut fz = 0.0f64;
227 let pi_over_rhoi2 = if densities[i].abs() > 1e-30 {
228 pressures[i] / (densities[i] * densities[i])
229 } else {
230 0.0
231 };
232 for j in 0..n {
233 if i == j {
234 continue;
235 }
236 let r_vec = [
237 positions[i][0] - positions[j][0],
238 positions[i][1] - positions[j][1],
239 positions[i][2] - positions[j][2],
240 ];
241 let r = (r_vec[0] * r_vec[0] + r_vec[1] * r_vec[1] + r_vec[2] * r_vec[2]).sqrt();
242 let grad = kernel_gradient(r_vec, r, ¶ms, SphKernel::CubicSpline);
243 let pj_over_rhoj2 = if densities[j].abs() > 1e-30 {
244 pressures[j] / (densities[j] * densities[j])
245 } else {
246 0.0
247 };
248 let coeff = -masses[j] * (pi_over_rhoi2 + pj_over_rhoj2);
249 fx += coeff * grad[0];
250 fy += coeff * grad[1];
251 fz += coeff * grad[2];
252 }
253 forces[i] = [fx, fy, fz];
254 }
255 forces
256}
257
258pub fn viscosity_force(
262 positions: &[[f64; 3]],
263 velocities: &[[f64; 3]],
264 densities: &[f64],
265 masses: &[f64],
266 h: f64,
267 mu: f64,
268) -> Vec<[f64; 3]> {
269 let n = positions.len();
270 let mut forces = vec![[0.0f64; 3]; n];
271 for i in 0..n {
272 let mut fx = 0.0f64;
273 let mut fy = 0.0f64;
274 let mut fz = 0.0f64;
275 for j in 0..n {
276 if i == j {
277 continue;
278 }
279 let dx = positions[i][0] - positions[j][0];
280 let dy = positions[i][1] - positions[j][1];
281 let dz = positions[i][2] - positions[j][2];
282 let r = (dx * dx + dy * dy + dz * dz).sqrt();
283 if r >= h || r < 1e-12 {
284 continue;
285 }
286 let lap = viscosity_laplacian(r, h);
287 let rho_j = if densities[j].abs() > 1e-30 {
288 densities[j]
289 } else {
290 1.0
291 };
292 fx += mu * masses[j] * (velocities[j][0] - velocities[i][0]) / rho_j * lap;
293 fy += mu * masses[j] * (velocities[j][1] - velocities[i][1]) / rho_j * lap;
294 fz += mu * masses[j] * (velocities[j][2] - velocities[i][2]) / rho_j * lap;
295 }
296 forces[i] = [fx, fy, fz];
297 }
298 forces
299}
300
301pub struct NeighborList {
310 cell_size: f64,
312 grid_dims: [usize; 3],
314 origin: [f64; 3],
316 cells: Vec<Vec<usize>>,
318}
319
320impl NeighborList {
321 pub fn new(origin: [f64; 3], domain_size: [f64; 3], cell_size: f64) -> Self {
323 let nx = (domain_size[0] / cell_size).ceil() as usize;
324 let ny = (domain_size[1] / cell_size).ceil() as usize;
325 let nz = (domain_size[2] / cell_size).ceil() as usize;
326 let total = nx.max(1) * ny.max(1) * nz.max(1);
327 Self {
328 cell_size,
329 grid_dims: [nx.max(1), ny.max(1), nz.max(1)],
330 origin,
331 cells: vec![Vec::new(); total],
332 }
333 }
334
335 pub fn build(&mut self, positions: &[[f64; 3]]) {
337 for cell in &mut self.cells {
338 cell.clear();
339 }
340 for (idx, pos) in positions.iter().enumerate() {
341 let ci = self.cell_index(pos);
342 self.cells[ci].push(idx);
343 }
344 }
345
346 fn cell_index(&self, pos: &[f64; 3]) -> usize {
348 let ix = ((pos[0] - self.origin[0]) / self.cell_size).floor() as usize;
349 let iy = ((pos[1] - self.origin[1]) / self.cell_size).floor() as usize;
350 let iz = ((pos[2] - self.origin[2]) / self.cell_size).floor() as usize;
351 let ix = ix.min(self.grid_dims[0] - 1);
352 let iy = iy.min(self.grid_dims[1] - 1);
353 let iz = iz.min(self.grid_dims[2] - 1);
354 iz * self.grid_dims[1] * self.grid_dims[0] + iy * self.grid_dims[0] + ix
355 }
356
357 pub fn neighbors(&self, pos: &[f64; 3]) -> Vec<usize> {
360 let ix = ((pos[0] - self.origin[0]) / self.cell_size).floor() as i64;
361 let iy = ((pos[1] - self.origin[1]) / self.cell_size).floor() as i64;
362 let iz = ((pos[2] - self.origin[2]) / self.cell_size).floor() as i64;
363
364 let mut result = Vec::new();
365 let dims = self.grid_dims;
366
367 for dz in -1i64..=1 {
368 for dy in -1i64..=1 {
369 for dx in -1i64..=1 {
370 let cx = ix + dx;
371 let cy = iy + dy;
372 let cz = iz + dz;
373 if cx < 0 || cy < 0 || cz < 0 {
374 continue;
375 }
376 let cx = cx as usize;
377 let cy = cy as usize;
378 let cz = cz as usize;
379 if cx >= dims[0] || cy >= dims[1] || cz >= dims[2] {
380 continue;
381 }
382 let ci = cz * dims[1] * dims[0] + cy * dims[0] + cx;
383 result.extend_from_slice(&self.cells[ci]);
384 }
385 }
386 }
387 result
388 }
389
390 pub fn num_cells(&self) -> usize {
392 self.grid_dims[0] * self.grid_dims[1] * self.grid_dims[2]
393 }
394
395 pub fn grid_dims(&self) -> [usize; 3] {
397 self.grid_dims
398 }
399}
400
401pub struct SphDispatchConfig {
407 pub n_particles: usize,
409 pub h: f64,
411 pub mu: f64,
413 pub k_eos: f64,
415 pub rho0: f64,
417 pub workgroup_size: u32,
419}
420
421impl SphDispatchConfig {
422 pub fn new(n_particles: usize, h: f64) -> Self {
424 Self {
425 n_particles,
426 h,
427 mu: 0.1,
428 k_eos: 1000.0,
429 rho0: 1000.0,
430 workgroup_size: 64,
431 }
432 }
433
434 pub fn pressure_from_density(&self, rho: f64) -> f64 {
436 self.k_eos * (rho - self.rho0).max(0.0)
437 }
438
439 pub fn num_workgroups(&self) -> u32 {
441 (self.n_particles as u32).div_ceil(self.workgroup_size)
442 }
443}
444
445pub struct SphBufferLayout {
451 pub n_particles: usize,
453 pub position_size: usize,
455 pub velocity_size: usize,
457 pub mass_size: usize,
459 pub density_size: usize,
461 pub pressure_size: usize,
463 pub force_size: usize,
465}
466
467impl SphBufferLayout {
468 pub fn new(n_particles: usize) -> Self {
470 Self {
471 n_particles,
472 position_size: 3 * n_particles,
473 velocity_size: 3 * n_particles,
474 mass_size: n_particles,
475 density_size: n_particles,
476 pressure_size: n_particles,
477 force_size: 3 * n_particles,
478 }
479 }
480
481 pub fn total_elements(&self) -> usize {
483 self.position_size
484 + self.velocity_size
485 + self.mass_size
486 + self.density_size
487 + self.pressure_size
488 + self.force_size
489 }
490
491 pub fn total_bytes(&self) -> usize {
493 self.total_elements() * 8
494 }
495}
496
497pub struct SphDensityKernel;
507
508#[inline]
510fn poly6(r2: f64, h: f64) -> f64 {
511 let h2 = h * h;
512 if r2 >= h2 {
513 return 0.0;
514 }
515 let coeff = 315.0 / (64.0 * PI * h.powi(9));
516 coeff * (h2 - r2).powi(3)
517}
518
519#[inline]
521fn spiky_grad(r: f64, h: f64) -> f64 {
522 if r >= h || r < 1e-12 {
523 return 0.0;
524 }
525 let coeff = -45.0 / (PI * h.powi(6));
526 coeff * (h - r).powi(2)
527}
528
529#[inline]
531fn viscosity_laplacian(r: f64, h: f64) -> f64 {
532 if r >= h || r < 1e-12 {
533 return 0.0;
534 }
535 45.0 / (PI * h.powi(6)) * (h - r)
536}
537
538#[allow(clippy::needless_range_loop)]
539impl ComputeKernel for SphDensityKernel {
540 fn name(&self) -> &str {
541 "SphDensityKernel"
542 }
543
544 fn execute(&self, inputs: &[&[f64]], outputs: &mut [Vec<f64>], work_size: usize) {
545 if inputs.len() < 3 || outputs.is_empty() {
546 return;
547 }
548 let positions = inputs[0];
549 let masses = inputs[1];
550 let h = inputs[2][0];
551 let n = work_size;
552
553 let mut densities = vec![0.0; n];
554 for i in 0..n {
555 let xi = [positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]];
556 let mut rho = 0.0;
557 for j in 0..n {
558 let xj = [positions[j * 3], positions[j * 3 + 1], positions[j * 3 + 2]];
559 let dx = xi[0] - xj[0];
560 let dy = xi[1] - xj[1];
561 let dz = xi[2] - xj[2];
562 let r2 = dx * dx + dy * dy + dz * dz;
563 rho += masses[j] * poly6(r2, h);
564 }
565 densities[i] = rho;
566 }
567 outputs[0] = densities;
568 }
569}
570
571pub struct SphForceKernel;
584
585#[allow(clippy::needless_range_loop)]
586impl ComputeKernel for SphForceKernel {
587 fn name(&self) -> &str {
588 "SphForceKernel"
589 }
590
591 fn execute(&self, inputs: &[&[f64]], outputs: &mut [Vec<f64>], work_size: usize) {
592 if inputs.len() < 6 || outputs.is_empty() {
593 return;
594 }
595 let pos = inputs[0];
596 let vel = inputs[1];
597 let density = inputs[2];
598 let pressure = inputs[3];
599 let mass = inputs[4];
600 let h = inputs[5][0];
601 let mu = inputs[5][1]; let n = work_size;
603
604 let mut forces = vec![0.0; n * 3];
605 for i in 0..n {
606 let xi = [pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]];
607 let vi = [vel[i * 3], vel[i * 3 + 1], vel[i * 3 + 2]];
608 let mut fx = 0.0;
609 let mut fy = 0.0;
610 let mut fz = 0.0;
611 for j in 0..n {
612 if i == j {
613 continue;
614 }
615 let xj = [pos[j * 3], pos[j * 3 + 1], pos[j * 3 + 2]];
616 let vj = [vel[j * 3], vel[j * 3 + 1], vel[j * 3 + 2]];
617 let dx = xi[0] - xj[0];
618 let dy = xi[1] - xj[1];
619 let dz = xi[2] - xj[2];
620 let r = (dx * dx + dy * dy + dz * dz).sqrt();
621 if r < 1e-12 || r >= h {
622 continue;
623 }
624 let p_term =
626 -mass[j] * (pressure[i] + pressure[j]) / (2.0 * density[j]) * spiky_grad(r, h);
627 fx += p_term * dx / r;
628 fy += p_term * dy / r;
629 fz += p_term * dz / r;
630 let v_lap = viscosity_laplacian(r, h);
632 fx += mu * mass[j] * (vj[0] - vi[0]) / density[j] * v_lap;
633 fy += mu * mass[j] * (vj[1] - vi[1]) / density[j] * v_lap;
634 fz += mu * mass[j] * (vj[2] - vi[2]) / density[j] * v_lap;
635 }
636 forces[i * 3] = fx;
637 forces[i * 3 + 1] = fy;
638 forces[i * 3 + 2] = fz;
639 }
640 outputs[0] = forces;
641 }
642}
643
644pub struct SphNeighborListKernel;
653
654#[allow(clippy::needless_range_loop)]
655impl ComputeKernel for SphNeighborListKernel {
656 fn name(&self) -> &str {
657 "SphNeighborListKernel"
658 }
659
660 fn execute(&self, inputs: &[&[f64]], outputs: &mut [Vec<f64>], work_size: usize) {
661 if inputs.len() < 2 || outputs.is_empty() {
662 return;
663 }
664 let positions = inputs[0];
665 let params = inputs[1];
666 if params.len() < 7 {
667 return;
668 }
669 let h = params[0];
670 let origin = [params[1], params[2], params[3]];
671 let _domain = [params[4], params[5], params[6]];
672 let n = work_size;
673
674 let nx = (_domain[0] / h).ceil() as usize;
676 let ny = (_domain[1] / h).ceil() as usize;
677 let nx = nx.max(1);
678 let ny = ny.max(1);
679
680 let mut cell_indices = vec![0.0f64; n];
681 for i in 0..n {
682 let px = positions[i * 3] - origin[0];
683 let py = positions[i * 3 + 1] - origin[1];
684 let pz = positions[i * 3 + 2] - origin[2];
685 let ix = (px / h).floor() as usize;
686 let iy = (py / h).floor() as usize;
687 let iz = (pz / h).floor() as usize;
688 cell_indices[i] = (iz * ny * nx + iy * nx + ix) as f64;
689 }
690 outputs[0] = cell_indices;
691 }
692}
693
694#[allow(clippy::too_many_arguments)]
716pub fn surface_tension_force(
717 positions: &[[f64; 3]],
718 color_fn: &[f64],
719 masses: &[f64],
720 densities: &[f64],
721 h: f64,
722 sigma: f64,
723) -> Vec<[f64; 3]> {
724 let params = SphKernelParams::new(h);
725 let n = positions.len();
726 let mut forces = vec![[0.0f64; 3]; n];
727
728 let mut grad_c = vec![[0.0f64; 3]; n];
730 for i in 0..n {
731 let rho_i = if densities[i].abs() > 1e-30 {
732 densities[i]
733 } else {
734 1.0
735 };
736 let mut gx = 0.0f64;
737 let mut gy = 0.0f64;
738 let mut gz = 0.0f64;
739 for j in 0..n {
740 if i == j {
741 continue;
742 }
743 let r_vec = [
744 positions[i][0] - positions[j][0],
745 positions[i][1] - positions[j][1],
746 positions[i][2] - positions[j][2],
747 ];
748 let r = (r_vec[0] * r_vec[0] + r_vec[1] * r_vec[1] + r_vec[2] * r_vec[2]).sqrt();
749 let grad_w = kernel_gradient(r_vec, r, ¶ms, SphKernel::CubicSpline);
750 let rho_j = if densities[j].abs() > 1e-30 {
751 densities[j]
752 } else {
753 1.0
754 };
755 let dc = masses[j] / rho_j * (color_fn[j] - color_fn[i]);
756 gx += dc * grad_w[0];
757 gy += dc * grad_w[1];
758 gz += dc * grad_w[2];
759 }
760 grad_c[i] = [gx, gy, gz];
761
762 let prefactor = sigma * masses[i] / rho_i;
764 forces[i] = [prefactor * gx, prefactor * gy, prefactor * gz];
765 }
766 forces
767}
768
769pub fn cfl_timestep(velocities: &[[f64; 3]], h: f64, c_sound: f64, cfl_factor: f64) -> f64 {
786 let max_signal = velocities
787 .iter()
788 .map(|v| {
789 let speed = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
790 speed + c_sound
791 })
792 .fold(0.0f64, f64::max);
793
794 let denominator = if max_signal > 1e-30 {
795 max_signal
796 } else {
797 c_sound.max(1e-30)
798 };
799 cfl_factor * h / denominator
800}
801
802pub fn radix_sort_by_density(densities: &[f64]) -> Vec<usize> {
814 let mut indices: Vec<usize> = (0..densities.len()).collect();
815 indices.sort_by(|&a, &b| {
816 densities[a]
817 .partial_cmp(&densities[b])
818 .unwrap_or(std::cmp::Ordering::Equal)
819 });
820 indices
821}
822
823#[allow(dead_code)]
835pub fn density_accumulation(
836 positions: &[[f64; 3]],
837 masses: &[f64],
838 h: f64,
839 neighbor_list: &NeighborList,
840) -> Vec<f64> {
841 let params = SphKernelParams::new(h);
842 let n = positions.len();
843 let mut densities = vec![0.0f64; n];
844 for i in 0..n {
845 let neighbors = neighbor_list.neighbors(&positions[i]);
846 let mut rho = 0.0;
847 for j in neighbors {
848 let dx = positions[i][0] - positions[j][0];
849 let dy = positions[i][1] - positions[j][1];
850 let dz = positions[i][2] - positions[j][2];
851 let r = (dx * dx + dy * dy + dz * dz).sqrt();
852 rho += masses[j] * kernel_value(r, ¶ms, SphKernel::CubicSpline);
853 }
854 densities[i] = rho;
855 }
856 densities
857}
858
859#[allow(dead_code)]
867pub fn pressure_force_kernel(
868 positions: &[[f64; 3]],
869 densities: &[f64],
870 pressures: &[f64],
871 masses: &[f64],
872 h: f64,
873 neighbor_list: &NeighborList,
874) -> Vec<[f64; 3]> {
875 let params = SphKernelParams::new(h);
876 let n = positions.len();
877 let mut forces = vec![[0.0f64; 3]; n];
878 for i in 0..n {
879 let pi_over_rho2 = if densities[i].abs() > 1e-30 {
880 pressures[i] / (densities[i] * densities[i])
881 } else {
882 0.0
883 };
884 let neighbors = neighbor_list.neighbors(&positions[i]);
885 let mut fx = 0.0;
886 let mut fy = 0.0;
887 let mut fz = 0.0;
888 for j in neighbors {
889 if i == j {
890 continue;
891 }
892 let r_vec = [
893 positions[i][0] - positions[j][0],
894 positions[i][1] - positions[j][1],
895 positions[i][2] - positions[j][2],
896 ];
897 let r = (r_vec[0] * r_vec[0] + r_vec[1] * r_vec[1] + r_vec[2] * r_vec[2]).sqrt();
898 let grad = kernel_gradient(r_vec, r, ¶ms, SphKernel::CubicSpline);
899 let pj_over_rho2 = if densities[j].abs() > 1e-30 {
900 pressures[j] / (densities[j] * densities[j])
901 } else {
902 0.0
903 };
904 let coeff = -masses[j] * (pi_over_rho2 + pj_over_rho2);
905 fx += coeff * grad[0];
906 fy += coeff * grad[1];
907 fz += coeff * grad[2];
908 }
909 forces[i] = [fx, fy, fz];
910 }
911 forces
912}
913
914#[allow(dead_code)]
926#[allow(clippy::too_many_arguments)]
927pub fn artificial_viscosity_force(
928 positions: &[[f64; 3]],
929 velocities: &[[f64; 3]],
930 densities: &[f64],
931 masses: &[f64],
932 h: f64,
933 c_s: f64,
934 alpha: f64,
935 beta: f64,
936) -> Vec<[f64; 3]> {
937 let params = SphKernelParams::new(h);
938 let n = positions.len();
939 let mut forces = vec![[0.0f64; 3]; n];
940 for i in 0..n {
941 let mut fx = 0.0;
942 let mut fy = 0.0;
943 let mut fz = 0.0;
944 for j in 0..n {
945 if i == j {
946 continue;
947 }
948 let r_vec = [
949 positions[i][0] - positions[j][0],
950 positions[i][1] - positions[j][1],
951 positions[i][2] - positions[j][2],
952 ];
953 let v_vec = [
954 velocities[i][0] - velocities[j][0],
955 velocities[i][1] - velocities[j][1],
956 velocities[i][2] - velocities[j][2],
957 ];
958 let r2 = r_vec[0] * r_vec[0] + r_vec[1] * r_vec[1] + r_vec[2] * r_vec[2];
959 let r = r2.sqrt();
960 if r >= h || r < 1e-12 {
961 continue;
962 }
963 let vr = v_vec[0] * r_vec[0] + v_vec[1] * r_vec[1] + v_vec[2] * r_vec[2];
964 if vr >= 0.0 {
965 continue;
966 } let mu_ij = h * vr / (r2 + 0.01 * h * h);
968 let rho_ij = 0.5 * (densities[i] + densities[j]).max(1e-30);
969 let pi_ij = (-alpha * c_s * mu_ij + beta * mu_ij * mu_ij) / rho_ij;
970 let grad = kernel_gradient(r_vec, r, ¶ms, SphKernel::CubicSpline);
971 let coeff = -masses[j] * pi_ij;
972 fx += coeff * grad[0];
973 fy += coeff * grad[1];
974 fz += coeff * grad[2];
975 }
976 forces[i] = [fx, fy, fz];
977 }
978 forces
979}
980
981#[allow(dead_code)]
989pub fn wcsph_pressure(rho: f64, rho0: f64, b: f64, gamma: f64) -> f64 {
990 b * ((rho / rho0).powf(gamma) - 1.0)
991}
992
993#[allow(dead_code)]
998pub fn wcsph_euler_step(
999 positions: &[[f64; 3]],
1000 velocities: &[[f64; 3]],
1001 forces: &[[f64; 3]],
1002 masses: &[f64],
1003 dt: f64,
1004) -> (Vec<[f64; 3]>, Vec<[f64; 3]>) {
1005 let n = positions.len();
1006 let mut new_pos = positions.to_vec();
1007 let mut new_vel = velocities.to_vec();
1008 for i in 0..n {
1009 let m = masses[i].max(1e-30);
1010 let ax = forces[i][0] / m;
1011 let ay = forces[i][1] / m;
1012 let az = forces[i][2] / m;
1013 new_vel[i] = [
1014 velocities[i][0] + dt * ax,
1015 velocities[i][1] + dt * ay,
1016 velocities[i][2] + dt * az,
1017 ];
1018 new_pos[i] = [
1019 positions[i][0] + dt * new_vel[i][0],
1020 positions[i][1] + dt * new_vel[i][1],
1021 positions[i][2] + dt * new_vel[i][2],
1022 ];
1023 }
1024 (new_pos, new_vel)
1025}
1026
1027#[allow(dead_code)]
1031pub fn wcsph_leapfrog_velocity_half(
1032 velocities: &[[f64; 3]],
1033 forces: &[[f64; 3]],
1034 masses: &[f64],
1035 dt: f64,
1036) -> Vec<[f64; 3]> {
1037 let n = velocities.len();
1038 let mut new_vel = velocities.to_vec();
1039 for i in 0..n {
1040 let m = masses[i].max(1e-30);
1041 new_vel[i][0] += dt * forces[i][0] / m;
1042 new_vel[i][1] += dt * forces[i][1] / m;
1043 new_vel[i][2] += dt * forces[i][2] / m;
1044 }
1045 new_vel
1046}
1047
1048#[allow(dead_code)]
1058pub fn surface_normal_kernel(
1059 positions: &[[f64; 3]],
1060 densities: &[f64],
1061 masses: &[f64],
1062 h: f64,
1063) -> Vec<[f64; 3]> {
1064 let params = SphKernelParams::new(h);
1065 let n = positions.len();
1066 let mut normals = vec![[0.0f64; 3]; n];
1067 for i in 0..n {
1068 let mut nx = 0.0;
1069 let mut ny = 0.0;
1070 let mut nz = 0.0;
1071 for j in 0..n {
1072 if i == j {
1073 continue;
1074 }
1075 let r_vec = [
1076 positions[i][0] - positions[j][0],
1077 positions[i][1] - positions[j][1],
1078 positions[i][2] - positions[j][2],
1079 ];
1080 let r = (r_vec[0] * r_vec[0] + r_vec[1] * r_vec[1] + r_vec[2] * r_vec[2]).sqrt();
1081 let rho_j = densities[j].max(1e-30);
1082 let grad = kernel_gradient(r_vec, r, ¶ms, SphKernel::CubicSpline);
1083 let coeff = masses[j] / rho_j;
1084 nx += coeff * grad[0];
1085 ny += coeff * grad[1];
1086 nz += coeff * grad[2];
1087 }
1088 normals[i] = [nx, ny, nz];
1089 }
1090 normals
1091}
1092
1093#[allow(dead_code)]
1095pub fn normalize_normal(n: [f64; 3]) -> [f64; 3] {
1096 let mag = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1097 if mag < 1e-30 {
1098 [0.0; 3]
1099 } else {
1100 [n[0] / mag, n[1] / mag, n[2] / mag]
1101 }
1102}
1103
1104#[allow(dead_code)]
1113pub fn build_neighbor_list_explicit(positions: &[[f64; 3]], h: f64) -> Vec<Vec<usize>> {
1114 let n = positions.len();
1115 let mut neighbors = vec![Vec::new(); n];
1116 for i in 0..n {
1117 for j in 0..n {
1118 let dx = positions[i][0] - positions[j][0];
1119 let dy = positions[i][1] - positions[j][1];
1120 let dz = positions[i][2] - positions[j][2];
1121 if dx * dx + dy * dy + dz * dz < h * h {
1122 neighbors[i].push(j);
1123 }
1124 }
1125 }
1126 neighbors
1127}
1128
1129#[allow(dead_code)]
1131pub fn mean_neighbor_count(neighbors: &[Vec<usize>]) -> f64 {
1132 if neighbors.is_empty() {
1133 return 0.0;
1134 }
1135 let total: usize = neighbors.iter().map(|v| v.len()).sum();
1136 total as f64 / neighbors.len() as f64
1137}
1138
1139#[allow(dead_code)]
1147pub fn integrate_kernel_sphere(h: f64, kernel: SphKernel, n_samples: usize) -> f64 {
1148 let params = SphKernelParams::new(h);
1149 let dr = h / n_samples as f64;
1151 let mut integral = 0.0;
1152 for k in 0..n_samples {
1153 let r = (k as f64 + 0.5) * dr;
1154 let w = kernel_value(r, ¶ms, kernel);
1155 integral += w * 4.0 * PI * r * r * dr;
1156 }
1157 integral
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162 use super::*;
1163
1164 #[test]
1165 fn test_sph_kernel_density_sum() {
1166 let h_val = 1.0_f64;
1170 let offsets: &[(f64, f64, f64)] = &[
1171 (0.4, 0.4, 0.4),
1172 (-0.4, 0.4, 0.4),
1173 (0.4, -0.4, 0.4),
1174 (0.4, 0.4, -0.4),
1175 (-0.4, -0.4, 0.4),
1176 (-0.4, 0.4, -0.4),
1177 (0.4, -0.4, -0.4),
1178 (-0.4, -0.4, -0.4),
1179 ];
1180 let mut positions: Vec<f64> = vec![0.0, 0.0, 0.0];
1182 for &(x, y, z) in offsets {
1183 positions.extend_from_slice(&[x, y, z]);
1184 }
1185 let n = 9_usize;
1186 let masses = vec![1.0_f64; n];
1187 let h_slice = vec![h_val];
1188
1189 let mut outputs = vec![Vec::new()];
1190 SphDensityKernel.execute(&[&positions, &masses, &h_slice], &mut outputs, n);
1191
1192 assert_eq!(outputs[0].len(), n, "density output length should equal n");
1193 let central_density = outputs[0][0];
1195 assert!(
1196 central_density > 0.0,
1197 "central particle density should be > 0, got {central_density}"
1198 );
1199 }
1200
1201 #[test]
1202 fn sph_density_uniform_distribution() {
1203 let positions = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1205 let masses = vec![1.0, 1.0];
1206 let h = vec![1.0];
1207 let mut outputs = vec![Vec::new()];
1208 SphDensityKernel.execute(&[&positions, &masses, &h], &mut outputs, 2);
1209 assert_eq!(outputs[0].len(), 2);
1210 assert!(outputs[0][0] > 0.0);
1212 assert!((outputs[0][0] - outputs[0][1]).abs() < 1e-12);
1213 }
1214
1215 #[test]
1216 fn sph_force_produces_finite_forces() {
1217 let n = 3;
1218 let positions = vec![0.0, 0.0, 0.0, 0.3, 0.0, 0.0, 0.6, 0.0, 0.0];
1220 let velocities = vec![0.0; 9];
1221 let densities = vec![1000.0; 3];
1222 let pressures = vec![100.0, 200.0, 100.0];
1223 let masses = vec![1.0; 3];
1224 let params = vec![1.0, 0.1]; let mut outputs = vec![Vec::new()];
1227 SphForceKernel.execute(
1228 &[
1229 &positions,
1230 &velocities,
1231 &densities,
1232 &pressures,
1233 &masses,
1234 ¶ms,
1235 ],
1236 &mut outputs,
1237 n,
1238 );
1239 assert_eq!(outputs[0].len(), 9);
1240 for &f in &outputs[0] {
1242 assert!(f.is_finite(), "force component is not finite: {f}");
1243 }
1244 }
1245
1246 #[test]
1251 fn kernel_value_positive_within_support() {
1252 let h = 1.0_f64;
1253 let params = SphKernelParams::new(h);
1254 for &k in &[
1255 SphKernel::CubicSpline,
1256 SphKernel::Wendland,
1257 SphKernel::Poly6,
1258 SphKernel::Spiky,
1259 ] {
1260 let w0 = kernel_value(0.0, ¶ms, k);
1262 assert!(w0 > 0.0, "{k:?}: W(0) should be > 0, got {w0}");
1263 let wh = kernel_value(h, ¶ms, k);
1265 assert_eq!(wh, 0.0, "{k:?}: W(h) should be 0, got {wh}");
1266 }
1267 }
1268
1269 #[test]
1271 fn kernel_value_symmetric() {
1272 let h = 2.0_f64;
1273 let params = SphKernelParams::new(h);
1274 let r = 0.7 * h;
1275 for &k in &[
1276 SphKernel::CubicSpline,
1277 SphKernel::Wendland,
1278 SphKernel::Poly6,
1279 SphKernel::Spiky,
1280 ] {
1281 let w_pos = kernel_value(r, ¶ms, k);
1282 let w_same = kernel_value(r, ¶ms, k);
1284 assert!(
1285 (w_pos - w_same).abs() < 1e-15,
1286 "{k:?}: kernel not symmetric at r={r}"
1287 );
1288 }
1289 }
1290
1291 #[test]
1293 fn density_summation_self_contribution() {
1294 let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0]];
1295 let masses = vec![1.0, 1.0];
1296 let h = 1.0;
1297 let densities = density_summation(&positions, &masses, h);
1298 assert_eq!(densities.len(), 2);
1299 assert!(
1300 densities[0] > 0.0,
1301 "density[0] should be > 0, got {}",
1302 densities[0]
1303 );
1304 assert!(
1305 densities[1] > 0.0,
1306 "density[1] should be > 0, got {}",
1307 densities[1]
1308 );
1309 }
1310
1311 #[test]
1313 fn pressure_force_finite_and_newtons_third_law() {
1314 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0], [0.6, 0.0, 0.0]];
1315 let velocities = vec![[0.0; 3]; 3];
1316 let densities = vec![1000.0, 1000.0, 1000.0];
1317 let pressures = vec![100.0, 200.0, 100.0];
1318 let masses = vec![1.0, 1.0, 1.0];
1319 let h = 1.0;
1320
1321 let forces = pressure_force(&positions, &velocities, &densities, &pressures, &masses, h);
1322 assert_eq!(forces.len(), 3);
1323 for (i, f) in forces.iter().enumerate() {
1324 for &c in f.iter() {
1325 assert!(c.is_finite(), "force[{i}] component not finite: {c}");
1326 }
1327 }
1328 let total_fx: f64 = forces.iter().map(|f| f[0]).sum();
1330 assert!(
1331 total_fx.abs() < 1e-8,
1332 "total x-force should be ~0 (Newton III), got {total_fx}"
1333 );
1334 }
1335
1336 #[test]
1339 fn test_density_summation_kernel_poly6() {
1340 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0]];
1341 let masses = vec![1.0, 1.0];
1342 let h = 1.0;
1343 let densities = density_summation_kernel(&positions, &masses, h, SphKernel::Poly6);
1344 assert_eq!(densities.len(), 2);
1345 assert!(densities[0] > 0.0);
1346 assert!(densities[1] > 0.0);
1347 }
1348
1349 #[test]
1350 fn test_density_summation_kernel_wendland() {
1351 let positions = vec![[0.0, 0.0, 0.0]];
1352 let masses = vec![1.0];
1353 let h = 1.0;
1354 let densities = density_summation_kernel(&positions, &masses, h, SphKernel::Wendland);
1355 assert_eq!(densities.len(), 1);
1356 assert!(densities[0] > 0.0);
1357 }
1358
1359 #[test]
1360 fn test_density_summation_kernel_spiky() {
1361 let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0]];
1362 let masses = vec![1.0, 1.0];
1363 let h = 1.0;
1364 let densities = density_summation_kernel(&positions, &masses, h, SphKernel::Spiky);
1365 assert!(densities[0] > 0.0);
1366 assert!(densities[1] > 0.0);
1367 }
1368
1369 #[test]
1370 fn test_viscosity_force_finite() {
1371 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0]];
1372 let velocities = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
1373 let densities = vec![1000.0, 1000.0];
1374 let masses = vec![1.0, 1.0];
1375 let h = 1.0;
1376 let mu = 0.1;
1377 let forces = viscosity_force(&positions, &velocities, &densities, &masses, h, mu);
1378 assert_eq!(forces.len(), 2);
1379 for f in &forces {
1380 for &c in f {
1381 assert!(c.is_finite(), "viscosity force not finite: {c}");
1382 }
1383 }
1384 }
1385
1386 #[test]
1387 fn test_viscosity_force_zero_for_same_velocity() {
1388 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0]];
1389 let velocities = vec![[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1390 let densities = vec![1000.0, 1000.0];
1391 let masses = vec![1.0, 1.0];
1392 let h = 1.0;
1393 let mu = 0.1;
1394 let forces = viscosity_force(&positions, &velocities, &densities, &masses, h, mu);
1395 for f in &forces {
1396 for &c in f {
1397 assert!(
1398 c.abs() < 1e-12,
1399 "viscosity should be zero for uniform velocity"
1400 );
1401 }
1402 }
1403 }
1404
1405 #[test]
1406 fn test_neighbor_list_build_and_query() {
1407 let positions = vec![
1408 [0.5, 0.5, 0.5],
1409 [0.6, 0.5, 0.5],
1410 [5.0, 5.0, 5.0], ];
1412 let mut nlist = NeighborList::new([0.0, 0.0, 0.0], [10.0, 10.0, 10.0], 1.0);
1413 nlist.build(&positions);
1414
1415 let neighbors = nlist.neighbors(&[0.5, 0.5, 0.5]);
1416 assert!(neighbors.contains(&0));
1418 assert!(neighbors.contains(&1));
1419 assert!(!neighbors.contains(&2));
1420 }
1421
1422 #[test]
1423 fn test_neighbor_list_num_cells() {
1424 let nlist = NeighborList::new([0.0, 0.0, 0.0], [10.0, 10.0, 10.0], 1.0);
1425 assert_eq!(nlist.num_cells(), 1000); assert_eq!(nlist.grid_dims(), [10, 10, 10]);
1427 }
1428
1429 #[test]
1430 fn test_neighbor_list_single_particle() {
1431 let positions = vec![[0.5, 0.5, 0.5]];
1432 let mut nlist = NeighborList::new([0.0, 0.0, 0.0], [5.0, 5.0, 5.0], 1.0);
1433 nlist.build(&positions);
1434 let neighbors = nlist.neighbors(&[0.5, 0.5, 0.5]);
1435 assert!(neighbors.contains(&0));
1436 }
1437
1438 #[test]
1439 fn test_sph_dispatch_config() {
1440 let config = SphDispatchConfig::new(1000, 0.1);
1441 assert_eq!(config.n_particles, 1000);
1442 assert_eq!(config.num_workgroups(), 16); }
1444
1445 #[test]
1446 fn test_sph_dispatch_config_pressure() {
1447 let config = SphDispatchConfig::new(100, 0.1);
1448 let p = config.pressure_from_density(1100.0);
1449 assert!((p - 100_000.0).abs() < 1e-6); let p_zero = config.pressure_from_density(500.0);
1451 assert!((p_zero).abs() < 1e-12); }
1453
1454 #[test]
1455 fn test_sph_buffer_layout() {
1456 let layout = SphBufferLayout::new(1000);
1457 assert_eq!(layout.position_size, 3000);
1458 assert_eq!(layout.velocity_size, 3000);
1459 assert_eq!(layout.mass_size, 1000);
1460 assert_eq!(layout.density_size, 1000);
1461 assert_eq!(layout.pressure_size, 1000);
1462 assert_eq!(layout.force_size, 3000);
1463 assert_eq!(layout.total_elements(), 12000);
1464 assert_eq!(layout.total_bytes(), 96000);
1465 }
1466
1467 #[test]
1468 fn test_neighbor_list_kernel_executes() {
1469 let positions = vec![0.5, 0.5, 0.5, 1.5, 1.5, 1.5];
1470 let params = vec![1.0, 0.0, 0.0, 0.0, 5.0, 5.0, 5.0];
1471 let mut outputs = vec![Vec::new()];
1472 SphNeighborListKernel.execute(&[&positions, ¶ms], &mut outputs, 2);
1473 assert_eq!(outputs[0].len(), 2);
1474 assert!(outputs[0][0] >= 0.0);
1476 assert!(outputs[0][1] >= 0.0);
1477 assert!((outputs[0][0] - outputs[0][1]).abs() > 0.5);
1479 }
1480
1481 #[test]
1482 fn test_kernel_gradient_at_origin_is_zero() {
1483 let h = 1.0;
1484 let params = SphKernelParams::new(h);
1485 for &k in &[
1486 SphKernel::CubicSpline,
1487 SphKernel::Wendland,
1488 SphKernel::Poly6,
1489 SphKernel::Spiky,
1490 ] {
1491 let grad = kernel_gradient([0.0, 0.0, 0.0], 0.0, ¶ms, k);
1492 assert_eq!(
1493 grad,
1494 [0.0, 0.0, 0.0],
1495 "{k:?}: gradient at origin should be zero"
1496 );
1497 }
1498 }
1499
1500 #[test]
1501 fn test_kernel_gradient_outside_support_is_zero() {
1502 let h = 1.0;
1503 let params = SphKernelParams::new(h);
1504 for &k in &[
1505 SphKernel::CubicSpline,
1506 SphKernel::Wendland,
1507 SphKernel::Poly6,
1508 SphKernel::Spiky,
1509 ] {
1510 let grad = kernel_gradient([2.0, 0.0, 0.0], 2.0, ¶ms, k);
1511 assert_eq!(
1512 grad,
1513 [0.0, 0.0, 0.0],
1514 "{k:?}: gradient outside support should be zero"
1515 );
1516 }
1517 }
1518
1519 #[test]
1522 fn test_surface_tension_force_finite() {
1523 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0], [0.6, 0.0, 0.0]];
1524 let color_fn = vec![1.0, 1.0, 0.0];
1525 let masses = vec![1.0, 1.0, 1.0];
1526 let densities = vec![1000.0, 1000.0, 1000.0];
1527 let h = 1.0;
1528 let sigma = 0.07;
1529 let forces = surface_tension_force(&positions, &color_fn, &masses, &densities, h, sigma);
1530 assert_eq!(forces.len(), 3);
1531 for f in &forces {
1532 for &c in f {
1533 assert!(c.is_finite(), "surface tension force not finite: {c}");
1534 }
1535 }
1536 }
1537
1538 #[test]
1539 fn test_surface_tension_zero_sigma() {
1540 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0]];
1541 let color_fn = vec![1.0, 0.0];
1542 let masses = vec![1.0, 1.0];
1543 let densities = vec![1000.0, 1000.0];
1544 let h = 1.0;
1545 let forces = surface_tension_force(&positions, &color_fn, &masses, &densities, h, 0.0);
1546 for f in &forces {
1547 for &c in f {
1548 assert!(
1549 c.abs() < 1e-30,
1550 "surface tension with sigma=0 should be zero"
1551 );
1552 }
1553 }
1554 }
1555
1556 #[test]
1559 fn test_cfl_timestep_basic() {
1560 let velocities = vec![[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 0.5]];
1561 let h = 1.0;
1562 let c_sound = 10.0;
1563 let dt = cfl_timestep(&velocities, h, c_sound, 0.3);
1564 assert!(dt > 0.0, "CFL dt should be positive");
1565 assert!(dt.is_finite(), "CFL dt should be finite");
1566 assert!(dt <= 0.3 * h / (2.0 + c_sound) + 1e-12);
1568 }
1569
1570 #[test]
1571 fn test_cfl_timestep_zero_velocity() {
1572 let velocities = vec![[0.0; 3]; 5];
1573 let h = 0.5;
1574 let c_sound = 5.0;
1575 let dt = cfl_timestep(&velocities, h, c_sound, 0.25);
1576 let expected = 0.25 * h / c_sound;
1578 assert!(
1579 (dt - expected).abs() < 1e-10,
1580 "expected {expected}, got {dt}"
1581 );
1582 }
1583
1584 #[test]
1587 fn test_radix_sort_by_density_ordered() {
1588 let densities = vec![3.0, 1.0, 4.0, 1.5, 9.0, 2.6, 5.0, 3.5];
1589 let indices = radix_sort_by_density(&densities);
1590 assert_eq!(indices.len(), densities.len());
1591 for w in indices.windows(2) {
1593 assert!(
1594 densities[w[0]] <= densities[w[1]],
1595 "not sorted: {} > {}",
1596 densities[w[0]],
1597 densities[w[1]]
1598 );
1599 }
1600 }
1601
1602 #[test]
1603 fn test_radix_sort_by_density_single() {
1604 let densities = vec![42.0];
1605 let indices = radix_sort_by_density(&densities);
1606 assert_eq!(indices, vec![0]);
1607 }
1608
1609 #[test]
1610 fn test_radix_sort_by_density_empty() {
1611 let indices = radix_sort_by_density(&[]);
1612 assert!(indices.is_empty());
1613 }
1614
1615 #[test]
1616 fn test_radix_sort_is_permutation() {
1617 let densities = vec![5.0, 3.0, 8.0, 1.0, 2.0];
1618 let indices = radix_sort_by_density(&densities);
1619 let mut check = indices.clone();
1620 check.sort_unstable();
1621 assert_eq!(
1622 check,
1623 vec![0, 1, 2, 3, 4],
1624 "indices should be a permutation"
1625 );
1626 }
1627
1628 #[test]
1631 fn test_density_accumulation_matches_direct() {
1632 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0], [0.6, 0.0, 0.0]];
1633 let masses = vec![1.0, 1.0, 1.0];
1634 let h = 1.0;
1635 let mut nlist = NeighborList::new([0.0, 0.0, 0.0], [5.0, 5.0, 5.0], h);
1636 nlist.build(&positions);
1637 let rho_nl = density_accumulation(&positions, &masses, h, &nlist);
1638 let rho_direct = density_summation(&positions, &masses, h);
1639 for i in 0..3 {
1641 assert!(
1642 (rho_nl[i] - rho_direct[i]).abs() < 1e-10,
1643 "density_accumulation vs direct at {i}: {} vs {}",
1644 rho_nl[i],
1645 rho_direct[i]
1646 );
1647 }
1648 }
1649
1650 #[test]
1651 fn test_density_accumulation_positive() {
1652 let positions = vec![[0.5, 0.5, 0.5], [0.6, 0.5, 0.5]];
1653 let masses = vec![1.0, 1.0];
1654 let h = 1.0;
1655 let mut nlist = NeighborList::new([0.0, 0.0, 0.0], [5.0, 5.0, 5.0], h);
1656 nlist.build(&positions);
1657 let rho = density_accumulation(&positions, &masses, h, &nlist);
1658 assert!(rho[0] > 0.0 && rho[1] > 0.0);
1659 }
1660
1661 #[test]
1664 fn test_pressure_force_kernel_finite() {
1665 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0]];
1666 let densities = vec![1000.0, 1000.0];
1667 let pressures = vec![100.0, 200.0];
1668 let masses = vec![1.0, 1.0];
1669 let h = 1.0;
1670 let mut nlist = NeighborList::new([0.0, 0.0, 0.0], [5.0, 5.0, 5.0], h);
1671 nlist.build(&positions);
1672 let forces = pressure_force_kernel(&positions, &densities, &pressures, &masses, h, &nlist);
1673 assert_eq!(forces.len(), 2);
1674 for f in &forces {
1675 for &c in f {
1676 assert!(c.is_finite(), "pressure_force_kernel not finite: {c}");
1677 }
1678 }
1679 }
1680
1681 #[test]
1682 fn test_pressure_force_kernel_zero_gradient() {
1683 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0]];
1685 let densities = vec![1000.0, 1000.0];
1686 let pressures = vec![100.0, 100.0];
1687 let masses = vec![1.0, 1.0];
1688 let h = 1.0;
1689 let mut nlist = NeighborList::new([0.0, 0.0, 0.0], [5.0, 5.0, 5.0], h);
1690 nlist.build(&positions);
1691 let forces = pressure_force_kernel(&positions, &densities, &pressures, &masses, h, &nlist);
1692 let total_fx: f64 = forces.iter().map(|f| f[0]).sum();
1694 assert!(
1695 total_fx.abs() < 1e-6,
1696 "total pressure force with uniform pressure = {total_fx}"
1697 );
1698 }
1699
1700 #[test]
1703 fn test_artificial_viscosity_finite() {
1704 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0]];
1705 let velocities = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]]; let densities = vec![1000.0, 1000.0];
1707 let masses = vec![1.0, 1.0];
1708 let h = 1.0;
1709 let forces = artificial_viscosity_force(
1710 &positions,
1711 &velocities,
1712 &densities,
1713 &masses,
1714 h,
1715 100.0,
1716 1.0,
1717 2.0,
1718 );
1719 for f in &forces {
1720 for &c in f {
1721 assert!(c.is_finite(), "art. visc. force not finite: {c}");
1722 }
1723 }
1724 }
1725
1726 #[test]
1727 fn test_artificial_viscosity_zero_for_diverging() {
1728 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0]];
1730 let velocities = vec![[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0]]; let densities = vec![1000.0, 1000.0];
1732 let masses = vec![1.0, 1.0];
1733 let h = 1.0;
1734 let forces = artificial_viscosity_force(
1735 &positions,
1736 &velocities,
1737 &densities,
1738 &masses,
1739 h,
1740 100.0,
1741 1.0,
1742 2.0,
1743 );
1744 for f in &forces {
1745 for &c in f {
1746 assert!(
1747 c.abs() < 1e-30,
1748 "diverging particles: art visc should be 0, got {c}"
1749 );
1750 }
1751 }
1752 }
1753
1754 #[test]
1757 fn test_wcsph_pressure_at_rest_density() {
1758 let p = wcsph_pressure(1000.0, 1000.0, 100.0, 7.0);
1760 assert!(p.abs() < 1e-8, "pressure at rest density = {p}");
1761 }
1762
1763 #[test]
1764 fn test_wcsph_pressure_above_rest_density() {
1765 let p = wcsph_pressure(1010.0, 1000.0, 100.0, 7.0);
1767 assert!(
1768 p > 0.0,
1769 "pressure above rest density should be positive: {p}"
1770 );
1771 }
1772
1773 #[test]
1774 fn test_wcsph_euler_step_positions_change() {
1775 let positions = vec![[0.0, 0.0, 0.0]];
1776 let velocities = vec![[1.0, 0.0, 0.0]];
1777 let forces = vec![[0.0, 0.0, 0.0]];
1778 let masses = vec![1.0];
1779 let dt = 0.01;
1780 let (new_pos, _) = wcsph_euler_step(&positions, &velocities, &forces, &masses, dt);
1781 assert!(
1782 (new_pos[0][0] - 0.01).abs() < 1e-12,
1783 "position should advance by v*dt"
1784 );
1785 }
1786
1787 #[test]
1788 fn test_wcsph_euler_step_velocity_changes() {
1789 let positions = vec![[0.0, 0.0, 0.0]];
1790 let velocities = vec![[0.0, 0.0, 0.0]];
1791 let forces = vec![[1.0, 0.0, 0.0]]; let masses = vec![1.0];
1793 let dt = 0.1;
1794 let (_, new_vel) = wcsph_euler_step(&positions, &velocities, &forces, &masses, dt);
1795 assert!(
1796 (new_vel[0][0] - 0.1).abs() < 1e-12,
1797 "velocity should increase by a*dt"
1798 );
1799 }
1800
1801 #[test]
1802 fn test_wcsph_leapfrog_velocity_half() {
1803 let velocities = vec![[1.0, 0.0, 0.0]];
1804 let forces = vec![[2.0, 0.0, 0.0]];
1805 let masses = vec![1.0];
1806 let dt = 0.1;
1807 let new_vel = wcsph_leapfrog_velocity_half(&velocities, &forces, &masses, dt);
1808 assert!(
1810 (new_vel[0][0] - 1.2).abs() < 1e-12,
1811 "leapfrog v = {}",
1812 new_vel[0][0]
1813 );
1814 }
1815
1816 #[test]
1819 fn test_surface_normal_kernel_finite() {
1820 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0], [0.6, 0.0, 0.0]];
1821 let densities = vec![1000.0, 1000.0, 1000.0];
1822 let masses = vec![1.0, 1.0, 1.0];
1823 let h = 1.0;
1824 let normals = surface_normal_kernel(&positions, &densities, &masses, h);
1825 assert_eq!(normals.len(), 3);
1826 for n in &normals {
1827 for &c in n {
1828 assert!(c.is_finite(), "surface normal not finite: {c}");
1829 }
1830 }
1831 }
1832
1833 #[test]
1834 fn test_normalize_normal_unit_vector() {
1835 let n = normalize_normal([3.0, 0.0, 4.0]);
1836 let mag = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1837 assert!((mag - 1.0).abs() < 1e-12, "normalized magnitude = {mag}");
1838 }
1839
1840 #[test]
1841 fn test_normalize_normal_zero_vector() {
1842 let n = normalize_normal([0.0, 0.0, 0.0]);
1843 assert_eq!(n, [0.0, 0.0, 0.0]);
1844 }
1845
1846 #[test]
1849 fn test_build_neighbor_list_explicit_finds_close() {
1850 let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0], [5.0, 0.0, 0.0]];
1851 let h = 1.0;
1852 let neighbors = build_neighbor_list_explicit(&positions, h);
1853 assert!(
1854 neighbors[0].contains(&1),
1855 "particle 0 should find particle 1"
1856 );
1857 assert!(
1858 !neighbors[0].contains(&2),
1859 "particle 0 should not find particle 2"
1860 );
1861 }
1862
1863 #[test]
1864 fn test_build_neighbor_list_self_included() {
1865 let positions = vec![[0.0, 0.0, 0.0]];
1866 let h = 1.0;
1867 let neighbors = build_neighbor_list_explicit(&positions, h);
1868 assert!(neighbors[0].contains(&0), "particle should find itself");
1869 }
1870
1871 #[test]
1872 fn test_mean_neighbor_count() {
1873 let positions = vec![[0.0, 0.0, 0.0], [0.3, 0.0, 0.0], [0.6, 0.0, 0.0]];
1874 let h = 1.0;
1875 let neighbors = build_neighbor_list_explicit(&positions, h);
1876 let mean = mean_neighbor_count(&neighbors);
1877 assert!(mean >= 1.0, "mean neighbors should be >= 1: {mean}");
1878 }
1879
1880 #[test]
1881 fn test_mean_neighbor_count_empty() {
1882 let mean = mean_neighbor_count(&[]);
1883 assert_eq!(mean, 0.0);
1884 }
1885
1886 #[test]
1889 fn test_integrate_kernel_sphere_cubic_spline() {
1890 let h = 1.0;
1891 let integral = integrate_kernel_sphere(h, SphKernel::CubicSpline, 1000);
1892 assert!(
1894 integral > 0.5 && integral < 2.0,
1895 "CubicSpline integral = {integral} (expected ~1)"
1896 );
1897 }
1898
1899 #[test]
1900 fn test_integrate_kernel_sphere_wendland() {
1901 let h = 1.0;
1902 let integral = integrate_kernel_sphere(h, SphKernel::Wendland, 1000);
1903 assert!(
1904 integral > 0.5 && integral < 2.0,
1905 "Wendland integral = {integral} (expected ~1)"
1906 );
1907 }
1908
1909 #[test]
1910 fn test_integrate_kernel_sphere_zero_at_boundary() {
1911 let h = 1.0;
1913 let params = SphKernelParams::new(h);
1914 for &k in &[SphKernel::CubicSpline, SphKernel::Wendland] {
1915 let w = kernel_value(h, ¶ms, k);
1916 assert_eq!(w, 0.0, "{k:?}: W(h) should be 0");
1917 }
1918 }
1919}