Detecting Stiffness
Before you can choose the right solver, you need to know whether your problem is stiff. Sometimes this is obvious from the physics (chemical kinetics, circuit simulation), but often it is not. This section covers both analytical techniques and practical diagnostic approaches, including Numra’s built-in stiffness detection.
Analytical Detection: Jacobian Eigenvalues
Section titled “Analytical Detection: Jacobian Eigenvalues”The most direct test is to compute the Jacobian at some representative point and examine its eigenvalues.
For a system , the Jacobian is the matrix:
Stiffness indicators from the eigenvalue spectrum:
| Criterion | Value | Interpretation |
|---|---|---|
| Very stiff | ||
| Moderately stiff | ||
| Non-stiff | ||
| All | Required | Stable (stiffness is meaningful) |
Example: For the Robertson system at , :
The eigenvalues are . This does not look stiff at the initial point! But at , once becomes nonzero, the eigenvalues become — a stiffness ratio of nearly .
This illustrates that stiffness is often transient and depends on the current state.
Numra’s Automatic Detection
Section titled “Numra’s Automatic Detection”Numra’s Auto solver includes a built-in stiffness detector that examines the
Jacobian at the initial point. The algorithm works by sampling Jacobian elements
via finite differences and computing the ratio of largest to smallest magnitudes.
The Detection Algorithm
Section titled “The Detection Algorithm”The Auto::detect_stiffness function:
- Evaluates the right-hand side .
- Perturbs each of the first 5 state components to estimate Jacobian column entries.
- Computes the ratio of the largest to smallest nonzero Jacobian element magnitudes.
- Classifies the result:
| Ratio | Classification | Solver selected |
|---|---|---|
VeryStiff | BDF (standard accuracy) or Radau5 (high accuracy) | |
ModeratelyStiff | Esdirk54 | |
NonStiff | Tsit5 (standard), Vern6/8 (high accuracy) |
Using Auto with Stiffness Detection
Section titled “Using Auto with Stiffness Detection”The simplest approach is to let Auto handle everything:
use numra::ode::{Auto, Solver, OdeProblem, SolverOptions};
let problem = OdeProblem::new( |_t, y: &[f64], dydt: &mut [f64]| { dydt[0] = -1000.0 * y[0] + y[1]; dydt[1] = y[0] - y[1]; }, 0.0, 10.0, vec![1.0, 1.0],);
let options = SolverOptions::default().rtol(1e-6).atol(1e-9);
// Auto detects stiffness and selects an appropriate solverlet result = Auto::solve(&problem, 0.0, 10.0, &[1.0, 1.0], &options) .expect("Auto solver should succeed");Providing Hints
Section titled “Providing Hints”If you know your problem is stiff, you can bypass the detection step and provide
hints directly using SolverHints:
use numra::ode::{Auto, Solver, OdeProblem, SolverOptions};use numra::ode::auto::{SolverHints, Stiffness, Accuracy};
let problem = OdeProblem::new( |_t, y: &[f64], dydt: &mut [f64]| { dydt[0] = -1e6 * y[0]; }, 0.0, 1.0, vec![1.0],);
let options = SolverOptions::default().rtol(1e-8).atol(1e-10);
// Tell Auto exactly what kind of problem this islet hints = SolverHints::new() .stiffness(Stiffness::VeryStiff) .accuracy(Accuracy::High);
let result = Auto::solve_with_hints( &problem, 0.0, 1.0, &[1.0], &options, &hints,).expect("Should use Radau5 for very stiff + high accuracy");The available stiffness classifications are:
Stiffness::NonStiff— forces explicit solver selectionStiffness::ModeratelyStiff— selects ESDIRK54Stiffness::VeryStiff— selects BDF or Radau5 depending on accuracyStiffness::Unknown— enables automatic detection (default)
And the accuracy levels:
Accuracy::Low— rtol aroundAccuracy::Standard— rtol aroundAccuracy::High— rtol aroundAccuracy::VeryHigh— rtol around or tighter
Manual Detection Approaches
Section titled “Manual Detection Approaches”Approach 1: Finite-Difference Jacobian Sampling
Section titled “Approach 1: Finite-Difference Jacobian Sampling”You can compute the Jacobian yourself using the same technique Numra uses
internally. The OdeSystem trait provides a default jacobian method via
finite differences:
use numra::ode::{OdeProblem, OdeSystem};
let problem = OdeProblem::new( |_t, y: &[f64], dydt: &mut [f64]| { dydt[0] = -1000.0 * y[0] + y[1]; dydt[1] = y[0] - y[1]; }, 0.0, 10.0, vec![1.0, 1.0],);
let dim = 2;let mut jac = vec![0.0; dim * dim];problem.jacobian(0.0, &[1.0, 1.0], &mut jac);
// jac is now stored row-major:// J = [ jac[0] jac[1] ] = [ -1000 1 ]// [ jac[2] jac[3] ] [ 1 -1 ]println!("Jacobian diagonal: {}, {}", jac[0], jac[3]);// The ratio |jac[0]| / |jac[3]| = 1000 => moderately to very stiffApproach 2: Trial Run with an Explicit Solver
Section titled “Approach 2: Trial Run with an Explicit Solver”Run DoPri5 or Tsit5 on a short integration interval. If the solver:
- Takes an unexpectedly large number of steps, or
- Has a high rejection ratio (), or
- Fails outright with step size collapse,
then the problem is likely stiff.
use numra::ode::{Tsit5, Solver, OdeProblem, SolverOptions};
let problem = OdeProblem::new( |_t, y: &[f64], dydt: &mut [f64]| { dydt[0] = -1000.0 * y[0] + y[1]; dydt[1] = y[0] - y[1]; }, 0.0, 1.0, vec![1.0, 1.0],);
let options = SolverOptions::default() .rtol(1e-6) .atol(1e-9) .max_steps(10000);
match Tsit5::solve(&problem, 0.0, 1.0, &[1.0, 1.0], &options) { Ok(result) => { let ratio = result.stats.n_reject as f64 / result.stats.n_accept.max(1) as f64; if ratio > 0.5 || result.stats.n_accept > 5000 { println!("Likely stiff! Reject ratio: {:.2}, steps: {}", ratio, result.stats.n_accept); } } Err(_) => { println!("Explicit solver failed -- almost certainly stiff!"); }}This is essentially what Auto does in Stiffness::Unknown mode: it tries Tsit5
first, checks whether the rejection ratio is reasonable, and falls back to Esdirk54
if it is not.
Approach 3: Physical Reasoning
Section titled “Approach 3: Physical Reasoning”Many problem domains are known to produce stiff systems:
| Domain | Why stiff | Typical stiffness ratio |
|---|---|---|
| Chemical kinetics | Fast/slow reactions | |
| Electrical circuits | Parasitic capacitances | |
| Control systems | Fast actuators + slow plant | |
| Structural mechanics | High-frequency vibrations | |
| Combustion | Radical chemistry | |
| Semiconductor devices | Carrier dynamics |
If your problem comes from one of these domains, start with an implicit solver.
Diagnostic Signs During Integration
Section titled “Diagnostic Signs During Integration”Even without advance analysis, certain runtime behaviors are strong indicators of stiffness:
Step Size Collapse
Section titled “Step Size Collapse”The solver repeatedly reduces the step size until it hits the minimum. For explicit solvers, this is the classic symptom: the step controller demands smaller and smaller to satisfy stability, not accuracy.
High Rejection Ratio
Section titled “High Rejection Ratio”A healthy explicit integration has a rejection ratio below 10%. If you see 50%+
rejections, stability constraints are dominating. Check result.stats.n_reject
and result.stats.n_accept.
Excessive Function Evaluations
Section titled “Excessive Function Evaluations”For a 5th-order method integrating over , you might expect roughly steps. If the actual step count is orders of magnitude higher, stiffness is the likely culprit.
Oscillating Step Size
Section titled “Oscillating Step Size”The step controller alternates between large and small steps, unable to find a stable rhythm. This often happens when the problem transitions between stiff and non-stiff regions.
Summary: Detection Decision Tree
Section titled “Summary: Detection Decision Tree”Is the problem from a known stiff domain? | +-- Yes --> Use an implicit solver (Radau5, BDF, ESDIRK) | +-- No / Unsure | Can you compute the Jacobian eigenvalues? | +-- Yes --> Stiffness ratio > 100? --> Stiff | --> Non-stiff | +-- No --> Use Auto (it detects stiffness automatically) or try an explicit solver first and check: - Rejection ratio > 50%? --> Stiff - Step count >> expected? --> Stiff - Solver fails entirely? --> StiffWhen in doubt, use Auto — it adds negligible overhead for non-stiff problems
and automatically switches to an appropriate implicit method when stiffness is
detected.