use nalgebra::DVector;
use crate::context::error::OxiflowError;
use crate::context::value::ContextValue;
use crate::mesh::structured::UniformGrid1D;
use crate::mesh::Mesh;
use crate::operators::DiscreteOperator;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Forward,
Backward,
}
#[derive(Debug, Clone, Copy)]
pub struct UpwindGradient {
direction: Direction,
}
impl UpwindGradient {
pub fn new(direction: Direction) -> Self {
Self { direction }
}
pub(crate) fn compute_from_dx(
u: &DVector<f64>,
dx: f64,
direction: Direction,
) -> Result<DVector<f64>, OxiflowError> {
let n = u.len();
if n < 2 {
return Err(OxiflowError::InvalidDomain(format!(
"UpwindGradient requires at least 2 nodes, got {n}"
)));
}
let mut grad = DVector::zeros(n);
for i in 0..n {
grad[i] = match direction {
Direction::Forward => {
if i < n - 1 {
(u[i + 1] - u[i]) / dx
} else {
(u[n - 1] - u[n - 2]) / dx
}
}
Direction::Backward => {
if i > 0 {
(u[i] - u[i - 1]) / dx
} else {
(u[1] - u[0]) / dx
}
}
};
}
Ok(grad)
}
}
impl DiscreteOperator for UpwindGradient {
type MeshType = UniformGrid1D;
fn apply(
&self,
field: &ContextValue,
mesh: &Self::MeshType,
) -> Result<ContextValue, OxiflowError> {
let u = field.as_scalar_field()?;
let dx = mesh.characteristic_length();
let grad = Self::compute_from_dx(u, dx, self.direction)?;
Ok(ContextValue::ScalarField(grad))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CenteredGradient;
impl CenteredGradient {
pub fn new() -> Self {
Self
}
pub(crate) fn compute_from_dx(u: &DVector<f64>, dx: f64) -> Result<DVector<f64>, OxiflowError> {
let n = u.len();
if n < 2 {
return Err(OxiflowError::InvalidDomain(format!(
"CenteredGradient requires at least 2 nodes, got {n}"
)));
}
let mut grad = DVector::zeros(n);
for i in 0..n {
grad[i] = if i == 0 {
(u[1] - u[0]) / dx
} else if i == n - 1 {
(u[n - 1] - u[n - 2]) / dx
} else {
(u[i + 1] - u[i - 1]) / (2.0 * dx)
};
}
Ok(grad)
}
}
impl DiscreteOperator for CenteredGradient {
type MeshType = UniformGrid1D;
fn apply(
&self,
field: &ContextValue,
mesh: &Self::MeshType,
) -> Result<ContextValue, OxiflowError> {
let u = field.as_scalar_field()?;
let dx = mesh.characteristic_length();
let grad = Self::compute_from_dx(u, dx)?;
Ok(ContextValue::ScalarField(grad))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CenteredLaplacian;
impl CenteredLaplacian {
pub fn new() -> Self {
Self
}
pub(crate) fn compute_from_dx(u: &DVector<f64>, dx: f64) -> Result<DVector<f64>, OxiflowError> {
let n = u.len();
if n < 3 {
return Err(OxiflowError::InvalidDomain(format!(
"CenteredLaplacian requires at least 3 nodes, got {n}"
)));
}
let dx2 = dx * dx;
let mut lap = DVector::zeros(n);
lap[0] = (u[0] - 2.0 * u[1] + u[2]) / dx2;
for i in 1..n - 1 {
lap[i] = (u[i - 1] - 2.0 * u[i] + u[i + 1]) / dx2;
}
lap[n - 1] = (u[n - 3] - 2.0 * u[n - 2] + u[n - 1]) / dx2;
Ok(lap)
}
}
impl DiscreteOperator for CenteredLaplacian {
type MeshType = UniformGrid1D;
fn apply(
&self,
field: &ContextValue,
mesh: &Self::MeshType,
) -> Result<ContextValue, OxiflowError> {
let u = field.as_scalar_field()?;
let dx = mesh.characteristic_length();
let lap = Self::compute_from_dx(u, dx)?;
Ok(ContextValue::ScalarField(lap))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mesh(n: usize) -> UniformGrid1D {
UniformGrid1D::new(n, 0.0, 1.0).unwrap()
}
fn max_error(computed: &DVector<f64>, analytical: &DVector<f64>, indices: &[usize]) -> f64 {
indices
.iter()
.map(|&i| (computed[i] - analytical[i]).abs())
.fold(0.0, f64::max)
}
fn interior(n: usize) -> Vec<usize> {
(1..n - 1).collect()
}
fn all_nodes(n: usize) -> Vec<usize> {
(0..n).collect()
}
#[test]
fn upwind_forward_on_linear_field_is_exact() {
let m = mesh(5);
let dx = m.characteristic_length();
let u = DVector::from_vec((0..5).map(|i| i as f64 * dx).collect());
let grad = UpwindGradient::compute_from_dx(&u, dx, Direction::Forward).unwrap();
assert!(grad.iter().all(|&g| (g - 1.0).abs() < 1e-10));
}
#[test]
fn upwind_backward_fallback_at_left_boundary() {
let m = mesh(5);
let dx = m.characteristic_length();
let u = DVector::from_vec((0..5).map(|i| i as f64 * dx).collect());
let grad = UpwindGradient::compute_from_dx(&u, dx, Direction::Backward).unwrap();
assert!((grad[0] - 1.0).abs() < 1e-10);
}
#[test]
fn upwind_forward_is_first_order() {
let errors: Vec<f64> = [21usize, 41]
.iter()
.map(|&n| {
let m = mesh(n);
let dx = m.characteristic_length();
let u = DVector::from_vec((0..n).map(|i| (i as f64 * dx).powi(2)).collect());
let analytical = DVector::from_vec((0..n).map(|i| 2.0 * i as f64 * dx).collect());
let grad = UpwindGradient::compute_from_dx(&u, dx, Direction::Forward).unwrap();
max_error(&grad, &analytical, &all_nodes(n))
})
.collect();
let ratio = errors[0] / errors[1];
assert!(
(1.5..=2.5).contains(&ratio),
"expected ratio ≈ 2, got {ratio}"
);
}
#[test]
fn centered_gradient_of_linear_field_is_exact() {
let m = mesh(5);
let dx = m.characteristic_length();
let u = DVector::from_vec((0..5).map(|i| i as f64 * dx).collect());
let grad = CenteredGradient::compute_from_dx(&u, dx).unwrap();
assert!(grad.iter().all(|&g| (g - 1.0).abs() < 1e-10));
}
#[test]
fn centered_gradient_is_second_order_at_interior_nodes() {
let errors: Vec<f64> = [21usize, 41]
.iter()
.map(|&n| {
let m = mesh(n);
let dx = m.characteristic_length();
let u = DVector::from_vec((0..n).map(|i| (i as f64 * dx).sin()).collect());
let analytical = DVector::from_vec((0..n).map(|i| (i as f64 * dx).cos()).collect());
let grad = CenteredGradient::compute_from_dx(&u, dx).unwrap();
max_error(&grad, &analytical, &interior(n))
})
.collect();
let ratio = errors[0] / errors[1];
assert!(
(3.0..=5.0).contains(&ratio),
"expected ratio ≈ 4, got {ratio}"
);
}
#[test]
fn centered_laplacian_of_quadratic_field_is_exact_at_interior() {
let m = mesh(7);
let dx = m.characteristic_length();
let n = 7;
let u = DVector::from_vec((0..n).map(|i| (i as f64 * dx).powi(2)).collect());
let lap = CenteredLaplacian::compute_from_dx(&u, dx).unwrap();
for &i in &interior(n) {
assert!((lap[i] - 2.0).abs() < 1e-8, "node {i}: got {}", lap[i]);
}
}
#[test]
fn centered_laplacian_is_second_order_at_interior_nodes() {
let errors: Vec<f64> = [21usize, 41]
.iter()
.map(|&n| {
let m = mesh(n);
let dx = m.characteristic_length();
let u = DVector::from_vec((0..n).map(|i| (i as f64 * dx).sin()).collect());
let analytical =
DVector::from_vec((0..n).map(|i| -(i as f64 * dx).sin()).collect());
let lap = CenteredLaplacian::compute_from_dx(&u, dx).unwrap();
max_error(&lap, &analytical, &interior(n))
})
.collect();
let ratio = errors[0] / errors[1];
assert!(
(3.0..=5.0).contains(&ratio),
"expected ratio ≈ 4, got {ratio}"
);
}
#[test]
fn upwind_gradient_rejects_single_node_field() {
let err =
UpwindGradient::compute_from_dx(&DVector::from_vec(vec![1.0]), 0.1, Direction::Forward)
.unwrap_err();
assert!(matches!(err, OxiflowError::InvalidDomain(_)));
}
#[test]
fn centered_gradient_rejects_single_node_field() {
let err =
CenteredGradient::compute_from_dx(&DVector::from_vec(vec![1.0]), 0.1).unwrap_err();
assert!(matches!(err, OxiflowError::InvalidDomain(_)));
}
#[test]
fn centered_laplacian_rejects_two_node_field() {
let err = CenteredLaplacian::compute_from_dx(&DVector::from_vec(vec![0.0, 1.0]), 0.1)
.unwrap_err();
assert!(matches!(err, OxiflowError::InvalidDomain(_)));
}
#[test]
fn upwind_gradient_apply_via_discrete_operator() {
let m = mesh(5);
let dx = m.characteristic_length();
let u =
ContextValue::ScalarField(DVector::from_vec((0..5).map(|i| i as f64 * dx).collect()));
let op = UpwindGradient::new(Direction::Forward);
let result = op.apply(&u, &m).unwrap();
assert!(result
.as_scalar_field()
.unwrap()
.iter()
.all(|&g| (g - 1.0).abs() < 1e-10));
}
}