SPDE

SPDE Heat — Adaptive Time Stepping

Stochastic heat equation comparing fixed vs. adaptive time stepping; convergence to deterministic limit.

  • advanced
  • stiff

A more advanced cousin of stochastic_heat. Instead of running a single fixed-step integration, this example wires the stochastic heat equation through both fixed and adaptive time-step controllers and shows how an SPDE adaptive stepper handles the noise-driven stiffness spikes.

du/dt = α ∂²u/∂x² + σ dW(x, t)
u(0, t) = u(1, t) = 0,    u(x, 0) = sin(πx)

Use it as a template for your own SPDE work — the SpdeSystem trait implementation is the part most likely to transplant.

Run it:

cargo run --release --example spde_heat

Source

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

use numra::pde::Grid1D;
use numra::spde::{MolSdeSolver, SpdeOptions, SpdeSolver, SpdeSystem};

/// Stochastic heat equation: du = alpha * u_xx * dt + sigma * dW
struct StochasticHeat {
    alpha: f64,
    sigma: f64,
}

impl SpdeSystem<f64> for StochasticHeat {
    fn drift(&self, _t: f64, u: &[f64], du: &mut [f64], grid: &Grid1D<f64>) {
        let dx = grid.dx_uniform();
        let dx2 = dx * dx;
        let n = u.len();

        for i in 0..n {
            let u_left = if i == 0 { 0.0 } else { u[i - 1] };
            let u_right = if i == n - 1 { 0.0 } else { u[i + 1] };
            du[i] = self.alpha * (u_left - 2.0 * u[i] + u_right) / dx2;
        }
    }

    fn diffusion(&self, _t: f64, _u: &[f64], sigma: &mut [f64], _grid: &Grid1D<f64>) {
        for s in sigma.iter_mut() {
            *s = self.sigma;
        }
    }

    fn is_additive(&self) -> bool {
        true
    }
}

/// Compute the deterministic (exact) solution at time t and spatial points.
fn exact_solution(x: &[f64], t: f64, alpha: f64) -> Vec<f64> {
    let pi = std::f64::consts::PI;
    let decay = (-alpha * pi * pi * t).exp();
    x.iter().map(|&xi| (pi * xi).sin() * decay).collect()
}

fn main() {
    println!("=== Stochastic Heat Equation: Adaptive vs. Fixed Stepping ===\n");

    let alpha = 0.01;
    let length = 1.0;
    let t_final = 0.5;
    let n_points = 31; // total grid points (including boundaries)

    println!("PDE: du/dt = alpha * d^2u/dx^2 + sigma * dW");
    println!(
        "Domain: [0, {}], Dirichlet BCs: u(0)=u({})=0",
        length, length
    );
    println!("IC: u(x,0) = sin(pi*x)\n");

    // Create spatial grid
    let grid = Grid1D::uniform(0.0, length, n_points);
    let dx = grid.dx_uniform();

    println!(
        "Spatial grid: {} total points, {} interior, dx = {:.5}",
        n_points,
        n_points - 2,
        dx
    );

    // Initial condition on interior points
    let interior = grid.interior_points();
    let u0: Vec<f64> = interior
        .iter()
        .map(|&x| (std::f64::consts::PI * x / length).sin())
        .collect();

    // Stability limit for explicit Euler: dt < dx^2 / (2*alpha)
    let dt_stability = dx * dx / (2.0 * alpha);
    let dt_fixed = (dt_stability * 0.4).min(0.001);
    println!("Stability limit dt_max = {:.6}", dt_stability);
    println!("Fixed time step dt = {:.6}\n", dt_fixed);

    // =============================================
    // Part 1: Convergence as noise goes to zero
    // =============================================
    println!("=== Part 1: Noise-to-Zero Convergence ===\n");

    let noise_levels = [0.0, 0.01, 0.05, 0.1, 0.2];
    let exact_final = exact_solution(interior, t_final, alpha);

    println!(
        "{:>8} | {:>12} | {:>12} | {:>8}",
        "sigma", "L2 error", "Linf error", "steps"
    );
    println!("{:-<8}|{:-<13}|{:-<13}|{:-<9}", "", "", "", "");

    for &sigma in &noise_levels {
        let system = StochasticHeat { alpha, sigma };

        let options = SpdeOptions::default().dt(dt_fixed).n_output(50).seed(42);

        let result = MolSdeSolver::solve(&system, 0.0, t_final, &u0, &grid, &options)
            .expect("Solver failed");

        let y_final = result.y_final().unwrap();

        // Compute errors vs. deterministic solution
        let mut l2_err = 0.0;
        let mut linf_err: f64 = 0.0;
        for (i, &exact_val) in exact_final.iter().enumerate() {
            let diff = (y_final[i] - exact_val).abs();
            l2_err += diff * diff;
            linf_err = linf_err.max(diff);
        }
        l2_err = (l2_err / exact_final.len() as f64).sqrt();

        println!(
            "{:>8.3} | {:>12.6} | {:>12.6} | {:>8}",
            sigma, l2_err, linf_err, result.stats.n_steps
        );
    }

    println!("\nNote: With sigma=0 the error is purely from spatial/temporal discretization.");
    println!("As sigma increases, stochastic fluctuations dominate the error.\n");

    // =============================================
    // Part 2: Fixed vs. Adaptive Time Stepping
    // =============================================
    println!("=== Part 2: Fixed vs. Adaptive Time Stepping ===\n");

    let sigma = 0.05;
    let system = StochasticHeat { alpha, sigma };

    // Fixed time stepping
    let options_fixed = SpdeOptions::default().dt(dt_fixed).n_output(50).seed(42);

    println!("Solving with fixed time step (dt = {:.6})...", dt_fixed);
    let result_fixed = MolSdeSolver::solve(&system, 0.0, t_final, &u0, &grid, &options_fixed)
        .expect("Fixed solver failed");

    println!("  Steps: {}", result_fixed.stats.n_steps);
    println!("  Drift evals: {}", result_fixed.stats.n_drift);
    println!("  Diffusion evals: {}", result_fixed.stats.n_diffusion);

    // Adaptive time stepping
    let options_adaptive = SpdeOptions::default()
        .adaptive(true)
        .dt(dt_fixed)
        .rtol(1e-3)
        .atol(1e-6)
        .dt_min(1e-8)
        .dt_max(dt_stability * 0.8)
        .n_output(50)
        .seed(42);

    println!("\nSolving with adaptive time stepping...");
    let result_adaptive = MolSdeSolver::solve(&system, 0.0, t_final, &u0, &grid, &options_adaptive)
        .expect("Adaptive solver failed");

    println!("  Steps: {}", result_adaptive.stats.n_steps);
    println!("  Rejected steps: {}", result_adaptive.stats.n_reject);
    println!("  Drift evals: {}", result_adaptive.stats.n_drift);
    println!("  Diffusion evals: {}", result_adaptive.stats.n_diffusion);

    // Compare final solutions
    let y_fixed = result_fixed.y_final().unwrap();
    let y_adaptive = result_adaptive.y_final().unwrap();

    println!("\n  Comparison at final time t = {}:", t_final);
    println!(
        "  {:>8} | {:>10} | {:>10} | {:>10}",
        "x", "Fixed", "Adaptive", "Exact"
    );
    println!("  {:-<8}|{:-<11}|{:-<11}|{:-<11}", "", "", "", "");

    for i in (0..interior.len()).step_by((interior.len() / 8).max(1)) {
        let x = interior[i];
        println!(
            "  {:>8.4} | {:>10.5} | {:>10.5} | {:>10.5}",
            x, y_fixed[i], y_adaptive[i], exact_final[i]
        );
    }

    // =============================================
    // Part 3: Spatial Profile Evolution
    // =============================================
    println!("\n=== Part 3: Spatial Profile at Selected Times ===\n");

    let system = StochasticHeat { alpha, sigma };
    let options = SpdeOptions::default().dt(dt_fixed).n_output(100).seed(42);

    let result =
        MolSdeSolver::solve(&system, 0.0, t_final, &u0, &grid, &options).expect("Solver failed");

    // Print profiles at selected times
    let target_times = [0.0, 0.1, 0.2, 0.3, 0.5];
    let mid_idx = interior.len() / 2;

    println!(
        "  {:>8} | {:>10} | {:>10} | {:>10}",
        "t", "u(0.5) num", "u(0.5) exact", "difference"
    );
    println!("  {:-<8}|{:-<11}|{:-<11}|{:-<11}", "", "", "", "");

    let mut target_idx = 0;
    for (i, &t) in result.t.iter().enumerate() {
        if target_idx < target_times.len() && t >= target_times[target_idx] {
            let u_mid = result.y[i][mid_idx];
            let exact_mid = (std::f64::consts::PI * interior[mid_idx]).sin()
                * (-alpha * std::f64::consts::PI * std::f64::consts::PI * t).exp();
            let diff = u_mid - exact_mid;
            println!(
                "  {:>8.3} | {:>10.5} | {:>10.5} | {:>+10.5}",
                t, u_mid, exact_mid, diff
            );
            target_idx += 1;
        }
    }

    // Summary
    println!("\n=== Summary ===\n");
    println!("The stochastic heat equation adds white noise to the classical heat equation.");
    println!("Key observations:");
    println!("  - The deterministic solution decays exponentially: u ~ exp(-alpha*pi^2*t)");
    println!("  - Noise causes fluctuations around the deterministic trajectory");
    println!("  - With sigma=0, only discretization error remains");
    println!("  - Adaptive time stepping adjusts dt based on local error estimates");

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