Skip to main content

oxiphysics_gpu/
flux_compute.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! CPU-side parallel flux and advection compute kernels.
5//!
6//! Provides scalar and vector flux computation on structured 3-D Cartesian
7//! grids: upwind advection, central-difference divergence, Lax-Friedrichs
8//! flux splitting, Godunov interface reconstruction, limiter functions
9//! (minmod, superbee, van Leer), and a simple finite-volume time-stepping
10//! helper.  All kernels use Rayon for data-parallel execution.
11
12use rayon::prelude::*;
13
14// ---------------------------------------------------------------------------
15// 1. FluxGrid3D — a 3-D scalar field on a Cartesian grid
16// ---------------------------------------------------------------------------
17
18/// A 3-D scalar field stored in row-major order on a uniform Cartesian grid.
19///
20/// Index layout: `values[i*(ny*nz) + j*nz + k]` for cell `(i, j, k)`.
21#[derive(Debug, Clone)]
22pub struct FluxGrid3D {
23    /// Cells in the x-direction.
24    pub nx: usize,
25    /// Cells in the y-direction.
26    pub ny: usize,
27    /// Cells in the z-direction.
28    pub nz: usize,
29    /// Uniform cell spacing.
30    pub dx: f64,
31    /// Scalar field values.
32    pub values: Vec<f64>,
33}
34
35impl FluxGrid3D {
36    /// Create a grid filled with `fill`.
37    pub fn new(nx: usize, ny: usize, nz: usize, dx: f64, fill: f64) -> Self {
38        Self {
39            nx,
40            ny,
41            nz,
42            dx,
43            values: vec![fill; nx * ny * nz],
44        }
45    }
46
47    /// Flat index for cell `(i, j, k)`.
48    #[inline]
49    pub fn idx(&self, i: usize, j: usize, k: usize) -> usize {
50        i * self.ny * self.nz + j * self.nz + k
51    }
52
53    /// Read cell `(i, j, k)`.
54    #[inline]
55    pub fn get(&self, i: usize, j: usize, k: usize) -> f64 {
56        self.values[self.idx(i, j, k)]
57    }
58
59    /// Write cell `(i, j, k)`.
60    #[inline]
61    pub fn set(&mut self, i: usize, j: usize, k: usize, v: f64) {
62        let idx = self.idx(i, j, k);
63        self.values[idx] = v;
64    }
65
66    /// Total number of cells.
67    pub fn len(&self) -> usize {
68        self.nx * self.ny * self.nz
69    }
70
71    /// True when the grid has no cells.
72    pub fn is_empty(&self) -> bool {
73        self.len() == 0
74    }
75
76    /// Clamp-to-boundary accessor: returns the value at `(i, j, k)` with
77    /// zero-gradient boundary conditions.
78    pub fn get_clamped(&self, i: isize, j: isize, k: isize) -> f64 {
79        let ci = i.clamp(0, self.nx as isize - 1) as usize;
80        let cj = j.clamp(0, self.ny as isize - 1) as usize;
81        let ck = k.clamp(0, self.nz as isize - 1) as usize;
82        self.get(ci, cj, ck)
83    }
84
85    /// L2 norm of all values.
86    pub fn l2_norm(&self) -> f64 {
87        let sum_sq: f64 = self.values.iter().map(|&v| v * v).sum();
88        sum_sq.sqrt()
89    }
90
91    /// Element-wise maximum value.
92    pub fn max_val(&self) -> f64 {
93        self.values
94            .iter()
95            .copied()
96            .fold(f64::NEG_INFINITY, f64::max)
97    }
98
99    /// Element-wise minimum value.
100    pub fn min_val(&self) -> f64 {
101        self.values.iter().copied().fold(f64::INFINITY, f64::min)
102    }
103}
104
105// ---------------------------------------------------------------------------
106// 2. VectorFluxGrid3D — three-component (u, v, w) velocity field
107// ---------------------------------------------------------------------------
108
109/// Three-component 3-D velocity / flux field.
110///
111/// Components `u` (x), `v` (y), `w` (z) are stored as separate [`FluxGrid3D`]
112/// instances sharing the same dimensions and spacing.
113#[derive(Debug, Clone)]
114pub struct VectorFluxGrid3D {
115    /// x-component.
116    pub u: FluxGrid3D,
117    /// y-component.
118    pub v: FluxGrid3D,
119    /// z-component.
120    pub w: FluxGrid3D,
121}
122
123impl VectorFluxGrid3D {
124    /// Create a zero-filled vector field.
125    pub fn zeros(nx: usize, ny: usize, nz: usize, dx: f64) -> Self {
126        Self {
127            u: FluxGrid3D::new(nx, ny, nz, dx, 0.0),
128            v: FluxGrid3D::new(nx, ny, nz, dx, 0.0),
129            w: FluxGrid3D::new(nx, ny, nz, dx, 0.0),
130        }
131    }
132
133    /// Set velocity at cell `(i, j, k)`.
134    pub fn set_vel(&mut self, i: usize, j: usize, k: usize, vel: [f64; 3]) {
135        self.u.set(i, j, k, vel[0]);
136        self.v.set(i, j, k, vel[1]);
137        self.w.set(i, j, k, vel[2]);
138    }
139
140    /// Get velocity at cell `(i, j, k)`.
141    pub fn get_vel(&self, i: usize, j: usize, k: usize) -> [f64; 3] {
142        [
143            self.u.get(i, j, k),
144            self.v.get(i, j, k),
145            self.w.get(i, j, k),
146        ]
147    }
148}
149
150// ---------------------------------------------------------------------------
151// 3. Slope limiter functions
152// ---------------------------------------------------------------------------
153
154/// Minmod slope limiter: smallest slope among `a` and `b` with same sign.
155///
156/// Returns zero when `a` and `b` have opposite signs.
157#[inline]
158pub fn minmod(a: f64, b: f64) -> f64 {
159    if a * b <= 0.0 {
160        0.0
161    } else if a.abs() < b.abs() {
162        a
163    } else {
164        b
165    }
166}
167
168/// Superbee limiter: most compressive TVD limiter.
169pub fn superbee(a: f64, b: f64) -> f64 {
170    if a * b <= 0.0 {
171        return 0.0;
172    }
173    let s = if a >= 0.0 { 1.0 } else { -1.0 };
174    s * a.abs().max(b.abs()).min(2.0 * a.abs().min(b.abs()))
175}
176
177/// Van Leer limiter: smooth TVD limiter.
178pub fn van_leer(a: f64, b: f64) -> f64 {
179    if a * b <= 0.0 {
180        return 0.0;
181    }
182    2.0 * a * b / (a + b)
183}
184
185/// MC (Monotonised Central) limiter.
186pub fn mc_limiter(a: f64, b: f64) -> f64 {
187    if a * b <= 0.0 {
188        return 0.0;
189    }
190    let centered = 0.5 * (a + b);
191    let s = if a >= 0.0 { 1.0 } else { -1.0 };
192    s * centered.abs().min(2.0 * a.abs()).min(2.0 * b.abs())
193}
194
195// ---------------------------------------------------------------------------
196// 4. Upwind advection (1-D per axis, assembled into 3-D)
197// ---------------------------------------------------------------------------
198
199/// First-order upwind advection flux for a scalar `phi` transported by
200/// uniform velocity `vel` in one dimension.
201///
202/// Returns `F_right - F_left` (net flux into cell `i`) for each cell.
203///
204/// Uses zero-gradient boundary conditions.
205pub fn upwind_flux_1d(phi: &[f64], vel: f64, dx: f64, dt: f64) -> Vec<f64> {
206    let n = phi.len();
207    let mut flux = vec![0.0; n];
208    let cfl = vel * dt / dx;
209    for i in 0..n {
210        let phi_l = if i == 0 { phi[0] } else { phi[i - 1] };
211        let phi_r = if i + 1 >= n { phi[n - 1] } else { phi[i + 1] };
212        if vel >= 0.0 {
213            // Upwind from left
214            flux[i] = -cfl * (phi[i] - phi_l);
215        } else {
216            // Upwind from right
217            flux[i] = -cfl * (phi_r - phi[i]);
218        }
219    }
220    flux
221}
222
223/// Apply first-order upwind advection to a [`FluxGrid3D`] for one time step.
224///
225/// Returns a new grid with updated values.
226pub fn advect_upwind_3d(phi: &FluxGrid3D, vel: &VectorFluxGrid3D, dt: f64) -> FluxGrid3D {
227    let _nx = phi.nx;
228    let ny = phi.ny;
229    let nz = phi.nz;
230    let dx = phi.dx;
231
232    let mut out = phi.clone();
233
234    out.values.par_iter_mut().enumerate().for_each(|(idx, v)| {
235        let i = idx / (ny * nz);
236        let j = (idx / nz) % ny;
237        let k = idx % nz;
238
239        let ux = vel.u.get(i, j, k);
240        let uy = vel.v.get(i, j, k);
241        let uz = vel.w.get(i, j, k);
242
243        let phi_xm = phi.get_clamped(i as isize - 1, j as isize, k as isize);
244        let phi_xp = phi.get_clamped(i as isize + 1, j as isize, k as isize);
245        let phi_ym = phi.get_clamped(i as isize, j as isize - 1, k as isize);
246        let phi_yp = phi.get_clamped(i as isize, j as isize + 1, k as isize);
247        let phi_zm = phi.get_clamped(i as isize, j as isize, k as isize - 1);
248        let phi_zp = phi.get_clamped(i as isize, j as isize, k as isize + 1);
249        let phi_c = phi.get(i, j, k);
250
251        let flux_x = if ux >= 0.0 {
252            ux * (phi_c - phi_xm) / dx
253        } else {
254            ux * (phi_xp - phi_c) / dx
255        };
256        let flux_y = if uy >= 0.0 {
257            uy * (phi_c - phi_ym) / dx
258        } else {
259            uy * (phi_yp - phi_c) / dx
260        };
261        let flux_z = if uz >= 0.0 {
262            uz * (phi_c - phi_zm) / dx
263        } else {
264            uz * (phi_zp - phi_c) / dx
265        };
266
267        *v = phi_c - dt * (flux_x + flux_y + flux_z);
268    });
269
270    out
271}
272
273// ---------------------------------------------------------------------------
274// 5. Lax-Friedrichs flux splitting
275// ---------------------------------------------------------------------------
276
277/// Lax-Friedrichs numerical flux at the interface between cells `i` and `i+1`.
278///
279/// `f(u)` is the physical flux function, `alpha` is the maximum wave speed.
280#[inline]
281pub fn lax_friedrichs_flux(u_l: f64, u_r: f64, f_l: f64, f_r: f64, alpha: f64) -> f64 {
282    0.5 * (f_l + f_r) - 0.5 * alpha * (u_r - u_l)
283}
284
285/// Lax-Friedrichs advection update for a 1-D scalar array.
286///
287/// Physical flux: `f(u) = vel * u`.
288/// Returns the updated scalar array after one time step.
289pub fn lax_friedrichs_advect_1d(u: &[f64], vel: f64, dx: f64, dt: f64) -> Vec<f64> {
290    let n = u.len();
291    if n == 0 {
292        return Vec::new();
293    }
294    let alpha = vel.abs();
295    let mut u_new = vec![0.0; n];
296    for i in 0..n {
297        let u_l = if i == 0 { u[0] } else { u[i - 1] };
298        let u_r = if i + 1 >= n { u[n - 1] } else { u[i + 1] };
299        let f_l = vel * u_l;
300        let f_r = vel * u_r;
301        let f_left = lax_friedrichs_flux(u_l, u[i], f_l, vel * u[i], alpha);
302        let f_right = lax_friedrichs_flux(u[i], u_r, vel * u[i], f_r, alpha);
303        u_new[i] = u[i] - dt / dx * (f_right - f_left);
304    }
305    u_new
306}
307
308// ---------------------------------------------------------------------------
309// 6. Divergence and gradient on the 3-D grid
310// ---------------------------------------------------------------------------
311
312/// Compute the divergence of a vector field at every interior cell.
313///
314/// Uses central differences: `div(F) ≈ (Fx_{i+1} - Fx_{i-1}) / (2dx) + ...`
315pub fn divergence_3d(vel: &VectorFluxGrid3D) -> FluxGrid3D {
316    let nx = vel.u.nx;
317    let ny = vel.u.ny;
318    let nz = vel.u.nz;
319    let dx = vel.u.dx;
320
321    let mut div = FluxGrid3D::new(nx, ny, nz, dx, 0.0);
322    div.values.par_iter_mut().enumerate().for_each(|(idx, d)| {
323        let i = idx / (ny * nz);
324        let j = (idx / nz) % ny;
325        let k = idx % nz;
326        let ii = i as isize;
327        let jj = j as isize;
328        let kk = k as isize;
329        let du_dx =
330            (vel.u.get_clamped(ii + 1, jj, kk) - vel.u.get_clamped(ii - 1, jj, kk)) / (2.0 * dx);
331        let dv_dy =
332            (vel.v.get_clamped(ii, jj + 1, kk) - vel.v.get_clamped(ii, jj - 1, kk)) / (2.0 * dx);
333        let dw_dz =
334            (vel.w.get_clamped(ii, jj, kk + 1) - vel.w.get_clamped(ii, jj, kk - 1)) / (2.0 * dx);
335        *d = du_dx + dv_dy + dw_dz;
336    });
337    div
338}
339
340/// Compute the gradient of a scalar field at every cell.
341///
342/// Returns a [`VectorFluxGrid3D`] where each component is a partial derivative.
343pub fn gradient_3d(phi: &FluxGrid3D) -> VectorFluxGrid3D {
344    let nx = phi.nx;
345    let ny = phi.ny;
346    let nz = phi.nz;
347    let dx = phi.dx;
348
349    let mut grad = VectorFluxGrid3D::zeros(nx, ny, nz, dx);
350
351    let u_vals: Vec<f64> = (0..nx * ny * nz)
352        .into_par_iter()
353        .map(|idx| {
354            let i = idx / (ny * nz);
355            let j = (idx / nz) % ny;
356            let k = idx % nz;
357            let ii = i as isize;
358            let jj = j as isize;
359            let kk = k as isize;
360            (phi.get_clamped(ii + 1, jj, kk) - phi.get_clamped(ii - 1, jj, kk)) / (2.0 * dx)
361        })
362        .collect();
363
364    let v_vals: Vec<f64> = (0..nx * ny * nz)
365        .into_par_iter()
366        .map(|idx| {
367            let i = idx / (ny * nz);
368            let j = (idx / nz) % ny;
369            let k = idx % nz;
370            let ii = i as isize;
371            let jj = j as isize;
372            let kk = k as isize;
373            (phi.get_clamped(ii, jj + 1, kk) - phi.get_clamped(ii, jj - 1, kk)) / (2.0 * dx)
374        })
375        .collect();
376
377    let w_vals: Vec<f64> = (0..nx * ny * nz)
378        .into_par_iter()
379        .map(|idx| {
380            let i = idx / (ny * nz);
381            let j = (idx / nz) % ny;
382            let k = idx % nz;
383            let ii = i as isize;
384            let jj = j as isize;
385            let kk = k as isize;
386            (phi.get_clamped(ii, jj, kk + 1) - phi.get_clamped(ii, jj, kk - 1)) / (2.0 * dx)
387        })
388        .collect();
389
390    grad.u.values = u_vals;
391    grad.v.values = v_vals;
392    grad.w.values = w_vals;
393    grad
394}
395
396// ---------------------------------------------------------------------------
397// 7. Finite-volume Euler step helper
398// ---------------------------------------------------------------------------
399
400/// Advance `phi` by one Euler time step using explicit upwind advection.
401///
402/// Convenience wrapper: `phi_{n+1} = advect_upwind_3d(phi_n, vel, dt)`.
403pub fn euler_step_advect(phi: &FluxGrid3D, vel: &VectorFluxGrid3D, dt: f64) -> FluxGrid3D {
404    advect_upwind_3d(phi, vel, dt)
405}
406
407/// Compute the CFL-limited maximum stable time step.
408///
409/// `dt ≤ CFL_factor * dx / max(|u|, |v|, |w|)`.
410pub fn cfl_dt(vel: &VectorFluxGrid3D, cfl_factor: f64) -> f64 {
411    let max_u = vel
412        .u
413        .values
414        .iter()
415        .copied()
416        .map(f64::abs)
417        .fold(0.0_f64, f64::max);
418    let max_v = vel
419        .v
420        .values
421        .iter()
422        .copied()
423        .map(f64::abs)
424        .fold(0.0_f64, f64::max);
425    let max_w = vel
426        .w
427        .values
428        .iter()
429        .copied()
430        .map(f64::abs)
431        .fold(0.0_f64, f64::max);
432    let max_vel = max_u.max(max_v).max(max_w);
433    let dx = vel.u.dx;
434    if max_vel < 1e-300 {
435        f64::INFINITY
436    } else {
437        cfl_factor * dx / max_vel
438    }
439}
440
441// ---------------------------------------------------------------------------
442// 8. Conservative state vector for Euler equations
443// ---------------------------------------------------------------------------
444
445/// Conservative variable vector for the Euler equations: \[rho, rho*u, rho*v, rho*w, E\].
446#[derive(Debug, Clone, Copy)]
447pub struct EulerState {
448    /// Density.
449    pub rho: f64,
450    /// x-momentum.
451    pub rho_u: f64,
452    /// y-momentum.
453    pub rho_v: f64,
454    /// z-momentum.
455    pub rho_w: f64,
456    /// Total energy density.
457    pub e: f64,
458}
459
460impl EulerState {
461    /// Create from primitives `(rho, u, v, w, p)` with ratio of specific heats `gamma`.
462    pub fn from_primitives(rho: f64, u: f64, v: f64, w: f64, p: f64, gamma: f64) -> Self {
463        let ke = 0.5 * rho * (u * u + v * v + w * w);
464        let e = p / (gamma - 1.0) + ke;
465        Self {
466            rho,
467            rho_u: rho * u,
468            rho_v: rho * v,
469            rho_w: rho * w,
470            e,
471        }
472    }
473
474    /// Reconstruct velocity components.
475    pub fn velocity(&self) -> [f64; 3] {
476        let rho = if self.rho.abs() > 1e-30 {
477            self.rho
478        } else {
479            1e-30
480        };
481        [self.rho_u / rho, self.rho_v / rho, self.rho_w / rho]
482    }
483
484    /// Reconstruct pressure from conserved variables.
485    pub fn pressure(&self, gamma: f64) -> f64 {
486        let u = self.velocity();
487        let ke = 0.5 * self.rho * (u[0] * u[0] + u[1] * u[1] + u[2] * u[2]);
488        (gamma - 1.0) * (self.e - ke)
489    }
490
491    /// Sound speed.
492    pub fn sound_speed(&self, gamma: f64) -> f64 {
493        let p = self.pressure(gamma);
494        let rho = if self.rho.abs() > 1e-30 {
495            self.rho
496        } else {
497            1e-30
498        };
499        (gamma * p / rho).sqrt().max(0.0)
500    }
501
502    /// x-direction Euler flux vector.
503    pub fn flux_x(&self, gamma: f64) -> [f64; 5] {
504        let u = self.velocity();
505        let p = self.pressure(gamma);
506        [
507            self.rho_u,
508            self.rho_u * u[0] + p,
509            self.rho_v * u[0],
510            self.rho_w * u[0],
511            (self.e + p) * u[0],
512        ]
513    }
514
515    /// Linear interpolation between two states.
516    pub fn lerp(&self, other: &EulerState, t: f64) -> EulerState {
517        EulerState {
518            rho: self.rho + t * (other.rho - self.rho),
519            rho_u: self.rho_u + t * (other.rho_u - self.rho_u),
520            rho_v: self.rho_v + t * (other.rho_v - self.rho_v),
521            rho_w: self.rho_w + t * (other.rho_w - self.rho_w),
522            e: self.e + t * (other.e - self.e),
523        }
524    }
525}
526
527// ---------------------------------------------------------------------------
528// 9. Van Albada slope limiter
529// ---------------------------------------------------------------------------
530
531/// Van Albada slope limiter: smooth TVD limiter with quadratic shape.
532///
533/// `φ(r) = (r² + r) / (r² + 1)` for `r = a/b`.
534pub fn van_albada(a: f64, b: f64) -> f64 {
535    if a * b <= 0.0 {
536        return 0.0;
537    }
538    let r = a / b;
539    b * (r * r + r) / (r * r + 1.0)
540}
541
542// ---------------------------------------------------------------------------
543// 10. MUSCL reconstruction (piecewise-linear, limited)
544// ---------------------------------------------------------------------------
545
546/// Reconstruct left and right interface values from cell-centred values
547/// using the MUSCL scheme with a given limiter.
548///
549/// Returns `(u_L, u_R)` at the interface between cells `i` and `i+1`.
550///
551/// Uses: `u_L = u[i]   + 0.5 * phi(delta_m, delta_p) * delta_m`
552///       `u_R = u[i+1] - 0.5 * phi(delta_p, delta_m) * delta_p`
553/// where `delta_m = u[i] - u[i-1]`, `delta_p = u[i+1] - u[i]`.
554pub fn muscl_reconstruct(u: &[f64], i: usize, limiter: fn(f64, f64) -> f64) -> (f64, f64) {
555    let n = u.len();
556    let u_im = if i == 0 { u[0] } else { u[i - 1] };
557    let u_i = u[i];
558    let u_ip = if i + 1 < n { u[i + 1] } else { u[n - 1] };
559    let u_ipp = if i + 2 < n { u[i + 2] } else { u[n - 1] };
560
561    let delta_m = u_i - u_im;
562    let delta_p = u_ip - u_i;
563    let delta_pp = u_ipp - u_ip;
564
565    let sigma_l = limiter(delta_m, delta_p);
566    let sigma_r = limiter(delta_p, delta_pp);
567
568    let u_l = u_i + 0.5 * sigma_l;
569    let u_r = u_ip - 0.5 * sigma_r;
570    (u_l, u_r)
571}
572
573/// Apply MUSCL reconstruction across all interfaces in a 1-D array.
574///
575/// Returns a `Vec<(f64, f64)>` of `(u_L, u_R)` at interfaces `0..n-1`.
576pub fn muscl_reconstruct_all(u: &[f64], limiter: fn(f64, f64) -> f64) -> Vec<(f64, f64)> {
577    let n = u.len();
578    if n < 2 {
579        return Vec::new();
580    }
581    (0..n - 1)
582        .map(|i| muscl_reconstruct(u, i, limiter))
583        .collect()
584}
585
586// ---------------------------------------------------------------------------
587// 11. Godunov flux (exact Riemann solver for linear advection)
588// ---------------------------------------------------------------------------
589
590/// Godunov flux for a scalar advection equation `u_t + a * u_x = 0`.
591///
592/// Uses upwinding: the Godunov flux is `F = a * u_L` if `a >= 0`, else `a * u_R`.
593pub fn godunov_flux_advection(u_l: f64, u_r: f64, wave_speed: f64) -> f64 {
594    if wave_speed >= 0.0 {
595        wave_speed * u_l
596    } else {
597        wave_speed * u_r
598    }
599}
600
601/// Godunov-type flux for Burgers' equation `u_t + (u²/2)_x = 0`.
602///
603/// Handles the sonic point (where the characteristic speed changes sign).
604pub fn godunov_flux_burgers(u_l: f64, u_r: f64) -> f64 {
605    // Rankine-Hugoniot shock speed: s = (u_l + u_r) / 2
606    // Entropy fix: rarefaction fans may straddle u=0
607    if u_l >= u_r {
608        // Shock
609        let s = 0.5 * (u_l + u_r);
610        if s >= 0.0 {
611            0.5 * u_l * u_l
612        } else {
613            0.5 * u_r * u_r
614        }
615    } else {
616        // Rarefaction
617        if u_l >= 0.0 {
618            0.5 * u_l * u_l
619        } else if u_r <= 0.0 {
620            0.5 * u_r * u_r
621        } else {
622            0.0 // entropy-satisfying: sonic point at u=0
623        }
624    }
625}
626
627// ---------------------------------------------------------------------------
628// 12. Roe flux for the linear-advection system
629// ---------------------------------------------------------------------------
630
631/// Roe-averaged scalar flux for a 1-D scalar conservation law.
632///
633/// For `u_t + f(u)_x = 0` with `f(u) = a * u`:
634/// `F_Roe = 0.5*(f_L + f_R) - 0.5*|a_Roe|*(u_R - u_L)`
635pub fn roe_flux_scalar(u_l: f64, u_r: f64, a_roe: f64) -> f64 {
636    let f_l = a_roe * u_l;
637    let f_r = a_roe * u_r;
638    0.5 * (f_l + f_r) - 0.5 * a_roe.abs() * (u_r - u_l)
639}
640
641/// Roe flux for 1-D Euler equations (x-direction).
642///
643/// Returns the interface flux `[F_rho, F_rhou, F_e]` using the Roe-average state.
644/// `gamma` is the ratio of specific heats (1.4 for air).
645pub fn roe_flux_euler_1d(
646    rho_l: f64,
647    u_l: f64,
648    p_l: f64,
649    rho_r: f64,
650    u_r: f64,
651    p_r: f64,
652    gamma: f64,
653) -> [f64; 3] {
654    // Roe averages
655    let sqrt_rl = rho_l.sqrt();
656    let sqrt_rr = rho_r.sqrt();
657    let denom = sqrt_rl + sqrt_rr;
658
659    let u_roe = (sqrt_rl * u_l + sqrt_rr * u_r) / denom;
660    let h_l = (p_l / (rho_l * (gamma - 1.0)) + p_l / rho_l) + 0.5 * u_l * u_l;
661    let h_r = (p_r / (rho_r * (gamma - 1.0)) + p_r / rho_r) + 0.5 * u_r * u_r;
662    let h_roe = (sqrt_rl * h_l + sqrt_rr * h_r) / denom;
663
664    let a2 = (gamma - 1.0) * (h_roe - 0.5 * u_roe * u_roe);
665    let a_roe = if a2 > 0.0 { a2.sqrt() } else { 0.0 };
666
667    // Conservative variables
668    let e_l = p_l / (gamma - 1.0) + 0.5 * rho_l * u_l * u_l;
669    let e_r = p_r / (gamma - 1.0) + 0.5 * rho_r * u_r * u_r;
670
671    // Physical fluxes
672    let fl = [rho_l * u_l, rho_l * u_l * u_l + p_l, (e_l + p_l) * u_l];
673    let fr = [rho_r * u_r, rho_r * u_r * u_r + p_r, (e_r + p_r) * u_r];
674
675    // Delta conserved variables
676    let du = [rho_r - rho_l, rho_r * u_r - rho_l * u_l, e_r - e_l];
677
678    // Eigenvalues
679    let lam = [u_roe - a_roe, u_roe, u_roe + a_roe];
680
681    // Roe dissipation matrix applied to du (simplified diagonal form)
682    let r_inv_du = [
683        (du[0] * u_roe - du[1]) / (2.0 * a_roe.max(1e-30)),
684        du[0] - (du[0] * u_roe - du[1]) / (a_roe.max(1e-30)),
685        (du[0] * u_roe + du[1]) / (2.0 * a_roe.max(1e-30)),
686    ];
687
688    // Right eigenvectors (simplified form)
689    let r_mat = [
690        [1.0, 1.0, 1.0],
691        [u_roe - a_roe, u_roe, u_roe + a_roe],
692        [
693            h_roe - u_roe * a_roe,
694            0.5 * u_roe * u_roe,
695            h_roe + u_roe * a_roe,
696        ],
697    ];
698
699    let mut dissipation = [0.0f64; 3];
700    for i in 0..3 {
701        for k in 0..3 {
702            dissipation[i] += lam[k].abs() * r_inv_du[k] * r_mat[i][k];
703        }
704    }
705
706    [
707        0.5 * (fl[0] + fr[0]) - 0.5 * dissipation[0],
708        0.5 * (fl[1] + fr[1]) - 0.5 * dissipation[1],
709        0.5 * (fl[2] + fr[2]) - 0.5 * dissipation[2],
710    ]
711}
712
713// ---------------------------------------------------------------------------
714// 13. HLL flux
715// ---------------------------------------------------------------------------
716
717/// HLL (Harten-Lax-van Leer) numerical flux for a scalar conservation law.
718///
719/// Wave speed estimates `s_l` and `s_r` are the left and right running speeds.
720/// `f_l = f(u_l)`, `f_r = f(u_r)`.
721pub fn hll_flux(u_l: f64, u_r: f64, f_l: f64, f_r: f64, s_l: f64, s_r: f64) -> f64 {
722    if s_l >= 0.0 {
723        f_l
724    } else if s_r <= 0.0 {
725        f_r
726    } else {
727        (s_r * f_l - s_l * f_r + s_l * s_r * (u_r - u_l)) / (s_r - s_l)
728    }
729}
730
731/// Estimate HLL wave speeds for the 1-D Euler equations.
732///
733/// Uses Davis' estimates: `s_l = min(u_l - a_l, u_r - a_r)`, `s_r = max(u_l + a_l, u_r + a_r)`.
734pub fn hll_wave_speeds(u_l: f64, a_l: f64, u_r: f64, a_r: f64) -> (f64, f64) {
735    let s_l = (u_l - a_l).min(u_r - a_r);
736    let s_r = (u_l + a_l).max(u_r + a_r);
737    (s_l, s_r)
738}
739
740/// HLL flux for 1-D Euler equations.
741///
742/// Returns `[F_rho, F_rhou, F_e]`.
743pub fn hll_flux_euler_1d(
744    rho_l: f64,
745    u_l: f64,
746    p_l: f64,
747    rho_r: f64,
748    u_r: f64,
749    p_r: f64,
750    gamma: f64,
751) -> [f64; 3] {
752    let a_l = (gamma * p_l / rho_l).sqrt().max(0.0);
753    let a_r = (gamma * p_r / rho_r).sqrt().max(0.0);
754    let (s_l, s_r) = hll_wave_speeds(u_l, a_l, u_r, a_r);
755
756    let e_l = p_l / (gamma - 1.0) + 0.5 * rho_l * u_l * u_l;
757    let e_r = p_r / (gamma - 1.0) + 0.5 * rho_r * u_r * u_r;
758
759    let fl = [rho_l * u_l, rho_l * u_l * u_l + p_l, (e_l + p_l) * u_l];
760    let fr = [rho_r * u_r, rho_r * u_r * u_r + p_r, (e_r + p_r) * u_r];
761    let ul = [rho_l, rho_l * u_l, e_l];
762    let ur = [rho_r, rho_r * u_r, e_r];
763
764    let mut f = [0.0f64; 3];
765    for i in 0..3 {
766        f[i] = hll_flux(ul[i], ur[i], fl[i], fr[i], s_l, s_r);
767    }
768    f
769}
770
771// ---------------------------------------------------------------------------
772// 14. HLLC flux for 1-D Euler equations
773// ---------------------------------------------------------------------------
774
775/// HLLC (Harten-Lax-van Leer-Contact) flux for 1-D Euler equations.
776///
777/// The HLLC flux restores the contact wave missing in HLL.
778/// Returns `[F_rho, F_rhou, F_e]`.
779pub fn hllc_flux_euler_1d(
780    rho_l: f64,
781    u_l: f64,
782    p_l: f64,
783    rho_r: f64,
784    u_r: f64,
785    p_r: f64,
786    gamma: f64,
787) -> [f64; 3] {
788    let a_l = (gamma * p_l / rho_l.max(1e-30)).sqrt();
789    let a_r = (gamma * p_r / rho_r.max(1e-30)).sqrt();
790
791    // Davis wave speed estimates
792    let (s_l, s_r) = hll_wave_speeds(u_l, a_l, u_r, a_r);
793
794    // Contact wave speed (HLLC star state)
795    let s_star = (p_r - p_l + rho_l * u_l * (s_l - u_l) - rho_r * u_r * (s_r - u_r))
796        / (rho_l * (s_l - u_l) - rho_r * (s_r - u_r) + 1e-200);
797
798    let e_l = p_l / (gamma - 1.0) + 0.5 * rho_l * u_l * u_l;
799    let e_r = p_r / (gamma - 1.0) + 0.5 * rho_r * u_r * u_r;
800
801    let fl = [rho_l * u_l, rho_l * u_l * u_l + p_l, (e_l + p_l) * u_l];
802    let fr = [rho_r * u_r, rho_r * u_r * u_r + p_r, (e_r + p_r) * u_r];
803
804    let hllc_flux_side = |rho: f64, u: f64, e: f64, p: f64, f: &[f64; 3], s: f64| -> [f64; 3] {
805        let coeff = rho * (s - u) / (s - s_star + 1e-200);
806        let u_star = [
807            coeff,
808            coeff * s_star,
809            coeff * (e / rho + (s_star - u) * (s_star + p / (rho * (s - u) + 1e-200))),
810        ];
811        [
812            f[0] + s * (u_star[0] - rho),
813            f[1] + s * (u_star[1] - rho * u),
814            f[2] + s * (u_star[2] - e),
815        ]
816    };
817
818    if s_l >= 0.0 {
819        fl
820    } else if s_star >= 0.0 {
821        hllc_flux_side(rho_l, u_l, e_l, p_l, &fl, s_l)
822    } else if s_r >= 0.0 {
823        hllc_flux_side(rho_r, u_r, e_r, p_r, &fr, s_r)
824    } else {
825        fr
826    }
827}
828
829// ---------------------------------------------------------------------------
830// 15. Characteristic decomposition for 1-D advection systems
831// ---------------------------------------------------------------------------
832
833/// Project a vector of conservative variables onto the characteristic variables
834/// for a simple 2-component advection system.
835///
836/// Eigenvalues `lambda = [a, -a]` (rightward and leftward waves).
837/// Returns `(w_plus, w_minus)` — the characteristic amplitudes.
838pub fn characteristic_decompose_2wave(u0: f64, u1: f64, a: f64) -> (f64, f64) {
839    let w_plus = 0.5 * (u0 + u1 / a);
840    let w_minus = 0.5 * (u0 - u1 / a);
841    (w_plus, w_minus)
842}
843
844/// Reconstruct conservative variables from characteristic variables.
845pub fn characteristic_recompose_2wave(w_plus: f64, w_minus: f64, a: f64) -> (f64, f64) {
846    let u0 = w_plus + w_minus;
847    let u1 = a * (w_plus - w_minus);
848    (u0, u1)
849}
850
851/// Apply a characteristic-based limiter to 1-D reconstruction.
852///
853/// Projects to characteristic variables, limits each wave separately, then
854/// reconstructs in physical space.  This is important for multi-component
855/// systems to prevent spurious oscillations across contact waves.
856pub fn characteristic_limited_reconstruct(
857    u0: &[f64],
858    u1: &[f64],
859    i: usize,
860    a: f64,
861    limiter: fn(f64, f64) -> f64,
862) -> ([f64; 2], [f64; 2]) {
863    let n = u0.len();
864    let get = |arr: &[f64], j: usize| -> f64 { arr[j.min(n - 1)] };
865
866    // Project cell values around i to characteristic space
867    let (wm_im, _) = characteristic_decompose_2wave(
868        get(u0, i.saturating_sub(1)),
869        get(u1, i.saturating_sub(1)),
870        a,
871    );
872    let (wm_i, wp_i) = characteristic_decompose_2wave(get(u0, i), get(u1, i), a);
873    let (wm_ip, wp_ip) = characteristic_decompose_2wave(get(u0, i + 1), get(u1, i + 1), a);
874    let (wm_ipp, wp_ipp) = if i + 2 < n {
875        characteristic_decompose_2wave(get(u0, i + 2), get(u1, i + 2), a)
876    } else {
877        (wm_ip, wp_ip)
878    };
879    let (_, wp_im) = characteristic_decompose_2wave(
880        get(u0, i.saturating_sub(1)),
881        get(u1, i.saturating_sub(1)),
882        a,
883    );
884
885    // Limit each characteristic wave
886    let sig_p_l = limiter(wp_i - wp_im, wp_ip - wp_i);
887    let sig_p_r = limiter(wp_ip - wp_i, wp_ipp - wp_ip);
888    let sig_m_l = limiter(wm_i - wm_im, wm_ip - wm_i);
889    let sig_m_r = limiter(wm_ip - wm_i, wm_ipp - wm_ip);
890
891    let wp_l = wp_i + 0.5 * sig_p_l;
892    let wp_r = wp_ip - 0.5 * sig_p_r;
893    let wm_l = wm_i + 0.5 * sig_m_l;
894    let wm_r = wm_ip - 0.5 * sig_m_r;
895
896    let (u0_l, u1_l) = characteristic_recompose_2wave(wp_l, wm_l, a);
897    let (u0_r, u1_r) = characteristic_recompose_2wave(wp_r, wm_r, a);
898
899    ([u0_l, u1_l], [u0_r, u1_r])
900}
901
902// ---------------------------------------------------------------------------
903// 16. Second-order Runge-Kutta (TVD RK2) time stepping
904// ---------------------------------------------------------------------------
905
906/// Advance `phi` using the TVD Runge-Kutta 2 (Heun) scheme.
907///
908/// `phi_{n+1} = 0.5 * (phi_n + phi_n^(2))`
909/// `phi_n^(1) = phi_n + dt * L(phi_n)` (Euler predictor)
910/// `phi_n^(2) = phi_n^(1) + dt * L(phi_n^(1))` (corrector)
911///
912/// where `L` is the upwind advection operator.
913pub fn tvd_rk2_advect(phi: &FluxGrid3D, vel: &VectorFluxGrid3D, dt: f64) -> FluxGrid3D {
914    // Stage 1: phi^(1) = phi + dt * L(phi)
915    let phi_1 = advect_upwind_3d(phi, vel, dt);
916    // Stage 2: phi^(2) = phi^(1) + dt * L(phi^(1))
917    let phi_2 = advect_upwind_3d(&phi_1, vel, dt);
918    // Combine: phi_{n+1} = 0.5*(phi + phi^(2))
919    let mut result = phi.clone();
920    result
921        .values
922        .iter_mut()
923        .zip(phi.values.iter().zip(phi_2.values.iter()))
924        .for_each(|(r, (&a, &b))| *r = 0.5 * (a + b));
925    result
926}
927
928// ---------------------------------------------------------------------------
929// 17. Flux limiter in ratio form
930// ---------------------------------------------------------------------------
931
932/// Compute the minmod limiter in ratio form: `phi(r) = max(0, min(1, r))`.
933///
934/// Input `r = delta_{i-1/2} / delta_{i+1/2}` (ratio of consecutive differences).
935pub fn minmod_ratio(r: f64) -> f64 {
936    if r <= 0.0 {
937        0.0
938    } else if r <= 1.0 {
939        r
940    } else {
941        1.0
942    }
943}
944
945/// Superbee limiter in ratio form: `phi(r) = max(0, max(min(2r,1), min(r,2)))`.
946pub fn superbee_ratio(r: f64) -> f64 {
947    if r <= 0.0 {
948        0.0
949    } else {
950        (2.0 * r).min(1.0_f64).max(r.min(2.0_f64)).max(0.0_f64)
951    }
952}
953
954/// Van Leer limiter in ratio form: `phi(r) = (r + |r|) / (1 + |r|)`.
955pub fn van_leer_ratio(r: f64) -> f64 {
956    (r + r.abs()) / (1.0 + r.abs())
957}
958
959// ---------------------------------------------------------------------------
960// Tests
961// ---------------------------------------------------------------------------
962
963#[cfg(test)]
964mod flux_compute_tests {
965    use super::*;
966
967    #[test]
968    fn test_flux_grid3d_get_set() {
969        let mut g = FluxGrid3D::new(4, 4, 4, 0.1, 0.0);
970        g.set(1, 2, 3, 7.5);
971        assert!((g.get(1, 2, 3) - 7.5).abs() < 1e-12);
972    }
973
974    #[test]
975    fn test_flux_grid3d_clamped_boundary() {
976        let g = FluxGrid3D::new(5, 5, 5, 0.1, 3.0);
977        // Negative indices and out-of-bounds indices should clamp.
978        assert!((g.get_clamped(-1, 0, 0) - 3.0).abs() < 1e-12);
979        assert!((g.get_clamped(100, 0, 0) - 3.0).abs() < 1e-12);
980    }
981
982    #[test]
983    fn test_l2_norm_uniform() {
984        // A 2×2×2 grid filled with 1.0: l2 = sqrt(8).
985        let g = FluxGrid3D::new(2, 2, 2, 0.5, 1.0);
986        let expected = (8.0_f64).sqrt();
987        assert!((g.l2_norm() - expected).abs() < 1e-10);
988    }
989
990    #[test]
991    fn test_minmod_same_sign() {
992        assert!(
993            (minmod(2.0, 3.0) - 2.0).abs() < 1e-12,
994            "minmod picks smaller"
995        );
996        assert!((minmod(-1.0, -4.0) - (-1.0)).abs() < 1e-12);
997    }
998
999    #[test]
1000    fn test_minmod_opposite_sign() {
1001        assert!(
1002            (minmod(2.0, -1.0)).abs() < 1e-12,
1003            "minmod returns 0 for opposite signs"
1004        );
1005    }
1006
1007    #[test]
1008    fn test_van_leer_smooth() {
1009        // van_leer(a, b) = 2ab/(a+b) for same-sign inputs.
1010        let vl = van_leer(2.0, 2.0);
1011        assert!(
1012            (vl - 2.0).abs() < 1e-12,
1013            "symmetric inputs: van_leer = value"
1014        );
1015        let vl2 = van_leer(1.0, 3.0);
1016        let expected = 2.0 * 1.0 * 3.0 / (1.0 + 3.0);
1017        assert!((vl2 - expected).abs() < 1e-12);
1018    }
1019
1020    #[test]
1021    fn test_upwind_flux_1d_positive_velocity() {
1022        // Uniform phi = 1.0, positive velocity: no flux.
1023        let phi = vec![1.0; 10];
1024        let flux = upwind_flux_1d(&phi, 1.0, 0.1, 0.05);
1025        for f in &flux {
1026            assert!(
1027                f.abs() < 1e-12,
1028                "uniform field with positive vel: zero flux"
1029            );
1030        }
1031    }
1032
1033    #[test]
1034    fn test_lax_friedrichs_1d_preserves_mass() {
1035        // Total mass ≈ integral of u dx. Advection should conserve mass
1036        // (up to boundary corrections). Check sum is approximately preserved.
1037        let u: Vec<f64> = (0..20).map(|i| (i as f64 * 0.1 - 1.0).powi(2)).collect();
1038        let sum_before: f64 = u.iter().sum();
1039        let u_new = lax_friedrichs_advect_1d(&u, 0.5, 0.1, 0.01);
1040        let sum_after: f64 = u_new.iter().sum();
1041        // Small boundary effect allowed.
1042        assert!(
1043            (sum_after - sum_before).abs() < sum_before * 0.05 + 1.0,
1044            "mass should be approximately conserved"
1045        );
1046    }
1047
1048    #[test]
1049    fn test_divergence_constant_field_is_zero() {
1050        // Uniform velocity field → divergence = 0 everywhere.
1051        let n = 6;
1052        let mut vel = VectorFluxGrid3D::zeros(n, n, n, 0.1);
1053        for i in 0..n {
1054            for j in 0..n {
1055                for k in 0..n {
1056                    vel.set_vel(i, j, k, [1.0, 2.0, 3.0]);
1057                }
1058            }
1059        }
1060        let div = divergence_3d(&vel);
1061        for &d in &div.values {
1062            assert!(
1063                d.abs() < 1e-10,
1064                "divergence of uniform field must be 0, got {d}"
1065            );
1066        }
1067    }
1068
1069    #[test]
1070    fn test_gradient_linear_field() {
1071        // phi(i,j,k) = x = i*dx. Gradient_x should be ~1.0 everywhere.
1072        let n = 8;
1073        let dx = 0.25;
1074        let mut phi = FluxGrid3D::new(n, n, n, dx, 0.0);
1075        for i in 0..n {
1076            for j in 0..n {
1077                for k in 0..n {
1078                    phi.set(i, j, k, i as f64 * dx);
1079                }
1080            }
1081        }
1082        let grad = gradient_3d(&phi);
1083        // Check interior cells.
1084        for i in 1..n - 1 {
1085            for j in 1..n - 1 {
1086                for k in 1..n - 1 {
1087                    let gx = grad.u.get(i, j, k);
1088                    assert!(
1089                        (gx - 1.0).abs() < 1e-10,
1090                        "gradient_x of linear field should be 1, got {gx} at ({i},{j},{k})"
1091                    );
1092                }
1093            }
1094        }
1095    }
1096
1097    #[test]
1098    fn test_cfl_dt_uniform_velocity() {
1099        let mut vel = VectorFluxGrid3D::zeros(4, 4, 4, 0.1);
1100        for i in 0..4 {
1101            for j in 0..4 {
1102                for k in 0..4 {
1103                    vel.set_vel(i, j, k, [2.0, 0.0, 0.0]);
1104                }
1105            }
1106        }
1107        // CFL dt = 0.5 * dx / 2.0 = 0.025
1108        let dt = cfl_dt(&vel, 0.5);
1109        assert!(
1110            (dt - 0.025).abs() < 1e-10,
1111            "cfl_dt should be 0.025, got {dt}"
1112        );
1113    }
1114
1115    #[test]
1116    fn test_cfl_dt_zero_velocity() {
1117        let vel = VectorFluxGrid3D::zeros(4, 4, 4, 0.1);
1118        let dt = cfl_dt(&vel, 0.5);
1119        assert!(dt.is_infinite(), "zero velocity → infinite dt");
1120    }
1121
1122    // ── Additional FluxGrid3D tests ──────────────────────────────────────────
1123
1124    #[test]
1125    fn test_flux_grid3d_len_empty() {
1126        let g = FluxGrid3D::new(0, 5, 5, 0.1, 0.0);
1127        assert_eq!(g.len(), 0);
1128        assert!(g.is_empty());
1129    }
1130
1131    #[test]
1132    fn test_flux_grid3d_len_nonempty() {
1133        let g = FluxGrid3D::new(3, 4, 5, 0.1, 0.0);
1134        assert_eq!(g.len(), 60);
1135        assert!(!g.is_empty());
1136    }
1137
1138    #[test]
1139    fn test_flux_grid3d_max_val() {
1140        let mut g = FluxGrid3D::new(2, 2, 2, 0.1, 0.0);
1141        g.set(0, 0, 0, 5.0);
1142        g.set(1, 1, 1, -3.0);
1143        assert!((g.max_val() - 5.0).abs() < 1e-12);
1144    }
1145
1146    #[test]
1147    fn test_flux_grid3d_min_val() {
1148        let mut g = FluxGrid3D::new(2, 2, 2, 0.1, 0.0);
1149        g.set(0, 0, 0, 5.0);
1150        g.set(1, 1, 1, -3.0);
1151        assert!((g.min_val() - (-3.0)).abs() < 1e-12);
1152    }
1153
1154    #[test]
1155    fn test_flux_grid3d_l2_norm_zeros() {
1156        let g = FluxGrid3D::new(3, 3, 3, 0.1, 0.0);
1157        assert!(g.l2_norm() < 1e-15);
1158    }
1159
1160    #[test]
1161    fn test_flux_grid3d_index_layout() {
1162        let g = FluxGrid3D::new(3, 4, 5, 0.1, 0.0);
1163        // Check index formula: i*ny*nz + j*nz + k
1164        assert_eq!(g.idx(0, 0, 0), 0);
1165        assert_eq!(g.idx(1, 0, 0), 20); // 1 * 4 * 5
1166        assert_eq!(g.idx(0, 1, 0), 5); // 0 * 20 + 1 * 5
1167        assert_eq!(g.idx(0, 0, 1), 1);
1168    }
1169
1170    #[test]
1171    fn test_flux_grid3d_clamped_low_indices() {
1172        let mut g = FluxGrid3D::new(5, 5, 5, 0.1, 0.0);
1173        g.set(0, 0, 0, 99.0);
1174        // Clamping -1 → 0
1175        assert!((g.get_clamped(-1, 0, 0) - 99.0).abs() < 1e-12);
1176        assert!((g.get_clamped(0, -5, 0) - 99.0).abs() < 1e-12);
1177    }
1178
1179    // ── VectorFluxGrid3D tests ───────────────────────────────────────────────
1180
1181    #[test]
1182    fn test_vector_flux_grid_zeros() {
1183        let v = VectorFluxGrid3D::zeros(3, 3, 3, 0.1);
1184        for i in 0..3 {
1185            for j in 0..3 {
1186                for k in 0..3 {
1187                    let vel = v.get_vel(i, j, k);
1188                    assert_eq!(vel, [0.0, 0.0, 0.0]);
1189                }
1190            }
1191        }
1192    }
1193
1194    #[test]
1195    fn test_vector_flux_grid_set_get_vel() {
1196        let mut v = VectorFluxGrid3D::zeros(4, 4, 4, 0.1);
1197        v.set_vel(1, 2, 3, [1.5, 2.5, 3.5]);
1198        let vel = v.get_vel(1, 2, 3);
1199        assert!((vel[0] - 1.5).abs() < 1e-12);
1200        assert!((vel[1] - 2.5).abs() < 1e-12);
1201        assert!((vel[2] - 3.5).abs() < 1e-12);
1202    }
1203
1204    // ── Limiter tests ────────────────────────────────────────────────────────
1205
1206    #[test]
1207    fn test_superbee_same_sign_positive() {
1208        // superbee(a, b) = max(min(2a, b), min(a, 2b)) for same-sign
1209        let s = superbee(1.0, 2.0);
1210        assert!(s > 0.0, "superbee of positives should be positive, got {s}");
1211    }
1212
1213    #[test]
1214    fn test_superbee_opposite_signs() {
1215        assert_eq!(superbee(1.0, -1.0), 0.0);
1216        assert_eq!(superbee(-2.0, 3.0), 0.0);
1217    }
1218
1219    #[test]
1220    fn test_superbee_both_zero() {
1221        assert_eq!(superbee(0.0, 0.0), 0.0);
1222    }
1223
1224    #[test]
1225    fn test_mc_limiter_same_sign() {
1226        let mc = mc_limiter(2.0, 4.0);
1227        // centered = 3, limited by 2*min(2,4) = 4, min(2*2, 2*4) = 4, answer ≤ 3
1228        assert!(mc > 0.0 && mc <= 3.0, "mc_limiter = {mc}");
1229    }
1230
1231    #[test]
1232    fn test_mc_limiter_opposite_sign() {
1233        assert_eq!(mc_limiter(1.0, -1.0), 0.0);
1234    }
1235
1236    #[test]
1237    fn test_van_leer_zero_sum() {
1238        // When a + b ≈ 0 this should handle gracefully
1239        // van_leer(a, -a) = 0 because a * -a < 0
1240        let vl = van_leer(2.0, -2.0);
1241        assert_eq!(vl, 0.0);
1242    }
1243
1244    #[test]
1245    fn test_minmod_equal_values() {
1246        // minmod(a, a) = a
1247        assert!((minmod(3.0, 3.0) - 3.0).abs() < 1e-12);
1248        assert!((minmod(-2.0, -2.0) - (-2.0)).abs() < 1e-12);
1249    }
1250
1251    // ── Upwind flux tests ────────────────────────────────────────────────────
1252
1253    #[test]
1254    fn test_upwind_flux_1d_negative_velocity() {
1255        // Uniform phi = 1.0, negative velocity: no flux.
1256        let phi = vec![1.0; 8];
1257        let flux = upwind_flux_1d(&phi, -1.0, 0.1, 0.05);
1258        for f in &flux {
1259            assert!(
1260                f.abs() < 1e-12,
1261                "uniform field with negative vel: zero flux"
1262            );
1263        }
1264    }
1265
1266    #[test]
1267    fn test_upwind_flux_1d_step_function() {
1268        // Step function: 0 left, 1 right. Positive velocity should transport left.
1269        let phi = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
1270        let flux = upwind_flux_1d(&phi, 1.0, 0.1, 0.01);
1271        // At boundary cell (index 3), phi_l = 0, phi_c = 1 → flux negative
1272        assert!(
1273            flux[3] < 0.0,
1274            "step up with positive vel: negative flux at boundary"
1275        );
1276    }
1277
1278    #[test]
1279    fn test_upwind_flux_1d_length() {
1280        let phi = vec![1.0; 7];
1281        let flux = upwind_flux_1d(&phi, 1.0, 0.1, 0.01);
1282        assert_eq!(flux.len(), 7);
1283    }
1284
1285    // ── Lax-Friedrichs tests ─────────────────────────────────────────────────
1286
1287    #[test]
1288    fn test_lax_friedrichs_flux_zero_difference() {
1289        // u_l == u_r, f_l == f_r: LF flux = f_l = f_r
1290        let f = lax_friedrichs_flux(1.0, 1.0, 2.0, 2.0, 1.0);
1291        assert!((f - 2.0).abs() < 1e-12, "LF flux = {f}");
1292    }
1293
1294    #[test]
1295    fn test_lax_friedrichs_advect_1d_empty() {
1296        let result = lax_friedrichs_advect_1d(&[], 1.0, 0.1, 0.01);
1297        assert!(result.is_empty());
1298    }
1299
1300    #[test]
1301    fn test_lax_friedrichs_advect_1d_length() {
1302        let u = vec![1.0; 6];
1303        let u_new = lax_friedrichs_advect_1d(&u, 0.5, 0.1, 0.01);
1304        assert_eq!(u_new.len(), 6);
1305    }
1306
1307    #[test]
1308    fn test_lax_friedrichs_uniform_field_stable() {
1309        // Uniform field should not change much.
1310        let u = vec![1.0; 10];
1311        let u_new = lax_friedrichs_advect_1d(&u, 1.0, 0.1, 0.05);
1312        let diff: f64 = u_new.iter().zip(u.iter()).map(|(a, b)| (a - b).abs()).sum();
1313        assert!(
1314            diff < 1e-10,
1315            "uniform field should not change under LF advection"
1316        );
1317    }
1318
1319    // ── Divergence / gradient tests ──────────────────────────────────────────
1320
1321    #[test]
1322    fn test_divergence_size_matches_input() {
1323        let vel = VectorFluxGrid3D::zeros(3, 4, 5, 0.1);
1324        let div = divergence_3d(&vel);
1325        assert_eq!(div.nx, 3);
1326        assert_eq!(div.ny, 4);
1327        assert_eq!(div.nz, 5);
1328    }
1329
1330    #[test]
1331    fn test_gradient_size_matches_input() {
1332        let phi = FluxGrid3D::new(3, 4, 5, 0.1, 0.0);
1333        let grad = gradient_3d(&phi);
1334        assert_eq!(grad.u.nx, 3);
1335        assert_eq!(grad.v.ny, 4);
1336        assert_eq!(grad.w.nz, 5);
1337    }
1338
1339    #[test]
1340    fn test_gradient_constant_field_is_zero() {
1341        let phi = FluxGrid3D::new(5, 5, 5, 0.1, 7.0);
1342        let grad = gradient_3d(&phi);
1343        for &g in &grad.u.values {
1344            assert!(g.abs() < 1e-10, "gradient of constant = 0");
1345        }
1346    }
1347
1348    #[test]
1349    fn test_divergence_linear_vx_field() {
1350        // u(i,j,k) = i*dx, v=w=0 → div = ∂u/∂x = 1 at interior cells
1351        let n = 8;
1352        let dx = 0.5;
1353        let mut vel = VectorFluxGrid3D::zeros(n, n, n, dx);
1354        for i in 0..n {
1355            for j in 0..n {
1356                for k in 0..n {
1357                    vel.u.set(i, j, k, i as f64 * dx);
1358                }
1359            }
1360        }
1361        let div = divergence_3d(&vel);
1362        // Interior cells: central diff of i*dx → 1
1363        for i in 1..n - 1 {
1364            for j in 1..n - 1 {
1365                for k in 1..n - 1 {
1366                    let d = div.get(i, j, k);
1367                    assert!(
1368                        (d - 1.0).abs() < 1e-10,
1369                        "div at ({i},{j},{k}) = {d}, expected 1"
1370                    );
1371                }
1372            }
1373        }
1374    }
1375
1376    // ── Euler step test ──────────────────────────────────────────────────────
1377
1378    #[test]
1379    fn test_euler_step_advect_preserves_size() {
1380        let phi = FluxGrid3D::new(4, 4, 4, 0.1, 1.0);
1381        let mut vel = VectorFluxGrid3D::zeros(4, 4, 4, 0.1);
1382        for i in 0..4 {
1383            for j in 0..4 {
1384                for k in 0..4 {
1385                    vel.set_vel(i, j, k, [1.0, 0.0, 0.0]);
1386                }
1387            }
1388        }
1389        let phi_new = euler_step_advect(&phi, &vel, 0.01);
1390        assert_eq!(phi_new.nx, phi.nx);
1391        assert_eq!(phi_new.ny, phi.ny);
1392        assert_eq!(phi_new.nz, phi.nz);
1393    }
1394
1395    #[test]
1396    fn test_euler_step_constant_phi_no_change() {
1397        // Uniform phi, uniform velocity → upwind flux is zero → phi unchanged
1398        let phi = FluxGrid3D::new(5, 5, 5, 0.1, 3.0);
1399        let mut vel = VectorFluxGrid3D::zeros(5, 5, 5, 0.1);
1400        for i in 0..5 {
1401            for j in 0..5 {
1402                for k in 0..5 {
1403                    vel.set_vel(i, j, k, [1.0, 1.0, 1.0]);
1404                }
1405            }
1406        }
1407        let phi_new = euler_step_advect(&phi, &vel, 0.01);
1408        for (&a, &b) in phi.values.iter().zip(phi_new.values.iter()) {
1409            assert!(
1410                (a - b).abs() < 1e-10,
1411                "constant phi should not change: {a} vs {b}"
1412            );
1413        }
1414    }
1415
1416    // ── CFL dt tests ─────────────────────────────────────────────────────────
1417
1418    #[test]
1419    fn test_cfl_dt_negative_velocity() {
1420        // Negative velocity magnitude should still work.
1421        let mut vel = VectorFluxGrid3D::zeros(4, 4, 4, 0.2);
1422        vel.set_vel(0, 0, 0, [0.0, -4.0, 0.0]);
1423        let dt = cfl_dt(&vel, 1.0);
1424        assert!((dt - 0.05).abs() < 1e-10, "cfl_dt = {dt}, expected 0.05");
1425    }
1426
1427    #[test]
1428    fn test_cfl_dt_multiple_components() {
1429        let mut vel = VectorFluxGrid3D::zeros(3, 3, 3, 0.1);
1430        // Set u=1, v=2, w=3 somewhere — max is 3
1431        vel.set_vel(1, 1, 1, [1.0, 2.0, 3.0]);
1432        let dt = cfl_dt(&vel, 1.0);
1433        assert!((dt - 0.1 / 3.0).abs() < 1e-10, "cfl_dt = {dt}");
1434    }
1435
1436    // ── EulerState tests ─────────────────────────────────────────────────────
1437
1438    #[test]
1439    fn test_euler_state_from_primitives() {
1440        let gamma = 1.4;
1441        let s = EulerState::from_primitives(1.0, 0.1, 0.0, 0.0, 1.0, gamma);
1442        assert!((s.rho - 1.0).abs() < 1e-12);
1443        let p_back = s.pressure(gamma);
1444        assert!((p_back - 1.0).abs() < 1e-10, "pressure roundtrip: {p_back}");
1445    }
1446
1447    #[test]
1448    fn test_euler_state_velocity() {
1449        let gamma = 1.4;
1450        let s = EulerState::from_primitives(2.0, 3.0, 4.0, 5.0, 1.0, gamma);
1451        let u = s.velocity();
1452        assert!((u[0] - 3.0).abs() < 1e-12);
1453        assert!((u[1] - 4.0).abs() < 1e-12);
1454        assert!((u[2] - 5.0).abs() < 1e-12);
1455    }
1456
1457    #[test]
1458    fn test_euler_state_sound_speed() {
1459        let gamma = 1.4;
1460        // c = sqrt(gamma*p/rho) = sqrt(1.4 * 1.0 / 1.0) ≈ 1.1832
1461        let s = EulerState::from_primitives(1.0, 0.0, 0.0, 0.0, 1.0, gamma);
1462        let c = s.sound_speed(gamma);
1463        let expected = (gamma).sqrt();
1464        assert!(
1465            (c - expected).abs() < 1e-10,
1466            "sound speed = {c}, expected {expected}"
1467        );
1468    }
1469
1470    #[test]
1471    fn test_euler_state_lerp() {
1472        let gamma = 1.4;
1473        let s0 = EulerState::from_primitives(1.0, 0.0, 0.0, 0.0, 1.0, gamma);
1474        let s1 = EulerState::from_primitives(2.0, 0.0, 0.0, 0.0, 2.0, gamma);
1475        let s05 = s0.lerp(&s1, 0.5);
1476        assert!((s05.rho - 1.5).abs() < 1e-12, "lerp rho = {}", s05.rho);
1477    }
1478
1479    #[test]
1480    fn test_euler_state_flux_x() {
1481        let gamma = 1.4;
1482        // At rest: F = [0, p, 0, 0, 0]
1483        let s = EulerState::from_primitives(1.0, 0.0, 0.0, 0.0, 1.0, gamma);
1484        let f = s.flux_x(gamma);
1485        assert!(f[0].abs() < 1e-12, "mass flux at rest = {}", f[0]);
1486        assert!((f[1] - 1.0).abs() < 1e-10, "pressure flux = {}", f[1]);
1487    }
1488
1489    // ── Van Albada tests ──────────────────────────────────────────────────────
1490
1491    #[test]
1492    fn test_van_albada_same_sign() {
1493        // van_albada should return positive for same-sign inputs
1494        let v = van_albada(2.0, 4.0);
1495        assert!(v > 0.0, "van_albada(2,4) = {v}");
1496        // Should be TVD: |result| <= min(2|a|, 2|b|, |a+b|/2...)
1497        assert!(v <= 4.0);
1498    }
1499
1500    #[test]
1501    fn test_van_albada_opposite_signs() {
1502        assert_eq!(van_albada(1.0, -1.0), 0.0);
1503        assert_eq!(van_albada(-2.0, 3.0), 0.0);
1504    }
1505
1506    #[test]
1507    fn test_van_albada_symmetry() {
1508        // van_albada(a, b) vs van_albada(b, a): not strictly symmetric but both > 0
1509        let v1 = van_albada(1.0, 3.0);
1510        let v2 = van_albada(3.0, 1.0);
1511        assert!(v1 > 0.0 && v2 > 0.0);
1512    }
1513
1514    // ── MUSCL tests ──────────────────────────────────────────────────────────
1515
1516    #[test]
1517    fn test_muscl_reconstruct_uniform() {
1518        // Uniform field: reconstruction should give exact values
1519        let u = vec![1.0_f64; 10];
1520        let (u_l, u_r) = muscl_reconstruct(&u, 3, minmod);
1521        assert!((u_l - 1.0).abs() < 1e-12, "uniform MUSCL u_l = {u_l}");
1522        assert!((u_r - 1.0).abs() < 1e-12, "uniform MUSCL u_r = {u_r}");
1523    }
1524
1525    #[test]
1526    fn test_muscl_reconstruct_all_length() {
1527        let u = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1528        let pairs = muscl_reconstruct_all(&u, minmod);
1529        assert_eq!(pairs.len(), 4, "MUSCL all: expected 4 interfaces");
1530    }
1531
1532    #[test]
1533    fn test_muscl_reconstruct_all_monotone() {
1534        // Linear field: reconstruction should preserve ordering
1535        let u: Vec<f64> = (0..8).map(|i| i as f64).collect();
1536        let pairs = muscl_reconstruct_all(&u, minmod);
1537        for (i, &(u_l, u_r)) in pairs.iter().enumerate() {
1538            assert!(u_l <= u_r + 1e-10, "interface {i}: u_l={u_l} > u_r={u_r}");
1539        }
1540    }
1541
1542    // ── Godunov flux tests ────────────────────────────────────────────────────
1543
1544    #[test]
1545    fn test_godunov_flux_advection_positive_speed() {
1546        // With a > 0, flux = a * u_l
1547        let f = godunov_flux_advection(2.0, 3.0, 1.0);
1548        assert!((f - 2.0).abs() < 1e-12, "Godunov advection +speed: {f}");
1549    }
1550
1551    #[test]
1552    fn test_godunov_flux_advection_negative_speed() {
1553        let f = godunov_flux_advection(2.0, 3.0, -1.0);
1554        assert!((f - (-3.0)).abs() < 1e-12, "Godunov advection -speed: {f}");
1555    }
1556
1557    #[test]
1558    fn test_godunov_flux_burgers_shock() {
1559        // Shock: u_l > u_r, s > 0 → f = 0.5 * u_l^2
1560        let f = godunov_flux_burgers(2.0, 1.0);
1561        assert!((f - 0.5 * 4.0).abs() < 1e-12, "Burgers shock: {f}");
1562    }
1563
1564    #[test]
1565    fn test_godunov_flux_burgers_rarefaction_sonic() {
1566        // Rarefaction spanning u=0: u_l < 0 < u_r → entropy-satisfying flux = 0
1567        let f = godunov_flux_burgers(-1.0, 1.0);
1568        assert!(f.abs() < 1e-12, "Burgers sonic: {f}");
1569    }
1570
1571    #[test]
1572    fn test_godunov_flux_burgers_rarefaction_positive() {
1573        // Rarefaction, all positive: u_l > 0, u_r > u_l → f = 0.5*u_l^2
1574        let f = godunov_flux_burgers(1.0, 2.0);
1575        assert!((f - 0.5).abs() < 1e-12, "Burgers pos rarefaction: {f}");
1576    }
1577
1578    // ── Roe flux tests ────────────────────────────────────────────────────────
1579
1580    #[test]
1581    fn test_roe_flux_scalar_symmetry() {
1582        // For uniform state, Roe flux = physical flux
1583        let f = roe_flux_scalar(2.0, 2.0, 1.5);
1584        assert!((f - 1.5 * 2.0).abs() < 1e-12, "Roe scalar uniform: {f}");
1585    }
1586
1587    #[test]
1588    fn test_roe_flux_scalar_upwind() {
1589        // For a > 0 and jump, Roe flux should propagate from left
1590        let f = roe_flux_scalar(1.0, 2.0, 1.0);
1591        // f = 0.5*(1+2) - 0.5*1*(2-1) = 1.5 - 0.5 = 1.0
1592        assert!((f - 1.0).abs() < 1e-12, "Roe scalar upwind: {f}");
1593    }
1594
1595    #[test]
1596    fn test_roe_flux_euler_1d_finite() {
1597        let f = roe_flux_euler_1d(1.0, 0.1, 1.0, 1.0, 0.1, 1.0, 1.4);
1598        for (i, &fi) in f.iter().enumerate() {
1599            assert!(fi.is_finite(), "Roe Euler flux[{i}] not finite: {fi}");
1600        }
1601    }
1602
1603    // ── HLL flux tests ────────────────────────────────────────────────────────
1604
1605    #[test]
1606    fn test_hll_flux_subsonic() {
1607        // s_l < 0 < s_r: active HLL formula
1608        let f = hll_flux(1.0, 2.0, 1.0, 2.0, -1.0, 1.0);
1609        assert!(f.is_finite(), "HLL subsonic: {f}");
1610    }
1611
1612    #[test]
1613    fn test_hll_flux_supersonic_right() {
1614        // s_l > 0: use f_l
1615        let f = hll_flux(1.0, 2.0, 3.0, 4.0, 1.0, 2.0);
1616        assert!((f - 3.0).abs() < 1e-12, "HLL supersonic right: {f}");
1617    }
1618
1619    #[test]
1620    fn test_hll_flux_supersonic_left() {
1621        // s_r < 0: use f_r
1622        let f = hll_flux(1.0, 2.0, 3.0, 4.0, -2.0, -1.0);
1623        assert!((f - 4.0).abs() < 1e-12, "HLL supersonic left: {f}");
1624    }
1625
1626    #[test]
1627    fn test_hll_flux_euler_1d_finite() {
1628        let f = hll_flux_euler_1d(1.0, 0.1, 1.0, 1.2, 0.05, 1.1, 1.4);
1629        for &fi in &f {
1630            assert!(fi.is_finite(), "HLL Euler flux not finite");
1631        }
1632    }
1633
1634    #[test]
1635    fn test_hll_wave_speeds() {
1636        let (s_l, s_r) = hll_wave_speeds(0.5, 1.0, -0.5, 1.0);
1637        assert!(s_l < 0.0 && s_r > 0.0, "wave speeds s_l={s_l} s_r={s_r}");
1638        assert!(s_l <= s_r);
1639    }
1640
1641    // ── HLLC flux tests ───────────────────────────────────────────────────────
1642
1643    #[test]
1644    fn test_hllc_flux_euler_1d_finite() {
1645        let f = hllc_flux_euler_1d(1.0, 0.1, 1.0, 1.2, 0.05, 1.1, 1.4);
1646        for &fi in &f {
1647            assert!(fi.is_finite(), "HLLC Euler flux not finite: {fi}");
1648        }
1649    }
1650
1651    #[test]
1652    fn test_hllc_agrees_with_hll_at_uniform_state() {
1653        // For uniform state (no jump), HLL and HLLC should give similar answers
1654        let f_hll = hll_flux_euler_1d(1.0, 0.1, 1.0, 1.0, 0.1, 1.0, 1.4);
1655        let f_hllc = hllc_flux_euler_1d(1.0, 0.1, 1.0, 1.0, 0.1, 1.0, 1.4);
1656        for i in 0..3 {
1657            assert!(
1658                (f_hll[i] - f_hllc[i]).abs() < 1e-6,
1659                "HLL vs HLLC at [{}]: {} vs {}",
1660                i,
1661                f_hll[i],
1662                f_hllc[i]
1663            );
1664        }
1665    }
1666
1667    // ── Characteristic decomposition tests ───────────────────────────────────
1668
1669    #[test]
1670    fn test_characteristic_roundtrip() {
1671        let u0 = 1.5;
1672        let u1 = 0.7;
1673        let a = 2.0;
1674        let (wp, wm) = characteristic_decompose_2wave(u0, u1, a);
1675        let (u0_back, u1_back) = characteristic_recompose_2wave(wp, wm, a);
1676        assert!((u0_back - u0).abs() < 1e-12, "roundtrip u0: {u0_back}");
1677        assert!((u1_back - u1).abs() < 1e-12, "roundtrip u1: {u1_back}");
1678    }
1679
1680    #[test]
1681    fn test_characteristic_limited_reconstruct_finite() {
1682        let u0: Vec<f64> = (0..8).map(|i| i as f64 * 0.1).collect();
1683        let u1: Vec<f64> = vec![0.5; 8];
1684        let (left, right) = characteristic_limited_reconstruct(&u0, &u1, 3, 1.0, minmod);
1685        for &v in left.iter().chain(right.iter()) {
1686            assert!(v.is_finite(), "characteristic reconstruct not finite: {v}");
1687        }
1688    }
1689
1690    // ── TVD RK2 tests ─────────────────────────────────────────────────────────
1691
1692    #[test]
1693    fn test_tvd_rk2_advect_uniform_unchanged() {
1694        let phi = FluxGrid3D::new(5, 5, 5, 0.1, 2.0);
1695        let mut vel = VectorFluxGrid3D::zeros(5, 5, 5, 0.1);
1696        for i in 0..5 {
1697            for j in 0..5 {
1698                for k in 0..5 {
1699                    vel.set_vel(i, j, k, [1.0, 0.0, 0.0]);
1700                }
1701            }
1702        }
1703        let phi_new = tvd_rk2_advect(&phi, &vel, 0.01);
1704        for (&a, &b) in phi.values.iter().zip(phi_new.values.iter()) {
1705            assert!((a - b).abs() < 1e-10, "TVD RK2 uniform: {a} vs {b}");
1706        }
1707    }
1708
1709    #[test]
1710    fn test_tvd_rk2_advect_size_preserved() {
1711        let phi = FluxGrid3D::new(4, 3, 2, 0.1, 1.0);
1712        let vel = VectorFluxGrid3D::zeros(4, 3, 2, 0.1);
1713        let phi_new = tvd_rk2_advect(&phi, &vel, 0.01);
1714        assert_eq!(phi_new.nx, phi.nx);
1715        assert_eq!(phi_new.ny, phi.ny);
1716        assert_eq!(phi_new.nz, phi.nz);
1717    }
1718
1719    // ── Limiter ratio-form tests ──────────────────────────────────────────────
1720
1721    #[test]
1722    fn test_minmod_ratio_clamp() {
1723        assert_eq!(minmod_ratio(-0.5), 0.0);
1724        assert!((minmod_ratio(0.5) - 0.5).abs() < 1e-12);
1725        assert!((minmod_ratio(2.0) - 1.0).abs() < 1e-12);
1726    }
1727
1728    #[test]
1729    fn test_superbee_ratio_basic() {
1730        assert_eq!(superbee_ratio(-1.0), 0.0);
1731        let v = superbee_ratio(1.5);
1732        assert!(v > 0.0 && v <= 2.0, "superbee_ratio(1.5) = {v}");
1733    }
1734
1735    #[test]
1736    fn test_van_leer_ratio_basic() {
1737        // van_leer_ratio(1.0) = 2*1/(1+1) = 1.0
1738        let v = van_leer_ratio(1.0);
1739        assert!((v - 1.0).abs() < 1e-12, "van_leer_ratio(1) = {v}");
1740        // Negative r → 0
1741        assert_eq!(van_leer_ratio(-0.5), 0.0);
1742    }
1743
1744    #[test]
1745    fn test_van_leer_ratio_monotone() {
1746        // phi(r) should be monotone increasing for r > 0
1747        let v1 = van_leer_ratio(0.5);
1748        let v2 = van_leer_ratio(1.0);
1749        let v3 = van_leer_ratio(2.0);
1750        assert!(v1 <= v2, "van_leer not monotone: {v1} > {v2}");
1751        assert!(v2 <= v3 + 1e-12, "van_leer not monotone: {v2} > {v3}");
1752    }
1753}