use crate::error::{IntegrateError, IntegrateResult};
use crate::ode::types::{ODEMethod, ODEOptions, ODEResult};
use crate::ode::utils::common::{
calculate_error_weights, estimate_initial_step, extrapolate, finite_difference_jacobian,
scaled_norm, solve_linear_system,
};
use crate::ode::utils::stiffness::integration::{AdaptiveMethodState, AdaptiveMethodType};
use crate::ode::utils::stiffness::StiffnessDetectionConfig;
use crate::IntegrateFloat;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
#[inline(always)]
fn const_f64<F: IntegrateFloat>(value: f64) -> F {
F::from_f64(value).expect("Failed to convert constant to target float type - this indicates an incompatible numeric type")
}
struct EnhancedLsodaState<F: IntegrateFloat> {
t: F,
y: Array1<F>,
dy: Array1<F>,
h: F,
t_history: Vec<F>,
y_history: Vec<Array1<F>>,
dy_history: Vec<Array1<F>>,
adaptive_state: AdaptiveMethodState<F>,
jacobian: Option<Array2<F>>,
jacobian_age: usize,
func_evals: usize,
n_lu: usize,
n_jac: usize,
steps: usize,
accepted_steps: usize,
rejected_steps: usize,
tol_scale: Array1<F>,
}
impl<F: IntegrateFloat> EnhancedLsodaState<F> {
fn new(t: F, y: Array1<F>, dy: Array1<F>, h: F, rtol: F, atol: F) -> Self {
let _n_dim = y.len();
let tol_scale = calculate_error_weights(&y, atol, rtol);
let stiffness_config = StiffnessDetectionConfig::default();
EnhancedLsodaState {
t,
y: y.clone(),
dy: dy.clone(),
h,
t_history: vec![t],
y_history: vec![y],
dy_history: vec![dy],
adaptive_state: AdaptiveMethodState::with_config(stiffness_config),
jacobian: None,
jacobian_age: 0,
func_evals: 0,
n_lu: 0,
n_jac: 0,
steps: 0,
accepted_steps: 0,
rejected_steps: 0,
tol_scale,
}
}
fn update_tol_scale(&mut self, rtol: F, atol: F) {
self.tol_scale = calculate_error_weights(&self.y, atol, rtol);
}
fn add_to_history(&mut self) {
self.t_history.push(self.t);
self.y_history.push(self.y.clone());
self.dy_history.push(self.dy.clone());
let max_history = match self.adaptive_state.method_type {
AdaptiveMethodType::Explicit => 12, AdaptiveMethodType::Implicit => 5, AdaptiveMethodType::Adams => 12, AdaptiveMethodType::BDF => 5, AdaptiveMethodType::RungeKutta => 4, };
if self.t_history.len() > max_history {
self.t_history.remove(0);
self.y_history.remove(0);
self.dy_history.remove(0);
}
}
fn switch_method(&mut self, _newmethod: AdaptiveMethodType) -> IntegrateResult<()> {
self.adaptive_state.switch_method(_newmethod, self.steps)?;
match _newmethod {
AdaptiveMethodType::Implicit | AdaptiveMethodType::BDF => {
self.jacobian = None;
self.jacobian_age = 0;
}
AdaptiveMethodType::Explicit | AdaptiveMethodType::Adams => {
if self.rejected_steps > 2 {
self.h *= const_f64::<F>(0.5);
}
}
AdaptiveMethodType::RungeKutta => {
self.h *= const_f64::<F>(0.8);
}
}
Ok(())
}
}
#[allow(dead_code)]
pub fn enhanced_lsoda_method<F, Func>(
f: Func,
t_span: [F; 2],
y0: Array1<F>,
opts: ODEOptions<F>,
) -> IntegrateResult<ODEResult<F>>
where
F: IntegrateFloat,
Func: Fn(F, ArrayView1<F>) -> Array1<F>,
{
let [t_start, t_end] = t_span;
let _n_dim = y0.len();
let dy0 = f(t_start, y0.view());
let mut func_evals = 1;
let h0 = opts.h0.unwrap_or_else(|| {
let tol = opts.atol + opts.rtol;
estimate_initial_step(&f, t_start, &y0, &dy0, tol, t_end)
});
let min_step = opts.min_step.unwrap_or_else(|| {
let _span = t_end - t_start;
_span * const_f64::<F>(1e-10) });
let max_step = opts.max_step.unwrap_or_else(|| {
t_end - t_start });
let mut state = EnhancedLsodaState::new(t_start, y0.clone(), dy0, h0, opts.rtol, opts.atol);
let mut t_values = vec![t_start];
let mut y_values = vec![y0.clone()];
while state.t < t_end && state.steps < opts.max_steps {
if state.t + state.h > t_end {
state.h = t_end - state.t;
}
state.h = state.h.min(max_step).max(min_step);
let step_result = match state.adaptive_state.method_type {
AdaptiveMethodType::Explicit | AdaptiveMethodType::Adams => {
enhanced_adams_step(&mut state, &f, &opts, &mut func_evals)
}
AdaptiveMethodType::Implicit | AdaptiveMethodType::BDF => {
enhanced_bdf_step(&mut state, &f, &opts, &mut func_evals)
}
AdaptiveMethodType::RungeKutta => {
Err(IntegrateError::NotImplementedError(
"enhanced_lsoda_method: AdaptiveMethodType::RungeKutta is not a supported \
auto-switching target (this module only switches between Adams and BDF); \
use ODEMethod::RK45, RK23, or DOP853 directly for explicit Runge-Kutta \
integration"
.to_string(),
))
}
};
state.steps += 1;
match step_result {
Ok((accepted, error, newton_iterations)) => {
state
.adaptive_state
.record_step(state.h, error, newton_iterations, !accepted);
if accepted {
state.add_to_history();
t_values.push(state.t);
y_values.push(state.y.clone());
state.accepted_steps += 1;
if let Some(new_method) = state.adaptive_state.check_method_switch() {
state.switch_method(new_method)?;
}
state.update_tol_scale(opts.rtol, opts.atol);
if state.adaptive_state.method_type == AdaptiveMethodType::Implicit
&& state.jacobian.is_some()
{
state.jacobian_age += 1;
}
} else {
state.rejected_steps += 1;
}
}
Err(e) => {
let currently_nonstiff = matches!(
state.adaptive_state.method_type,
AdaptiveMethodType::Explicit | AdaptiveMethodType::Adams
);
let currently_stiff = matches!(
state.adaptive_state.method_type,
AdaptiveMethodType::Implicit | AdaptiveMethodType::BDF
);
match &e {
IntegrateError::ConvergenceError(msg)
if msg.contains("stiff") && currently_nonstiff =>
{
state.switch_method(AdaptiveMethodType::Implicit)?;
state.h *= const_f64::<F>(0.5);
if state.h < min_step {
return Err(IntegrateError::ConvergenceError(
"Step size too small after method switch".to_string(),
));
}
}
IntegrateError::ConvergenceError(msg)
if msg.contains("non-stiff") && currently_stiff =>
{
state.switch_method(AdaptiveMethodType::Explicit)?;
state.h *= const_f64::<F>(0.5);
if state.h < min_step {
return Err(IntegrateError::ConvergenceError(
"Step size too small after method switch".to_string(),
));
}
}
_ => return Err(e), }
}
}
}
let success = state.t >= t_end;
let message = if !success {
Some(format!(
"Maximum number of steps ({}) reached",
opts.max_steps
))
} else {
Some(state.adaptive_state.generate_diagnostic_message())
};
Ok(ODEResult {
t: t_values,
y: y_values,
success,
message,
n_eval: func_evals,
n_steps: state.steps,
n_accepted: state.accepted_steps,
n_rejected: state.rejected_steps,
n_lu: state.n_lu,
n_jac: state.n_jac,
method: ODEMethod::LSODA,
})
}
#[allow(dead_code)]
fn enhanced_adams_step<F, Func>(
state: &mut EnhancedLsodaState<F>,
f: &Func,
opts: &ODEOptions<F>,
func_evals: &mut usize,
) -> IntegrateResult<(bool, F, usize)>
where
F: IntegrateFloat,
Func: Fn(F, ArrayView1<F>) -> Array1<F>,
{
let ab_coeffs: [Vec<F>; 12] = [
vec![F::one()],
vec![const_f64::<F>(3.0 / 2.0), const_f64::<F>(-1.0 / 2.0)],
vec![
const_f64::<F>(23.0 / 12.0),
const_f64::<F>(-16.0 / 12.0),
const_f64::<F>(5.0 / 12.0),
],
vec![
const_f64::<F>(55.0 / 24.0),
const_f64::<F>(-59.0 / 24.0),
const_f64::<F>(37.0 / 24.0),
const_f64::<F>(-9.0 / 24.0),
],
vec![
const_f64::<F>(1901.0 / 720.0),
const_f64::<F>(-2774.0 / 720.0),
const_f64::<F>(2616.0 / 720.0),
const_f64::<F>(-1274.0 / 720.0),
const_f64::<F>(251.0 / 720.0),
],
vec![
const_f64::<F>(4277.0 / 1440.0),
const_f64::<F>(-7923.0 / 1440.0),
const_f64::<F>(9982.0 / 1440.0),
const_f64::<F>(-7298.0 / 1440.0),
const_f64::<F>(2877.0 / 1440.0),
const_f64::<F>(-475.0 / 1440.0),
],
vec![
const_f64::<F>(198721.0 / 60480.0),
const_f64::<F>(-447288.0 / 60480.0),
const_f64::<F>(705549.0 / 60480.0),
const_f64::<F>(-688256.0 / 60480.0),
const_f64::<F>(407139.0 / 60480.0),
const_f64::<F>(-134472.0 / 60480.0),
const_f64::<F>(19087.0 / 60480.0),
],
vec![
const_f64::<F>(434241.0 / 120960.0),
const_f64::<F>(-1152169.0 / 120960.0),
const_f64::<F>(2183877.0 / 120960.0),
const_f64::<F>(-2664477.0 / 120960.0),
const_f64::<F>(2102243.0 / 120960.0),
const_f64::<F>(-1041723.0 / 120960.0),
const_f64::<F>(295767.0 / 120960.0),
const_f64::<F>(-36799.0 / 120960.0),
],
vec![
const_f64::<F>(14097247.0 / 3628800.0),
const_f64::<F>(-43125206.0 / 3628800.0),
const_f64::<F>(95476786.0 / 3628800.0),
const_f64::<F>(-139855262.0 / 3628800.0),
const_f64::<F>(137968480.0 / 3628800.0),
const_f64::<F>(-91172642.0 / 3628800.0),
const_f64::<F>(38833486.0 / 3628800.0),
const_f64::<F>(-9664106.0 / 3628800.0),
const_f64::<F>(1070017.0 / 3628800.0),
],
vec![
const_f64::<F>(30277247.0 / 7257600.0),
const_f64::<F>(-104995189.0 / 7257600.0),
const_f64::<F>(265932680.0 / 7257600.0),
const_f64::<F>(-454661776.0 / 7257600.0),
const_f64::<F>(538363838.0 / 7257600.0),
const_f64::<F>(-444772162.0 / 7257600.0),
const_f64::<F>(252618224.0 / 7257600.0),
const_f64::<F>(-94307320.0 / 7257600.0),
const_f64::<F>(20884811.0 / 7257600.0),
const_f64::<F>(-2082753.0 / 7257600.0),
],
vec![
const_f64::<F>(35256204767.0 / 7983360000.0),
const_f64::<F>(-134336876800.0 / 7983360000.0),
const_f64::<F>(385146025457.0 / 7983360000.0),
const_f64::<F>(-754734083733.0 / 7983360000.0),
const_f64::<F>(1045594573504.0 / 7983360000.0),
const_f64::<F>(-1029725952608.0 / 7983360000.0),
const_f64::<F>(717313887930.0 / 7983360000.0),
const_f64::<F>(-344156361067.0 / 7983360000.0),
const_f64::<F>(109301088672.0 / 7983360000.0),
const_f64::<F>(-21157613775.0 / 7983360000.0),
const_f64::<F>(1832380165.0 / 7983360000.0),
],
vec![
const_f64::<F>(77737505967.0 / 16876492800.0),
const_f64::<F>(-328202700680.0 / 16876492800.0),
const_f64::<F>(1074851727475.0 / 16876492800.0),
const_f64::<F>(-2459572352768.0 / 16876492800.0),
const_f64::<F>(4013465151807.0 / 16876492800.0),
const_f64::<F>(-4774671405984.0 / 16876492800.0),
const_f64::<F>(4127030565077.0 / 16876492800.0),
const_f64::<F>(-2538584431976.0 / 16876492800.0),
const_f64::<F>(1077984741336.0 / 16876492800.0),
const_f64::<F>(-295501032385.0 / 16876492800.0),
const_f64::<F>(48902348238.0 / 16876492800.0),
const_f64::<F>(-3525779602.0 / 16876492800.0),
],
];
let am_coeffs: [Vec<F>; 12] = [
vec![F::one()],
vec![const_f64::<F>(1.0 / 2.0), const_f64::<F>(1.0 / 2.0)],
vec![
const_f64::<F>(5.0 / 12.0),
const_f64::<F>(8.0 / 12.0),
const_f64::<F>(-1.0 / 12.0),
],
vec![
const_f64::<F>(9.0 / 24.0),
const_f64::<F>(19.0 / 24.0),
const_f64::<F>(-5.0 / 24.0),
const_f64::<F>(1.0 / 24.0),
],
vec![F::zero()],
vec![F::zero()],
vec![F::zero()],
vec![F::zero()],
vec![F::zero()],
vec![F::zero()],
vec![F::zero()],
vec![F::zero()],
];
let order = state
.adaptive_state
.order
.min(state.dy_history.len() + 1)
.min(12);
if order == 1 || state.dy_history.is_empty() {
let next_t = state.t + state.h;
let next_y = &state.y + &(state.dy.clone() * state.h);
let next_dy = f(next_t, next_y.view());
*func_evals += 1;
state.func_evals += 1;
state.t = next_t;
state.y = next_y;
state.dy = next_dy;
if state.adaptive_state.order < 2 {
state.adaptive_state.order += 1;
}
return Ok((true, F::zero(), 0));
}
let next_t = state.t + state.h;
let ab_coefs = &ab_coeffs[order - 1];
let mut ab_sum = state.dy.clone() * ab_coefs[0];
for (i, &coeff) in ab_coefs.iter().enumerate().take(order).skip(1) {
if i <= state.dy_history.len() {
let idx = state.dy_history.len() - i;
ab_sum += &(state.dy_history[idx].clone() * coeff);
}
}
let y_pred = &state.y + &(ab_sum * state.h);
let dy_pred = f(next_t, y_pred.view());
*func_evals += 1;
state.func_evals += 1;
let am_order = order.min(4); let am_coefs = &am_coeffs[am_order - 1];
let mut am_sum = dy_pred.clone() * am_coefs[0];
for (i, &coeff) in am_coefs.iter().enumerate().take(am_order).skip(1) {
if i == 1 {
am_sum += &(state.dy.clone() * coeff);
} else if i - 1 < state.dy_history.len() {
let idx = state.dy_history.len() - (i - 1);
am_sum += &(state.dy_history[idx].clone() * coeff);
}
}
let y_corr = &state.y + &(am_sum * state.h);
let dy_corr = f(next_t, y_corr.view());
*func_evals += 1;
state.func_evals += 1;
let error = scaled_norm(&(&y_corr - &y_pred), &state.tol_scale);
let err_order = F::from_usize(order + 1).expect("Failed to convert order to Float type"); let err_factor = if error > F::zero() {
const_f64::<F>(0.9) * (F::one() / error).powf(F::one() / err_order)
} else {
const_f64::<F>(5.0) };
let safety = const_f64::<F>(0.9);
let factor_max = const_f64::<F>(5.0);
let factor_min = const_f64::<F>(0.2);
let factor = safety * err_factor.min(factor_max).max(factor_min);
if error <= F::one() {
state.t = next_t;
state.y = y_corr;
state.dy = dy_corr;
state.h *= factor;
if order < 12 && error < opts.rtol && state.dy_history.len() >= order {
state.adaptive_state.order = (state.adaptive_state.order + 1).min(12);
} else if order > 1 && error > const_f64::<F>(0.5) {
state.adaptive_state.order = (state.adaptive_state.order - 1).max(1);
}
Ok((true, error, 0))
} else {
state.h *= factor;
if error > const_f64::<F>(10.0) {
return Err(IntegrateError::ConvergenceError(
"Problem appears stiff - consider using BDF method".to_string(),
));
}
Ok((false, error, 0))
}
}
fn bdf_variable_step_coeffs<F: IntegrateFloat>(nodes: &[F], h_ref: F) -> Vec<F> {
let q = nodes.len();
let tau: Vec<F> = nodes.iter().map(|&x| (x - nodes[0]) / h_ref).collect();
let mut coeffs = vec![F::zero(); q];
for (i, coeff) in coeffs.iter_mut().enumerate() {
let raw = if i == 0 {
let mut sum = F::zero();
for &tj in tau.iter().skip(1) {
sum += F::one() / (tau[0] - tj);
}
sum
} else {
let mut numer = F::one();
let mut denom = F::one();
for (j, &tj) in tau.iter().enumerate() {
if j == i {
continue;
}
denom *= tau[i] - tj;
if j != 0 {
numer *= tau[0] - tj;
}
}
numer / denom
};
*coeff = raw / h_ref;
}
coeffs
}
#[allow(dead_code)]
fn enhanced_bdf_step<F, Func>(
state: &mut EnhancedLsodaState<F>,
f: &Func,
opts: &ODEOptions<F>,
func_evals: &mut usize,
) -> IntegrateResult<(bool, F, usize)>
where
F: IntegrateFloat,
Func: Fn(F, ArrayView1<F>) -> Array1<F>,
{
let order = state.adaptive_state.order.min(state.y_history.len()).min(5);
if order == 1 || state.y_history.is_empty() {
let next_t = state.t + state.h;
let y_pred = state.y.clone();
let max_newton_iters = 10;
let newton_tol = const_f64::<F>(1e-8);
let mut y_next = y_pred.clone();
let mut converged = false;
let mut iter_count = 0;
let mut f_eval = f(next_t, y_next.view());
*func_evals += 1;
state.func_evals += 1;
while iter_count < max_newton_iters {
let residual = &y_next - &state.y - &(f_eval.clone() * state.h);
let error = scaled_norm(&residual, &state.tol_scale);
if error <= newton_tol {
converged = true;
break;
}
let eps = const_f64::<F>(1e-8);
let n_dim = y_next.len();
let compute_new_jacobian =
state.jacobian.is_none() || state.jacobian_age > 20 || iter_count == 0;
let jacobian = if compute_new_jacobian {
state.n_jac += 1;
let new_jacobian = finite_difference_jacobian(f, next_t, &y_next, &f_eval, eps);
let mut jac = Array2::<F>::eye(n_dim);
for i in 0..n_dim {
for j in 0..n_dim {
jac[[i, j]] = if i == j { F::one() } else { F::zero() };
jac[[i, j]] -= state.h * new_jacobian[[i, j]];
}
}
state.jacobian = Some(jac.clone());
state.jacobian_age = 0;
jac
} else {
state
.jacobian
.clone()
.expect("Jacobian should exist when not computing new one")
};
state.n_lu += 1;
let delta_y = match solve_linear_system(&jacobian, &residual) {
Ok(delta) => delta,
Err(_) => {
state.h *= const_f64::<F>(0.5);
return Ok((false, error.max(const_f64::<F>(2.0)), iter_count));
}
};
y_next = &y_next - &delta_y;
f_eval = f(next_t, y_next.view());
*func_evals += 1;
state.func_evals += 1;
iter_count += 1;
}
if !converged {
let final_residual = &y_next - &state.y - &(f_eval.clone() * state.h);
let final_error = scaled_norm(&final_residual, &state.tol_scale).max(F::one());
state.h *= const_f64::<F>(0.5);
if state.h < opts.min_step.unwrap_or(const_f64::<F>(1e-10)) {
return Err(IntegrateError::ConvergenceError(
"BDF1 failed to converge - problem might be non-stiff".to_string(),
));
}
return Ok((false, final_error, iter_count));
}
let error = scaled_norm(&(&y_next - &y_pred), &state.tol_scale);
state.t = next_t;
state.y = y_next;
state.dy = f_eval;
if state.adaptive_state.order < 2 {
state.adaptive_state.order += 1;
}
return Ok((true, error, iter_count));
}
let next_t = state.t + state.h;
let mut y_pred = state.y.clone();
if order > 1 && !state.y_history.is_empty() {
y_pred = extrapolate(&state.t_history[..], &state.y_history[..], next_t)?;
}
let hist_len = state.t_history.len();
let mut nodes: Vec<F> = Vec::with_capacity(order + 1);
nodes.push(next_t);
for k in 0..order {
nodes.push(state.t_history[hist_len - 1 - k]);
}
let coeffs = bdf_variable_step_coeffs(&nodes, state.h);
let max_newton_iters = 10;
let newton_tol = const_f64::<F>(1e-8);
let mut y_next = y_pred.clone();
let mut converged = false;
let mut iter_count = 0;
let mut last_newton_error = F::zero();
let mut f_eval = f(next_t, y_next.view());
*func_evals += 1;
state.func_evals += 1;
while iter_count < max_newton_iters {
let mut residual = y_next.clone() * coeffs[0];
residual += &(state.y.clone() * coeffs[1]);
for k in 1..order {
residual += &(state.y_history[hist_len - 1 - k].clone() * coeffs[k + 1]);
}
residual -= &f_eval;
let eps = const_f64::<F>(1e-8);
let n_dim = y_next.len();
let compute_new_jacobian =
state.jacobian.is_none() || state.jacobian_age > 20 || iter_count == 0;
let jacobian = if compute_new_jacobian {
state.n_jac += 1;
let new_jacobian = finite_difference_jacobian(f, next_t, &y_next, &f_eval, eps);
let mut jac = Array2::<F>::zeros((n_dim, n_dim));
for i in 0..n_dim {
for j in 0..n_dim {
jac[[i, j]] = if i == j { coeffs[0] } else { F::zero() };
jac[[i, j]] -= new_jacobian[[i, j]];
}
}
state.jacobian = Some(jac.clone());
state.jacobian_age = 0;
jac
} else {
state
.jacobian
.clone()
.expect("Jacobian should exist when not computing new one")
};
state.n_lu += 1;
let delta_y = match solve_linear_system(&jacobian, &residual) {
Ok(delta) => delta,
Err(_) => {
let residual_error = scaled_norm(&residual, &state.tol_scale);
state.h *= const_f64::<F>(0.5);
return Ok((false, residual_error.max(const_f64::<F>(2.0)), iter_count));
}
};
let step_size_error = scaled_norm(&delta_y, &state.tol_scale);
last_newton_error = step_size_error;
y_next = &y_next - &delta_y;
f_eval = f(next_t, y_next.view());
*func_evals += 1;
state.func_evals += 1;
iter_count += 1;
if step_size_error <= newton_tol {
converged = true;
break;
}
}
if !converged {
if state.adaptive_state.order > 1 {
state.adaptive_state.order -= 1;
}
let final_error = last_newton_error.max(F::one());
state.h *= const_f64::<F>(0.5);
if state.h < opts.min_step.unwrap_or(const_f64::<F>(1e-10)) {
return Err(IntegrateError::ConvergenceError(
"BDF failed to converge - problem might be non-stiff".to_string(),
));
}
return Ok((false, final_error, iter_count));
}
let error = scaled_norm(&(&y_next - &y_pred), &state.tol_scale);
state.t = next_t;
state.y = y_next;
state.dy = f_eval;
if iter_count <= 2 {
state.h *= const_f64::<F>(1.1);
if state.adaptive_state.order < 5 && state.y_history.len() >= state.adaptive_state.order {
state.adaptive_state.order += 1;
}
} else if iter_count >= 8 {
state.h *= const_f64::<F>(0.8);
if state.adaptive_state.order > 1 {
state.adaptive_state.order -= 1;
}
}
state.jacobian_age += 1;
Ok((true, error, iter_count))
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn enhanced_lsoda_matches_analytical_exponential_decay() {
let k = 3.0_f64;
let f = move |_t: f64, y: ArrayView1<f64>| -> Array1<f64> { array![-k * y[0]] };
let opts = ODEOptions {
method: ODEMethod::EnhancedLSODA,
rtol: 1e-6,
atol: 1e-9,
max_steps: 10_000,
..Default::default()
};
let result = enhanced_lsoda_method(f, [0.0_f64, 1.0], array![2.0_f64], opts)
.expect("EnhancedLSODA exponential decay solve failed");
assert!(result.success, "EnhancedLSODA solve did not succeed");
for w in result.y.windows(2) {
assert!(
w[1][0] <= w[0][0],
"exponential decay must be monotonically non-increasing: {} then {}",
w[0][0],
w[1][0]
);
assert!(
w[1][0] >= 0.0 && w[1][0] <= 2.0,
"y left the physically sane [0, y0] range: {}",
w[1][0]
);
}
let y_final = result.y.last().expect("empty result")[0];
let y_exact = 2.0 * (-k * 1.0_f64).exp();
assert!(
(y_final - y_exact).abs() < 1e-2,
"EnhancedLSODA result too far from analytical: {y_final} vs {y_exact}"
);
}
#[test]
fn enhanced_lsoda_stiff_linear_problem_converges_accurately() {
let lambda = 500.0_f64;
let f = move |_t: f64, y: ArrayView1<f64>| -> Array1<f64> { array![-lambda * y[0]] };
let opts = ODEOptions {
method: ODEMethod::EnhancedLSODA,
rtol: 1e-6,
atol: 1e-9,
max_steps: 10_000,
..Default::default()
};
let result = enhanced_lsoda_method(f, [0.0_f64, 0.5], array![1.0_f64], opts)
.expect("EnhancedLSODA stiff solve failed");
assert!(result.success, "EnhancedLSODA stiff solve did not succeed");
let y_final = result.y.last().expect("empty result")[0];
let y_exact = (-lambda * 0.5_f64).exp();
assert!(
(y_final - y_exact).abs() < 1e-3,
"EnhancedLSODA stiff result too far from analytical: {y_final} vs {y_exact}"
);
}
}