Lorenz Attractor
Chaotic three-state ODE solved with DoPri5, plus uncertainty propagation across initial conditions.
- beginner
The Lorenz system is the canonical “first chaotic ODE” — three coupled
nonlinear equations that produce a butterfly-shaped strange attractor.
This example integrates with the DoPri5 adaptive Runge-Kutta solver
and then runs the same integration through numra::uncertainty to show
how a small perturbation in initial conditions diverges over time.
The math:
dx/dt = σ(y - x)
dy/dt = x(ρ - z) - y
dz/dt = xy - βz
with the standard parameters σ = 10, ρ = 28, β = 8/3.
Run it:
cargo run --release --example lorenz Source
numra/examples/lorenz.rs on GitHub
·
Related book chapter
use numra::ode::{DoPri5, OdeProblem, Solver, SolverOptions};
use numra::uncertainty::{compute_sensitivities, Uncertain};
/// Lorenz system parameters
struct LorenzParams {
sigma: f64,
rho: f64,
beta: f64,
}
impl Default for LorenzParams {
fn default() -> Self {
Self {
sigma: 10.0,
rho: 28.0,
beta: 8.0 / 3.0,
}
}
}
fn main() {
println!("=== Lorenz Attractor with Uncertainty ===\n");
// Standard Lorenz parameters
let params = LorenzParams::default();
// Initial conditions (with uncertainty)
let x0 = Uncertain::from_std(1.0, 0.1); // x = 1.0 ± 0.1
let y0 = Uncertain::from_std(1.0, 0.1); // y = 1.0 ± 0.1
let z0 = Uncertain::from_std(1.0, 0.1); // z = 1.0 ± 0.1
println!("Initial conditions:");
println!(" x₀ = {:.3} ± {:.3}", x0.mean, x0.std());
println!(" y₀ = {:.3} ± {:.3}", y0.mean, y0.std());
println!(" z₀ = {:.3} ± {:.3}", z0.mean, z0.std());
println!();
// Create the ODE problem
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
let (x, y_val, z) = (y[0], y[1], y[2]);
let sigma = 10.0;
let rho = 28.0;
let beta = 8.0 / 3.0;
dydt[0] = sigma * (y_val - x);
dydt[1] = x * (rho - z) - y_val;
dydt[2] = x * y_val - beta * z;
},
0.0,
20.0,
vec![x0.mean, y0.mean, z0.mean],
);
// Solve with default options
let options = SolverOptions::default().rtol(1e-8).atol(1e-10);
println!("Solving Lorenz system from t=0 to t=20...");
let result = DoPri5::solve(&problem, 0.0, 20.0, &[x0.mean, y0.mean, z0.mean], &options)
.expect("Solver failed");
// Get solution statistics
println!(" Steps accepted: {}", result.stats.n_accept);
println!(" Steps rejected: {}", result.stats.n_reject);
println!(" Function evals: {}", result.stats.n_eval);
println!();
// Final state
let y_final = result.y_final().expect("No final state");
println!("Final state at t=20:");
println!(" x = {:.6}", y_final[0]);
println!(" y = {:.6}", y_final[1]);
println!(" z = {:.6}", y_final[2]);
println!();
// Trajectory summary
println!("Trajectory has {} points", result.t.len());
println!();
// === Sensitivity Analysis ===
println!("=== Sensitivity Analysis ===\n");
// Compute sensitivity of final z-coordinate with respect to parameters
let solve_for_z = |p: &[f64]| -> f64 {
let problem = OdeProblem::new(
move |_t, y: &[f64], dydt: &mut [f64]| {
let (x, y_val, z) = (y[0], y[1], y[2]);
dydt[0] = p[0] * (y_val - x); // σ
dydt[1] = x * (p[1] - z) - y_val; // ρ
dydt[2] = x * y_val - p[2] * z; // β
},
0.0,
5.0, // Shorter time for sensitivity (chaos makes long-time sensitivity ill-defined)
vec![1.0, 1.0, 1.0],
);
let options = SolverOptions::default().rtol(1e-6);
let result = DoPri5::solve(&problem, 0.0, 5.0, &[1.0, 1.0, 1.0], &options)
.expect("Solver failed in sensitivity");
result.y_final().unwrap()[2] // Return final z
};
let nominal_params = [params.sigma, params.rho, params.beta];
let param_names = ["σ", "ρ", "β"];
let sens_result = compute_sensitivities(solve_for_z, &nominal_params, ¶m_names, None);
println!("Sensitivity of z(t=5) to parameters:");
for s in &sens_result.sensitivities {
println!(
" ∂z/∂{}: {:.4} (normalized: {:.4})",
s.name, s.coefficient, s.normalized
);
}
println!();
// Find most sensitive parameter
if let Some(most_sens) = sens_result.most_sensitive() {
println!(
"Most sensitive parameter: {} (|normalized| = {:.4})",
most_sens.name,
most_sens.normalized.abs()
);
}
println!();
// === Uncertainty Propagation ===
println!("=== Uncertainty Propagation ===\n");
// Assume 5% uncertainty on each parameter
let param_std = [0.5, 1.4, 0.133]; // 5% of nominal
let param_vars: Vec<f64> = param_std.iter().map(|s| s * s).collect();
let total_var = sens_result.propagate_uncertainty(¶m_vars);
let total_std = total_var.sqrt();
println!("Parameter uncertainties:");
println!(" σ = {:.2} ± {:.3}", params.sigma, param_std[0]);
println!(" ρ = {:.2} ± {:.3}", params.rho, param_std[1]);
println!(" β = {:.4} ± {:.4}", params.beta, param_std[2]);
println!();
println!("Propagated uncertainty in z(t=5):");
println!(" z = {:.4} ± {:.4}", sens_result.output, total_std);
println!();
// Individual contributions
println!("Uncertainty contributions:");
for (i, s) in sens_result.sensitivities.iter().enumerate() {
let contribution = (s.coefficient * param_std[i]).abs();
let pct = 100.0 * contribution * contribution / total_var;
println!(" {}: {:.2}%", s.name, pct);
}
println!();
// === Ensemble of trajectories ===
println!("=== Ensemble with Perturbed Initial Conditions ===\n");
let n_samples = 5;
println!("Running {} trajectories with perturbed ICs...", n_samples);
let perturbations = [
[1.0, 1.0, 1.0],
[1.1, 1.0, 1.0],
[1.0, 1.1, 1.0],
[0.9, 1.0, 1.0],
[1.0, 0.9, 1.0],
];
for ic in perturbations.iter() {
let result = DoPri5::solve(&problem, 0.0, 10.0, ic, &options).expect("Solver failed");
let y_end = result.y_final().unwrap();
println!(
" IC [{:.1}, {:.1}, {:.1}] → z(10) = {:.4}",
ic[0], ic[1], ic[2], y_end[2]
);
}
println!();
println!("Note: The Lorenz system is chaotic - small perturbations in ICs");
println!("lead to exponentially diverging trajectories over time.");
println!();
println!("=== Done ===");
}