ODE Integration¶
numeris provides fixed-step and adaptive ODE integrators for non-stiff and stiff systems. All solvers work on Vector<T, N> state vectors and closure-based dynamics functions.
Requires the ode feature (default).
Fixed-Step RK4¶
Classic 4th-order Runge-Kutta — no error estimation, fixed step size.
use numeris::ode::{rk4, rk4_step};
use numeris::Vector;
let f = |_t: f64, y: &Vector<f64, 2>| {
Vector::from_array([y[1], -y[0]]) // harmonic oscillator
};
let y0 = Vector::from_array([1.0_f64, 0.0]);
let dt = 0.01;
let t_end = 2.0 * std::f64::consts::PI;
// Integrate from t=0 to t=t_end
let sol = rk4(0.0, t_end, &y0, f, dt);
// Or take a single step
let y1 = rk4_step(0.0, &y0, f, dt);
RK4 is fully no-std and no-alloc — suitable for embedded real-time control loops.
Adaptive Solvers¶
Seven adaptive Runge-Kutta solvers via the RKAdaptive trait. All use embedded error estimation and a PI step-size controller (Söderlind & Wang 2006) for automatic step adjustment.
use numeris::ode::{RKAdaptive, RKTS54, AdaptiveSettings};
use numeris::Vector;
let y0 = Vector::from_array([1.0_f64, 0.0]);
let tau = 2.0 * std::f64::consts::PI;
let sol = RKTS54::integrate(
0.0, tau, &y0,
|_t, y| Vector::from_array([y[1], -y[0]]),
&AdaptiveSettings::default(),
).unwrap();
println!("y(2π) = {:?}", sol.y); // final state
println!("steps taken = {}", sol.steps); // number of accepted steps
Solver Table¶
| Solver | Stages | Order | FSAL | Interpolant | Best for |
|---|---|---|---|---|---|
RKF45 |
6 | 5(4) | no | — | Classic baseline |
RKTS54 |
7 | 5(4) | yes | 4th degree | General purpose |
RKV65 |
10 | 6(5) | no | 6th degree | Moderate accuracy |
RKV87 |
17 | 8(7) | no | 7th degree | High accuracy |
RKV98 |
21 | 9(8) | no | 8th degree | Very high accuracy |
RKV98NoInterp |
16 | 9(8) | no | — | Very high acc., no dense output |
RKV98Efficient |
26 | 9(8) | no | 9th degree | Max accuracy + dense output |
FSAL (First Same As Last): the last function evaluation of one step is reused as the first of the next, saving one function evaluation per step.
AdaptiveSettings¶
use numeris::ode::AdaptiveSettings;
let settings = AdaptiveSettings {
rtol: 1e-8, // relative tolerance (default 1e-6)
atol: 1e-10, // absolute tolerance (default 1e-9)
h0: Some(0.01), // initial step size (auto if None)
hmax: Some(1.0), // maximum step size (unlimited if None)
max_steps: 100_000, // step limit
..AdaptiveSettings::default()
};
Solution struct¶
let sol = RKTS54::integrate(0.0, 1.0, &y0, f, &settings).unwrap();
let y_final = &sol.y; // final state Vector<T, N>
let t_final = sol.t; // final time (= t_end if successful)
let n_steps = sol.steps; // number of accepted steps
let n_reject = sol.rejected; // number of rejected steps
Dense Output (Interpolation)¶
Solvers with an interpolant can return intermediate values at arbitrary times without re-integrating. Requires std feature (uses Vec internally).
use numeris::ode::{RKAdaptive, RKTS54, AdaptiveSettings};
use numeris::Vector;
let y0 = Vector::from_array([1.0_f64, 0.0]);
let mut sol = RKTS54::integrate_dense(
0.0, 6.28, &y0,
|_t, y| Vector::from_array([y[1], -y[0]]),
&AdaptiveSettings::default(),
).unwrap();
// Interpolate at arbitrary time point
let y_at_pi = sol.interpolate(std::f64::consts::PI);
// y[0] ≈ -1.0 (cosine at π)
Stiff Solver: RODAS4¶
For stiff ODEs (chemical kinetics, circuit simulation, orbital mechanics with drag), use RODAS4 — an L-stable linearly-implicit Rosenbrock method. It solves linear systems involving the Jacobian instead of nonlinear Newton iterations, making it suitable for very stiff problems without tuning.
use numeris::ode::{Rosenbrock, RODAS4, AdaptiveSettings};
use numeris::{Vector, Matrix};
// Stiff decay: y' = -1000y, y(0) = 1
let y0 = Vector::from_array([1.0_f64]);
// With user-supplied Jacobian
let sol = RODAS4::integrate(
0.0, 0.01, &y0,
|_t, y| Vector::from_array([-1000.0 * y[0]]),
|_t, _y| Matrix::new([[-1000.0_f64]]),
&AdaptiveSettings::default(),
).unwrap();
assert!((sol.y[0] - (-10.0_f64).exp()).abs() < 1e-8);
// With automatic finite-difference Jacobian (no need to supply ∂f/∂y)
let sol2 = RODAS4::integrate_auto(
0.0, 0.01, &y0,
|_t, y| Vector::from_array([-1000.0 * y[0]]),
&AdaptiveSettings::default(),
).unwrap();
Van der Pol oscillator¶
use numeris::ode::{Rosenbrock, RODAS4, AdaptiveSettings};
use numeris::{Vector, Matrix};
let mu = 20.0_f64;
let y0 = Vector::from_array([2.0_f64, 0.0]);
let sol = RODAS4::integrate(
0.0, 120.0, &y0,
|_t, y| Vector::from_array([
y[1],
mu * (1.0 - y[0] * y[0]) * y[1] - y[0],
]),
|_t, y| Matrix::new([
[0.0, 1.0],
[-2.0 * mu * y[0] * y[1] - 1.0, mu * (1.0 - y[0] * y[0])],
]),
&AdaptiveSettings::default(),
).unwrap();
RODAS4 Properties¶
| Property | Value |
|---|---|
| Stages | 6 |
| Order | 4(3) embedded pair |
| L-stable | Yes (no oscillation for arbitrarily large step sizes) |
| Stiffly accurate | Yes |
| Jacobian | User-supplied or auto finite-difference |
Error Handling¶
use numeris::ode::OdeError;
match RKTS54::integrate(0.0, 1.0, &y0, f, &settings) {
Ok(sol) => { /* success */ }
Err(OdeError::MaxStepsExceeded) => { /* increase max_steps or rtol/atol */ }
Err(OdeError::StepSizeTooSmall) => { /* likely a stiff problem — use RODAS4 */ }
Err(OdeError::SingularJacobian) => { /* RODAS4 specific */ }
}