ODE

Van der Pol Oscillator

Stiff ODE benchmark comparing explicit DoPri5 against implicit Radau5 across stiffness levels.

  • intermediate
  • stiff

The Van der Pol oscillator is the canonical stiff-ODE test problem. For small μ the system is non-stiff and any explicit solver works fine; for large μ (e.g. μ = 1000) it develops sharp transitions between slow and fast dynamics, and an implicit method becomes necessary.

x'' - μ(1 - x²)x' + x = 0

This example sweeps μ ∈ {1, 10, 100} with both DoPri5 (explicit) and Radau5 (implicit), printing step counts and wall-clock so the crossover is observable.

Run it:

cargo run --release --example van_der_pol

Source

numra/examples/van_der_pol.rs on GitHub · Related book chapter

use numra::ode::{DoPri5, OdeProblem, Radau5, Solver, SolverOptions};
use std::time::Instant;

fn main() {
    println!("=== Van der Pol Oscillator ===\n");
    println!("System: x'' - μ(1 - x²)x' + x = 0");
    println!("Initial conditions: x(0) = 2, x'(0) = 0\n");

    // Test with increasing stiffness
    let mu_values = [1.0, 10.0, 100.0];

    for &mu in &mu_values {
        println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        println!("μ = {}", mu);
        println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

        // Integration time depends on stiffness (period ≈ (3 - 2ln2)μ for large μ)
        let tf = if mu <= 10.0 { 20.0 } else { 2.0 * mu };

        // Create the ODE problem
        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,
            tf,
            vec![2.0, 0.0],
        );

        let options = SolverOptions::default().rtol(1e-4).atol(1e-6);

        // Try DoPri5 (explicit)
        println!("DoPri5 (Explicit Runge-Kutta):");
        let start = Instant::now();
        let result_dopri = DoPri5::solve(&problem, 0.0, tf, &[2.0, 0.0], &options);
        let elapsed_dopri = start.elapsed();

        match result_dopri {
            Ok(result) => {
                let y_final = result.y_final().unwrap();
                println!("  ✓ Completed in {:.2?}", elapsed_dopri);
                println!(
                    "  Final state: x = {:.6}, y = {:.6}",
                    y_final[0], y_final[1]
                );
                println!(
                    "  Steps: {} accepted, {} rejected",
                    result.stats.n_accept, result.stats.n_reject
                );
                println!("  Function evaluations: {}", result.stats.n_eval);
            }
            Err(e) => {
                println!("  ✗ Failed: {}", e);
            }
        }
        println!();

        // Try Radau5 (implicit)
        println!("Radau5 (Implicit Runge-Kutta):");
        let start = Instant::now();
        let result_radau = Radau5::solve(&problem, 0.0, tf, &[2.0, 0.0], &options);
        let elapsed_radau = start.elapsed();

        match result_radau {
            Ok(result) => {
                let y_final = result.y_final().unwrap();
                println!("  ✓ Completed in {:.2?}", elapsed_radau);
                println!(
                    "  Final state: x = {:.6}, y = {:.6}",
                    y_final[0], y_final[1]
                );
                println!(
                    "  Steps: {} accepted, {} rejected",
                    result.stats.n_accept, result.stats.n_reject
                );
                println!("  Function evaluations: {}", result.stats.n_eval);
                println!("  Jacobian evaluations: {}", result.stats.n_jac);
                println!("  LU factorizations: {}", result.stats.n_lu);
            }
            Err(e) => {
                println!("  ✗ Failed: {}", e);
            }
        }
        println!();
    }

    // Special case: Very stiff μ = 1000
    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    println!("μ = 1000 (VERY STIFF - Week 3 Deliverable)");
    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

    // For μ=1000, the transient is over in about 2*μ time units
    // We integrate just past the initial transient to show the solver handles stiffness
    let mu = 1000.0;
    let tf = 11.0; // Just past first quasi-period

    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,
        tf,
        vec![2.0, 0.0],
    );

    // Relaxed tolerances for very stiff problem
    let options = SolverOptions::default().rtol(1e-2).atol(1e-3);

    println!("Radau5 (Implicit Runge-Kutta):");
    println!("  Tolerances: rtol = 1e-2, atol = 1e-3");
    println!("  Integration to t = {} (just past initial transient)", tf);
    let start = Instant::now();
    let result = Radau5::solve(&problem, 0.0, tf, &[2.0, 0.0], &options);
    let elapsed = start.elapsed();

    match result {
        Ok(result) => {
            let y_final = result.y_final().unwrap();
            println!("  ✓ Completed in {:.2?}", elapsed);
            println!(
                "  Final state: x = {:.6}, y = {:.6}",
                y_final[0], y_final[1]
            );
            println!(
                "  Steps: {} accepted, {} rejected",
                result.stats.n_accept, result.stats.n_reject
            );
            println!("  Function evaluations: {}", result.stats.n_eval);
            println!("  Jacobian evaluations: {}", result.stats.n_jac);
            println!("  LU factorizations: {}", result.stats.n_lu);

            // For μ=1000, the solution should be near x ≈ -2 after first jump
            println!("\n  ✓ Successfully integrated through stiff transient!");
        }
        Err(e) => {
            println!("  ✗ Failed: {}", e);
            println!("\n  Note: For μ=1000, this is an extremely stiff problem.");
        }
    }

    // Also demonstrate with explicit solver for comparison
    println!("\nDoPri5 (Explicit Runge-Kutta) for comparison:");
    let options_dopri = SolverOptions::default().rtol(1e-2).atol(1e-3);
    let start = Instant::now();
    let result_dopri = DoPri5::solve(&problem, 0.0, tf, &[2.0, 0.0], &options_dopri);
    let elapsed_dopri = start.elapsed();

    match result_dopri {
        Ok(result) => {
            let y_final = result.y_final().unwrap();
            println!("  ✓ Completed in {:.2?}", elapsed_dopri);
            println!(
                "  Final state: x = {:.6}, y = {:.6}",
                y_final[0], y_final[1]
            );
            println!(
                "  Steps: {} accepted, {} rejected",
                result.stats.n_accept, result.stats.n_reject
            );
        }
        Err(e) => {
            println!("  ✗ Failed: {}", e);
        }
    }

    println!("\n=== Summary ===\n");
    println!("The Van der Pol oscillator demonstrates the importance of");
    println!("implicit methods for stiff ODEs:");
    println!();
    println!("• For μ ≤ 10: Both explicit (DoPri5) and implicit (Radau5) work well");
    println!("• For μ = 100: Explicit methods struggle, implicit methods excel");
    println!("• For μ = 1000: Only robust implicit methods can handle the stiffness");
    println!();
    println!("Radau5 is L-stable, making it ideal for stiff problems where the");
    println!("solution has fast transients that need to be damped quickly.");
}