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::limiters::{LimitedFlux, Limiter};
use crate::operators::weno::WENO3;
use crate::operators::{
check_cfl, ghost_cell_wide_divergence, periodic_wide_divergence, truncated_wide_divergence,
FluxBoundary, FluxDivergenceOperator,
};
#[derive(Debug, Clone)]
pub struct AdaptiveFlux {
velocity: f64,
diffusion: f64,
peclet_threshold: f64,
weno: WENO3,
limited: LimitedFlux,
boundary: FluxBoundary,
}
impl AdaptiveFlux {
pub fn new(
velocity: f64,
diffusion: f64,
limiter: Limiter,
peclet_threshold: f64,
boundary: FluxBoundary,
) -> Self {
Self {
velocity,
diffusion,
peclet_threshold,
weno: WENO3::new(velocity, diffusion, boundary.clone()),
limited: LimitedFlux::new(velocity, diffusion, limiter, boundary.clone()),
boundary,
}
}
fn peclet(&self, dx: f64) -> f64 {
self.velocity.abs() * dx / self.diffusion
}
fn face_flux(&self, dx: f64, u: &DVector<f64>, n: usize, i: usize, gate_active: bool) -> f64 {
let f_weno = self.weno.face_flux(dx, u, n, i);
if !gate_active {
return f_weno;
}
let gate = self.weno.smoothness(u, n, i);
let f_limited = self.limited.face_flux(dx, u, n, i);
(1.0 - gate) * f_weno + gate * f_limited
}
}
impl RequiresContext for AdaptiveFlux {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
}
impl FluxDivergenceOperator for AdaptiveFlux {
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("AdaptiveFlux", self.velocity, ctx.time_step(), dx)?;
let gate_active = self.peclet(dx) > self.peclet_threshold;
let face = |u: &DVector<f64>, n: usize, i: usize| self.face_flux(dx, u, n, i, gate_active);
let div = match &self.boundary {
FluxBoundary::Periodic => periodic_wide_divergence(u, dx, 3, "AdaptiveFlux", face)?,
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, "AdaptiveFlux", face)?
}
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()),
"AdaptiveFlux",
face,
)?
}
};
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 below_peclet_threshold_matches_pure_weno3_exactly() {
let m = mesh(8);
let dx = m.characteristic_length();
let u = ContextValue::ScalarField(DVector::from_vec(vec![
0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0,
]));
let velocity = 0.1;
let diffusion = 10.0;
let op = AdaptiveFlux::new(
velocity,
diffusion,
Limiter::Superbee,
1000.0, FluxBoundary::Periodic,
);
let weno = WENO3::new(velocity, diffusion, FluxBoundary::Periodic);
let adaptive_result = op.apply(&u, &m, &ctx(0.001 * dx)).unwrap();
let weno_result = weno.apply(&u, &m, &ctx(0.001 * dx)).unwrap();
let adaptive = adaptive_result.as_scalar_field().unwrap();
let expected = weno_result.as_scalar_field().unwrap();
for i in 0..adaptive.len() {
assert!(
(adaptive[i] - expected[i]).abs() < 1e-12,
"cell {i}: adaptive={}, pure WENO3={}",
adaptive[i],
expected[i]
);
}
}
#[test]
fn above_peclet_threshold_diverges_from_pure_weno3_on_a_step() {
let m = mesh(8);
let dx = m.characteristic_length();
let u = ContextValue::ScalarField(DVector::from_vec(vec![
0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0,
]));
let velocity = 1.0;
let diffusion = 1e-6;
let op = AdaptiveFlux::new(
velocity,
diffusion,
Limiter::Superbee,
1.0, FluxBoundary::Periodic,
);
let weno = WENO3::new(velocity, diffusion, FluxBoundary::Periodic);
let adaptive_result = op.apply(&u, &m, &ctx(0.0001 * dx)).unwrap();
let weno_result = weno.apply(&u, &m, &ctx(0.0001 * dx)).unwrap();
let adaptive = adaptive_result.as_scalar_field().unwrap();
let pure_weno = weno_result.as_scalar_field().unwrap();
let max_diff = (0..adaptive.len())
.map(|i| (adaptive[i] - pure_weno[i]).abs())
.fold(0.0, f64::max);
assert!(
max_diff > 1e-8,
"expected a measurable difference from pure WENO3 near the step, got max diff {max_diff}"
);
}
#[test]
fn peclet_is_infinite_for_pure_advection_and_always_gates() {
let op = AdaptiveFlux::new(1.0, 0.0, Limiter::MinMod, 1e6, FluxBoundary::Periodic);
assert!(op.peclet(0.1).is_infinite());
}
#[test]
fn 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 = AdaptiveFlux::new(0.8, 0.05, Limiter::VanLeer, 0.5, 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-9, "expected ~0, got {sum}");
}
#[test]
fn truncation_boundary_does_not_panic_and_matches_weno3_margins() {
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 = AdaptiveFlux::new(1.0, 1e-6, Limiter::MinMod, 1.0, FluxBoundary::Truncation);
assert!(op.apply(&u, &m, &ctx(0.01 * dx)).is_ok());
}
#[test]
fn truncation_rejects_field_below_minimum_for_direction() {
let m = mesh(3);
let op = AdaptiveFlux::new(1.0, 1e-6, Limiter::MinMod, 1.0, 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(_)));
}
#[test]
fn rejects_cfl_violation() {
let m = mesh(5);
let op = AdaptiveFlux::new(10.0, 0.0, Limiter::VanLeer, 1.0, 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 { .. }));
}
}