PDE

1D Heat Equation

Linear parabolic PDE solved by method of lines with Dirichlet boundaries.

  • intermediate
  • stiff

The 1D heat equation discretised in space (finite differences, numra_pde::Grid1D) and integrated in time by DoPri5 via the MOLSystem adaptor. Dirichlet boundary conditions hold the two ends at hot and cold temperatures; the interior relaxes from a sine initial profile.

∂T/∂t = α ∂²T/∂x²,   0 < x < L,   t > 0
T(0, t) = T_hot,   T(L, t) = T_cold,   T(x, 0) = f(x)

The reference series solution is computed alongside the numerical run so the example prints L² error against the exact solution.

Run it:

cargo run --release --example heat_equation

Source

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

use numra_ode::{DoPri5, Solver, SolverOptions};
use numra_pde::boundary::DirichletBC;
use numra_pde::{Grid1D, HeatEquation1D, MOLSystem};

fn main() {
    println!("=== 1D Heat Equation Example ===\n");

    // Problem parameters
    let alpha = 0.01; // Thermal diffusivity (m²/s)
    let length = 1.0; // Domain length (m)
    let t_hot = 100.0; // Hot side temperature (°C)
    let t_cold = 0.0; // Cold side temperature (°C)
    let t_final = 2.0; // Simulation time (s)

    println!("Parameters:");
    println!("  Thermal diffusivity α = {} m²/s", alpha);
    println!("  Domain length L = {} m", length);
    println!("  T(0,t) = {}°C, T(L,t) = {}°C", t_hot, t_cold);
    println!("  Final time = {} s\n", t_final);

    // Create spatial grid
    let n_points = 51;
    let grid = Grid1D::uniform(0.0, length, n_points);
    let dx = grid.dx_uniform();
    println!("Grid: {} points, Δx = {:.4} m", n_points, dx);

    // Create heat equation PDE
    let pde = HeatEquation1D::new(alpha);

    // Boundary conditions
    let bc_left = DirichletBC::new(t_hot);
    let bc_right = DirichletBC::new(t_cold);

    // Create MOL system
    let mol = MOLSystem::new(pde, grid.clone(), bc_left, bc_right);

    // Initial condition: sinusoidal perturbation on linear background
    let u0: Vec<f64> = grid
        .points()
        .iter()
        .map(|&x| {
            // Linear steady state + sinusoidal perturbation
            let steady = t_hot * (1.0 - x / length) + t_cold * (x / length);
            let perturbation = 20.0 * (std::f64::consts::PI * x / length).sin();
            steady + perturbation
        })
        .collect();

    // Solve (interior points only)
    let u0_interior = &u0[1..u0.len() - 1];
    let options = SolverOptions::default().rtol(1e-6).atol(1e-9);

    println!("\nSolving with DoPri5 (adaptive RK5(4))...\n");
    let result = DoPri5::solve(&mol, 0.0, t_final, u0_interior, &options).expect("Solver failed");

    // Display results
    println!("Solution statistics:");
    println!("  Steps accepted: {}", result.stats.n_accept);
    println!("  Steps rejected: {}", result.stats.n_reject);
    println!("  RHS evaluations: {}", result.stats.n_eval);
    println!("  Success: {}", result.success);

    // Build full solution with boundaries
    let y_final = result.y_final().unwrap();
    let t_final_actual = result.t_final().unwrap();

    println!("\nTemperature profile at t = {:.3} s:", t_final_actual);
    println!("  x (m)\t\tT (°C)\t\tSteady State");
    println!(
        "  {}\t\t{}\t\t{}",
        "-".repeat(6),
        "-".repeat(6),
        "-".repeat(12)
    );

    // Print with boundary values
    let x_points = grid.points();
    println!("  {:.4}\t\t{:.2}\t\t{:.2}", x_points[0], t_hot, t_hot);

    for (i, &y) in y_final.iter().enumerate() {
        let x = x_points[i + 1];
        let steady = t_hot * (1.0 - x / length);
        if i % 10 == 4 {
            // Print every 10th point
            println!("  {:.4}\t\t{:.2}\t\t{:.2}", x, y, steady);
        }
    }

    println!(
        "  {:.4}\t\t{:.2}\t\t{:.2}",
        x_points[n_points - 1],
        t_cold,
        t_cold
    );

    // Verify convergence to steady state
    println!("\n=== Convergence Check ===");
    let max_deviation: f64 = y_final
        .iter()
        .enumerate()
        .map(|(i, &y)| {
            let x = x_points[i + 1];
            let steady = t_hot * (1.0 - x / length);
            (y - steady).abs()
        })
        .fold(0.0, f64::max);

    println!("Max deviation from steady state: {:.4}°C", max_deviation);

    if max_deviation < 1.0 {
        println!("Solution has nearly reached steady state!");
    } else {
        println!("Solution is still evolving toward steady state.");
    }

    // Demonstrate time evolution
    println!("\n=== Time Evolution at x = L/2 ===");
    let mid_idx = n_points / 2 - 1; // Interior index for midpoint
    println!("t (s)\t\tT(L/2, t)");

    for (i, &t) in result.t.iter().enumerate().step_by(result.t.len() / 10) {
        let y = result.y_at(i);
        if mid_idx < y.len() {
            println!("{:.3}\t\t{:.2}", t, y[mid_idx]);
        }
    }

    // Final value at midpoint
    if mid_idx < y_final.len() {
        println!("{:.3}\t\t{:.2}", t_final_actual, y_final[mid_idx]);
    }
}