use nalgebra::{DMatrix, DVector};
use super::evaluate_derivative;
use crate::context::error::OxiflowError;
use crate::context::value::ContextValue;
use crate::context::ContextCalculator;
use crate::solver::linear::LinearSolver;
use crate::solver::scenario::Domain;
const FD_EPSILON: f64 = 1e-7;
pub(crate) fn finite_difference_jacobian(
domain: &Domain,
chain: &[&dyn ContextCalculator],
state: &ContextValue,
t: f64,
dt: f64,
) -> Result<DMatrix<f64>, OxiflowError> {
let base_field = state.as_scalar_field()?.clone();
let n = base_field.len();
let mut base_state = state.clone();
let f0 = evaluate_derivative(domain, chain, &mut base_state, t, dt)?;
let f0_field = f0.as_scalar_field()?.clone();
let mut jacobian = DMatrix::<f64>::zeros(n, n);
for j in 0..n {
let mut perturbed_field = base_field.clone();
perturbed_field[j] += FD_EPSILON;
let mut perturbed_state = ContextValue::ScalarField(perturbed_field);
let f_j = evaluate_derivative(domain, chain, &mut perturbed_state, t, dt)?;
let f_j_field = f_j.as_scalar_field()?;
for i in 0..n {
jacobian[(i, j)] = (f_j_field[i] - f0_field[i]) / FD_EPSILON;
}
}
Ok(jacobian)
}
pub(crate) fn theta_method_step(
domain: &Domain,
chain: &[&dyn ContextCalculator],
state: &mut ContextValue,
t: f64,
dt: f64,
theta: f64,
linear_solver: &dyn LinearSolver,
) -> Result<ContextValue, OxiflowError> {
let f_n = evaluate_derivative(domain, chain, state, t, dt)?;
let u_n_field = state.as_scalar_field()?.clone();
let f_n_field = f_n.as_scalar_field()?.clone();
let n = u_n_field.len();
let jacobian = finite_difference_jacobian(domain, chain, state, t + dt, dt)?;
let identity = DMatrix::<f64>::identity(n, n);
let system_matrix = identity - jacobian * (theta * dt);
let rhs = f_n_field * dt;
let delta_u = linear_solver.solve(&system_matrix, &rhs)?;
let u_next: DVector<f64> = u_n_field + delta_u;
Ok(ContextValue::ScalarField(u_next))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::compute::ComputeContext;
use crate::context::variable::ContextVariable;
use crate::mesh::{Mesh, UniformGrid1D};
use crate::model::traits::{PhysicalModel, RequiresContext};
use crate::solver::chain::build_calculator_chain;
use crate::solver::linear::NalgebraDenseSolver;
use crate::solver::scenario::Scenario;
#[derive(Debug)]
struct ExponentialDecay {
lambda: f64,
}
impl RequiresContext for ExponentialDecay {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
}
impl PhysicalModel for ExponentialDecay {
fn compute_physics(
&self,
state: &ContextValue,
_ctx: &ComputeContext,
) -> Result<ContextValue, OxiflowError> {
let u = state.as_scalar_field()?;
Ok(ContextValue::ScalarField(u.map(|v| -self.lambda * v)))
}
fn initial_state(&self, mesh: &dyn Mesh) -> ContextValue {
ContextValue::ScalarField(DVector::from_element(mesh.n_dof(), 1.0))
}
fn name(&self) -> &str {
"exponential_decay"
}
}
fn make_mesh(n: usize) -> Box<dyn Mesh> {
Box::new(UniformGrid1D::new(n, 0.0, 1.0).unwrap())
}
#[test]
fn jacobian_of_linear_decay_is_minus_lambda_identity() {
let lambda = 2.5;
let scenario = Scenario::single(Box::new(ExponentialDecay { lambda }), make_mesh(3));
let domain = scenario.single_domain().unwrap();
let requirements = scenario.context_requirements();
let chain = build_calculator_chain(&requirements, &[]).unwrap();
let state = domain.model.initial_state(domain.mesh.as_ref());
let jac = finite_difference_jacobian(domain, &chain, &state, 0.0, 0.1).unwrap();
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { -lambda } else { 0.0 };
assert!(
(jac[(i, j)] - expected).abs() < 1e-4,
"jac[{i},{j}] = {} (expected {expected})",
jac[(i, j)]
);
}
}
}
#[test]
fn backward_euler_theta_one_matches_analytical_for_linear_decay() {
let lambda = 3.0;
let dt = 0.1;
let scenario = Scenario::single(Box::new(ExponentialDecay { lambda }), make_mesh(2));
let domain = scenario.single_domain().unwrap();
let requirements = scenario.context_requirements();
let chain = build_calculator_chain(&requirements, &[]).unwrap();
let mut state = domain.model.initial_state(domain.mesh.as_ref());
let next = theta_method_step(
domain,
&chain,
&mut state,
0.0,
dt,
1.0,
&NalgebraDenseSolver,
)
.unwrap();
let expected = 1.0 / (1.0 + lambda * dt);
let field = next.as_scalar_field().unwrap();
for v in field.iter() {
assert!((v - expected).abs() < 1e-9, "got {v}, expected {expected}");
}
}
#[test]
fn crank_nicolson_theta_half_matches_analytical_for_linear_decay() {
let lambda = 3.0;
let dt = 0.1;
let scenario = Scenario::single(Box::new(ExponentialDecay { lambda }), make_mesh(2));
let domain = scenario.single_domain().unwrap();
let requirements = scenario.context_requirements();
let chain = build_calculator_chain(&requirements, &[]).unwrap();
let mut state = domain.model.initial_state(domain.mesh.as_ref());
let next = theta_method_step(
domain,
&chain,
&mut state,
0.0,
dt,
0.5,
&NalgebraDenseSolver,
)
.unwrap();
let expected = (1.0 - lambda * dt / 2.0) / (1.0 + lambda * dt / 2.0);
let field = next.as_scalar_field().unwrap();
for v in field.iter() {
assert!((v - expected).abs() < 1e-9, "got {v}, expected {expected}");
}
}
#[test]
fn backward_euler_stable_for_very_stiff_problem() {
let lambda = 1.0e4;
let dt = 0.1; let scenario = Scenario::single(Box::new(ExponentialDecay { lambda }), make_mesh(2));
let domain = scenario.single_domain().unwrap();
let requirements = scenario.context_requirements();
let chain = build_calculator_chain(&requirements, &[]).unwrap();
let mut state = domain.model.initial_state(domain.mesh.as_ref());
let next = theta_method_step(
domain,
&chain,
&mut state,
0.0,
dt,
1.0,
&NalgebraDenseSolver,
)
.unwrap();
let field = next.as_scalar_field().unwrap();
for v in field.iter() {
assert!(v.is_finite(), "value diverged: {v}");
assert!(v.abs() < 1.0, "expected strong damping, got {v}");
}
}
}