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,
};
const WENO_EPSILON: f64 = 1e-6;
fn weno_combine2(q0: f64, q1: f64, beta0: f64, beta1: f64, d0: f64, d1: f64) -> f64 {
let tau = (beta0 - beta1).abs();
let a0 = d0 * (1.0 + tau / (WENO_EPSILON + beta0));
let a1 = d1 * (1.0 + tau / (WENO_EPSILON + beta1));
(a0 * q0 + a1 * q1) / (a0 + a1)
}
fn weno_combine3(q: [f64; 3], beta: [f64; 3], d: [f64; 3]) -> f64 {
let tau = (beta[0] - beta[2]).abs();
let a: [f64; 3] = std::array::from_fn(|k| d[k] * (1.0 + tau / (WENO_EPSILON + beta[k])));
let sum: f64 = a.iter().sum();
(0..3).map(|k| a[k] * q[k]).sum::<f64>() / sum
}
fn weno3_left(a: f64, b: f64, c: f64) -> f64 {
let q0 = (-a + 3.0 * b) / 2.0;
let q1 = (b + c) / 2.0;
let beta0 = (b - a).powi(2);
let beta1 = (c - b).powi(2);
weno_combine2(q0, q1, beta0, beta1, 1.0 / 3.0, 2.0 / 3.0)
}
fn weno3_right(b: f64, c: f64, d: f64) -> f64 {
let q0 = (b + c) / 2.0;
let q1 = (3.0 * c - d) / 2.0;
let beta0 = (c - b).powi(2);
let beta1 = (d - c).powi(2);
weno_combine2(q0, q1, beta0, beta1, 2.0 / 3.0, 1.0 / 3.0)
}
fn weno5_left(a: f64, b: f64, c: f64, d: f64, e: f64) -> f64 {
let q0 = (2.0 * a - 7.0 * b + 11.0 * c) / 6.0;
let q1 = (-b + 5.0 * c + 2.0 * d) / 6.0;
let q2 = (2.0 * c + 5.0 * d - e) / 6.0;
let beta0 = 13.0 / 12.0 * (a - 2.0 * b + c).powi(2) + 0.25 * (a - 4.0 * b + 3.0 * c).powi(2);
let beta1 = 13.0 / 12.0 * (b - 2.0 * c + d).powi(2) + 0.25 * (b - d).powi(2);
let beta2 = 13.0 / 12.0 * (c - 2.0 * d + e).powi(2) + 0.25 * (3.0 * c - 4.0 * d + e).powi(2);
weno_combine3([q0, q1, q2], [beta0, beta1, beta2], [0.1, 0.6, 0.3])
}
fn weno5_right(b: f64, c: f64, d: f64, e: f64, f: f64) -> f64 {
let q_a = (-b + 5.0 * c + 2.0 * d) / 6.0;
let q_b = (2.0 * c + 5.0 * d - e) / 6.0;
let q_c = (11.0 * d - 7.0 * e + 2.0 * f) / 6.0;
let beta_a = 13.0 / 12.0 * (b - 2.0 * c + d).powi(2) + 0.25 * (b - 4.0 * c + 3.0 * d).powi(2);
let beta_b = 13.0 / 12.0 * (c - 2.0 * d + e).powi(2) + 0.25 * (c - e).powi(2);
let beta_c = 13.0 / 12.0 * (d - 2.0 * e + f).powi(2) + 0.25 * (3.0 * d - 4.0 * e + f).powi(2);
weno_combine3([q_a, q_b, q_c], [beta_a, beta_b, beta_c], [0.3, 0.6, 0.1])
}
#[derive(Debug, Clone)]
pub struct WENO3 {
velocity: f64,
diffusion: f64,
boundary: FluxBoundary,
}
impl WENO3 {
pub fn new(velocity: f64, diffusion: f64, boundary: FluxBoundary) -> Self {
Self {
velocity,
diffusion,
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 {
weno3_left(u[wrap(i, -1, n)], u[i], u[wrap(i, 1, n)])
} else {
weno3_right(u[i], u[wrap(i, 1, n)], u[wrap(i, 2, n)])
};
let diffusive = self.diffusion * (u[wrap(i, 1, n)] - u[i]) / dx;
v * u_face - diffusive
}
pub(crate) fn smoothness(&self, u: &DVector<f64>, n: usize, i: usize) -> f64 {
let (beta0, beta1) = if self.velocity >= 0.0 {
let (a, b, c) = (u[wrap(i, -1, n)], u[i], u[wrap(i, 1, n)]);
((b - a).powi(2), (c - b).powi(2))
} else {
let (b, c, d) = (u[i], u[wrap(i, 1, n)], u[wrap(i, 2, n)]);
((c - b).powi(2), (d - c).powi(2))
};
(beta0 - beta1).abs() / (beta0 + beta1 + WENO_EPSILON)
}
}
impl RequiresContext for WENO3 {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
}
impl FluxDivergenceOperator for WENO3 {
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("WENO3", self.velocity, ctx.time_step(), dx)?;
let div = match &self.boundary {
FluxBoundary::Periodic => {
periodic_wide_divergence(u, dx, 3, "WENO3", |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, "WENO3", |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()),
"WENO3",
|u, n, i| self.face_flux(dx, u, n, i),
)?
}
};
Ok(ContextValue::ScalarField(div))
}
}
#[derive(Debug, Clone)]
pub struct WENO5 {
velocity: f64,
diffusion: f64,
boundary: FluxBoundary,
}
impl WENO5 {
pub fn new(velocity: f64, diffusion: f64, boundary: FluxBoundary) -> Self {
Self {
velocity,
diffusion,
boundary,
}
}
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 {
weno5_left(
u[wrap(i, -2, n)],
u[wrap(i, -1, n)],
u[i],
u[wrap(i, 1, n)],
u[wrap(i, 2, n)],
)
} else {
weno5_right(
u[wrap(i, -1, n)],
u[i],
u[wrap(i, 1, n)],
u[wrap(i, 2, n)],
u[wrap(i, 3, n)],
)
};
let diffusive = self.diffusion * (u[wrap(i, 1, n)] - u[i]) / dx;
v * u_face - diffusive
}
}
impl RequiresContext for WENO5 {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
}
impl FluxDivergenceOperator for WENO5 {
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("WENO5", self.velocity, ctx.time_step(), dx)?;
let div = match &self.boundary {
FluxBoundary::Periodic => {
periodic_wide_divergence(u, dx, 5, "WENO5", |u, n, i| self.face_flux(dx, u, n, i))?
}
FluxBoundary::Truncation => {
let (margin_left, margin_right) =
if self.velocity >= 0.0 { (2, 2) } else { (1, 3) };
truncated_wide_divergence(u, dx, margin_left, margin_right, "WENO5", |u, n, i| {
self.face_flux(dx, u, n, i)
})?
}
FluxBoundary::GhostCell(left_bc, right_bc) => {
let margins = if self.velocity >= 0.0 { (3, 2) } else { (2, 3) };
ghost_cell_wide_divergence(
u,
dx,
margins,
(left_bc.as_ref(), right_bc.as_ref()),
"WENO5",
|u, n, i| self.face_flux(dx, u, n, i),
)?
}
};
Ok(ContextValue::ScalarField(div))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
use std::sync::Arc;
fn mesh(n: usize) -> UniformGrid1D {
UniformGrid1D::new(n, 0.0, 1.0).unwrap()
}
fn ctx(dt: f64) -> ComputeContext {
ComputeContext::new(0.0, dt)
}
#[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 weno5_conserves_sum_for_periodic_problem() {
let m = mesh(8);
let dx = m.characteristic_length();
let values = vec![0.2, 1.3, -0.7, 2.1, 0.0, -1.5, 0.9, 0.4];
let u = ContextValue::ScalarField(DVector::from_vec(values));
let op = WENO5::new(0.5, 0.02, FluxBoundary::Periodic);
let result = op.apply(&u, &m, &ctx(0.01 * dx)).unwrap();
let sum: f64 = result.as_scalar_field().unwrap().iter().sum();
assert!(sum.abs() < 1e-9, "expected 0, got {sum}");
}
#[test]
fn weno3_conserves_sum_for_periodic_problem() {
let m = mesh(8);
let dx = m.characteristic_length();
let values = vec![0.2, 1.3, -0.7, 2.1, 0.0, -1.5, 0.9, 0.4];
let u = ContextValue::ScalarField(DVector::from_vec(values));
let op = WENO3::new(-0.3, 0.01, FluxBoundary::Periodic);
let result = op.apply(&u, &m, &ctx(0.01 * dx)).unwrap();
let sum: f64 = result.as_scalar_field().unwrap().iter().sum();
assert!(sum.abs() < 1e-9, "expected 0, got {sum}");
}
fn periodic_mesh(n: usize) -> UniformGrid1D {
UniformGrid1D::new(n, 0.0, (n - 1) as f64 / n as f64).unwrap()
}
fn rms_error(computed: &DVector<f64>, analytical: &[f64]) -> f64 {
let n = analytical.len();
let sum_sq: f64 = computed
.iter()
.zip(analytical.iter())
.map(|(&d, &a)| (d - a).powi(2))
.sum();
(sum_sq / n as f64).sqrt()
}
fn sinc_factor(dx: f64) -> f64 {
let arg = PI * dx;
arg.sin() / arg
}
#[test]
fn weno3_smooth_periodic_solution_converges_faster_than_first_order() {
let errors: Vec<f64> = [21usize, 41]
.iter()
.map(|&n| {
let m = periodic_mesh(n);
let dx = m.characteristic_length();
let sinc = sinc_factor(dx);
let x: Vec<f64> = (0..n).map(|i| i as f64 * dx).collect();
let u =
DVector::from_vec(x.iter().map(|&xi| (2.0 * PI * xi).sin() * sinc).collect());
let op = WENO3::new(1.0, 0.0, FluxBoundary::Periodic);
let field = ContextValue::ScalarField(u);
let result = op.apply(&field, &m, &ctx(1e-6)).unwrap();
let div = result.as_scalar_field().unwrap().clone();
let analytical: Vec<f64> = x
.iter()
.map(|&xi| 2.0 * PI * (2.0 * PI * xi).cos() * sinc)
.collect();
rms_error(&div, &analytical)
})
.collect();
let ratio = errors[0] / errors[1];
assert!(
ratio > 2.5,
"expected order clearly better than 1st (ratio > 2.5) — see comment above on WENO3's known weight-tolerance limitation, got {ratio}"
);
}
#[test]
fn weno5_smooth_periodic_solution_converges_faster_than_third_order() {
let errors: Vec<f64> = [21usize, 41]
.iter()
.map(|&n| {
let m = periodic_mesh(n);
let dx = m.characteristic_length();
let sinc = sinc_factor(dx);
let x: Vec<f64> = (0..n).map(|i| i as f64 * dx).collect();
let u =
DVector::from_vec(x.iter().map(|&xi| (2.0 * PI * xi).sin() * sinc).collect());
let op = WENO5::new(1.0, 0.0, FluxBoundary::Periodic);
let field = ContextValue::ScalarField(u);
let result = op.apply(&field, &m, &ctx(1e-6)).unwrap();
let div = result.as_scalar_field().unwrap().clone();
let analytical: Vec<f64> = x
.iter()
.map(|&xi| 2.0 * PI * (2.0 * PI * xi).cos() * sinc)
.collect();
rms_error(&div, &analytical)
})
.collect();
let ratio = errors[0] / errors[1];
assert!(
ratio > 15.0,
"expected clearly better than 3rd order (ratio > 15), got {ratio}"
);
}
#[test]
fn weno5_left_biased_reconstruction_stays_within_data_bounds_on_step() {
let (a, b, c, d, e) = (0.0, 0.0, 0.0, 1.0, 1.0);
let recon = weno5_left(a, b, c, d, e);
assert!(
(-0.05..=1.05).contains(&recon),
"expected reconstruction within [0, 1] (± tolerance), got {recon}"
);
}
#[test]
fn weno3_left_biased_reconstruction_stays_within_data_bounds_on_step() {
let (a, b, c) = (0.0, 0.0, 1.0);
let recon = weno3_left(a, b, c);
assert!(
(-0.05..=1.05).contains(&recon),
"expected reconstruction within [0, 1] (± tolerance), got {recon}"
);
}
#[test]
fn weno5_rejects_undersized_field() {
let m = mesh(4);
let op = WENO5::new(1.0, 0.0, FluxBoundary::Periodic);
let u = ContextValue::ScalarField(DVector::from_element(4, 1.0));
let err = op.apply(&u, &m, &ctx(0.001)).unwrap_err();
assert!(matches!(err, OxiflowError::InvalidDomain(_)));
}
#[test]
fn weno3_rejects_undersized_field() {
let m = mesh(2);
let op = WENO3::new(1.0, 0.0, FluxBoundary::Periodic);
let u = ContextValue::ScalarField(DVector::from_element(2, 1.0));
let err = op.apply(&u, &m, &ctx(0.001)).unwrap_err();
assert!(matches!(err, OxiflowError::InvalidDomain(_)));
}
#[test]
fn weno5_rejects_cfl_violation() {
let m = mesh(5);
let op = WENO5::new(10.0, 0.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 { .. }));
}
#[test]
fn weno3_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 = WENO3::new(1.0, 0.0, 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[2]);
assert_eq!(div[1], div[2]);
assert_eq!(div[6], div[5]);
}
#[test]
fn weno3_truncation_right_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 = WENO3::new(-1.0, 0.0, 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[4]);
assert_eq!(div[5], div[4]);
}
#[test]
fn weno3_truncation_rejects_field_below_minimum_for_direction() {
let m = mesh(3);
let op = WENO3::new(1.0, 0.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 weno5_truncation_left_biased_boundary_matches_nearest_safe_cell() {
let m = mesh(9);
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, 0.9, -0.3,
]));
let op = WENO5::new(1.0, 0.0, FluxBoundary::Truncation);
let div = op.apply(&u, &m, &ctx(0.001 * dx)).unwrap();
let div = div.as_scalar_field().unwrap();
assert_eq!(div[0], div[3]);
assert_eq!(div[1], div[3]);
assert_eq!(div[2], div[3]);
assert_eq!(div[8], div[6]);
assert_eq!(div[7], div[6]);
}
#[test]
fn weno5_truncation_rejects_field_below_minimum_for_direction() {
let m = mesh(5);
let op = WENO5::new(1.0, 0.0, FluxBoundary::Truncation);
let u = ContextValue::ScalarField(DVector::from_element(5, 1.0));
let err = op.apply(&u, &m, &ctx(0.001)).unwrap_err();
assert!(matches!(err, OxiflowError::InvalidDomain(_)));
}
#[test]
fn weno3_ghost_cell_left_biased_matches_hand_built_extended_field() {
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 = WENO3::new(1.0, 0.0, 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 face =
|i: usize| WENO3::new(1.0, 0.0, FluxBoundary::Periodic).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 weno5_ghost_cell_left_biased_matches_hand_built_extended_field() {
let values = vec![0.2, 1.3, -0.7, 2.1, 0.0, -1.5, 0.6];
let m = mesh(values.len());
let dx = m.characteristic_length();
let u = ContextValue::ScalarField(DVector::from_vec(values.clone()));
let g = -0.3;
let left_bc = Arc::new(DirichletGhost(g));
let right_bc = Arc::new(DirichletGhost(g));
let op = WENO5::new(1.0, 0.0, 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_m3 = 2.0 * g - values[2];
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 ghost_p2 = 2.0 * g - values[n - 2];
let mut extended = vec![ghost_m3, ghost_m2, ghost_m1];
extended.extend(values.iter().copied());
extended.push(ghost_p1);
extended.push(ghost_p2);
let ext = DVector::from_vec(extended);
let m_ext = ext.len();
let face =
|i: usize| WENO5::new(1.0, 0.0, FluxBoundary::Periodic).face_flux(dx, &ext, m_ext, i);
let expected_0 = (face(3) - face(2)) / dx;
let expected_last = (face(3 + n - 1) - face(3 + 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 weno3_ghost_cell_fails_explicitly_when_bc_has_no_ghost_value() {
#[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 = WENO3::new(1.0, 0.0, FluxBoundary::GhostCell(left_bc, right_bc));
let err = op.apply(&u, &m, &ctx(0.001)).unwrap_err();
assert!(matches!(err, OxiflowError::PreconditionFailed { .. }));
}
}