Skip to main content

oxiphysics_gpu/
gpu_fluid_euler.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! GPU Eulerian fluid simulation on a staggered MAC grid (CPU mock backend).
5//!
6//! Implements a staggered Marker-and-Cell (MAC) grid fluid solver with:
7//! semi-Lagrangian advection, gravity forcing, Jacobi pressure projection,
8//! solid boundary conditions, vorticity confinement, and diagnostic utilities.
9
10// ── Data structures ──────────────────────────────────────────────────────────
11
12/// A 3-D staggered MAC grid for Eulerian fluid simulation.
13///
14/// Velocity components are face-centred; pressure and density are cell-centred.
15#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub struct GpuEulerGrid {
18    /// Number of cells in x.
19    pub nx: usize,
20    /// Number of cells in y.
21    pub ny: usize,
22    /// Number of cells in z.
23    pub nz: usize,
24    /// Grid spacing (m).
25    pub dx: f64,
26    /// Cell-centred density (kg/m³).
27    pub rho: Vec<f64>,
28    /// x-face velocity (m/s), size (nx+1)·ny·nz.
29    pub u: Vec<f64>,
30    /// y-face velocity (m/s), size nx·(ny+1)·nz.
31    pub v: Vec<f64>,
32    /// z-face velocity (m/s), size nx·ny·(nz+1).
33    pub w: Vec<f64>,
34    /// Cell-centred pressure (Pa).
35    pub p: Vec<f64>,
36}
37
38impl GpuEulerGrid {
39    /// Create a new `GpuEulerGrid` with `nx × ny × nz` cells and spacing `dx`.
40    pub fn new(nx: usize, ny: usize, nz: usize, dx: f64) -> Self {
41        let nc = nx * ny * nz;
42        Self {
43            nx,
44            ny,
45            nz,
46            dx,
47            rho: vec![1.0; nc],
48            u: vec![0.0; (nx + 1) * ny * nz],
49            v: vec![0.0; nx * (ny + 1) * nz],
50            w: vec![0.0; nx * ny * (nz + 1)],
51            p: vec![0.0; nc],
52        }
53    }
54
55    /// Flat cell index for cell (i, j, k).
56    pub fn index(&self, i: usize, j: usize, k: usize) -> usize {
57        k * self.nx * self.ny + j * self.nx + i
58    }
59
60    /// Total number of cells.
61    pub fn cell_count(&self) -> usize {
62        self.nx * self.ny * self.nz
63    }
64
65    /// Maximum speed over all velocity components.
66    pub fn max_velocity(&self) -> f64 {
67        let mu = self.u.iter().cloned().fold(0.0_f64, |a, v| a.max(v.abs()));
68        let mv = self.v.iter().cloned().fold(0.0_f64, |a, v| a.max(v.abs()));
69        let mw = self.w.iter().cloned().fold(0.0_f64, |a, v| a.max(v.abs()));
70        mu.max(mv).max(mw)
71    }
72
73    /// CFL-safe time step: `cfl · dx / max_velocity` (or `dx` if velocity is zero).
74    pub fn cfl_timestep(&self, cfl: f64) -> f64 {
75        let vmax = self.max_velocity();
76        if vmax < 1e-15 {
77            return self.dx;
78        }
79        cfl * self.dx / vmax
80    }
81
82    /// Apply gravitational acceleration to the v (y) velocity field.
83    pub fn gpu_apply_gravity(&mut self, gravity: f64, dt: f64) {
84        for val in self.v.iter_mut() {
85            *val += gravity * dt;
86        }
87    }
88
89    /// Compute the divergence field ∇·u at every cell.
90    pub fn gpu_compute_divergence(&self) -> Vec<f64> {
91        let nc = self.cell_count();
92        let mut div = vec![0.0f64; nc];
93        let dx = self.dx;
94        for k in 0..self.nz {
95            for j in 0..self.ny {
96                for i in 0..self.nx {
97                    let idx = self.index(i, j, k);
98                    // u on x-faces
99                    let ui_p = self.u[k * (self.nx + 1) * self.ny + j * (self.nx + 1) + i + 1];
100                    let ui_m = self.u[k * (self.nx + 1) * self.ny + j * (self.nx + 1) + i];
101                    // v on y-faces
102                    let vj_p = self.v[k * self.nx * (self.ny + 1) + (j + 1) * self.nx + i];
103                    let vj_m = self.v[k * self.nx * (self.ny + 1) + j * self.nx + i];
104                    // w on z-faces
105                    let wk_p = self.w[(k + 1) * self.nx * self.ny + j * self.nx + i];
106                    let wk_m = self.w[k * self.nx * self.ny + j * self.nx + i];
107                    div[idx] = (ui_p - ui_m + vj_p - vj_m + wk_p - wk_m) / dx;
108                }
109            }
110        }
111        div
112    }
113
114    /// Local divergence at cell (i, j, k).
115    pub fn divergence_at(&self, i: usize, j: usize, k: usize) -> f64 {
116        let div = self.gpu_compute_divergence();
117        div[self.index(i, j, k)]
118    }
119
120    /// Local vorticity (curl of velocity) at cell (i, j, k).
121    ///
122    /// Returns `[ωx, ωy, ωz]` using central differences.
123    pub fn vorticity_at(&self, i: usize, j: usize, k: usize) -> [f64; 3] {
124        let dx = self.dx;
125        // Use interior clamping for boundary cells
126        let ip = i.min(self.nx - 1);
127        let im = if i > 0 { i - 1 } else { 0 };
128        let jp = j.min(self.ny - 1);
129        let jm = if j > 0 { j - 1 } else { 0 };
130        let kp = k.min(self.nz - 1);
131        let km = if k > 0 { k - 1 } else { 0 };
132
133        // Sample cell-centred velocities from face averages
134        let u_jm = self.u[km * (self.nx + 1) * self.ny + jm * (self.nx + 1) + i];
135        let u_jp = self.u[kp * (self.nx + 1) * self.ny + jp * (self.nx + 1) + i];
136        let u_km = self.u[km * (self.nx + 1) * self.ny + j * (self.nx + 1) + i];
137        let u_kp = self.u[kp * (self.nx + 1) * self.ny + j * (self.nx + 1) + i];
138
139        let v_im = self.v[k * self.nx * (self.ny + 1) + j * self.nx + im];
140        let v_ip = self.v[k * self.nx * (self.ny + 1) + j * self.nx + ip];
141        let v_km = self.v[km * self.nx * (self.ny + 1) + j * self.nx + i];
142        let v_kp = self.v[kp * self.nx * (self.ny + 1) + j * self.nx + i];
143
144        let w_im = self.w[k * self.nx * self.ny + j * self.nx + im];
145        let w_ip = self.w[k * self.nx * self.ny + j * self.nx + ip];
146        let w_jm = self.w[k * self.nx * self.ny + jm * self.nx + i];
147        let w_jp = self.w[k * self.nx * self.ny + jp * self.nx + i];
148
149        let wx = (w_jp - w_jm) / (2.0 * dx) - (v_kp - v_km) / (2.0 * dx);
150        let wy = (u_kp - u_km) / (2.0 * dx) - (w_ip - w_im) / (2.0 * dx);
151        let wz = (v_ip - v_im) / (2.0 * dx) - (u_jp - u_jm) / (2.0 * dx);
152        [wx, wy, wz]
153    }
154
155    /// Zero normal velocities at domain boundaries (solid wall BC).
156    pub fn gpu_enforce_solid_bc(&mut self) {
157        // x-faces at i=0 and i=nx
158        for k in 0..self.nz {
159            for j in 0..self.ny {
160                self.u[k * (self.nx + 1) * self.ny + j * (self.nx + 1)] = 0.0;
161                self.u[k * (self.nx + 1) * self.ny + j * (self.nx + 1) + self.nx] = 0.0;
162            }
163        }
164        // y-faces at j=0 and j=ny
165        for k in 0..self.nz {
166            for i in 0..self.nx {
167                self.v[k * self.nx * (self.ny + 1) + i] = 0.0;
168                self.v[k * self.nx * (self.ny + 1) + self.ny * self.nx + i] = 0.0;
169            }
170        }
171        // z-faces at k=0 and k=nz
172        for j in 0..self.ny {
173            for i in 0..self.nx {
174                self.w[j * self.nx + i] = 0.0;
175                self.w[self.nz * self.nx * self.ny + j * self.nx + i] = 0.0;
176            }
177        }
178    }
179
180    /// Jacobi pressure projection to enforce incompressibility.
181    ///
182    /// Iterates up to `max_iter` times; returns the final L-inf residual.
183    pub fn gpu_pressure_projection(&mut self, max_iter: usize, tol: f64) -> f64 {
184        let dx = self.dx;
185        let dx2 = dx * dx;
186        let div = self.gpu_compute_divergence();
187        let nc = self.cell_count();
188        let mut residual = 0.0f64;
189
190        for _iter in 0..max_iter {
191            let p_old = self.p.clone();
192            residual = 0.0;
193            for k in 0..self.nz {
194                for j in 0..self.ny {
195                    for i in 0..self.nx {
196                        let idx = self.index(i, j, k);
197                        // Gather neighbour pressures (Neumann BC: ghost = interior)
198                        let px_p = if i + 1 < self.nx {
199                            p_old[self.index(i + 1, j, k)]
200                        } else {
201                            p_old[idx]
202                        };
203                        let px_m = if i > 0 {
204                            p_old[self.index(i - 1, j, k)]
205                        } else {
206                            p_old[idx]
207                        };
208                        let py_p = if j + 1 < self.ny {
209                            p_old[self.index(i, j + 1, k)]
210                        } else {
211                            p_old[idx]
212                        };
213                        let py_m = if j > 0 {
214                            p_old[self.index(i, j - 1, k)]
215                        } else {
216                            p_old[idx]
217                        };
218                        let pz_p = if k + 1 < self.nz {
219                            p_old[self.index(i, j, k + 1)]
220                        } else {
221                            p_old[idx]
222                        };
223                        let pz_m = if k > 0 {
224                            p_old[self.index(i, j, k - 1)]
225                        } else {
226                            p_old[idx]
227                        };
228                        let p_new =
229                            (px_p + px_m + py_p + py_m + pz_p + pz_m - dx2 * div[idx]) / 6.0;
230                        let diff = (p_new - self.p[idx]).abs();
231                        if diff > residual {
232                            residual = diff;
233                        }
234                        self.p[idx] = p_new;
235                    }
236                }
237            }
238            if residual < tol {
239                break;
240            }
241        }
242        let _ = nc;
243        residual
244    }
245
246    /// Update velocity from pressure gradient: u -= dt · ∇p.
247    pub fn gpu_update_velocity_from_pressure(&mut self, dt: f64) {
248        let dx = self.dx;
249        // Update u (x-faces)
250        for k in 0..self.nz {
251            for j in 0..self.ny {
252                for i in 1..self.nx {
253                    let idx_r = self.index(i, j, k);
254                    let idx_l = self.index(i - 1, j, k);
255                    let fi = k * (self.nx + 1) * self.ny + j * (self.nx + 1) + i;
256                    self.u[fi] -= dt * (self.p[idx_r] - self.p[idx_l]) / dx;
257                }
258            }
259        }
260        // Update v (y-faces)
261        for k in 0..self.nz {
262            for j in 1..self.ny {
263                for i in 0..self.nx {
264                    let idx_t = self.index(i, j, k);
265                    let idx_b = self.index(i, j - 1, k);
266                    let fj = k * self.nx * (self.ny + 1) + j * self.nx + i;
267                    self.v[fj] -= dt * (self.p[idx_t] - self.p[idx_b]) / dx;
268                }
269            }
270        }
271        // Update w (z-faces)
272        for k in 1..self.nz {
273            for j in 0..self.ny {
274                for i in 0..self.nx {
275                    let idx_f = self.index(i, j, k);
276                    let idx_b = self.index(i, j, k - 1);
277                    let fk = k * self.nx * self.ny + j * self.nx + i;
278                    self.w[fk] -= dt * (self.p[idx_f] - self.p[idx_b]) / dx;
279                }
280            }
281        }
282    }
283
284    /// Semi-Lagrangian advection of velocity (first-order back-trace).
285    pub fn gpu_advect_semi_lagrange(&mut self, dt: f64) {
286        let dx = self.dx;
287        let nx = self.nx;
288        let ny = self.ny;
289        let nz = self.nz;
290
291        // Advect u
292        let u_old = self.u.clone();
293        let v_old = self.v.clone();
294        let w_old = self.w.clone();
295        for k in 0..nz {
296            for j in 0..ny {
297                for i in 0..=nx {
298                    let fi = k * (nx + 1) * ny + j * (nx + 1) + i;
299                    // Average v and w at this face
300                    let vavg = if i < nx {
301                        0.5 * (v_old[k * nx * (ny + 1) + j * nx + i]
302                            + v_old[k * nx * (ny + 1) + (j + 1).min(ny) * nx + i])
303                    } else {
304                        0.0
305                    };
306                    let wavg = if i < nx {
307                        0.5 * (w_old[k * nx * ny + j * nx + i]
308                            + w_old[(k + 1).min(nz) * nx * ny + j * nx + i])
309                    } else {
310                        0.0
311                    };
312                    let xi = i as f64 * dx - u_old[fi] * dt;
313                    let yi = (j as f64 + 0.5) * dx - vavg * dt;
314                    let zi = (k as f64 + 0.5) * dx - wavg * dt;
315                    self.u[fi] = trilinear_u(&u_old, xi, yi, zi, nx, ny, nz, dx);
316                }
317            }
318        }
319        // Advect v
320        for k in 0..nz {
321            for j in 0..=ny {
322                for i in 0..nx {
323                    let fj = k * nx * (ny + 1) + j * nx + i;
324                    let uavg = if j < ny {
325                        0.5 * (u_old[k * (nx + 1) * ny + j * (nx + 1) + i]
326                            + u_old[k * (nx + 1) * ny + j * (nx + 1) + i + 1])
327                    } else {
328                        0.0
329                    };
330                    let wavg = if j < ny {
331                        0.5 * (w_old[k * nx * ny + j * nx + i]
332                            + w_old[(k + 1).min(nz) * nx * ny + j * nx + i])
333                    } else {
334                        0.0
335                    };
336                    let xi = (i as f64 + 0.5) * dx - uavg * dt;
337                    let yi = j as f64 * dx - v_old[fj] * dt;
338                    let zi = (k as f64 + 0.5) * dx - wavg * dt;
339                    self.v[fj] = trilinear_v(&v_old, xi, yi, zi, nx, ny, nz, dx);
340                }
341            }
342        }
343        // Advect w (simplified: re-use current values for now – keeps it O(N))
344        for k in 0..=nz {
345            for j in 0..ny {
346                for i in 0..nx {
347                    let fk = k * nx * ny + j * nx + i;
348                    let uavg = if k < nz {
349                        0.5 * (u_old[k * (nx + 1) * ny + j * (nx + 1) + i]
350                            + u_old[k * (nx + 1) * ny + j * (nx + 1) + i + 1])
351                    } else {
352                        0.0
353                    };
354                    let xi = (i as f64 + 0.5) * dx - uavg * dt;
355                    let zi = k as f64 * dx - w_old[fk] * dt;
356                    self.w[fk] = trilinear_w(&w_old, xi, zi, i, j, k, nx, ny, nz, dx);
357                }
358            }
359        }
360    }
361
362    /// Vorticity confinement: adds a force proportional to `epsilon` toward
363    /// regions of high vorticity to counteract numerical diffusion.
364    pub fn gpu_vorticity_confinement(&mut self, epsilon: f64) {
365        let nx = self.nx;
366        let ny = self.ny;
367        let nz = self.nz;
368        let dx = self.dx;
369        // Collect vorticity magnitudes
370        let mut eta: Vec<[f64; 3]> = vec![[0.0; 3]; nx * ny * nz];
371        for k in 0..nz {
372            for j in 0..ny {
373                for i in 0..nx {
374                    eta[k * nx * ny + j * nx + i] = self.vorticity_at(i, j, k);
375                }
376            }
377        }
378        // Compute normalised gradient of |eta| and apply confinement force to v
379        for k in 1..nz - 1 {
380            for j in 1..ny - 1 {
381                for i in 1..nx - 1 {
382                    let mag = |e: [f64; 3]| (e[0] * e[0] + e[1] * e[1] + e[2] * e[2]).sqrt();
383                    let m_ip = mag(eta[k * nx * ny + j * nx + i + 1]);
384                    let m_im = mag(eta[k * nx * ny + j * nx + i - 1]);
385                    let m_jp = mag(eta[k * nx * ny + (j + 1) * nx + i]);
386                    let m_jm = mag(eta[k * nx * ny + (j - 1) * nx + i]);
387                    let m_kp = mag(eta[(k + 1) * nx * ny + j * nx + i]);
388                    let m_km = mag(eta[(k - 1) * nx * ny + j * nx + i]);
389                    let nx_grad = (m_ip - m_im) / (2.0 * dx);
390                    let ny_grad = (m_jp - m_jm) / (2.0 * dx);
391                    let nz_grad = (m_kp - m_km) / (2.0 * dx);
392                    let norm = (nx_grad * nx_grad + ny_grad * ny_grad + nz_grad * nz_grad).sqrt();
393                    if norm > 1e-15 {
394                        let omega = eta[k * nx * ny + j * nx + i];
395                        let nx_n = nx_grad / norm;
396                        let ny_n = ny_grad / norm;
397                        let nz_n = nz_grad / norm;
398                        // f = epsilon * (N × omega)
399                        let fx = epsilon * (ny_n * omega[2] - nz_n * omega[1]);
400                        let fy = epsilon * (nz_n * omega[0] - nx_n * omega[2]);
401                        let _fz = epsilon * (nx_n * omega[1] - ny_n * omega[0]);
402                        // Apply to y-face velocity (simplified: affect cell-centre via faces)
403                        let fj = k * nx * (ny + 1) + j * nx + i;
404                        self.v[fj] += fy;
405                        let fi = k * (nx + 1) * ny + j * (nx + 1) + i;
406                        self.u[fi] += fx;
407                    }
408                }
409            }
410        }
411    }
412
413    /// Compute simulation diagnostics.
414    pub fn compute_stats(&self) -> FluidSimStats {
415        let max_velocity = self.max_velocity();
416        let total_kinetic_energy = self
417            .u
418            .iter()
419            .chain(self.v.iter())
420            .chain(self.w.iter())
421            .map(|&vi| 0.5 * vi * vi)
422            .sum();
423        let div = self.gpu_compute_divergence();
424        let max_divergence = div.iter().cloned().fold(0.0_f64, |a, v| a.max(v.abs()));
425        FluidSimStats {
426            max_velocity,
427            total_kinetic_energy,
428            max_divergence,
429            pressure_residual: 0.0,
430        }
431    }
432}
433
434// ── Trilinear interpolation helpers (private) ─────────────────────────────────
435
436fn clamp_idx(v: f64, n: usize) -> (usize, usize, f64) {
437    let scaled = v.max(0.0).min((n as f64) - 1.0 - 1e-9);
438    let i0 = scaled as usize;
439    let i1 = (i0 + 1).min(n - 1);
440    (i0, i1, scaled - i0 as f64)
441}
442
443#[allow(clippy::too_many_arguments)]
444fn trilinear_u(u: &[f64], x: f64, y: f64, z: f64, nx: usize, ny: usize, nz: usize, dx: f64) -> f64 {
445    let (i0, i1, tx) = clamp_idx(x / dx, nx + 1);
446    let (j0, j1, ty) = clamp_idx(y / dx - 0.5, ny);
447    let (k0, k1, tz) = clamp_idx(z / dx - 0.5, nz);
448    let idx = |i: usize, j: usize, k: usize| k * (nx + 1) * ny + j * (nx + 1) + i;
449    let u000 = u[idx(i0, j0, k0)];
450    let u100 = u[idx(i1, j0, k0)];
451    let u010 = u[idx(i0, j1, k0)];
452    let u110 = u[idx(i1, j1, k0)];
453    let u001 = u[idx(i0, j0, k1)];
454    let u101 = u[idx(i1, j0, k1)];
455    let u011 = u[idx(i0, j1, k1)];
456    let u111 = u[idx(i1, j1, k1)];
457    let lerp = |a: f64, b: f64, t: f64| a + t * (b - a);
458    lerp(
459        lerp(lerp(u000, u100, tx), lerp(u010, u110, tx), ty),
460        lerp(lerp(u001, u101, tx), lerp(u011, u111, tx), ty),
461        tz,
462    )
463}
464
465#[allow(clippy::too_many_arguments)]
466fn trilinear_v(v: &[f64], x: f64, y: f64, z: f64, nx: usize, ny: usize, nz: usize, dx: f64) -> f64 {
467    let (i0, i1, tx) = clamp_idx(x / dx - 0.5, nx);
468    let (j0, j1, ty) = clamp_idx(y / dx, ny + 1);
469    let (k0, k1, tz) = clamp_idx(z / dx - 0.5, nz);
470    let idx = |i: usize, j: usize, k: usize| k * nx * (ny + 1) + j * nx + i;
471    let v000 = v[idx(i0, j0, k0)];
472    let v100 = v[idx(i1, j0, k0)];
473    let v010 = v[idx(i0, j1, k0)];
474    let v110 = v[idx(i1, j1, k0)];
475    let v001 = v[idx(i0, j0, k1)];
476    let v101 = v[idx(i1, j0, k1)];
477    let v011 = v[idx(i0, j1, k1)];
478    let v111 = v[idx(i1, j1, k1)];
479    let lerp = |a: f64, b: f64, t: f64| a + t * (b - a);
480    lerp(
481        lerp(lerp(v000, v100, tx), lerp(v010, v110, tx), ty),
482        lerp(lerp(v001, v101, tx), lerp(v011, v111, tx), ty),
483        tz,
484    )
485}
486
487#[allow(clippy::too_many_arguments)]
488fn trilinear_w(
489    w: &[f64],
490    x: f64,
491    z: f64,
492    _i: usize,
493    j: usize,
494    k: usize,
495    nx: usize,
496    ny: usize,
497    nz: usize,
498    dx: f64,
499) -> f64 {
500    // Simplified: clamp back-trace to same j row
501    let (i0, i1, tx) = clamp_idx(x / dx - 0.5, nx);
502    let (k0, k1, tz) = clamp_idx(z / dx, nz + 1);
503    let idx = |ii: usize, jj: usize, kk: usize| kk * nx * ny + jj * nx + ii;
504    let j_c = j.min(ny - 1);
505    let w000 = w[idx(i0, j_c, k0)];
506    let w100 = w[idx(i1, j_c, k0)];
507    let w001 = w[idx(i0, j_c, k1)];
508    let w101 = w[idx(i1, j_c, k1)];
509    let lerp = |a: f64, b: f64, t: f64| a + t * (b - a);
510    let _ = (k, dx);
511    lerp(lerp(w000, w100, tx), lerp(w001, w101, tx), tz)
512}
513
514// ── Free functions ────────────────────────────────────────────────────────────
515
516/// Statistics for a fluid simulation step.
517#[allow(dead_code)]
518#[derive(Debug, Clone)]
519pub struct FluidSimStats {
520    /// Maximum speed across all velocity components.
521    pub max_velocity: f64,
522    /// Total kinetic energy (½ Σ v²).
523    pub total_kinetic_energy: f64,
524    /// Maximum absolute divergence across all cells.
525    pub max_divergence: f64,
526    /// Pressure solver residual from the last projection.
527    pub pressure_residual: f64,
528}
529
530/// Initialise fluid velocity in a rectangular region of the grid.
531///
532/// Sets u, v, w to `(u0, v0, w0)` for all cells with indices in `region = [i0, i1, j0, j1, k0, k1]`.
533pub fn marker_and_cell_init(
534    grid: &mut GpuEulerGrid,
535    region: [usize; 6],
536    u0: f64,
537    v0: f64,
538    w0: f64,
539) {
540    let [ri0, ri1, rj0, rj1, rk0, rk1] = region;
541    for k in rk0..rk1.min(grid.nz) {
542        for j in rj0..rj1.min(grid.ny) {
543            for i in ri0..ri1.min(grid.nx) {
544                // Set x-face velocities
545                let fi = k * (grid.nx + 1) * grid.ny + j * (grid.nx + 1) + i;
546                grid.u[fi] = u0;
547                // Set y-face velocities
548                let fj = k * grid.nx * (grid.ny + 1) + j * grid.nx + i;
549                grid.v[fj] = v0;
550                // Set z-face velocities
551                let fk = k * grid.nx * grid.ny + j * grid.nx + i;
552                grid.w[fk] = w0;
553            }
554        }
555    }
556}
557
558// ── Tests ────────────────────────────────────────────────────────────────────
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    fn make_grid() -> GpuEulerGrid {
565        GpuEulerGrid::new(4, 4, 4, 0.1)
566    }
567
568    #[test]
569    fn test_cell_count() {
570        let g = make_grid();
571        assert_eq!(g.cell_count(), 64);
572    }
573
574    #[test]
575    fn test_cell_count_custom() {
576        let g = GpuEulerGrid::new(3, 5, 7, 0.1);
577        assert_eq!(g.cell_count(), 3 * 5 * 7);
578    }
579
580    #[test]
581    fn test_index_origin() {
582        let g = make_grid();
583        assert_eq!(g.index(0, 0, 0), 0);
584    }
585
586    #[test]
587    fn test_index_last_cell() {
588        let g = make_grid();
589        assert_eq!(g.index(3, 3, 3), 63);
590    }
591
592    #[test]
593    fn test_index_roundtrip() {
594        let g = make_grid();
595        for k in 0..g.nz {
596            for j in 0..g.ny {
597                for i in 0..g.nx {
598                    let flat = g.index(i, j, k);
599                    let kk = flat / (g.nx * g.ny);
600                    let jj = (flat % (g.nx * g.ny)) / g.nx;
601                    let ii = flat % g.nx;
602                    assert_eq!((ii, jj, kk), (i, j, k));
603                }
604            }
605        }
606    }
607
608    #[test]
609    fn test_initial_max_velocity_zero() {
610        let g = make_grid();
611        assert!(g.max_velocity() < 1e-15);
612    }
613
614    #[test]
615    fn test_cfl_timestep_positive() {
616        let mut g = make_grid();
617        g.u[10] = 1.0;
618        let dt = g.cfl_timestep(0.5);
619        assert!(dt > 0.0);
620    }
621
622    #[test]
623    fn test_cfl_timestep_no_velocity() {
624        let g = make_grid();
625        let dt = g.cfl_timestep(0.5);
626        assert_eq!(dt, g.dx);
627    }
628
629    #[test]
630    fn test_gravity_increases_v() {
631        let mut g = make_grid();
632        let v_before: Vec<f64> = g.v.clone();
633        g.gpu_apply_gravity(-9.81, 0.01);
634        let v_after = &g.v;
635        // At least one v should have decreased (gravity = -9.81)
636        let changed = v_before
637            .iter()
638            .zip(v_after.iter())
639            .any(|(&a, &b)| (b - a).abs() > 1e-12);
640        assert!(changed);
641    }
642
643    #[test]
644    fn test_gravity_value() {
645        let mut g = GpuEulerGrid::new(2, 2, 2, 0.1);
646        g.gpu_apply_gravity(-10.0, 0.1);
647        // All v values should be -1.0
648        assert!(g.v.iter().all(|&vi| (vi + 1.0).abs() < 1e-12));
649    }
650
651    #[test]
652    fn test_solid_bc_zeroes_boundary_u() {
653        let mut g = make_grid();
654        for v in g.u.iter_mut() {
655            *v = 5.0;
656        }
657        g.gpu_enforce_solid_bc();
658        // Check i=0 face
659        for k in 0..g.nz {
660            for j in 0..g.ny {
661                assert_eq!(g.u[k * (g.nx + 1) * g.ny + j * (g.nx + 1)], 0.0);
662            }
663        }
664    }
665
666    #[test]
667    fn test_solid_bc_zeroes_boundary_v() {
668        let mut g = make_grid();
669        for v in g.v.iter_mut() {
670            *v = 3.0;
671        }
672        g.gpu_enforce_solid_bc();
673        // Check j=0 face
674        for k in 0..g.nz {
675            for i in 0..g.nx {
676                assert_eq!(g.v[k * g.nx * (g.ny + 1) + i], 0.0);
677            }
678        }
679    }
680
681    #[test]
682    fn test_solid_bc_zeroes_boundary_w() {
683        let mut g = make_grid();
684        for w in g.w.iter_mut() {
685            *w = 2.0;
686        }
687        g.gpu_enforce_solid_bc();
688        // Check k=0 face
689        for j in 0..g.ny {
690            for i in 0..g.nx {
691                assert_eq!(g.w[j * g.nx + i], 0.0);
692            }
693        }
694    }
695
696    #[test]
697    fn test_divergence_zero_initially() {
698        let g = make_grid();
699        let div = g.gpu_compute_divergence();
700        assert!(div.iter().all(|&d| d.abs() < 1e-12));
701    }
702
703    #[test]
704    fn test_pressure_projection_reduces_divergence() {
705        let mut g = GpuEulerGrid::new(4, 4, 4, 0.1);
706        // Introduce divergence
707        marker_and_cell_init(&mut g, [1, 3, 1, 3, 1, 3], 1.0, 0.5, 0.25);
708        let div_before: f64 = g.gpu_compute_divergence().iter().map(|v| v.abs()).sum();
709        g.gpu_pressure_projection(100, 1e-6);
710        g.gpu_update_velocity_from_pressure(0.01);
711        let div_after: f64 = g.gpu_compute_divergence().iter().map(|v| v.abs()).sum();
712        // Divergence should not increase after projection
713        assert!(div_after <= div_before + 1e-8);
714    }
715
716    #[test]
717    fn test_pressure_projection_returns_residual() {
718        let mut g = make_grid();
719        let res = g.gpu_pressure_projection(10, 1e-6);
720        assert!(res >= 0.0);
721    }
722
723    #[test]
724    fn test_update_velocity_from_pressure_finite() {
725        let mut g = make_grid();
726        g.p[0] = 10.0;
727        g.gpu_update_velocity_from_pressure(0.01);
728        assert!(g.u.iter().all(|v| v.is_finite()));
729        assert!(g.v.iter().all(|v| v.is_finite()));
730    }
731
732    #[test]
733    fn test_marker_and_cell_init_sets_u() {
734        let mut g = make_grid();
735        marker_and_cell_init(&mut g, [0, 2, 0, 2, 0, 2], 3.0, 0.0, 0.0);
736        // At least some u values should be 3.0
737        assert!(g.u.iter().any(|&v| (v - 3.0).abs() < 1e-12));
738    }
739
740    #[test]
741    fn test_vorticity_at_zero_field() {
742        let g = make_grid();
743        let vort = g.vorticity_at(2, 2, 2);
744        assert!(vort.iter().all(|v| v.abs() < 1e-12));
745    }
746
747    #[test]
748    fn test_divergence_at_cell() {
749        let g = make_grid();
750        let d = g.divergence_at(0, 0, 0);
751        assert!(d.abs() < 1e-12);
752    }
753
754    #[test]
755    fn test_stats_max_velocity_nonneg() {
756        let g = make_grid();
757        let stats = g.compute_stats();
758        assert!(stats.max_velocity >= 0.0);
759    }
760
761    #[test]
762    fn test_stats_kinetic_energy_nonneg() {
763        let g = make_grid();
764        let stats = g.compute_stats();
765        assert!(stats.total_kinetic_energy >= 0.0);
766    }
767
768    #[test]
769    fn test_stats_max_divergence_nonneg() {
770        let g = make_grid();
771        let stats = g.compute_stats();
772        assert!(stats.max_divergence >= 0.0);
773    }
774
775    #[test]
776    fn test_advect_does_not_panic() {
777        let mut g = GpuEulerGrid::new(4, 4, 4, 0.1);
778        g.u[5] = 0.5;
779        g.gpu_advect_semi_lagrange(0.01);
780        // Should complete without panic
781    }
782
783    #[test]
784    fn test_vorticity_confinement_does_not_panic() {
785        let mut g = GpuEulerGrid::new(4, 4, 4, 0.1);
786        g.u[10] = 1.0;
787        g.gpu_vorticity_confinement(0.01);
788    }
789
790    #[test]
791    fn test_grid_clone() {
792        let g = make_grid();
793        let g2 = g.clone();
794        assert_eq!(g2.cell_count(), g.cell_count());
795    }
796
797    #[test]
798    fn test_fluid_sim_stats_debug() {
799        let stats = FluidSimStats {
800            max_velocity: 1.0,
801            total_kinetic_energy: 2.0,
802            max_divergence: 0.1,
803            pressure_residual: 0.01,
804        };
805        let s = format!("{stats:?}");
806        assert!(s.contains("FluidSimStats"));
807    }
808
809    #[test]
810    fn test_cfl_formula() {
811        let mut g = GpuEulerGrid::new(4, 4, 4, 0.2);
812        g.u[0] = 2.0;
813        let dt = g.cfl_timestep(0.5);
814        assert!((dt - 0.5 * 0.2 / 2.0).abs() < 1e-12);
815    }
816
817    #[test]
818    fn test_rho_initialised_to_one() {
819        let g = make_grid();
820        assert!(g.rho.iter().all(|&r| (r - 1.0).abs() < 1e-12));
821    }
822}