use faer::{ComplexField, Conjugate, SimpleEntity};
use numra_core::Scalar;
use numra_linalg::{DenseMatrix, LUFactorization, Matrix};
use crate::error::SolverError;
use crate::problem::OdeSystem;
use crate::solver::{Solver, SolverOptions, SolverResult, SolverStats};
use crate::t_eval::{validate_grid, TEvalEmitter};
#[derive(Clone, Copy, Debug, Default)]
pub struct Bdf;
const MAX_ORDER: usize = 5;
const NEWTON_MAXITER: usize = 4;
const MIN_FACTOR: f64 = 0.2;
const MAX_FACTOR: f64 = 10.0;
const KAPPA: [f64; MAX_ORDER + 1] = [0.0, -0.1850, -1.0 / 9.0, -0.0823, -0.0415, 0.0];
const GAMMA: [f64; MAX_ORDER + 1] = [
0.0,
1.0,
1.0 + 1.0 / 2.0,
1.0 + 1.0 / 2.0 + 1.0 / 3.0,
1.0 + 1.0 / 2.0 + 1.0 / 3.0 + 1.0 / 4.0,
1.0 + 1.0 / 2.0 + 1.0 / 3.0 + 1.0 / 4.0 + 1.0 / 5.0,
];
const ALPHA: [f64; MAX_ORDER + 1] = [
(1.0 - KAPPA[0]) * GAMMA[0],
(1.0 - KAPPA[1]) * GAMMA[1],
(1.0 - KAPPA[2]) * GAMMA[2],
(1.0 - KAPPA[3]) * GAMMA[3],
(1.0 - KAPPA[4]) * GAMMA[4],
(1.0 - KAPPA[5]) * GAMMA[5],
];
const ERROR_CONST: [f64; MAX_ORDER + 1] = [
KAPPA[0] * GAMMA[0] + 1.0,
KAPPA[1] * GAMMA[1] + 1.0 / 2.0,
KAPPA[2] * GAMMA[2] + 1.0 / 3.0,
KAPPA[3] * GAMMA[3] + 1.0 / 4.0,
KAPPA[4] * GAMMA[4] + 1.0 / 5.0,
KAPPA[5] * GAMMA[5] + 1.0 / 6.0,
];
const RU_SIZE: usize = (MAX_ORDER + 1) * (MAX_ORDER + 1);
impl<S: Scalar + SimpleEntity + Conjugate<Canonical = S> + ComplexField> Solver<S> for Bdf {
fn solve<Sys: OdeSystem<S>>(
problem: &Sys,
t0: S,
tf: S,
y0: &[S],
options: &SolverOptions<S>,
) -> Result<SolverResult<S>, SolverError> {
solve_internal(problem, t0, tf, y0, options)
}
}
fn resolve_order_bounds<S: Scalar>(options: &SolverOptions<S>) -> (usize, usize) {
let max = options.max_order.unwrap_or(MAX_ORDER).clamp(1, MAX_ORDER);
let min = options.min_order.unwrap_or(1).clamp(1, max);
(min, max)
}
fn compute_r<S: Scalar>(order: usize, factor: S, out: &mut [S; RU_SIZE]) {
for slot in out.iter_mut() {
*slot = S::ZERO;
}
let stride = MAX_ORDER + 1;
for c in 0..=order {
out[c] = S::ONE;
}
for r in 1..=order {
for c in 1..=order {
let num = S::from_usize(r - 1) - factor * S::from_usize(c);
out[r * stride + c] = num / S::from_usize(r);
}
}
for r in 1..=order {
for c in 0..=order {
let prev = out[(r - 1) * stride + c];
out[r * stride + c] = out[r * stride + c] * prev;
}
}
}
fn change_d<S: Scalar>(d: &mut [Vec<S>], order: usize, factor: S) {
if (factor - S::ONE).abs() < S::from_f64(1e-15) {
return;
}
let mut r_mat = [S::ZERO; RU_SIZE];
let mut u_mat = [S::ZERO; RU_SIZE];
compute_r(order, factor, &mut r_mat);
compute_r(order, S::ONE, &mut u_mat);
let stride = MAX_ORDER + 1;
let mut ru = [S::ZERO; RU_SIZE];
for i in 0..=order {
for j in 0..=order {
let mut s = S::ZERO;
for k in 0..=order {
s = s + r_mat[i * stride + k] * u_mat[k * stride + j];
}
ru[i * stride + j] = s;
}
}
let dim = d[0].len();
let mut new_rows: Vec<Vec<S>> = (0..=order).map(|_| vec![S::ZERO; dim]).collect();
for i in 0..=order {
for k in 0..=order {
let coef = ru[k * stride + i];
if coef == S::ZERO {
continue;
}
for col in 0..dim {
new_rows[i][col] = new_rows[i][col] + coef * d[k][col];
}
}
}
for (i, row) in new_rows.into_iter().enumerate() {
d[i] = row;
}
}
struct NewtonOutcome<S: Scalar> {
converged: bool,
n_iter: usize,
y_new: Vec<S>,
d: Vec<S>,
}
struct AcceptedStep<S: Scalar> {
y_new: Vec<S>,
d_corr: Vec<S>,
err_norm: S,
h_abs_used: S,
safety: S,
}
#[allow(clippy::too_many_arguments)]
fn solve_bdf_system<S, Sys>(
problem: &Sys,
t_new: S,
y_predict: &[S],
c: S,
psi: &[S],
lu: &LUFactorization<S>,
scale: &[S],
tol: S,
mass: Option<&[S]>,
stats: &mut SolverStats,
dim: usize,
) -> Result<NewtonOutcome<S>, SolverError>
where
S: Scalar + SimpleEntity + Conjugate<Canonical = S> + ComplexField,
Sys: OdeSystem<S>,
{
let mut d = vec![S::ZERO; dim];
let mut y = y_predict.to_vec();
let mut f_buf = vec![S::ZERO; dim];
let mut rhs_buf = vec![S::ZERO; dim];
let mut m_psi = vec![S::ZERO; dim];
if let Some(m) = mass {
for i in 0..dim {
let mut s = S::ZERO;
for j in 0..dim {
s = s + m[i * dim + j] * psi[j];
}
m_psi[i] = s;
}
} else {
m_psi.copy_from_slice(psi);
}
let mut dy_norm_old: Option<S> = None;
let mut converged = false;
let mut n_iter_done = 0usize;
for iter in 0..NEWTON_MAXITER {
n_iter_done = iter + 1;
problem.rhs(t_new, &y, &mut f_buf);
stats.n_eval += 1;
if f_buf.iter().any(|v| !v.to_f64().is_finite()) {
break;
}
if let Some(m) = mass {
for i in 0..dim {
let mut md = S::ZERO;
for j in 0..dim {
md = md + m[i * dim + j] * d[j];
}
rhs_buf[i] = c * f_buf[i] - m_psi[i] - md;
}
} else {
for i in 0..dim {
rhs_buf[i] = c * f_buf[i] - psi[i] - d[i];
}
}
let dy = lu.solve(&rhs_buf)?;
let mut dy_norm_sq = S::ZERO;
for i in 0..dim {
let r = dy[i] / scale[i];
dy_norm_sq = dy_norm_sq + r * r;
}
let dy_norm = (dy_norm_sq / S::from_usize(dim)).sqrt();
let rate = dy_norm_old.and_then(|old| {
if old > S::ZERO {
Some(dy_norm / old)
} else {
None
}
});
if let Some(r) = rate {
if r >= S::ONE {
break;
}
let remaining = NEWTON_MAXITER - iter;
let r_pow = r.powf(S::from_usize(remaining));
if r_pow / (S::ONE - r) * dy_norm > tol {
break;
}
}
for i in 0..dim {
y[i] = y[i] + dy[i];
d[i] = d[i] + dy[i];
}
if dy_norm == S::ZERO {
converged = true;
break;
}
if let Some(r) = rate {
if r / (S::ONE - r) * dy_norm < tol {
converged = true;
break;
}
}
dy_norm_old = Some(dy_norm);
}
Ok(NewtonOutcome {
converged,
n_iter: n_iter_done,
y_new: y,
d,
})
}
fn solve_internal<S, Sys>(
problem: &Sys,
t0: S,
tf: S,
y0: &[S],
options: &SolverOptions<S>,
) -> Result<SolverResult<S>, SolverError>
where
S: Scalar + SimpleEntity + Conjugate<Canonical = S> + ComplexField,
Sys: OdeSystem<S>,
{
let (min_order, max_order) = resolve_order_bounds(options);
let dim = problem.dim();
if y0.len() != dim {
return Err(SolverError::DimensionMismatch {
expected: dim,
actual: y0.len(),
});
}
let mut stats = SolverStats::default();
let mut t = t0;
let direction = if tf > t0 { S::ONE } else { -S::ONE };
if let Some(grid) = options.t_eval.as_deref() {
validate_grid(grid, t0, tf)?;
}
let mut grid_emitter = options
.t_eval
.as_deref()
.map(|g| TEvalEmitter::new(g, direction));
let (mut t_out, mut y_out) = if grid_emitter.is_some() {
(Vec::new(), Vec::new())
} else {
(vec![t0], y0.to_vec())
};
let mut dy_old_buf = vec![S::ZERO; dim];
let mut dy_new_buf = vec![S::ZERO; dim];
if grid_emitter.is_some() {
problem.rhs(t0, y0, &mut dy_old_buf);
stats.n_eval += 1;
}
let mut f_eval = vec![S::ZERO; dim];
problem.rhs(t, y0, &mut f_eval);
stats.n_eval += 1;
let mut h_abs = initial_step_size(y0, &f_eval, options, dim);
let mut d_arr: Vec<Vec<S>> = (0..MAX_ORDER + 3).map(|_| vec![S::ZERO; dim]).collect();
d_arr[0].copy_from_slice(y0);
for i in 0..dim {
d_arr[1][i] = h_abs * f_eval[i] * direction;
}
let mass_data = if problem.has_mass_matrix() {
let mut m = vec![S::ZERO; dim * dim];
problem.mass_matrix(&mut m);
Some(m)
} else {
None
};
let mass_ref = mass_data.as_deref();
let mut jac = vec![S::ZERO; dim * dim];
problem.jacobian(t, y0, &mut jac);
stats.n_jac += 1;
let mut current_jac = true;
let mut lu: Option<LUFactorization<S>> = None;
let mut last_c: Option<S> = None;
let mut order = 1usize;
let mut n_equal_steps: usize = 0;
let uround = S::from_f64(2.220446049250313e-16);
let newton_tol =
(S::from_f64(10.0) * uround / options.rtol).max(S::from_f64(0.03).min(options.rtol.sqrt()));
let h_min = options.h_min;
let h_max = options.h_max.min((tf - t0).abs());
let mut step_count = 0usize;
while (tf - t) * direction > S::from_f64(1e-12) * (tf - t0).abs() {
if step_count >= options.max_steps {
return Err(SolverError::MaxIterationsExceeded { t: t.to_f64() });
}
let ulp_min = S::from_f64(10.0) * uround * (t.abs().max(tf.abs())).max(S::ONE);
let min_step = h_min.max(ulp_min);
if h_abs > h_max {
let factor = h_max / h_abs;
change_d(&mut d_arr, order, factor);
n_equal_steps = 0;
h_abs = h_max;
lu = None;
last_c = None;
} else if h_abs < min_step {
let factor = min_step / h_abs;
change_d(&mut d_arr, order, factor);
n_equal_steps = 0;
h_abs = min_step;
lu = None;
last_c = None;
}
let mut step_accepted = false;
let mut accepted: Option<AcceptedStep<S>> = None;
while !step_accepted {
if h_abs < min_step {
return Err(SolverError::StepSizeTooSmall {
t: t.to_f64(),
h: h_abs.to_f64(),
h_min: h_min.to_f64(),
});
}
let h = h_abs * direction;
let mut t_new = t + h;
let mut h_used = h;
let mut h_abs_used = h_abs;
if (t_new - tf) * direction > S::ZERO {
t_new = tf;
h_used = t_new - t;
h_abs_used = h_used.abs();
let factor = h_abs_used / h_abs;
change_d(&mut d_arr, order, factor);
n_equal_steps = 0;
h_abs = h_abs_used;
lu = None;
last_c = None;
}
let mut y_predict = vec![S::ZERO; dim];
for k in 0..=order {
for i in 0..dim {
y_predict[i] = y_predict[i] + d_arr[k][i];
}
}
let alpha_o = S::from_f64(ALPHA[order]);
let mut psi = vec![S::ZERO; dim];
for j in 1..=order {
let g = S::from_f64(GAMMA[j]);
for i in 0..dim {
psi[i] = psi[i] + g * d_arr[j][i];
}
}
for i in 0..dim {
psi[i] = psi[i] / alpha_o;
}
let c = h_used / alpha_o;
let mut scale = vec![S::ZERO; dim];
for i in 0..dim {
scale[i] =
(options.atol + options.rtol * y_predict[i].abs()).max(S::from_f64(1e-300));
}
let outcome;
loop {
if lu.is_none() || last_c != Some(c) {
let it = form_iteration_matrix(&jac, c, dim, mass_ref);
lu = Some(LUFactorization::new(&it)?);
stats.n_lu += 1;
last_c = Some(c);
}
let attempt = solve_bdf_system(
problem,
t_new,
&y_predict,
c,
&psi,
lu.as_ref().unwrap(),
&scale,
newton_tol,
mass_ref,
&mut stats,
dim,
)?;
if attempt.converged || current_jac {
outcome = attempt;
break;
}
problem.rhs(t_new, &y_predict, &mut f_eval);
stats.n_eval += 1;
problem.jacobian(t_new, &y_predict, &mut jac);
stats.n_jac += 1;
current_jac = true;
lu = None;
last_c = None;
}
if !outcome.converged {
let factor = S::from_f64(0.5);
change_d(&mut d_arr, order, factor);
n_equal_steps = 0;
h_abs = h_abs * factor;
stats.n_reject += 1;
lu = None;
last_c = None;
continue;
}
let safety = S::from_f64(0.9 * (2.0 * NEWTON_MAXITER as f64 + 1.0))
/ S::from_f64(2.0 * NEWTON_MAXITER as f64 + outcome.n_iter as f64);
let mut scale_new = vec![S::ZERO; dim];
for i in 0..dim {
scale_new[i] =
(options.atol + options.rtol * outcome.y_new[i].abs()).max(S::from_f64(1e-300));
}
let err_const = S::from_f64(ERROR_CONST[order]);
let mut err_sq = S::ZERO;
for i in 0..dim {
let e = err_const * outcome.d[i] / scale_new[i];
err_sq = err_sq + e * e;
}
let err_norm = (err_sq / S::from_usize(dim)).sqrt();
if err_norm > S::ONE {
let order_p1 = S::from_usize(order + 1);
let factor =
S::from_f64(MIN_FACTOR).max(safety * err_norm.powf(-S::ONE / order_p1));
change_d(&mut d_arr, order, factor);
n_equal_steps = 0;
h_abs = h_abs * factor;
stats.n_reject += 1;
last_c = None;
continue;
}
accepted = Some(AcceptedStep {
y_new: outcome.y_new,
d_corr: outcome.d,
err_norm,
h_abs_used,
safety,
});
step_accepted = true;
}
let AcceptedStep {
y_new,
d_corr,
err_norm,
h_abs_used,
safety,
} = accepted.unwrap();
stats.n_accept += 1;
n_equal_steps += 1;
let t_old = t;
let y_old_snapshot = if grid_emitter.is_some() {
Some(d_arr[0].clone())
} else {
None
};
t = t + h_abs_used * direction;
h_abs = h_abs_used;
for i in 0..dim {
d_arr[order + 2][i] = d_corr[i] - d_arr[order + 1][i];
d_arr[order + 1][i] = d_corr[i];
}
for k in (0..=order).rev() {
for i in 0..dim {
d_arr[k][i] = d_arr[k][i] + d_arr[k + 1][i];
}
}
debug_assert!({
let mut ok = true;
for i in 0..dim {
let diff = (d_arr[0][i] - y_new[i]).abs();
let scl = options.atol + options.rtol * y_new[i].abs().max(S::ONE);
if diff.to_f64() > scl.to_f64() * 1e3 {
ok = false;
break;
}
}
ok
});
if let Some(ref mut emitter) = grid_emitter {
problem.rhs(t, &d_arr[0], &mut dy_new_buf);
stats.n_eval += 1;
let y_old = y_old_snapshot.as_deref().unwrap();
emitter.emit_step(
t_old,
y_old,
&dy_old_buf,
t,
&d_arr[0],
&dy_new_buf,
&mut t_out,
&mut y_out,
);
dy_old_buf.copy_from_slice(&dy_new_buf);
} else {
t_out.push(t);
y_out.extend_from_slice(&d_arr[0]);
}
current_jac = false;
if n_equal_steps < order + 1 {
step_count += 1;
continue;
}
let mut err_m_norm = S::from_f64(f64::INFINITY);
if order > 1 {
let ec = S::from_f64(ERROR_CONST[order - 1]);
let mut s = S::ZERO;
for i in 0..dim {
let scl = (options.atol + options.rtol * y_new[i].abs()).max(S::from_f64(1e-300));
let e = ec * d_arr[order][i] / scl;
s = s + e * e;
}
err_m_norm = (s / S::from_usize(dim)).sqrt();
}
let mut err_p_norm = S::from_f64(f64::INFINITY);
if order < max_order {
let ec = S::from_f64(ERROR_CONST[order + 1]);
let mut s = S::ZERO;
for i in 0..dim {
let scl = (options.atol + options.rtol * y_new[i].abs()).max(S::from_f64(1e-300));
let e = ec * d_arr[order + 2][i] / scl;
s = s + e * e;
}
err_p_norm = (s / S::from_usize(dim)).sqrt();
}
let safe_pow = |e: S, p: usize| -> S {
let f = e.max(S::from_f64(1e-30));
f.powf(-S::ONE / S::from_usize(p))
};
let factor_m = if order > min_order && err_m_norm.to_f64().is_finite() {
safe_pow(err_m_norm, order)
} else {
S::ZERO
};
let factor_o = safe_pow(err_norm, order + 1);
let factor_p = if order < max_order && err_p_norm.to_f64().is_finite() {
safe_pow(err_p_norm, order + 2)
} else {
S::ZERO
};
let mut best = factor_o;
let mut delta_order: i32 = 0;
if factor_m > best {
best = factor_m;
delta_order = -1;
}
if factor_p > best {
best = factor_p;
delta_order = 1;
}
if !best.to_f64().is_finite() {
best = S::ONE;
}
order = ((order as i32) + delta_order) as usize;
let factor = (safety * best).min(S::from_f64(MAX_FACTOR));
let factor = factor.max(S::from_f64(MIN_FACTOR));
change_d(&mut d_arr, order, factor);
n_equal_steps = 0;
h_abs = h_abs * factor;
if h_abs > h_max {
let cap = h_max / h_abs;
change_d(&mut d_arr, order, cap);
h_abs = h_max;
}
lu = None;
last_c = None;
step_count += 1;
}
Ok(SolverResult::new(t_out, y_out, dim, stats))
}
fn initial_step_size<S: Scalar>(y0: &[S], f0: &[S], options: &SolverOptions<S>, dim: usize) -> S {
if let Some(h0) = options.h0 {
return h0;
}
let mut d0 = S::ZERO;
let mut d1 = S::ZERO;
for i in 0..dim {
let sc = (options.atol + options.rtol * y0[i].abs()).max(S::from_f64(1e-15));
d0 = d0 + (y0[i] / sc) * (y0[i] / sc);
d1 = d1 + (f0[i] / sc) * (f0[i] / sc);
}
d0 = (d0 / S::from_usize(dim)).sqrt();
d1 = (d1 / S::from_usize(dim)).sqrt();
let h0 = if d0 < S::from_f64(1e-5) || d1 < S::from_f64(1e-5) {
S::from_f64(1e-6)
} else {
S::from_f64(0.01) * d0 / d1
};
h0.min(options.h_max).max(options.h_min)
}
fn form_iteration_matrix<S>(jac: &[S], c: S, dim: usize, mass: Option<&[S]>) -> DenseMatrix<S>
where
S: Scalar + SimpleEntity + Conjugate<Canonical = S> + ComplexField,
{
let mut m = DenseMatrix::zeros(dim, dim);
for i in 0..dim {
for j in 0..dim {
let jij = jac[i * dim + j];
let mij = match mass {
Some(mass_data) => mass_data[i * dim + j],
None => {
if i == j {
S::ONE
} else {
S::ZERO
}
}
};
m.set(i, j, mij - c * jij);
}
}
m
}
#[cfg(test)]
mod tests {
use super::*;
use crate::problem::{DaeProblem, OdeProblem};
#[test]
fn test_bdf_exponential_decay() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
},
0.0,
5.0,
vec![1.0],
);
let options = SolverOptions::default().rtol(1e-3).atol(1e-5);
let result = Bdf::solve(&problem, 0.0, 5.0, &[1.0], &options).unwrap();
assert!(result.success);
let y_final = result.y_final().unwrap();
let expected = (-5.0_f64).exp();
assert!(
(y_final[0] - expected).abs() < 1e-2,
"BDF exponential: got {}, expected {}",
y_final[0],
expected
);
}
#[test]
fn test_bdf_stiff_decay() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -100.0 * y[0];
},
0.0,
0.1,
vec![1.0],
);
let options = SolverOptions::default().rtol(1e-2).atol(1e-4);
let result = Bdf::solve(&problem, 0.0, 0.1, &[1.0], &options).unwrap();
assert!(result.success);
let y_final = result.y_final().unwrap();
let expected = (-10.0_f64).exp();
assert!(
(y_final[0] - expected).abs() < 0.05,
"BDF stiff: got {}, expected {}",
y_final[0],
expected
);
}
#[test]
fn test_bdf_linear_2d() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0] + y[1];
dydt[1] = y[0] - y[1];
},
0.0,
5.0,
vec![1.0, 0.0],
);
let options = SolverOptions::default().rtol(1e-3).atol(1e-5);
let result = Bdf::solve(&problem, 0.0, 5.0, &[1.0, 0.0], &options).unwrap();
assert!(result.success);
let y_final = result.y_final().unwrap();
assert!((y_final[0] + y_final[1] - 1.0).abs() < 1e-2);
}
#[test]
fn test_bdf_van_der_pol() {
let mu = 10.0;
let problem = OdeProblem::new(
move |_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = y[1];
dydt[1] = mu * (1.0 - y[0] * y[0]) * y[1] - y[0];
},
0.0,
20.0,
vec![2.0, 0.0],
);
let options = SolverOptions::default().rtol(1e-2).atol(1e-4);
let result = Bdf::solve(&problem, 0.0, 20.0, &[2.0, 0.0], &options);
assert!(result.is_ok(), "BDF Van der Pol failed: {:?}", result.err());
}
#[test]
fn test_bdf_regression_silent_wrong_answer() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -0.5 * y[0];
dydt[1] = -0.5 * y[1] - y[0];
},
0.0,
2.0,
vec![1.0, 0.0],
);
let options = SolverOptions::default().rtol(1e-4).atol(1e-6);
let result = Bdf::solve(&problem, 0.0, 2.0, &[1.0, 0.0], &options).unwrap();
assert!(result.success);
let y_final = result.y_final().unwrap();
let y_expected = (-1.0_f64).exp();
let s_expected = -2.0 * (-1.0_f64).exp();
assert!(
(y_final[0] - y_expected).abs() < 1e-3,
"y deviated: got {}, expected {}",
y_final[0],
y_expected
);
assert!(
(y_final[1] - s_expected).abs() < 1e-3,
"S deviated: got {}, expected {}",
y_final[1],
s_expected
);
}
#[test]
fn test_bdf_regression_step_efficiency() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
},
0.0,
1.0,
vec![1.0],
);
let options = SolverOptions::default().rtol(1e-6).atol(1e-9);
let result = Bdf::solve(&problem, 0.0, 1.0, &[1.0], &options).unwrap();
assert!(result.success);
let exact = (-1.0_f64).exp();
assert!((result.y_final().unwrap()[0] - exact).abs() < 1e-5);
assert!(
result.stats.n_accept < 200,
"Too many accepted steps: {} (expected ~30, regression bound 200)",
result.stats.n_accept
);
}
#[test]
fn test_bdf_simple_dae() {
let dae = DaeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0] + y[1];
dydt[1] = y[0] - y[1];
},
|mass: &mut [f64]| {
for i in 0..4 {
mass[i] = 0.0;
}
mass[0] = 1.0;
},
0.0,
1.0,
vec![1.0, 1.0],
vec![1],
);
let options = SolverOptions::default()
.rtol(1e-4)
.atol(1e-6)
.max_steps(500_000);
let result = Bdf::solve(&dae, 0.0, 1.0, &[1.0, 1.0], &options);
assert!(result.is_ok(), "DAE solve failed: {:?}", result.err());
let sol = result.unwrap();
let yf = sol.y_final().unwrap();
assert!((yf[0] - 1.0).abs() < 1e-4);
assert!((yf[1] - 1.0).abs() < 1e-4);
assert!((yf[0] - yf[1]).abs() < 1e-4);
}
#[test]
fn test_bdf_dae_with_mass_identity() {
let dae = DaeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
},
|mass: &mut [f64]| {
mass[0] = 1.0;
},
0.0,
1.0,
vec![1.0],
vec![],
);
let options = SolverOptions::default().rtol(1e-4).atol(1e-6);
let result = Bdf::solve(&dae, 0.0, 1.0, &[1.0], &options);
assert!(result.is_ok());
let yf = result.unwrap().y_final().unwrap();
let exact = (-1.0_f64).exp();
assert!((yf[0] - exact).abs() < 1e-3);
}
#[test]
fn test_bdf_dae_scaled_mass() {
let dae = DaeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
},
|mass: &mut [f64]| {
mass[0] = 2.0;
},
0.0,
1.0,
vec![1.0],
vec![],
);
let options = SolverOptions::default().rtol(1e-3).atol(1e-5);
let result = Bdf::solve(&dae, 0.0, 1.0, &[1.0], &options);
assert!(result.is_ok());
let yf = result.unwrap().y_final().unwrap();
let exact = (-0.5_f64).exp();
assert!((yf[0] - exact).abs() < 1e-2);
}
#[test]
fn test_bdf_step_counts_vs_scipy() {
struct Probe {
name: &'static str,
n_accept: usize,
scipy_ref: usize,
bound: usize,
ok: bool,
}
let mut probes: Vec<Probe> = Vec::new();
for &(rtol, atol, scipy_ref, bound) in &[
(1e-4, 1e-6, 16usize, 60usize),
(1e-6, 1e-9, 27, 100),
(1e-8, 1e-11, 44, 200),
] {
let p = OdeProblem::new(
|_t, y: &[f64], dy: &mut [f64]| dy[0] = -y[0],
0.0,
1.0,
vec![1.0],
);
let opts = SolverOptions::default().rtol(rtol).atol(atol);
let r = Bdf::solve(&p, 0.0, 1.0, &[1.0], &opts).unwrap();
let exact = (-1.0_f64).exp();
let acc = (r.y_final().unwrap()[0] - exact).abs() < 10.0 * rtol;
probes.push(Probe {
name: Box::leak(format!("decay rtol={rtol:e}").into_boxed_str()),
n_accept: r.stats.n_accept,
scipy_ref,
bound,
ok: acc && r.success && r.stats.n_accept < bound,
});
}
{
let p = OdeProblem::new(
|_t, y: &[f64], dy: &mut [f64]| dy[0] = -100.0 * y[0],
0.0,
0.1,
vec![1.0],
);
let opts = SolverOptions::default().rtol(1e-2).atol(1e-4);
let r = Bdf::solve(&p, 0.0, 0.1, &[1.0], &opts).unwrap();
probes.push(Probe {
name: "stiff y'=-100y",
n_accept: r.stats.n_accept,
scipy_ref: 12,
bound: 50,
ok: r.success && r.stats.n_accept < 50,
});
}
{
let p = OdeProblem::new(
|_t, y: &[f64], dy: &mut [f64]| {
dy[0] = -0.5 * y[0];
dy[1] = -0.5 * y[1] - y[0];
},
0.0,
2.0,
vec![1.0, 0.0],
);
let opts = SolverOptions::default().rtol(1e-4).atol(1e-6);
let r = Bdf::solve(&p, 0.0, 2.0, &[1.0, 0.0], &opts).unwrap();
probes.push(Probe {
name: "2-state augmented",
n_accept: r.stats.n_accept,
scipy_ref: 21,
bound: 100,
ok: r.success && r.stats.n_accept < 100,
});
}
{
let mu = 10.0_f64;
let p = OdeProblem::new(
move |_t, y: &[f64], dy: &mut [f64]| {
dy[0] = y[1];
dy[1] = mu * (1.0 - y[0] * y[0]) * y[1] - y[0];
},
0.0,
20.0,
vec![2.0, 0.0],
);
let opts = SolverOptions::default().rtol(1e-2).atol(1e-4);
let r = Bdf::solve(&p, 0.0, 20.0, &[2.0, 0.0], &opts).unwrap();
probes.push(Probe {
name: "VdP μ=10",
n_accept: r.stats.n_accept,
scipy_ref: 80,
bound: 400,
ok: r.success && r.stats.n_accept < 400,
});
}
eprintln!("\n problem | n_accept | scipy | bound | status");
eprintln!(" -----------------------+----------+---------+---------+--------");
for p in &probes {
eprintln!(
" {:<22} | {:>8} | {:>7} | {:>7} | {}",
p.name,
p.n_accept,
p.scipy_ref,
p.bound,
if p.ok { "ok" } else { "OVER" },
);
}
assert!(
probes.iter().all(|p| p.ok),
"one or more BDF probes exceeded bound or failed accuracy"
);
}
}