use std::sync::Arc;
use nalgebra::DVector;
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;
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 n = u.len();
if n < 2 {
return Err(OxiflowError::PreconditionFailed {
context: "FDGradientCalculator",
message: format!("field must have at least 2 nodes, got {n}"),
});
}
let dx = self.mesh.characteristic_length();
let mut grad = DVector::zeros(n);
for i in 0..n {
grad[i] = match self.scheme {
FDScheme::Forward => {
if i < n - 1 {
(u[i + 1] - u[i]) / dx
} else {
(u[n - 1] - u[n - 2]) / dx
}
}
FDScheme::Backward => {
if i > 0 {
(u[i] - u[i - 1]) / dx
} else {
(u[1] - u[0]) / dx
}
}
FDScheme::Central => {
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)
}
}
#[allow(unreachable_patterns)]
_ => {
return Err(OxiflowError::PreconditionFailed {
context: "FDGradientCalculator",
message: "unsupported FDScheme variant".to_string(),
})
}
};
}
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 n = u.len();
if n < 3 {
return Err(OxiflowError::PreconditionFailed {
context: "FDLaplacianCalculator",
message: format!("field must have at least 3 nodes, got {n}"),
});
}
let dx = self.mesh.characteristic_length();
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(ContextValue::ScalarField(lap))
}
fn name(&self) -> &str {
"fd_laplacian (built-in)"
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
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 { .. })));
}
}