use nalgebra::DVector;
use crate::context::compute::ComputeContext;
use crate::context::error::OxiflowError;
use crate::context::value::ContextValue;
use crate::context::variable::ContextVariable;
use crate::mesh::structured::UniformGrid1D;
use crate::mesh::Mesh;
use crate::model::traits::RequiresContext;
use crate::operators::{
check_cfl, ghost_cell_wide_divergence, periodic_wide_divergence, truncated_wide_divergence,
wrap, FluxBoundary, FluxDivergenceOperator,
};
fn ratio(numer: f64, denom: f64) -> f64 {
const EPS: f64 = 1e-12;
if denom.abs() < EPS {
0.0
} else {
numer / denom
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Limiter {
MinMod,
VanLeer,
Superbee,
}
impl Limiter {
pub fn phi(&self, r: f64) -> f64 {
match self {
Limiter::MinMod => r.clamp(0.0, 1.0),
Limiter::VanLeer => (r + r.abs()) / (1.0 + r.abs()),
Limiter::Superbee => {
let a = (2.0 * r).min(1.0);
let b = r.min(2.0);
a.max(b).max(0.0)
}
}
}
}
#[derive(Debug, Clone)]
pub struct LimitedFlux {
velocity: f64,
diffusion: f64,
limiter: Limiter,
boundary: FluxBoundary,
}
impl LimitedFlux {
pub fn new(velocity: f64, diffusion: f64, limiter: Limiter, boundary: FluxBoundary) -> Self {
Self {
velocity,
diffusion,
limiter,
boundary,
}
}
pub(crate) fn face_flux(&self, dx: f64, u: &DVector<f64>, n: usize, i: usize) -> f64 {
let v = self.velocity;
let u_face = if v >= 0.0 {
let (a, b, c) = (u[wrap(i, -1, n)], u[i], u[wrap(i, 1, n)]);
let r = ratio(b - a, c - b);
b + 0.5 * self.limiter.phi(r) * (c - b)
} else {
let (b, c, d) = (u[i], u[wrap(i, 1, n)], u[wrap(i, 2, n)]);
let r = ratio(d - c, c - b);
c - 0.5 * self.limiter.phi(r) * (c - b)
};
let diffusive = self.diffusion * (u[wrap(i, 1, n)] - u[i]) / dx;
v * u_face - diffusive
}
}
impl RequiresContext for LimitedFlux {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
}
impl FluxDivergenceOperator for LimitedFlux {
type MeshType = UniformGrid1D;
fn apply(
&self,
field: &ContextValue,
mesh: &Self::MeshType,
ctx: &ComputeContext,
) -> Result<ContextValue, OxiflowError> {
let u = field.as_scalar_field()?;
let dx = mesh.characteristic_length();
check_cfl("LimitedFlux", self.velocity, ctx.time_step(), dx)?;
let div = match &self.boundary {
FluxBoundary::Periodic => {
periodic_wide_divergence(u, dx, 3, "LimitedFlux", |u, n, i| {
self.face_flux(dx, u, n, i)
})?
}
FluxBoundary::Truncation => {
let (margin_left, margin_right) =
if self.velocity >= 0.0 { (1, 1) } else { (0, 2) };
truncated_wide_divergence(
u,
dx,
margin_left,
margin_right,
"LimitedFlux",
|u, n, i| self.face_flux(dx, u, n, i),
)?
}
FluxBoundary::GhostCell(left_bc, right_bc) => {
let margins = if self.velocity >= 0.0 { (2, 1) } else { (1, 2) };
ghost_cell_wide_divergence(
u,
dx,
margins,
(left_bc.as_ref(), right_bc.as_ref()),
"LimitedFlux",
|u, n, i| self.face_flux(dx, u, n, i),
)?
}
};
Ok(ContextValue::ScalarField(div))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mesh(n: usize) -> UniformGrid1D {
UniformGrid1D::new(n, 0.0, 1.0).unwrap()
}
fn ctx(dt: f64) -> ComputeContext {
ComputeContext::new(0.0, dt)
}
#[test]
fn all_limiters_vanish_at_nonpositive_r() {
for limiter in [Limiter::MinMod, Limiter::VanLeer, Limiter::Superbee] {
for r in [-5.0, -1.0, 0.0] {
assert_eq!(limiter.phi(r), 0.0, "{limiter:?} at r={r}");
}
}
}
#[test]
fn all_limiters_are_exact_at_r_equals_one() {
for limiter in [Limiter::MinMod, Limiter::VanLeer, Limiter::Superbee] {
assert!((limiter.phi(1.0) - 1.0).abs() < 1e-12, "{limiter:?} at r=1");
}
}
#[test]
fn minmod_is_most_diffusive_superbee_is_least() {
for r in [1.5, 2.0, 3.0, 10.0] {
let minmod = Limiter::MinMod.phi(r);
let vanleer = Limiter::VanLeer.phi(r);
let superbee = Limiter::Superbee.phi(r);
assert!(
minmod <= vanleer && vanleer <= superbee,
"r={r}: expected MinMod({minmod}) <= VanLeer({vanleer}) <= Superbee({superbee})"
);
}
}
#[test]
fn minmod_caps_at_one_superbee_caps_at_two() {
assert_eq!(Limiter::MinMod.phi(100.0), 1.0);
assert_eq!(Limiter::Superbee.phi(100.0), 2.0);
}
#[test]
fn constant_field_produces_finite_zero_divergence() {
let m = mesh(6);
let dx = m.characteristic_length();
let u = ContextValue::ScalarField(DVector::from_element(6, 2.5));
for limiter in [Limiter::MinMod, Limiter::VanLeer, Limiter::Superbee] {
let op = LimitedFlux::new(0.7, 0.1, limiter, FluxBoundary::Periodic);
let result = op.apply(&u, &m, &ctx(0.01 * dx)).unwrap();
for &v in result.as_scalar_field().unwrap().iter() {
assert!(v.is_finite(), "{limiter:?}: non-finite divergence");
assert!(v.abs() < 1e-10, "{limiter:?}: expected ~0, got {v}");
}
}
}
#[test]
fn limited_flux_conserves_sum_for_periodic_problem() {
let m = mesh(6);
let dx = m.characteristic_length();
let u = ContextValue::ScalarField(DVector::from_vec(vec![0.2, 1.3, -0.7, 2.1, 0.0, -1.5]));
let op = LimitedFlux::new(0.8, 0.05, Limiter::VanLeer, FluxBoundary::Periodic);
let result = op.apply(&u, &m, &ctx(0.1 * dx)).unwrap();
let sum: f64 = result.as_scalar_field().unwrap().iter().sum();
assert!(sum.abs() < 1e-10, "expected 0, got {sum}");
}
#[test]
fn superbee_reconstruction_stays_within_data_bounds_on_step() {
let dx = 1.0; let values = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
let uv = DVector::from_vec(values);
let n = uv.len();
let op = LimitedFlux::new(1.0, 0.0, Limiter::Superbee, FluxBoundary::Periodic);
let face = op.face_flux(dx, &uv, n, 2); assert!(
(-0.05..=1.05).contains(&face),
"expected reconstruction within [0, 1] (± tolerance), got {face}"
);
}
#[test]
fn truncation_left_biased_boundary_matches_nearest_safe_cell() {
let m = mesh(7);
let dx = m.characteristic_length();
let u =
ContextValue::ScalarField(DVector::from_vec(vec![0.2, 1.3, -0.7, 2.1, 0.0, -1.5, 0.6]));
let op = LimitedFlux::new(1.0, 0.0, Limiter::VanLeer, FluxBoundary::Truncation);
let div = op.apply(&u, &m, &ctx(0.01 * dx)).unwrap();
let div = div.as_scalar_field().unwrap();
assert_eq!(div[0], div[1]);
assert_eq!(div[6], div[5]);
}
#[test]
fn truncation_rejects_field_below_minimum_for_direction() {
let m = mesh(3);
let op = LimitedFlux::new(1.0, 0.0, Limiter::MinMod, FluxBoundary::Truncation);
let u = ContextValue::ScalarField(DVector::from_element(3, 1.0));
let err = op.apply(&u, &m, &ctx(0.001)).unwrap_err();
assert!(matches!(err, OxiflowError::InvalidDomain(_)));
}
#[derive(Debug)]
struct DirichletGhost(f64);
impl RequiresContext for DirichletGhost {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
}
impl crate::boundary::BoundaryCondition for DirichletGhost {
fn boundary_type(&self) -> crate::boundary::BoundaryType {
crate::boundary::BoundaryType::Dirichlet
}
fn apply(
&self,
_state: &mut DVector<f64>,
_ctx: &ComputeContext,
_mesh: &dyn Mesh,
) -> Result<(), OxiflowError> {
Ok(())
}
fn ghost_value(&self, _depth: usize, interior_at_depth: f64, _dx: f64) -> Option<f64> {
Some(2.0 * self.0 - interior_at_depth)
}
}
#[test]
fn ghost_cell_left_biased_matches_hand_built_extended_field() {
use std::sync::Arc;
let values = vec![0.2, 1.3, -0.7, 2.1, 0.0];
let m = mesh(values.len());
let dx = m.characteristic_length();
let u = ContextValue::ScalarField(DVector::from_vec(values.clone()));
let g = 0.5;
let left_bc = Arc::new(DirichletGhost(g));
let right_bc = Arc::new(DirichletGhost(g));
let op = LimitedFlux::new(
1.0,
0.0,
Limiter::VanLeer,
FluxBoundary::GhostCell(left_bc, right_bc),
);
let div = op.apply(&u, &m, &ctx(0.001 * dx)).unwrap();
let div = div.as_scalar_field().unwrap();
let n = values.len();
let ghost_m2 = 2.0 * g - values[1];
let ghost_m1 = 2.0 * g - values[0];
let ghost_p1 = 2.0 * g - values[n - 1];
let mut extended = vec![ghost_m2, ghost_m1];
extended.extend(values.iter().copied());
extended.push(ghost_p1);
let ext = DVector::from_vec(extended);
let m_ext = ext.len();
let probe = LimitedFlux::new(1.0, 0.0, Limiter::VanLeer, FluxBoundary::Periodic);
let face = |i: usize| probe.face_flux(dx, &ext, m_ext, i);
let expected_0 = (face(2) - face(1)) / dx;
let expected_last = (face(2 + n - 1) - face(2 + n - 2)) / dx;
assert!((div[0] - expected_0).abs() < 1e-10, "got {}", div[0]);
assert!(
(div[n - 1] - expected_last).abs() < 1e-10,
"got {}",
div[n - 1]
);
}
#[test]
fn ghost_cell_fails_explicitly_when_bc_has_no_ghost_value() {
use std::sync::Arc;
#[derive(Debug)]
struct NoGhostBC;
impl RequiresContext for NoGhostBC {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
}
impl crate::boundary::BoundaryCondition for NoGhostBC {
fn boundary_type(&self) -> crate::boundary::BoundaryType {
crate::boundary::BoundaryType::Neumann
}
fn apply(
&self,
_state: &mut DVector<f64>,
_ctx: &ComputeContext,
_mesh: &dyn Mesh,
) -> Result<(), OxiflowError> {
Ok(())
}
}
let m = mesh(5);
let u = ContextValue::ScalarField(DVector::from_element(5, 1.0));
let left_bc = Arc::new(NoGhostBC);
let right_bc = Arc::new(DirichletGhost(0.0));
let op = LimitedFlux::new(
1.0,
0.0,
Limiter::MinMod,
FluxBoundary::GhostCell(left_bc, right_bc),
);
let err = op.apply(&u, &m, &ctx(0.001)).unwrap_err();
assert!(matches!(err, OxiflowError::PreconditionFailed { .. }));
}
#[test]
fn rejects_cfl_violation() {
let m = mesh(5); let op = LimitedFlux::new(10.0, 0.0, Limiter::VanLeer, FluxBoundary::Periodic);
let u = ContextValue::ScalarField(DVector::from_element(5, 1.0));
let err = op.apply(&u, &m, &ctx(1.0)).unwrap_err();
assert!(matches!(err, OxiflowError::PreconditionFailed { .. }));
}
#[test]
fn constant_parameters_require_no_context_variables() {
let op = LimitedFlux::new(1.0, 0.1, Limiter::MinMod, FluxBoundary::Periodic);
assert!(op.required_variables().is_empty());
}
}