1#[derive(Debug, Clone)]
18pub struct FluidGpuBuffer {
19 pub front: Vec<f64>,
21 pub back: Vec<f64>,
23 pub width: usize,
25 pub height: usize,
27 pub depth: usize,
29 pub components: usize,
31}
32
33impl FluidGpuBuffer {
34 pub fn new(width: usize, height: usize, depth: usize, components: usize) -> Self {
37 let n = width * height * depth * components;
38 Self {
39 front: vec![0.0; n],
40 back: vec![0.0; n],
41 width,
42 height,
43 depth,
44 components,
45 }
46 }
47
48 #[inline]
50 pub fn len(&self) -> usize {
51 self.front.len()
52 }
53
54 #[inline]
56 pub fn is_empty(&self) -> bool {
57 self.front.is_empty()
58 }
59
60 pub fn swap(&mut self) {
62 std::mem::swap(&mut self.front, &mut self.back);
63 }
64
65 #[inline]
67 pub fn idx(&self, x: usize, y: usize, z: usize, c: usize) -> usize {
68 ((z * self.height + y) * self.width + x) * self.components + c
69 }
70
71 #[inline]
73 pub fn read(&self, x: usize, y: usize, z: usize, c: usize) -> f64 {
74 self.front[self.idx(x, y, z, c)]
75 }
76
77 #[inline]
79 pub fn write(&mut self, x: usize, y: usize, z: usize, c: usize, val: f64) {
80 let i = self.idx(x, y, z, c);
81 self.back[i] = val;
82 }
83
84 pub fn fill(&mut self, value: f64) {
86 self.front.fill(value);
87 self.back.fill(value);
88 }
89
90 pub fn copy_front_to_back(&mut self) {
92 self.back.clone_from(&self.front);
93 }
94}
95
96#[derive(Debug, Clone)]
103pub struct NavierStokesGpu {
104 pub velocity: FluidGpuBuffer,
106 pub pressure: FluidGpuBuffer,
108 pub density: FluidGpuBuffer,
110 pub dx: f64,
112 pub viscosity: f64,
114}
115
116impl NavierStokesGpu {
117 pub fn new(nx: usize, ny: usize, nz: usize, dx: f64, viscosity: f64) -> Self {
119 Self {
120 velocity: FluidGpuBuffer::new(nx, ny, nz, 3),
121 pressure: FluidGpuBuffer::new(nx, ny, nz, 1),
122 density: FluidGpuBuffer::new(nx, ny, nz, 1),
123 dx,
124 viscosity,
125 }
126 }
127
128 pub fn nx(&self) -> usize {
130 self.velocity.width
131 }
132
133 pub fn ny(&self) -> usize {
135 self.velocity.height
136 }
137
138 pub fn nz(&self) -> usize {
140 self.velocity.depth
141 }
142
143 pub fn advect(&mut self, dt: f64) {
148 let nx = self.nx();
149 let ny = self.ny();
150 let nz = self.nz();
151 let dx = self.dx;
152
153 self.velocity.copy_front_to_back();
154
155 for z in 0..nz {
156 for y in 0..ny {
157 for x in 0..nx {
158 let u = self.velocity.read(x, y, z, 0);
159 let v = self.velocity.read(x, y, z, 1);
160 let w = self.velocity.read(x, y, z, 2);
161
162 let fx = (x as f64 - u * dt / dx).clamp(0.0, (nx - 1) as f64);
164 let fy = (y as f64 - v * dt / dx).clamp(0.0, (ny - 1) as f64);
165 let fz = (z as f64 - w * dt / dx).clamp(0.0, (nz - 1) as f64);
166
167 for c in 0..3 {
168 let val =
169 trilinear_sample(&self.velocity.front, nx, ny, nz, 3, fx, fy, fz, c);
170 self.velocity.write(x, y, z, c, val);
171 }
172 }
173 }
174 }
175 self.velocity.swap();
176 }
177
178 pub fn divergence(&self) -> Vec<f64> {
182 let nx = self.nx();
183 let ny = self.ny();
184 let nz = self.nz();
185 let inv2dx = 0.5 / self.dx;
186 let mut div = vec![0.0_f64; nx * ny * nz];
187
188 for z in 1..nz.saturating_sub(1) {
189 for y in 1..ny.saturating_sub(1) {
190 for x in 1..nx.saturating_sub(1) {
191 let du =
192 self.velocity.read(x + 1, y, z, 0) - self.velocity.read(x - 1, y, z, 0);
193 let dv =
194 self.velocity.read(x, y + 1, z, 1) - self.velocity.read(x, y - 1, z, 1);
195 let dw =
196 self.velocity.read(x, y, z + 1, 2) - self.velocity.read(x, y, z - 1, 2);
197 div[z * ny * nx + y * nx + x] = (du + dv + dw) * inv2dx;
198 }
199 }
200 }
201 div
202 }
203
204 pub fn pressure_projection(&mut self, iterations: usize) {
209 let nx = self.nx();
210 let ny = self.ny();
211 let nz = self.nz();
212 let dx2 = self.dx * self.dx;
213 let div = self.divergence();
214
215 for _ in 0..iterations {
217 self.pressure.copy_front_to_back();
218 for z in 1..nz.saturating_sub(1) {
219 for y in 1..ny.saturating_sub(1) {
220 for x in 1..nx.saturating_sub(1) {
221 let neighbours = self.pressure.read(x + 1, y, z, 0)
222 + self.pressure.read(x - 1, y, z, 0)
223 + self.pressure.read(x, y + 1, z, 0)
224 + self.pressure.read(x, y - 1, z, 0)
225 + self.pressure.read(x, y, z + 1, 0)
226 + self.pressure.read(x, y, z - 1, 0);
227 let rhs = div[z * ny * nx + y * nx + x] * dx2;
228 self.pressure.write(x, y, z, 0, (neighbours - rhs) / 6.0);
229 }
230 }
231 }
232 self.pressure.swap();
233 }
234
235 let inv2dx = 0.5 / self.dx;
237 self.velocity.copy_front_to_back();
238 for z in 1..nz.saturating_sub(1) {
239 for y in 1..ny.saturating_sub(1) {
240 for x in 1..nx.saturating_sub(1) {
241 let gx = (self.pressure.read(x + 1, y, z, 0)
242 - self.pressure.read(x - 1, y, z, 0))
243 * inv2dx;
244 let gy = (self.pressure.read(x, y + 1, z, 0)
245 - self.pressure.read(x, y - 1, z, 0))
246 * inv2dx;
247 let gz = (self.pressure.read(x, y, z + 1, 0)
248 - self.pressure.read(x, y, z - 1, 0))
249 * inv2dx;
250 self.velocity
251 .write(x, y, z, 0, self.velocity.read(x, y, z, 0) - gx);
252 self.velocity
253 .write(x, y, z, 1, self.velocity.read(x, y, z, 1) - gy);
254 self.velocity
255 .write(x, y, z, 2, self.velocity.read(x, y, z, 2) - gz);
256 }
257 }
258 }
259 self.velocity.swap();
260 }
261
262 pub fn curl(&self) -> Vec<f64> {
266 let nx = self.nx();
267 let ny = self.ny();
268 let nz = self.nz();
269 let inv2dx = 0.5 / self.dx;
270 let mut omega = vec![0.0_f64; nx * ny * nz * 3];
271
272 for z in 1..nz.saturating_sub(1) {
273 for y in 1..ny.saturating_sub(1) {
274 for x in 1..nx.saturating_sub(1) {
275 let dw_dy = (self.velocity.read(x, y + 1, z, 2)
276 - self.velocity.read(x, y - 1, z, 2))
277 * inv2dx;
278 let dv_dz = (self.velocity.read(x, y, z + 1, 1)
279 - self.velocity.read(x, y, z - 1, 1))
280 * inv2dx;
281 let du_dz = (self.velocity.read(x, y, z + 1, 0)
282 - self.velocity.read(x, y, z - 1, 0))
283 * inv2dx;
284 let dw_dx = (self.velocity.read(x + 1, y, z, 2)
285 - self.velocity.read(x - 1, y, z, 2))
286 * inv2dx;
287 let dv_dx = (self.velocity.read(x + 1, y, z, 1)
288 - self.velocity.read(x - 1, y, z, 1))
289 * inv2dx;
290 let du_dy = (self.velocity.read(x, y + 1, z, 0)
291 - self.velocity.read(x, y - 1, z, 0))
292 * inv2dx;
293
294 let base = (z * ny * nx + y * nx + x) * 3;
295 omega[base] = dw_dy - dv_dz; omega[base + 1] = du_dz - dw_dx; omega[base + 2] = dv_dx - du_dy; }
299 }
300 }
301 omega
302 }
303}
304
305fn trilinear_sample(
311 data: &[f64],
312 nx: usize,
313 ny: usize,
314 nz: usize,
315 stride: usize,
316 fx: f64,
317 fy: f64,
318 fz: f64,
319 c: usize,
320) -> f64 {
321 let x0 = (fx as usize).min(nx - 1);
322 let y0 = (fy as usize).min(ny - 1);
323 let z0 = (fz as usize).min(nz - 1);
324 let x1 = (x0 + 1).min(nx - 1);
325 let y1 = (y0 + 1).min(ny - 1);
326 let z1 = (z0 + 1).min(nz - 1);
327
328 let tx = fx.fract();
329 let ty = fy.fract();
330 let tz = fz.fract();
331
332 let idx =
333 |x: usize, y: usize, z: usize| -> f64 { data[(z * ny * nx + y * nx + x) * stride + c] };
334
335 let c000 = idx(x0, y0, z0);
336 let c100 = idx(x1, y0, z0);
337 let c010 = idx(x0, y1, z0);
338 let c110 = idx(x1, y1, z0);
339 let c001 = idx(x0, y0, z1);
340 let c101 = idx(x1, y0, z1);
341 let c011 = idx(x0, y1, z1);
342 let c111 = idx(x1, y1, z1);
343
344 let c00 = c000 * (1.0 - tx) + c100 * tx;
345 let c10 = c010 * (1.0 - tx) + c110 * tx;
346 let c01 = c001 * (1.0 - tx) + c101 * tx;
347 let c11 = c011 * (1.0 - tx) + c111 * tx;
348 let c_0 = c00 * (1.0 - ty) + c10 * ty;
349 let c_1 = c01 * (1.0 - ty) + c11 * ty;
350 c_0 * (1.0 - tz) + c_1 * tz
351}
352
353pub const D3Q19_Q: usize = 19;
357
358pub const D3Q19_WEIGHTS: [f64; D3Q19_Q] = [
360 1.0 / 3.0, 1.0 / 18.0,
362 1.0 / 18.0,
363 1.0 / 18.0, 1.0 / 18.0,
365 1.0 / 18.0,
366 1.0 / 18.0, 1.0 / 36.0,
368 1.0 / 36.0,
369 1.0 / 36.0, 1.0 / 36.0,
371 1.0 / 36.0,
372 1.0 / 36.0, 1.0 / 36.0,
374 1.0 / 36.0,
375 1.0 / 36.0, 1.0 / 36.0,
377 1.0 / 36.0,
378 1.0 / 36.0, ];
380
381#[derive(Debug, Clone)]
383pub struct LBMCollisionKernelSpec {
384 pub tau: f64,
386 pub cs2: f64,
388 pub q: usize,
390}
391
392#[derive(Debug, Clone)]
394pub struct LBMStreamingSpec {
395 pub dims: [usize; 3],
397 pub periodic: bool,
399}
400
401#[derive(Debug, Clone)]
403pub struct LBMBoundaryKernelSpec {
404 pub kind: LBMBoundaryKind,
406 pub solid_cells: Vec<usize>,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq)]
412pub enum LBMBoundaryKind {
413 BounceBack,
415 MovingWall,
417 ZouHeInlet,
419 ZouHeOutlet,
421}
422
423#[derive(Debug, Clone)]
428pub struct LBMGpuKernels {
429 pub f: FluidGpuBuffer,
431 pub collision: LBMCollisionKernelSpec,
433 pub streaming: LBMStreamingSpec,
435 pub boundary: LBMBoundaryKernelSpec,
437}
438
439impl LBMGpuKernels {
440 pub fn new(
444 nx: usize,
445 ny: usize,
446 nz: usize,
447 tau: f64,
448 periodic: bool,
449 solid_cells: Vec<usize>,
450 ) -> Self {
451 let q = D3Q19_Q;
452 let mut f = FluidGpuBuffer::new(nx, ny, nz, q);
453 for i in 0..f.front.len() {
455 let c = i % q;
456 f.front[i] = D3Q19_WEIGHTS[c];
457 f.back[i] = D3Q19_WEIGHTS[c];
458 }
459 Self {
460 f,
461 collision: LBMCollisionKernelSpec {
462 tau,
463 cs2: 1.0 / 3.0,
464 q,
465 },
466 streaming: LBMStreamingSpec {
467 dims: [nx, ny, nz],
468 periodic,
469 },
470 boundary: LBMBoundaryKernelSpec {
471 kind: LBMBoundaryKind::BounceBack,
472 solid_cells,
473 },
474 }
475 }
476
477 pub fn dispatch_collision(&mut self) {
481 let nx = self.f.width;
482 let ny = self.f.height;
483 let nz = self.f.depth;
484 let tau = self.collision.tau;
485 let cs2 = self.collision.cs2;
486 let q = self.collision.q;
487
488 for z in 0..nz {
489 for y in 0..ny {
490 for x in 0..nx {
491 let mut rho = 0.0_f64;
493 let mut ux = 0.0_f64;
494 let mut uy = 0.0_f64;
495 let mut uz = 0.0_f64;
496 for i in 0..q {
497 let fi = self.f.read(x, y, z, i);
498 rho += fi;
499 ux += fi * (i as f64 / q as f64 - 0.5);
501 uy += fi * ((i * 7 % q) as f64 / q as f64 - 0.5);
502 uz += fi * ((i * 13 % q) as f64 / q as f64 - 0.5);
503 }
504 if rho > 1e-12 {
505 ux /= rho;
506 uy /= rho;
507 uz /= rho;
508 }
509
510 for (i, &w) in D3Q19_WEIGHTS.iter().enumerate().take(q) {
512 let u_sq = ux * ux + uy * uy + uz * uz;
513 let feq = rho
514 * w
515 * (1.0
516 + (ux + uy + uz) / cs2
517 + (u_sq / (2.0 * cs2)) * ((ux + uy + uz) / cs2)
518 - u_sq / (2.0 * cs2));
519 let fi = self.f.read(x, y, z, i);
520 self.f.write(x, y, z, i, fi - (fi - feq) / tau);
521 }
522 }
523 }
524 }
525 self.f.swap();
526 }
527
528 pub fn dispatch_streaming(&mut self) {
532 let nx = self.f.width;
533 let ny = self.f.height;
534 let nz = self.f.depth;
535 self.f.copy_front_to_back();
536 for _z in 0..nz {
538 for _y in 0..ny {
539 for _x in 0..nx {
540 }
542 }
543 }
544 self.f.swap();
545 }
546
547 pub fn dispatch_boundary(&mut self) {
549 for &cell_idx in &self.boundary.solid_cells.clone() {
550 let q = self.collision.q;
551 for c in 0..q {
552 let i = cell_idx * q + c;
553 if i < self.f.front.len() {
554 let opp = q - 1 - c;
556 let opp_i = cell_idx * q + opp;
557 if opp_i < self.f.front.len() {
558 self.f.front.swap(i, opp_i);
559 }
560 }
561 }
562 }
563 }
564
565 pub fn density_at(&self, x: usize, y: usize, z: usize) -> f64 {
567 (0..self.collision.q).map(|i| self.f.read(x, y, z, i)).sum()
568 }
569}
570
571#[derive(Debug, Clone)]
575pub struct SPHGpuKernels {
576 pub positions: Vec<[f64; 3]>,
578 pub velocities: Vec<[f64; 3]>,
580 pub densities: Vec<f64>,
582 pub pressures: Vec<f64>,
584 pub smoothing_length: f64,
586 pub rest_density: f64,
588 pub stiffness: f64,
590 pub viscosity: f64,
592 pub mass: f64,
594}
595
596impl SPHGpuKernels {
597 pub fn new(
599 positions: Vec<[f64; 3]>,
600 smoothing_length: f64,
601 rest_density: f64,
602 stiffness: f64,
603 viscosity: f64,
604 mass: f64,
605 ) -> Self {
606 let n = positions.len();
607 Self {
608 velocities: vec![[0.0; 3]; n],
609 densities: vec![rest_density; n],
610 pressures: vec![0.0; n],
611 positions,
612 smoothing_length,
613 rest_density,
614 stiffness,
615 viscosity,
616 mass,
617 }
618 }
619
620 pub fn n_particles(&self) -> usize {
622 self.positions.len()
623 }
624
625 pub fn kernel_w(&self, r: f64) -> f64 {
627 let h = self.smoothing_length;
628 let q = r / h;
629 let sigma = 8.0 / (std::f64::consts::PI * h * h * h);
630 if q >= 1.0 {
631 0.0
632 } else if q >= 0.5 {
633 let t = 1.0 - q;
634 sigma * 2.0 * t * t * t
635 } else {
636 sigma * (6.0 * q * q * q - 6.0 * q * q + 1.0)
637 }
638 }
639
640 pub fn kernel_grad_w(&self, rij: [f64; 3], r: f64) -> [f64; 3] {
642 let h = self.smoothing_length;
643 if r < 1e-12 || r >= h {
644 return [0.0; 3];
645 }
646 let q = r / h;
647 let sigma = 8.0 / (std::f64::consts::PI * h * h * h);
648 let dw_dq = if q >= 0.5 {
649 let t = 1.0 - q;
650 -6.0 * sigma * t * t / h
651 } else {
652 sigma * (18.0 * q * q - 12.0 * q) / h
653 };
654 let inv_r = 1.0 / r;
655 [
656 rij[0] * dw_dq * inv_r,
657 rij[1] * dw_dq * inv_r,
658 rij[2] * dw_dq * inv_r,
659 ]
660 }
661
662 pub fn compute_density(&mut self) {
664 let n = self.n_particles();
665 let mass = self.mass;
666 let mut new_densities = vec![0.0_f64; n];
667
668 for (i, &pos_i) in self.positions.iter().enumerate() {
669 let mut rho = 0.0;
670 for &pos_j in self.positions.iter() {
671 let dx = pos_i[0] - pos_j[0];
672 let dy = pos_i[1] - pos_j[1];
673 let dz = pos_i[2] - pos_j[2];
674 let r = (dx * dx + dy * dy + dz * dz).sqrt();
675 rho += mass * self.kernel_w(r);
676 }
677 new_densities[i] = rho.max(self.rest_density * 0.01);
678 }
679 self.densities = new_densities;
680 }
681
682 pub fn compute_pressure(&mut self) {
684 for (pr, &rho) in self.pressures.iter_mut().zip(self.densities.iter()) {
685 *pr = self.stiffness * (rho - self.rest_density).max(0.0);
686 }
687 }
688
689 pub fn pressure_force(&self) -> Vec<[f64; 3]> {
693 let n = self.n_particles();
694 let mut forces = vec![[0.0_f64; 3]; n];
695 let mass = self.mass;
696
697 for (i, &pos_i) in self.positions.iter().enumerate() {
698 let mut fx = 0.0;
699 let mut fy = 0.0;
700 let mut fz = 0.0;
701 for j in 0..n {
702 if i == j {
703 continue;
704 }
705 let dx = pos_i[0] - self.positions[j][0];
706 let dy = pos_i[1] - self.positions[j][1];
707 let dz = pos_i[2] - self.positions[j][2];
708 let r = (dx * dx + dy * dy + dz * dz).sqrt();
709 if r < 1e-12 {
710 continue;
711 }
712 let grad = self.kernel_grad_w([dx, dy, dz], r);
713 let pi = self.pressures[i];
714 let pj = self.pressures[j];
715 let rhoj = self.densities[j].max(1e-12);
716 let rhoi = self.densities[i].max(1e-12);
717 let coeff = -mass * (pi / (rhoi * rhoi) + pj / (rhoj * rhoj));
718 fx += coeff * grad[0];
719 fy += coeff * grad[1];
720 fz += coeff * grad[2];
721 }
722 forces[i] = [fx, fy, fz];
723 }
724 forces
725 }
726
727 pub fn viscosity_force(&self) -> Vec<[f64; 3]> {
731 let n = self.n_particles();
732 let mut forces = vec![[0.0_f64; 3]; n];
733 let mass = self.mass;
734 let mu = self.viscosity;
735
736 for (i, &pos_i) in self.positions.iter().enumerate() {
737 let mut fx = 0.0;
738 let mut fy = 0.0;
739 let mut fz = 0.0;
740 for j in 0..n {
741 if i == j {
742 continue;
743 }
744 let dx = pos_i[0] - self.positions[j][0];
745 let dy = pos_i[1] - self.positions[j][1];
746 let dz = pos_i[2] - self.positions[j][2];
747 let r = (dx * dx + dy * dy + dz * dz).sqrt();
748 if r < 1e-12 {
749 continue;
750 }
751 let dvx = self.velocities[j][0] - self.velocities[i][0];
752 let dvy = self.velocities[j][1] - self.velocities[i][1];
753 let dvz = self.velocities[j][2] - self.velocities[i][2];
754 let rhoj = self.densities[j].max(1e-12);
755 let w = self.kernel_w(r);
756 let coeff = mu * mass / rhoj * w;
757 fx += coeff * dvx;
758 fy += coeff * dvy;
759 fz += coeff * dvz;
760 }
761 forces[i] = [fx, fy, fz];
762 }
763 forces
764 }
765}
766
767#[derive(Debug, Clone, PartialEq, Eq)]
771pub enum BarrierKind {
772 Compute,
774 Memory,
776 Full,
778}
779
780#[derive(Debug, Clone)]
782pub struct PipelinePass {
783 pub label: String,
785 pub barrier: BarrierKind,
787 pub estimated_bytes: usize,
789}
790
791#[derive(Debug, Clone)]
796pub struct FluidGpuPipeline {
797 pub passes: Vec<PipelinePass>,
799 pub total_bytes: usize,
801}
802
803impl FluidGpuPipeline {
804 pub fn default_ns(n_cells: usize) -> Self {
806 let bytes_per_cell = 8 * std::mem::size_of::<f64>(); let b = n_cells * bytes_per_cell;
808 let passes = vec![
809 PipelinePass {
810 label: "advect_velocity".into(),
811 barrier: BarrierKind::Memory,
812 estimated_bytes: b * 2,
813 },
814 PipelinePass {
815 label: "advect_density".into(),
816 barrier: BarrierKind::Memory,
817 estimated_bytes: b,
818 },
819 PipelinePass {
820 label: "divergence".into(),
821 barrier: BarrierKind::Compute,
822 estimated_bytes: b,
823 },
824 PipelinePass {
825 label: "pressure_jacobi_0".into(),
826 barrier: BarrierKind::Memory,
827 estimated_bytes: b * 2,
828 },
829 PipelinePass {
830 label: "pressure_jacobi_1".into(),
831 barrier: BarrierKind::Memory,
832 estimated_bytes: b * 2,
833 },
834 PipelinePass {
835 label: "gradient_subtract".into(),
836 barrier: BarrierKind::Compute,
837 estimated_bytes: b,
838 },
839 PipelinePass {
840 label: "vorticity_confinement".into(),
841 barrier: BarrierKind::Full,
842 estimated_bytes: b,
843 },
844 ];
845 let total_bytes = passes.iter().map(|p| p.estimated_bytes).sum();
846 Self {
847 passes,
848 total_bytes,
849 }
850 }
851
852 pub fn push(&mut self, pass: PipelinePass) {
854 self.total_bytes += pass.estimated_bytes;
855 self.passes.push(pass);
856 }
857
858 pub fn bandwidth_gb_s(&self, elapsed_secs: f64) -> f64 {
860 if elapsed_secs <= 0.0 {
861 return 0.0;
862 }
863 self.total_bytes as f64 / elapsed_secs / 1e9
864 }
865}
866
867#[derive(Debug, Clone)]
874pub struct VortexConfinement {
875 pub epsilon: f64,
877 pub dx: f64,
879}
880
881impl VortexConfinement {
882 pub fn new(epsilon: f64, dx: f64) -> Self {
884 Self { epsilon, dx }
885 }
886
887 pub fn apply(&self, ns: &mut NavierStokesGpu, dt: f64) {
891 let nx = ns.nx();
892 let ny = ns.ny();
893 let nz = ns.nz();
894 let omega = ns.curl();
895 let inv2dx = 0.5 / self.dx;
896
897 let mut mag = vec![0.0_f64; nx * ny * nz];
899 for z in 0..nz {
900 for y in 0..ny {
901 for x in 0..nx {
902 let base = (z * ny * nx + y * nx + x) * 3;
903 let wx = omega[base];
904 let wy = omega[base + 1];
905 let wz = omega[base + 2];
906 mag[z * ny * nx + y * nx + x] = (wx * wx + wy * wy + wz * wz).sqrt();
907 }
908 }
909 }
910
911 ns.velocity.copy_front_to_back();
913 for z in 1..nz.saturating_sub(1) {
914 for y in 1..ny.saturating_sub(1) {
915 for x in 1..nx.saturating_sub(1) {
916 let gx = (mag[z * ny * nx + y * nx + x + 1]
917 - mag[z * ny * nx + y * nx + x - 1])
918 * inv2dx;
919 let gy = (mag[z * ny * nx + (y + 1) * nx + x]
920 - mag[z * ny * nx + (y - 1) * nx + x])
921 * inv2dx;
922 let gz = (mag[(z + 1) * ny * nx + y * nx + x]
923 - mag[(z - 1) * ny * nx + y * nx + x])
924 * inv2dx;
925
926 let grad_len = (gx * gx + gy * gy + gz * gz).sqrt();
927 if grad_len < 1e-12 {
928 continue;
929 }
930 let nx_ = gx / grad_len;
931 let ny_ = gy / grad_len;
932 let nz_ = gz / grad_len;
933
934 let base = (z * ny * nx + y * nx + x) * 3;
935 let wx = omega[base];
936 let wy = omega[base + 1];
937 let wz = omega[base + 2];
938
939 let fcx = ny_ * wz - nz_ * wy;
941 let fcy = nz_ * wx - nx_ * wz;
942 let fcz = nx_ * wy - ny_ * wx;
943
944 let force = self.epsilon * self.dx;
945 let u = ns.velocity.read(x, y, z, 0) + force * fcx * dt;
946 let v = ns.velocity.read(x, y, z, 1) + force * fcy * dt;
947 let w = ns.velocity.read(x, y, z, 2) + force * fcz * dt;
948 ns.velocity.write(x, y, z, 0, u);
949 ns.velocity.write(x, y, z, 1, v);
950 ns.velocity.write(x, y, z, 2, w);
951 }
952 }
953 }
954 ns.velocity.swap();
955 }
956}
957
958#[derive(Debug, Clone)]
965pub struct WaterSimGpu {
966 pub height: FluidGpuBuffer,
968 pub velocity: FluidGpuBuffer,
970 pub rest_depth: f64,
972 pub gravity: f64,
974 pub dx: f64,
976 pub damping: f64,
978}
979
980impl WaterSimGpu {
981 pub fn new(nx: usize, ny: usize, dx: f64, rest_depth: f64, gravity: f64) -> Self {
983 Self {
984 height: FluidGpuBuffer::new(nx, ny, 1, 1),
985 velocity: FluidGpuBuffer::new(nx, ny, 1, 2),
986 rest_depth,
987 gravity,
988 dx,
989 damping: 0.999,
990 }
991 }
992
993 pub fn nx(&self) -> usize {
995 self.height.width
996 }
997
998 pub fn ny(&self) -> usize {
1000 self.height.height
1001 }
1002
1003 pub fn add_disturbance(&mut self, cx: usize, cy: usize, amp: f64, sigma: f64) {
1006 let nx = self.nx();
1007 let ny = self.ny();
1008 for y in 0..ny {
1009 for x in 0..nx {
1010 let dx = x as f64 - cx as f64;
1011 let dy = y as f64 - cy as f64;
1012 let r2 = dx * dx + dy * dy;
1013 let h = self.height.read(x, y, 0, 0);
1014 let idx = self.height.idx(x, y, 0, 0);
1015 self.height.front[idx] = h + amp * (-r2 / (2.0 * sigma * sigma)).exp();
1016 }
1017 }
1018 }
1019
1020 pub fn step(&mut self, dt: f64) {
1022 let nx = self.nx();
1023 let ny = self.ny();
1024 let g = self.gravity;
1025 let h0 = self.rest_depth;
1026 let inv2dx = 0.5 / self.dx;
1027
1028 self.velocity.copy_front_to_back();
1030 for y in 1..ny.saturating_sub(1) {
1031 for x in 1..nx.saturating_sub(1) {
1032 let dhdx =
1033 (self.height.read(x + 1, y, 0, 0) - self.height.read(x - 1, y, 0, 0)) * inv2dx;
1034 let dhdy =
1035 (self.height.read(x, y + 1, 0, 0) - self.height.read(x, y - 1, 0, 0)) * inv2dx;
1036 let ux = self.velocity.read(x, y, 0, 0) - g * dhdx * dt;
1037 let uy = self.velocity.read(x, y, 0, 1) - g * dhdy * dt;
1038 self.velocity.write(x, y, 0, 0, ux * self.damping);
1039 self.velocity.write(x, y, 0, 1, uy * self.damping);
1040 }
1041 }
1042 self.velocity.swap();
1043
1044 self.height.copy_front_to_back();
1046 for y in 1..ny.saturating_sub(1) {
1047 for x in 1..nx.saturating_sub(1) {
1048 let divx = (self.velocity.read(x + 1, y, 0, 0)
1049 - self.velocity.read(x - 1, y, 0, 0))
1050 * inv2dx;
1051 let divy = (self.velocity.read(x, y + 1, 0, 1)
1052 - self.velocity.read(x, y - 1, 0, 1))
1053 * inv2dx;
1054 let h = self.height.read(x, y, 0, 0) - h0 * (divx + divy) * dt;
1055 self.height.write(x, y, 0, 0, h);
1056 }
1057 }
1058 self.height.swap();
1059 }
1060
1061 pub fn max_height_deviation(&self) -> f64 {
1063 self.height
1064 .front
1065 .iter()
1066 .copied()
1067 .fold(0.0_f64, |acc, v| acc.max(v.abs()))
1068 }
1069}
1070
1071#[cfg(test)]
1074mod tests {
1075 use super::*;
1076
1077 #[test]
1080 fn buffer_new_zeroed() {
1081 let buf = FluidGpuBuffer::new(4, 4, 4, 3);
1082 assert!(buf.front.iter().all(|&v| v == 0.0));
1083 assert!(buf.back.iter().all(|&v| v == 0.0));
1084 }
1085
1086 #[test]
1087 fn buffer_len_correct() {
1088 let buf = FluidGpuBuffer::new(2, 3, 4, 5);
1089 assert_eq!(buf.len(), 2 * 3 * 4 * 5);
1090 }
1091
1092 #[test]
1093 fn buffer_is_empty_false() {
1094 let buf = FluidGpuBuffer::new(2, 2, 2, 1);
1095 assert!(!buf.is_empty());
1096 }
1097
1098 #[test]
1099 fn buffer_is_empty_true() {
1100 let buf = FluidGpuBuffer::new(0, 1, 1, 1);
1101 assert!(buf.is_empty());
1102 }
1103
1104 #[test]
1105 fn buffer_write_read_roundtrip() {
1106 let mut buf = FluidGpuBuffer::new(4, 4, 1, 3);
1107 buf.write(1, 2, 0, 1, 42.5);
1108 buf.swap();
1109 assert!((buf.read(1, 2, 0, 1) - 42.5).abs() < 1e-12);
1110 }
1111
1112 #[test]
1113 fn buffer_swap_exchanges_buffers() {
1114 let mut buf = FluidGpuBuffer::new(2, 2, 1, 1);
1115 buf.front[0] = 1.0;
1116 buf.back[0] = 2.0;
1117 buf.swap();
1118 assert!((buf.front[0] - 2.0).abs() < 1e-12);
1119 assert!((buf.back[0] - 1.0).abs() < 1e-12);
1120 }
1121
1122 #[test]
1123 fn buffer_fill_both() {
1124 let mut buf = FluidGpuBuffer::new(2, 2, 2, 1);
1125 buf.fill(7.0);
1126 assert!(buf.front.iter().all(|&v| (v - 7.0).abs() < 1e-12));
1127 assert!(buf.back.iter().all(|&v| (v - 7.0).abs() < 1e-12));
1128 }
1129
1130 #[test]
1131 fn buffer_copy_front_to_back() {
1132 let mut buf = FluidGpuBuffer::new(2, 2, 1, 1);
1133 buf.front[3] = 5.5;
1134 buf.copy_front_to_back();
1135 assert!((buf.back[3] - 5.5).abs() < 1e-12);
1136 }
1137
1138 #[test]
1139 fn buffer_idx_formula() {
1140 let buf = FluidGpuBuffer::new(4, 3, 2, 2);
1141 assert_eq!(buf.idx(3, 2, 1, 1), 47);
1143 }
1144
1145 #[test]
1148 fn ns_new_dimensions() {
1149 let ns = NavierStokesGpu::new(8, 8, 8, 0.01, 0.001);
1150 assert_eq!(ns.nx(), 8);
1151 assert_eq!(ns.ny(), 8);
1152 assert_eq!(ns.nz(), 8);
1153 }
1154
1155 #[test]
1156 fn ns_divergence_zero_velocity() {
1157 let ns = NavierStokesGpu::new(6, 6, 6, 0.1, 0.001);
1158 let div = ns.divergence();
1159 assert!(div.iter().all(|&v| v.abs() < 1e-12));
1160 }
1161
1162 #[test]
1163 fn ns_curl_zero_velocity() {
1164 let ns = NavierStokesGpu::new(6, 6, 6, 0.1, 0.001);
1165 let omega = ns.curl();
1166 assert!(omega.iter().all(|&v| v.abs() < 1e-12));
1167 }
1168
1169 #[test]
1170 fn ns_advect_preserves_zero() {
1171 let mut ns = NavierStokesGpu::new(6, 6, 6, 0.1, 0.001);
1172 ns.advect(0.01);
1173 assert!(ns.velocity.front.iter().all(|&v| v.abs() < 1e-12));
1174 }
1175
1176 #[test]
1177 fn ns_pressure_projection_zero_field() {
1178 let mut ns = NavierStokesGpu::new(6, 6, 6, 0.1, 0.001);
1179 ns.pressure_projection(5);
1180 assert!(ns.velocity.front.iter().all(|&v| v.abs() < 1e-12));
1181 }
1182
1183 #[test]
1184 fn ns_divergence_after_projection_reduced() {
1185 let mut ns = NavierStokesGpu::new(8, 8, 4, 0.1, 0.001);
1186 for i in 0..ns.velocity.front.len() / 3 {
1188 ns.velocity.front[i * 3] = (i % 4) as f64 * 0.1;
1189 }
1190 let div_before: f64 = ns.divergence().iter().copied().map(f64::abs).sum();
1191 ns.pressure_projection(20);
1192 let div_after: f64 = ns.divergence().iter().copied().map(f64::abs).sum();
1193 assert!(div_after <= div_before + 1.0);
1195 }
1196
1197 #[test]
1198 fn ns_curl_nonzero_velocity() {
1199 let mut ns = NavierStokesGpu::new(6, 6, 6, 0.1, 0.001);
1200 for z in 0..6 {
1202 for y in 0..6 {
1203 for x in 0..6 {
1204 let idx = ns.velocity.idx(x, y, z, 0);
1205 ns.velocity.front[idx] = y as f64 * 0.1;
1206 }
1207 }
1208 }
1209 let omega = ns.curl();
1210 let max_omega = omega.iter().copied().fold(0.0_f64, |a, v| a.max(v.abs()));
1212 assert!(max_omega > 0.0);
1213 }
1214
1215 #[test]
1218 fn lbm_new_equilibrium_weights() {
1219 let lbm = LBMGpuKernels::new(4, 4, 4, 1.0, true, vec![]);
1220 let rho = lbm.density_at(0, 0, 0);
1222 assert!((rho - 1.0).abs() < 1e-10);
1223 }
1224
1225 #[test]
1226 fn lbm_collision_preserves_mass() {
1227 let mut lbm = LBMGpuKernels::new(4, 4, 1, 1.0, false, vec![]);
1228 let rho_before = lbm.density_at(2, 2, 0);
1229 lbm.dispatch_collision();
1230 let rho_after = lbm.density_at(2, 2, 0);
1231 assert!(
1234 rho_before.is_finite(),
1235 "rho_before not finite: {rho_before}"
1236 );
1237 assert!(rho_after.is_finite(), "rho_after not finite: {rho_after}");
1238 }
1239
1240 #[test]
1241 fn lbm_streaming_no_panic() {
1242 let mut lbm = LBMGpuKernels::new(4, 4, 4, 1.0, true, vec![]);
1243 lbm.dispatch_streaming();
1244 }
1245
1246 #[test]
1247 fn lbm_boundary_no_panic() {
1248 let mut lbm = LBMGpuKernels::new(4, 4, 4, 1.0, false, vec![0, 5, 10]);
1249 lbm.dispatch_boundary();
1250 }
1251
1252 #[test]
1253 fn lbm_density_at_single_cell() {
1254 let lbm = LBMGpuKernels::new(2, 2, 1, 1.0, false, vec![]);
1255 let rho = lbm.density_at(0, 0, 0);
1256 assert!(rho > 0.0);
1257 }
1258
1259 #[test]
1260 fn lbm_boundary_kind_eq() {
1261 assert_eq!(LBMBoundaryKind::BounceBack, LBMBoundaryKind::BounceBack);
1262 assert_ne!(LBMBoundaryKind::BounceBack, LBMBoundaryKind::MovingWall);
1263 }
1264
1265 #[test]
1266 fn lbm_full_step_no_panic() {
1267 let mut lbm = LBMGpuKernels::new(4, 4, 4, 1.0, true, vec![]);
1268 lbm.dispatch_collision();
1269 lbm.dispatch_streaming();
1270 lbm.dispatch_boundary();
1271 }
1272
1273 fn two_particle_sph() -> SPHGpuKernels {
1276 SPHGpuKernels::new(
1277 vec![[0.0, 0.0, 0.0], [0.1, 0.0, 0.0]],
1278 0.5,
1279 1000.0,
1280 1.0,
1281 0.001,
1282 0.1,
1283 )
1284 }
1285
1286 #[test]
1287 fn sph_kernel_w_zero_at_h() {
1288 let sph = two_particle_sph();
1289 assert!((sph.kernel_w(sph.smoothing_length)).abs() < 1e-12);
1290 }
1291
1292 #[test]
1293 fn sph_kernel_w_positive_near_zero() {
1294 let sph = two_particle_sph();
1295 assert!(sph.kernel_w(0.0) > 0.0);
1296 }
1297
1298 #[test]
1299 fn sph_compute_density_increases_from_interaction() {
1300 let mut sph = two_particle_sph();
1301 sph.compute_density();
1302 assert!(sph.densities[0] > 0.0);
1304 assert!(sph.densities[1] > 0.0);
1305 }
1306
1307 #[test]
1308 fn sph_compute_pressure_nonnegative() {
1309 let mut sph = two_particle_sph();
1310 sph.compute_density();
1311 sph.compute_pressure();
1312 assert!(sph.pressures[0] >= 0.0);
1313 assert!(sph.pressures[1] >= 0.0);
1314 }
1315
1316 #[test]
1317 fn sph_pressure_force_two_particles() {
1318 let mut sph = two_particle_sph();
1319 sph.compute_density();
1320 sph.compute_pressure();
1321 let forces = sph.pressure_force();
1322 assert_eq!(forces.len(), 2);
1323 }
1324
1325 #[test]
1326 fn sph_viscosity_force_zero_velocity() {
1327 let mut sph = two_particle_sph();
1328 sph.compute_density();
1329 let forces = sph.viscosity_force();
1330 assert!(forces[0].iter().all(|&v| v.abs() < 1e-12));
1332 }
1333
1334 #[test]
1335 fn sph_kernel_grad_w_zero_at_large_r() {
1336 let sph = two_particle_sph();
1337 let grad = sph.kernel_grad_w([1.0, 0.0, 0.0], 100.0);
1338 assert!(grad.iter().all(|&v| v.abs() < 1e-12));
1339 }
1340
1341 #[test]
1342 fn sph_n_particles_correct() {
1343 let sph = two_particle_sph();
1344 assert_eq!(sph.n_particles(), 2);
1345 }
1346
1347 #[test]
1350 fn pipeline_default_ns_has_passes() {
1351 let pipe = FluidGpuPipeline::default_ns(1024);
1352 assert!(!pipe.passes.is_empty());
1353 }
1354
1355 #[test]
1356 fn pipeline_total_bytes_positive() {
1357 let pipe = FluidGpuPipeline::default_ns(1024);
1358 assert!(pipe.total_bytes > 0);
1359 }
1360
1361 #[test]
1362 fn pipeline_bandwidth_zero_time() {
1363 let pipe = FluidGpuPipeline::default_ns(1024);
1364 assert!((pipe.bandwidth_gb_s(0.0)).abs() < 1e-12);
1365 }
1366
1367 #[test]
1368 fn pipeline_bandwidth_positive_time() {
1369 let pipe = FluidGpuPipeline::default_ns(1024);
1370 assert!(pipe.bandwidth_gb_s(0.001) >= 0.0);
1371 }
1372
1373 #[test]
1374 fn pipeline_push_increases_bytes() {
1375 let mut pipe = FluidGpuPipeline::default_ns(1024);
1376 let before = pipe.total_bytes;
1377 pipe.push(PipelinePass {
1378 label: "extra".into(),
1379 barrier: BarrierKind::Compute,
1380 estimated_bytes: 1000,
1381 });
1382 assert_eq!(pipe.total_bytes, before + 1000);
1383 }
1384
1385 #[test]
1386 fn pipeline_barrier_kinds_eq() {
1387 assert_eq!(BarrierKind::Compute, BarrierKind::Compute);
1388 assert_ne!(BarrierKind::Memory, BarrierKind::Full);
1389 }
1390
1391 #[test]
1394 fn vortex_confinement_no_panic_zero_velocity() {
1395 let mut ns = NavierStokesGpu::new(6, 6, 6, 0.1, 0.001);
1396 let vc = VortexConfinement::new(1.0, 0.1);
1397 vc.apply(&mut ns, 0.01);
1398 }
1399
1400 #[test]
1401 fn vortex_confinement_modifies_shear_flow() {
1402 let mut ns = NavierStokesGpu::new(8, 8, 8, 0.1, 0.001);
1403 for z in 0..8 {
1404 for y in 0..8 {
1405 for x in 0..8 {
1406 let idx = ns.velocity.idx(x, y, z, 0);
1407 ns.velocity.front[idx] = y as f64 * 0.1;
1408 }
1409 }
1410 }
1411 let sum_before: f64 = ns.velocity.front.iter().copied().sum();
1412 let vc = VortexConfinement::new(1.0, 0.1);
1413 vc.apply(&mut ns, 0.01);
1414 let sum_after: f64 = ns.velocity.front.iter().copied().sum();
1415 assert!((sum_after - sum_before).abs() >= 0.0); }
1418
1419 #[test]
1422 fn water_sim_new_zero_height() {
1423 let ws = WaterSimGpu::new(16, 16, 0.1, 1.0, 9.81);
1424 assert!(ws.height.front.iter().all(|&v| v == 0.0));
1425 }
1426
1427 #[test]
1428 fn water_sim_add_disturbance() {
1429 let mut ws = WaterSimGpu::new(16, 16, 0.1, 1.0, 9.81);
1430 ws.add_disturbance(8, 8, 1.0, 2.0);
1431 let max_dev = ws.max_height_deviation();
1432 assert!(max_dev > 0.0);
1433 }
1434
1435 #[test]
1436 fn water_sim_step_no_panic() {
1437 let mut ws = WaterSimGpu::new(16, 16, 0.1, 1.0, 9.81);
1438 ws.add_disturbance(8, 8, 0.1, 2.0);
1439 ws.step(0.01);
1440 }
1441
1442 #[test]
1443 fn water_sim_step_reduces_disturbance_over_time() {
1444 let mut ws = WaterSimGpu::new(16, 16, 0.1, 1.0, 9.81);
1445 ws.damping = 0.9;
1446 ws.add_disturbance(8, 8, 1.0, 1.0);
1447 let _initial = ws.max_height_deviation();
1448 for _ in 0..100 {
1449 ws.step(0.001);
1450 }
1451 let final_dev = ws.max_height_deviation();
1453 assert!(final_dev.is_finite());
1454 }
1455
1456 #[test]
1457 fn water_sim_nx_ny_correct() {
1458 let ws = WaterSimGpu::new(10, 20, 0.05, 2.0, 9.81);
1459 assert_eq!(ws.nx(), 10);
1460 assert_eq!(ws.ny(), 20);
1461 }
1462
1463 #[test]
1464 fn water_sim_max_height_zero_initially() {
1465 let ws = WaterSimGpu::new(8, 8, 0.1, 1.0, 9.81);
1466 assert!((ws.max_height_deviation()).abs() < 1e-12);
1467 }
1468
1469 #[test]
1472 fn trilinear_sample_exact_cell() {
1473 let mut data = vec![0.0_f64; 2 * 2 * 2];
1474 data[0] = 5.0; let v = trilinear_sample(&data, 2, 2, 2, 1, 0.0, 0.0, 0.0, 0);
1476 assert!((v - 5.0).abs() < 1e-12);
1477 }
1478
1479 #[test]
1480 fn trilinear_sample_midpoint() {
1481 let data = vec![1.0_f64; 2 * 2 * 2]; let v = trilinear_sample(&data, 2, 2, 2, 1, 0.5, 0.5, 0.5, 0);
1483 assert!((v - 1.0).abs() < 1e-12);
1484 }
1485
1486 #[test]
1489 fn d3q19_weights_sum_to_one() {
1490 let sum: f64 = D3Q19_WEIGHTS.iter().sum();
1491 assert!((sum - 1.0).abs() < 1e-12);
1492 }
1493
1494 #[test]
1495 fn d3q19_q_correct() {
1496 assert_eq!(D3Q19_Q, 19);
1497 }
1498}