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