Hybrid

Bouncing Ball (Event Detection)

Hybrid system showing event detection at ground contact, with restitution-based velocity reset.

  • beginner
  • events
  • dense output

A ball falling under gravity, bouncing off the floor with coefficient of restitution e. The continuous dynamics are trivial; the interesting bit is the discrete reset at each ground contact.

y'' = -g
v_after = -e * v_before     at every event

The example chains integration segments — one per bounce — and uses the dense-output interpolant to locate ground contact precisely between steps. It’s the canonical introduction to numra’s event-detection API.

Run it:

cargo run --release --example bouncing_ball

Source

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

use numra::ode::events::{EventAction, EventDirection, EventFunction};
use numra::ode::{DoPri5, OdeProblem, Solver, SolverOptions};

/// Event function that detects when the ball hits the ground (height = 0).
struct GroundContact;

impl EventFunction<f64> for GroundContact {
    fn evaluate(&self, _t: f64, y: &[f64]) -> f64 {
        y[0] // height; event triggers when this crosses zero
    }

    fn direction(&self) -> EventDirection {
        EventDirection::Falling // only when height goes from positive to negative
    }

    fn action(&self) -> EventAction {
        EventAction::Stop // stop integration so we can apply the bounce
    }
}

fn main() {
    println!("=== Bouncing Ball with Event Detection ===\n");

    // Physical parameters
    let g = 9.81; // gravitational acceleration (m/s^2)
    let e = 0.8; // coefficient of restitution (energy loss per bounce)
    let h0 = 10.0; // initial height (m)
    let v0 = 0.0; // initial velocity (m/s), released from rest
    let t_end = 10.0; // maximum simulation time (s)
    let max_bounces = 20; // stop after this many bounces

    println!("Parameters:");
    println!("  Gravity g = {} m/s^2", g);
    println!("  Coefficient of restitution e = {}", e);
    println!("  Initial height h0 = {} m", h0);
    println!("  Initial velocity v0 = {} m/s", v0);
    println!("  Max simulation time = {} s", t_end);
    println!();

    // Analytical time to first bounce: t = sqrt(2*h0/g)
    let t_first_bounce = (2.0_f64 * h0 / g).sqrt();
    println!(
        "Analytical first bounce time: t = sqrt(2*h0/g) = {:.6} s",
        t_first_bounce
    );
    println!();

    // Storage for the full trajectory
    let mut all_t: Vec<f64> = Vec::new();
    let mut all_h: Vec<f64> = Vec::new();
    let mut all_v: Vec<f64> = Vec::new();

    // Bounce tracking
    let mut bounce_times: Vec<f64> = Vec::new();
    let mut bounce_velocities: Vec<f64> = Vec::new();

    // Current state
    let mut t_current = 0.0;
    let mut height = h0;
    let mut velocity = v0;
    let mut bounce_count = 0;

    println!("Simulating bounces...\n");
    println!(
        "  {:>6} | {:>10} | {:>12} | {:>12}",
        "Bounce", "Time (s)", "Impact v (m/s)", "Rebound v (m/s)"
    );
    println!("  {:-<6}|{:-<11}|{:-<13}|{:-<13}", "", "", "", "");

    while t_current < t_end && bounce_count < max_bounces {
        // Create ODE problem for this segment
        let problem = OdeProblem::new(
            |_t, y: &[f64], dydt: &mut [f64]| {
                dydt[0] = y[1]; // dh/dt = v
                dydt[1] = -g; // dv/dt = -g
            },
            t_current,
            t_end,
            vec![height, velocity],
        );

        let y0 = vec![height, velocity];

        let options = SolverOptions::default()
            .rtol(1e-10)
            .atol(1e-12)
            .event(Box::new(GroundContact));

        let result =
            DoPri5::solve(&problem, t_current, t_end, &y0, &options).expect("Solver failed");

        // Store trajectory points from this segment
        for (t, state) in result.iter() {
            all_t.push(t);
            all_h.push(state[0]);
            all_v.push(state[1]);
        }

        if result.terminated_by_event && !result.events.is_empty() {
            let ev = &result.events[0];
            bounce_count += 1;

            let impact_velocity = ev.y[1];
            let rebound_velocity = -e * impact_velocity;

            println!(
                "  {:>6} | {:>10.6} | {:>12.6} | {:>12.6}",
                bounce_count, ev.t, impact_velocity, rebound_velocity
            );

            bounce_times.push(ev.t);
            bounce_velocities.push(impact_velocity);

            // Update state for next segment
            t_current = ev.t;
            height = 0.0; // on the ground
            velocity = rebound_velocity;

            // Stop if rebound velocity is very small (ball has essentially stopped)
            if rebound_velocity.abs() < 0.01 {
                println!("\n  Ball has essentially stopped (rebound v < 0.01 m/s).");
                break;
            }
        } else {
            // No event: integration reached t_end
            t_current = t_end;
        }
    }

    println!();

    // Summary statistics
    println!("=== Summary ===\n");
    println!("  Total bounces detected: {}", bounce_count);
    println!("  Total trajectory points: {}", all_t.len());

    if let Some(&last_bounce_t) = bounce_times.last() {
        println!("  Time of last bounce: {:.6} s", last_bounce_t);
    }
    println!();

    // Verify first bounce against analytical solution
    if !bounce_times.is_empty() {
        let numerical_first = bounce_times[0];
        let error = (numerical_first - t_first_bounce).abs();
        println!("Verification (first bounce):");
        println!("  Analytical: t = {:.10} s", t_first_bounce);
        println!("  Numerical:  t = {:.10} s", numerical_first);
        println!("  Error:      {:.2e} s", error);
        println!();
    }

    // Energy analysis
    println!("Energy analysis:");
    println!(
        "  {:>6} | {:>12} | {:>12} | {:>8}",
        "Bounce", "Max height (m)", "KE at impact", "KE ratio"
    );
    println!("  {:-<6}|{:-<13}|{:-<13}|{:-<9}", "", "", "", "");

    let initial_ke = 0.5 * (2.0 * g * h0); // KE at first impact = mgh0
    for (i, &v_impact) in bounce_velocities.iter().enumerate() {
        let ke = 0.5 * v_impact * v_impact;
        let max_h = v_impact * v_impact / (2.0 * g); // height before this bounce
        let ke_ratio = ke / initial_ke;
        println!(
            "  {:>6} | {:>12.6} | {:>12.6} | {:>8.4}",
            i + 1,
            max_h,
            ke,
            ke_ratio
        );
    }
    println!();

    // Theoretical energy: at bounce n, the impact KE ratio is e^(2*(n-1))
    // because the first bounce has full energy, and each bounce loses factor e^2.
    println!("Energy loss verification:");
    println!("  After {} bounces:", bounce_count);
    let theoretical_ratio = e.powi(2 * (bounce_count - 1));
    let actual_ratio = if !bounce_velocities.is_empty() {
        let last_ke = 0.5 * bounce_velocities.last().unwrap().powi(2);
        last_ke / initial_ke
    } else {
        1.0
    };
    println!(
        "  Theoretical impact KE ratio at last bounce (e^{{2(n-1)}}): {:.8}",
        theoretical_ratio
    );
    println!(
        "  Numerical KE ratio:                                     {:.8}",
        actual_ratio
    );
    println!(
        "  Match: {}",
        if (theoretical_ratio - actual_ratio).abs() < 1e-4 {
            "YES"
        } else {
            "NO"
        }
    );
    println!();

    // Print a few sample trajectory points
    println!("Sample trajectory:");
    println!(
        "  {:>10} | {:>12} | {:>12}",
        "t (s)", "height (m)", "velocity (m/s)"
    );
    println!("  {:-<10}|{:-<13}|{:-<13}", "", "", "");

    let n_samples = 20;
    let step = (all_t.len() / n_samples).max(1);
    for i in (0..all_t.len()).step_by(step) {
        println!(
            "  {:>10.4} | {:>12.6} | {:>12.6}",
            all_t[i], all_h[i], all_v[i]
        );
    }
    // Always print the last point
    if let Some(last_idx) = all_t.len().checked_sub(1) {
        if last_idx % step != 0 {
            println!(
                "  {:>10.4} | {:>12.6} | {:>12.6}",
                all_t[last_idx], all_h[last_idx], all_v[last_idx]
            );
        }
    }

    println!();
    println!("=== Done ===");
}