use std::sync::Arc;
use crate::context::calculator::ContextCalculator;
use crate::context::calculators::FDScheme;
use crate::context::compute::ComputeContext;
use crate::context::error::OxiflowError;
use crate::context::value::ContextValue;
use crate::context::variable::ContextVariable;
use crate::mesh::Mesh;
use crate::model::traits::RequiresContext;
use crate::operators::fd::{CenteredGradient, CenteredLaplacian, Direction, UpwindGradient};
fn translate_domain_error(err: OxiflowError, context: &'static str) -> OxiflowError {
match err {
OxiflowError::InvalidDomain(message) => {
OxiflowError::PreconditionFailed { context, message }
}
other => other,
}
}
pub struct FDGradientCalculator {
mesh: Arc<dyn Mesh>,
dimension: usize,
component: Option<usize>,
scheme: FDScheme,
}
impl FDGradientCalculator {
pub fn new(
mesh: Arc<dyn Mesh>,
dimension: usize,
component: Option<usize>,
scheme: FDScheme,
) -> Self {
Self {
mesh,
dimension,
component,
scheme,
}
}
}
impl std::fmt::Debug for FDGradientCalculator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FDGradientCalculator")
.field("dimension", &self.dimension)
.field("component", &self.component)
.field("scheme", &self.scheme)
.field("mesh_n_dof", &self.mesh.n_dof())
.finish()
}
}
impl RequiresContext for FDGradientCalculator {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
fn priority(&self) -> u32 {
10
}
}
impl ContextCalculator for FDGradientCalculator {
fn provides(&self) -> ContextVariable {
ContextVariable::SpatialGradient {
dimension: self.dimension,
component: self.component,
}
}
fn compute(
&self,
state: &ContextValue,
_ctx: &ComputeContext,
) -> Result<ContextValue, OxiflowError> {
let u = state.as_scalar_field()?;
let dx = self.mesh.characteristic_length();
let grad = match self.scheme {
FDScheme::Forward => UpwindGradient::compute_from_dx(u, dx, Direction::Forward),
FDScheme::Backward => UpwindGradient::compute_from_dx(u, dx, Direction::Backward),
FDScheme::Central => CenteredGradient::compute_from_dx(u, dx),
#[allow(unreachable_patterns)]
_ => {
return Err(OxiflowError::PreconditionFailed {
context: "FDGradientCalculator",
message: "unsupported FDScheme variant".to_string(),
})
}
}
.map_err(|e| translate_domain_error(e, "FDGradientCalculator"))?;
Ok(ContextValue::ScalarField(grad))
}
fn name(&self) -> &str {
"fd_gradient (built-in)"
}
}
pub struct FDLaplacianCalculator {
mesh: Arc<dyn Mesh>,
variable: ContextVariable,
}
impl FDLaplacianCalculator {
pub fn new(mesh: Arc<dyn Mesh>, variable: ContextVariable) -> Self {
Self { mesh, variable }
}
}
impl std::fmt::Debug for FDLaplacianCalculator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FDLaplacianCalculator")
.field("variable", &self.variable)
.field("mesh_n_dof", &self.mesh.n_dof())
.finish()
}
}
impl RequiresContext for FDLaplacianCalculator {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
fn priority(&self) -> u32 {
10
}
}
impl ContextCalculator for FDLaplacianCalculator {
fn provides(&self) -> ContextVariable {
self.variable.clone()
}
fn compute(
&self,
state: &ContextValue,
_ctx: &ComputeContext,
) -> Result<ContextValue, OxiflowError> {
let u = state.as_scalar_field()?;
let dx = self.mesh.characteristic_length();
let lap = CenteredLaplacian::compute_from_dx(u, dx)
.map_err(|e| translate_domain_error(e, "FDLaplacianCalculator"))?;
Ok(ContextValue::ScalarField(lap))
}
fn name(&self) -> &str {
"fd_laplacian (built-in)"
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use nalgebra::DVector;
use super::*;
use crate::mesh::UniformGrid1D;
fn grid(n: usize) -> Arc<dyn Mesh> {
Arc::new(UniformGrid1D::new(n, 0.0, 1.0).unwrap())
}
fn ctx() -> ComputeContext {
ComputeContext::new(0.0, 0.01)
}
fn laplacian_var() -> ContextVariable {
ContextVariable::External {
name: Cow::Borrowed("laplacian"),
}
}
#[test]
fn gradient_provides_spatial_gradient_variable() {
let calc = FDGradientCalculator::new(grid(5), 0, None, FDScheme::Central);
assert_eq!(
calc.provides(),
ContextVariable::SpatialGradient {
dimension: 0,
component: None
}
);
}
#[test]
fn gradient_priority_is_ten() {
let calc = FDGradientCalculator::new(grid(5), 0, None, FDScheme::Forward);
assert_eq!(calc.priority(), 10);
}
#[test]
fn gradient_has_no_required_variables() {
let calc = FDGradientCalculator::new(grid(5), 0, None, FDScheme::Forward);
assert!(calc.required_variables().is_empty());
}
#[test]
fn central_gradient_of_linear_field_is_one() {
let n = 5;
let mesh = grid(n);
let dx = mesh.characteristic_length();
let u: Vec<f64> = (0..n).map(|i| i as f64 * dx).collect();
let calc = FDGradientCalculator::new(mesh, 0, None, FDScheme::Central);
let result = calc
.compute(&ContextValue::ScalarField(DVector::from_vec(u)), &ctx())
.unwrap();
let grad = result.as_scalar_field().unwrap();
for g in grad.iter() {
assert!((g - 1.0).abs() < 1e-10, "expected 1.0, got {g}");
}
}
#[test]
fn forward_gradient_interior_nodes_correct() {
let n = 5;
let mesh = grid(n);
let dx = mesh.characteristic_length();
let u: Vec<f64> = (0..n).map(|i| (i as f64 * dx).powi(2)).collect();
let calc = FDGradientCalculator::new(mesh, 0, None, FDScheme::Forward);
let result = calc
.compute(&ContextValue::ScalarField(DVector::from_vec(u)), &ctx())
.unwrap();
let grad = result.as_scalar_field().unwrap();
assert!(grad.iter().all(|g| g.is_finite()));
assert!((grad[n - 1] - grad[n - 2]).abs() < 1.0);
}
#[test]
fn backward_gradient_fallback_at_left_boundary() {
let n = 5;
let mesh = grid(n);
let dx = mesh.characteristic_length();
let u: Vec<f64> = (0..n).map(|i| i as f64 * dx).collect();
let calc = FDGradientCalculator::new(mesh, 0, None, FDScheme::Backward);
let result = calc
.compute(&ContextValue::ScalarField(DVector::from_vec(u)), &ctx())
.unwrap();
let grad = result.as_scalar_field().unwrap();
assert!((grad[0] - 1.0).abs() < 1e-10);
}
#[test]
fn gradient_error_on_single_node_field() {
let mesh = Arc::new(UniformGrid1D::new(2, 0.0, 1.0).unwrap());
let calc = FDGradientCalculator::new(mesh, 0, None, FDScheme::Central);
let result = calc.compute(
&ContextValue::ScalarField(DVector::from_vec(vec![1.0])),
&ctx(),
);
assert!(matches!(
result,
Err(OxiflowError::PreconditionFailed { .. })
));
}
#[test]
fn gradient_error_on_scalar_state() {
let calc = FDGradientCalculator::new(grid(5), 0, None, FDScheme::Central);
let result = calc.compute(&ContextValue::Scalar(1.0), &ctx());
assert!(matches!(result, Err(OxiflowError::TypeMismatch { .. })));
}
#[test]
fn laplacian_provides_configured_variable() {
let var = laplacian_var();
let calc = FDLaplacianCalculator::new(grid(5), var.clone());
assert_eq!(calc.provides(), var);
}
#[test]
fn laplacian_priority_is_ten() {
let calc = FDLaplacianCalculator::new(grid(5), laplacian_var());
assert_eq!(calc.priority(), 10);
}
#[test]
fn laplacian_of_quadratic_field_is_two_at_interior() {
let n = 7;
let mesh = grid(n);
let dx = mesh.characteristic_length();
let u: Vec<f64> = (0..n).map(|i| (i as f64 * dx).powi(2)).collect();
let calc = FDLaplacianCalculator::new(mesh, laplacian_var());
let result = calc
.compute(&ContextValue::ScalarField(DVector::from_vec(u)), &ctx())
.unwrap();
let lap = result.as_scalar_field().unwrap();
for i in 1..n - 1 {
assert!(
(lap[i] - 2.0).abs() < 1e-8,
"node {i}: expected 2.0, got {}",
lap[i]
);
}
}
#[test]
fn laplacian_of_linear_field_is_zero_at_interior() {
let n = 7;
let mesh = grid(n);
let dx = mesh.characteristic_length();
let u: Vec<f64> = (0..n).map(|i| i as f64 * dx).collect();
let calc = FDLaplacianCalculator::new(mesh, laplacian_var());
let result = calc
.compute(&ContextValue::ScalarField(DVector::from_vec(u)), &ctx())
.unwrap();
let lap = result.as_scalar_field().unwrap();
for i in 1..n - 1 {
assert!(
lap[i].abs() < 1e-10,
"node {i}: expected 0.0, got {}",
lap[i]
);
}
}
#[test]
fn laplacian_error_on_two_node_field() {
let mesh = Arc::new(UniformGrid1D::new(2, 0.0, 1.0).unwrap());
let calc = FDLaplacianCalculator::new(mesh, laplacian_var());
let result = calc.compute(
&ContextValue::ScalarField(DVector::from_vec(vec![0.0, 1.0])),
&ctx(),
);
assert!(matches!(
result,
Err(OxiflowError::PreconditionFailed { .. })
));
}
#[test]
fn laplacian_error_on_scalar_state() {
let calc = FDLaplacianCalculator::new(grid(5), laplacian_var());
let result = calc.compute(&ContextValue::Scalar(1.0), &ctx());
assert!(matches!(result, Err(OxiflowError::TypeMismatch { .. })));
}
}