DAE

Pendulum DAE

Pendulum solved both as an ODE in polar coordinates and as a DAE with a holonomic constraint.

  • intermediate

A simple pendulum admits two formulations: a low-dimensional ODE in polar coordinates, or a constrained system in Cartesian coordinates where the rod-length constraint x² + y² = L² becomes an algebraic equation alongside Newton’s law.

θ'' = -(g/L) sin(θ)              (ODE)
M y' = f(t, y),  g(y) = 0       (DAE, with M singular)

This example walks both formulations and reconciles the trajectories, useful as a teaching tool before the bigger DAE problems in chapter 3.

Run it:

cargo run --release --example pendulum_dae

Source

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

use numra::ode::{DaeProblem, DoPri5, OdeProblem, Solver, SolverOptions};

fn main() {
    println!("=== Simple Pendulum Example ===\n");

    // Physical parameters
    let l = 1.0; // Pendulum length
    let g = 9.81; // Gravity

    println!("Parameters:");
    println!("  Length L: {} m", l);
    println!("  Gravity g: {} m/s²", g);
    println!();

    // Initial conditions: pendulum at angle θ₀ = π/4, released from rest
    let theta0 = std::f64::consts::FRAC_PI_4; // 45 degrees
    let omega0 = 0.0; // Angular velocity

    println!("Initial conditions:");
    println!("  θ₀ = {:.2}° = {:.6} rad", theta0.to_degrees(), theta0);
    println!("  ω₀ = {} rad/s", omega0);
    println!();

    // ========================================
    // Part 1: Standard ODE formulation
    // ========================================
    println!("=== Part 1: Standard ODE (polar coordinates) ===\n");

    // State: [θ, ω] where ω = dθ/dt
    // θ' = ω
    // ω' = -(g/L) * sin(θ)
    let y0_ode = vec![theta0, omega0];

    let pendulum_ode = move |_t: f64, y: &[f64], dydt: &mut [f64]| {
        let theta = y[0];
        let omega = y[1];
        dydt[0] = omega;
        dydt[1] = -(g / l) * theta.sin();
    };

    let problem = OdeProblem::new(pendulum_ode, 0.0, 10.0, y0_ode.clone());
    let options = SolverOptions::default().rtol(1e-10).atol(1e-12);

    let result = DoPri5::solve(&problem, 0.0, 10.0, &y0_ode, &options)
        .expect("Failed to solve pendulum ODE");

    println!("ODE solved successfully!");
    println!("  Time points: {}", result.len());
    println!("  Accepted steps: {}", result.stats.n_accept);
    println!();

    // Compute period from small-angle approximation: T = 2π√(L/g)
    let period_approx = 2.0 * std::f64::consts::PI * (l / g).sqrt();
    println!("Theoretical period (small angle): {:.4} s", period_approx);
    println!();

    // Display trajectory
    println!("Trajectory (θ in degrees):");
    println!(
        "  {:>6} | {:>10} | {:>10} | {:>10}",
        "t", "θ (deg)", "ω (rad/s)", "E/E₀"
    );
    println!("  {:-<6}|{:-<11}|{:-<11}|{:-<11}", "", "", "", "");

    // Initial energy
    let e0 = 0.5 * l * l * omega0 * omega0 + g * l * (1.0 - theta0.cos());

    let sample_step = (result.len() / 15).max(1);
    for (i, &t) in result.t.iter().enumerate() {
        if i % sample_step == 0 || i == result.len() - 1 {
            let state = result.y_at(i);
            let theta = state[0];
            let omega = state[1];
            let energy = 0.5 * l * l * omega * omega + g * l * (1.0 - theta.cos());

            println!(
                "  {:>6.2} | {:>10.4} | {:>10.4} | {:>10.8}",
                t,
                theta.to_degrees(),
                omega,
                energy / e0
            );
        }
    }

    // ========================================
    // Part 2: Cartesian formulation (demonstrates DAE structure)
    // ========================================
    println!("\n=== Part 2: Cartesian DAE Structure ===\n");

    // Convert to Cartesian for DAE demonstration
    let x0 = l * theta0.sin();
    let y0_cart = -l * theta0.cos();
    let vx0 = 0.0;
    let vy0 = 0.0;

    println!("Cartesian initial conditions:");
    println!("  x₀ = {:.6}", x0);
    println!("  y₀ = {:.6}", y0_cart);
    println!(
        "  Constraint check: x² + y² = {:.6} (should be L² = {})",
        x0 * x0 + y0_cart * y0_cart,
        l * l
    );
    println!();

    // DAE structure: M * [x', y', vx', vy', λ']ᵀ = [vx, vy, -λx, -λy-g, constraint]ᵀ
    // where M = diag(1,1,1,1,0) - the last row has M[4,4]=0 (algebraic)

    // For a true DAE solver, we would use:
    let rhs = move |_t: f64, y: &[f64], dydt: &mut [f64]| {
        let x = y[0];
        let y_pos = y[1];
        let vx = y[2];
        let vy = y[3];
        let lambda = y[4];

        dydt[0] = vx;
        dydt[1] = vy;
        dydt[2] = -lambda * x;
        dydt[3] = -lambda * y_pos - g;
        // Constraint: x*vx + y*vy = 0 (velocity perpendicular to position)
        // This is the differentiated constraint from x² + y² = L²
        dydt[4] = x * vx + y_pos * vy;
    };

    let mass = |m: &mut [f64]| {
        for val in m.iter_mut().take(25) {
            *val = 0.0;
        }
        m[0] = 1.0; // M[0,0] = 1
        m[6] = 1.0; // M[1,1] = 1
        m[12] = 1.0; // M[2,2] = 1
        m[18] = 1.0; // M[3,3] = 1
                     // m[24] = 0.0 - algebraic equation for λ
    };

    // Initial λ from constraint at rest: vx' = vy' = 0
    // From 0 = -λ*y - g => λ = -g/y (at rest)
    let lambda0 = -g / y0_cart;

    let _dae_problem = DaeProblem::new(
        rhs,
        mass,
        0.0,
        10.0,
        vec![x0, y0_cart, vx0, vy0, lambda0],
        vec![4], // λ (index 4) is algebraic
    );

    println!("DAE structure created:");
    println!("  State dimension: 5 (x, y, vx, vy, λ)");
    println!("  Algebraic indices: [4] (constraint force λ)");
    println!("  Mass matrix: diag(1, 1, 1, 1, 0)");
    println!();
    println!("Note: Full DAE support with singular mass matrices in implicit solvers");
    println!("like Radau5 requires additional implementation (LU with pivoting for");
    println!("the augmented system). The ODE formulation above demonstrates the physics.");
    println!();

    // Verify conservation
    let final_state = result.y_final().unwrap();
    let theta_f = final_state[0];
    let omega_f = final_state[1];
    let e_final = 0.5 * l * l * omega_f * omega_f + g * l * (1.0 - theta_f.cos());

    println!("Final state verification:");
    println!("  Final θ: {:.4}°", theta_f.to_degrees());
    println!("  Final ω: {:.4} rad/s", omega_f);
    println!("  Energy conservation: E/E₀ = {:.10}", e_final / e0);
    println!("  (Should be 1.0 for a conservative system)");
}