use numra_core::Scalar;
pub trait OdeSystem<S: Scalar> {
fn dim(&self) -> usize;
fn rhs(&self, t: S, y: &[S], dydt: &mut [S]);
fn jacobian(&self, t: S, y: &[S], jac: &mut [S]) {
let n = self.dim();
let h_factor = S::EPSILON.sqrt();
let mut y_pert = y.to_vec();
let mut f0 = vec![S::ZERO; n];
let mut f1 = vec![S::ZERO; n];
self.rhs(t, y, &mut f0);
for j in 0..n {
let yj_save = y_pert[j];
let h = h_factor * (S::ONE + yj_save.abs());
y_pert[j] = yj_save + h;
self.rhs(t, &y_pert, &mut f1);
y_pert[j] = yj_save;
for i in 0..n {
jac[i * n + j] = (f1[i] - f0[i]) / h;
}
}
}
fn is_autonomous(&self) -> bool {
false
}
fn has_mass_matrix(&self) -> bool {
false
}
fn mass_matrix(&self, mass: &mut [S]) {
let n = self.dim();
for i in 0..n {
for j in 0..n {
mass[i * n + j] = if i == j { S::ONE } else { S::ZERO };
}
}
}
fn is_singular_mass(&self) -> bool {
false
}
fn algebraic_indices(&self) -> Vec<usize> {
Vec::new()
}
}
pub struct OdeProblem<S: Scalar, F>
where
F: Fn(S, &[S], &mut [S]),
{
pub f: F,
pub t0: S,
pub tf: S,
pub y0: Vec<S>,
}
impl<S: Scalar, F> OdeProblem<S, F>
where
F: Fn(S, &[S], &mut [S]),
{
pub fn new(f: F, t0: S, tf: S, y0: Vec<S>) -> Self {
Self { f, t0, tf, y0 }
}
pub fn dim(&self) -> usize {
self.y0.len()
}
pub fn tspan(&self) -> (S, S) {
(self.t0, self.tf)
}
pub fn direction(&self) -> S {
if self.tf >= self.t0 {
S::ONE
} else {
-S::ONE
}
}
}
impl<S: Scalar, F> OdeSystem<S> for OdeProblem<S, F>
where
F: Fn(S, &[S], &mut [S]),
{
fn dim(&self) -> usize {
self.y0.len()
}
fn rhs(&self, t: S, y: &[S], dydt: &mut [S]) {
(self.f)(t, y, dydt);
}
}
pub struct DaeProblem<S: Scalar, F, M>
where
F: Fn(S, &[S], &mut [S]),
M: Fn(&mut [S]),
{
pub f: F,
pub mass: M,
pub t0: S,
pub tf: S,
pub y0: Vec<S>,
pub alg_indices: Vec<usize>,
}
impl<S: Scalar, F, M> DaeProblem<S, F, M>
where
F: Fn(S, &[S], &mut [S]),
M: Fn(&mut [S]),
{
pub fn new(f: F, mass: M, t0: S, tf: S, y0: Vec<S>, alg_indices: Vec<usize>) -> Self {
Self {
f,
mass,
t0,
tf,
y0,
alg_indices,
}
}
pub fn dim(&self) -> usize {
self.y0.len()
}
}
impl<S: Scalar, F, M> OdeSystem<S> for DaeProblem<S, F, M>
where
F: Fn(S, &[S], &mut [S]),
M: Fn(&mut [S]),
{
fn dim(&self) -> usize {
self.y0.len()
}
fn rhs(&self, t: S, y: &[S], dydt: &mut [S]) {
(self.f)(t, y, dydt);
}
fn has_mass_matrix(&self) -> bool {
true
}
fn mass_matrix(&self, mass: &mut [S]) {
(self.mass)(mass);
}
fn is_singular_mass(&self) -> bool {
!self.alg_indices.is_empty()
}
fn algebraic_indices(&self) -> Vec<usize> {
self.alg_indices.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ode_problem_creation() {
let f = |_t: f64, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
};
let problem = OdeProblem::new(f, 0.0, 1.0, vec![1.0]);
assert_eq!(problem.dim(), 1);
assert_eq!(problem.tspan(), (0.0, 1.0));
}
#[test]
fn test_rhs_evaluation() {
let f = |_t: f64, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -2.0 * y[0];
};
let problem = OdeProblem::new(f, 0.0, 1.0, vec![1.0]);
let mut dydt = vec![0.0];
problem.rhs(0.0, &[1.0], &mut dydt);
assert!((dydt[0] + 2.0).abs() < 1e-10);
}
#[test]
fn test_jacobian_finite_diff() {
let f = |_t: f64, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -2.0 * y[0];
};
let problem = OdeProblem::new(f, 0.0, 1.0, vec![1.0]);
let mut jac = vec![0.0];
problem.jacobian(0.0, &[1.0], &mut jac);
assert!((jac[0] + 2.0).abs() < 1e-5);
}
#[test]
fn test_jacobian_finite_diff_f32() {
let f = |_t: f32, y: &[f32], dydt: &mut [f32]| {
dydt[0] = -2.0 * y[0];
};
let problem = OdeProblem::new(f, 0.0_f32, 1.0_f32, vec![1.0_f32]);
let mut jac = vec![0.0_f32];
problem.jacobian(0.0, &[1.0], &mut jac);
assert!(jac[0] != 0.0, "FD step quantised to zero on f32");
assert!(
(jac[0] + 2.0).abs() < 1e-3,
"FD Jacobian on f32 too inaccurate: jac[0]={}",
jac[0]
);
}
}