Stochastic Heat Equation
Linear parabolic SPDE with additive space-time white noise; deterministic limit recovered as σ → 0.
- advanced
- stiff
The stochastic heat equation adds spatially-correlated white noise to the standard parabolic heat equation:
∂T/∂t = α ∂²T/∂x² + σ ξ(x, t)
T(0, t) = T(L, t) = 0 (Dirichlet)
T(x, 0) = sin(πx/L) (initial condition)
The deterministic part decays as sin(πx/L) exp(-α π² t / L²); the
stochastic forcing perturbs trajectories around that mean. The example
runs at a few different noise intensities so you can watch the
solution relax back to the deterministic limit as σ → 0.
Run it:
cargo run --release --example stochastic_heat Source
numra/examples/stochastic_heat.rs on GitHub
·
Related book chapter
use numra_pde::Grid1D;
use numra_spde::{MolSdeSolver, SpdeEnsemble, SpdeOptions, SpdeSolver, SpdeSystem};
/// Stochastic heat equation system.
struct StochasticHeat {
/// Thermal diffusivity
alpha: f64,
/// Noise intensity
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();
// Heat equation: du/dt = α d²u/dx²
// Using central differences with Dirichlet BCs (T=0 at boundaries)
for i in 0..n {
let u_left = if i == 0 { 0.0 } else { u[i - 1] }; // Left BC
let u_right = if i == n - 1 { 0.0 } else { u[i + 1] }; // Right BC
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>) {
// Additive noise: constant diffusion coefficient
for s in sigma.iter_mut() {
*s = self.sigma;
}
}
fn is_additive(&self) -> bool {
true
}
}
fn main() {
println!("=== Stochastic Heat Equation (SPDE) ===\n");
// Physical parameters
let alpha = 0.01; // Thermal diffusivity
let sigma = 0.05; // Noise intensity
let length = 1.0; // Domain length
let t_final = 0.5; // Simulation time
println!("Parameters:");
println!(" Thermal diffusivity α = {}", alpha);
println!(" Noise intensity σ = {}", sigma);
println!(" Domain length L = {} m", length);
println!(" Final time = {} s\n", t_final);
// Create spatial grid
let n_points = 41;
let grid = Grid1D::uniform(0.0, length, n_points);
let dx = grid.dx_uniform();
println!("Grid: {} points, Δx = {:.4} m", n_points, dx);
// Create SPDE system
let system = StochasticHeat { alpha, sigma };
// Initial condition: sin(πx)
let u0: Vec<f64> = grid
.interior_points()
.iter()
.map(|&x| (std::f64::consts::PI * x / length).sin())
.collect();
// Time step (stability: dt < dx² / (2α) for explicit methods)
let dt_stability = dx * dx / (2.0 * alpha);
let dt = (dt_stability * 0.5).min(0.001); // Conservative time step
println!(
"Time step: {:.6} s (stability limit: {:.6} s)\n",
dt, dt_stability
);
// Solver options
let options = SpdeOptions::default().dt(dt).n_output(50).seed(12345);
// Single trajectory
println!("Solving single trajectory...\n");
let result = MolSdeSolver::solve(&system, 0.0, t_final, &u0, &grid, &options).unwrap();
println!("Solver Statistics:");
println!(" Steps taken: {}", result.stats.n_steps);
println!(" Drift evaluations: {}", result.stats.n_drift);
println!(" Diffusion evaluations: {}", result.stats.n_diffusion);
// Compare with deterministic solution
let y_final = result.y_final().unwrap();
let decay_rate = alpha * std::f64::consts::PI * std::f64::consts::PI / (length * length);
let decay_factor = (-decay_rate * t_final).exp();
println!("\n=== Solution at t = {} s ===", t_final);
println!("x (m)\t\tStochastic\tDeterministic");
println!("{}\t{}\t{}", "-".repeat(6), "-".repeat(10), "-".repeat(13));
let interior_pts = grid.interior_points();
for i in (0..y_final.len()).step_by(y_final.len() / 8) {
let x = interior_pts[i];
let deterministic = (std::f64::consts::PI * x / length).sin() * decay_factor;
println!("{:.4}\t\t{:.5}\t\t{:.5}", x, y_final[i], deterministic);
}
// Monte Carlo ensemble
println!("\n=== Monte Carlo Ensemble (20 trajectories) ===\n");
let n_trajectories = 20;
let ensemble_options = SpdeOptions::default().dt(dt).n_output(20).seed(42);
let results = SpdeEnsemble::solve(
&system,
0.0,
t_final,
&u0,
&grid,
&ensemble_options,
n_trajectories,
)
.unwrap();
// Compute ensemble statistics
let mean = SpdeEnsemble::mean(&results);
let std = SpdeEnsemble::std(&results, &mean);
// Get final time statistics
let final_idx = mean.len() - 1;
let mean_final = &mean[final_idx];
let std_final = &std[final_idx];
println!("Final time statistics at midpoint (x = L/2):");
let mid_idx = y_final.len() / 2;
let det_mid = (std::f64::consts::PI * 0.5).sin() * decay_factor;
println!(" Deterministic: {:.5}", det_mid);
println!(" Ensemble mean: {:.5}", mean_final[mid_idx]);
println!(" Ensemble std: {:.5}", std_final[mid_idx]);
println!(
" Mean error: {:.5}",
(mean_final[mid_idx] - det_mid).abs()
);
// Time evolution at midpoint
println!("\n=== Time Evolution at x = L/2 ===");
println!("t (s)\t\tMean\t\tStd\t\tDeterministic");
for (i, mean_t) in mean.iter().enumerate().step_by(mean.len() / 5) {
let t = results[0].t[i];
let det = (std::f64::consts::PI * 0.5).sin() * (-decay_rate * t).exp();
println!(
"{:.3}\t\t{:.4}\t\t{:.4}\t\t{:.4}",
t, mean_t[mid_idx], std[i][mid_idx], det
);
}
// Final row
let t_last = t_final;
let det_last = (std::f64::consts::PI * 0.5).sin() * (-decay_rate * t_last).exp();
println!(
"{:.3}\t\t{:.4}\t\t{:.4}\t\t{:.4}",
t_last, mean_final[mid_idx], std_final[mid_idx], det_last
);
// Spatial variance profile
println!("\n=== Final Spatial Variance Profile ===");
println!("x (m)\t\tStd");
for i in (0..std_final.len()).step_by(std_final.len() / 8) {
let x = interior_pts[i];
println!("{:.4}\t\t{:.5}", x, std_final[i]);
}
// Summary
println!("\n=== Summary ===");
let max_std: f64 = std_final.iter().cloned().fold(0.0, f64::max);
let mean_std: f64 = std_final.iter().sum::<f64>() / std_final.len() as f64;
println!("Noise intensity σ = {}", sigma);
println!("Maximum spatial std at t_final: {:.5}", max_std);
println!("Mean spatial std at t_final: {:.5}", mean_std);
println!("Std / σ ratio (spatial avg): {:.2}", mean_std / sigma);
}