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