Skip to main content

lorenz/
lorenz.rs

1//! Lorenz Attractor Example with Uncertainty Propagation
2//!
3//! This example demonstrates:
4//! 1. Solving the Lorenz system using DoPri5
5//! 2. Uncertainty propagation for initial conditions
6//! 3. Sensitivity analysis for parameters
7//!
8//! The Lorenz system:
9//!   dx/dt = σ(y - x)
10//!   dy/dt = x(ρ - z) - y
11//!   dz/dt = xy - βz
12//!
13//! Standard parameters: σ = 10, ρ = 28, β = 8/3
14//!
15//! Author: Moussa Leblouba
16//! Date: 4 February 2026
17//! Modified: 2 May 2026
18
19use numra::ode::{DoPri5, OdeProblem, Solver, SolverOptions};
20use numra::uncertainty::{compute_sensitivities, Uncertain};
21
22/// Lorenz system parameters
23struct LorenzParams {
24    sigma: f64,
25    rho: f64,
26    beta: f64,
27}
28
29impl Default for LorenzParams {
30    fn default() -> Self {
31        Self {
32            sigma: 10.0,
33            rho: 28.0,
34            beta: 8.0 / 3.0,
35        }
36    }
37}
38
39fn main() {
40    println!("=== Lorenz Attractor with Uncertainty ===\n");
41
42    // Standard Lorenz parameters
43    let params = LorenzParams::default();
44
45    // Initial conditions (with uncertainty)
46    let x0 = Uncertain::from_std(1.0, 0.1); // x = 1.0 ± 0.1
47    let y0 = Uncertain::from_std(1.0, 0.1); // y = 1.0 ± 0.1
48    let z0 = Uncertain::from_std(1.0, 0.1); // z = 1.0 ± 0.1
49
50    println!("Initial conditions:");
51    println!("  x₀ = {:.3} ± {:.3}", x0.mean, x0.std());
52    println!("  y₀ = {:.3} ± {:.3}", y0.mean, y0.std());
53    println!("  z₀ = {:.3} ± {:.3}", z0.mean, z0.std());
54    println!();
55
56    // Create the ODE problem
57    let problem = OdeProblem::new(
58        |_t, y: &[f64], dydt: &mut [f64]| {
59            let (x, y_val, z) = (y[0], y[1], y[2]);
60            let sigma = 10.0;
61            let rho = 28.0;
62            let beta = 8.0 / 3.0;
63
64            dydt[0] = sigma * (y_val - x);
65            dydt[1] = x * (rho - z) - y_val;
66            dydt[2] = x * y_val - beta * z;
67        },
68        0.0,
69        20.0,
70        vec![x0.mean, y0.mean, z0.mean],
71    );
72
73    // Solve with default options
74    let options = SolverOptions::default().rtol(1e-8).atol(1e-10);
75
76    println!("Solving Lorenz system from t=0 to t=20...");
77    let result = DoPri5::solve(&problem, 0.0, 20.0, &[x0.mean, y0.mean, z0.mean], &options)
78        .expect("Solver failed");
79
80    // Get solution statistics
81    println!("  Steps accepted: {}", result.stats.n_accept);
82    println!("  Steps rejected: {}", result.stats.n_reject);
83    println!("  Function evals: {}", result.stats.n_eval);
84    println!();
85
86    // Final state
87    let y_final = result.y_final().expect("No final state");
88    println!("Final state at t=20:");
89    println!("  x = {:.6}", y_final[0]);
90    println!("  y = {:.6}", y_final[1]);
91    println!("  z = {:.6}", y_final[2]);
92    println!();
93
94    // Trajectory summary
95    println!("Trajectory has {} points", result.t.len());
96    println!();
97
98    // === Sensitivity Analysis ===
99    println!("=== Sensitivity Analysis ===\n");
100
101    // Compute sensitivity of final z-coordinate with respect to parameters
102    let solve_for_z = |p: &[f64]| -> f64 {
103        let problem = OdeProblem::new(
104            move |_t, y: &[f64], dydt: &mut [f64]| {
105                let (x, y_val, z) = (y[0], y[1], y[2]);
106                dydt[0] = p[0] * (y_val - x); // σ
107                dydt[1] = x * (p[1] - z) - y_val; // ρ
108                dydt[2] = x * y_val - p[2] * z; // β
109            },
110            0.0,
111            5.0, // Shorter time for sensitivity (chaos makes long-time sensitivity ill-defined)
112            vec![1.0, 1.0, 1.0],
113        );
114
115        let options = SolverOptions::default().rtol(1e-6);
116        let result = DoPri5::solve(&problem, 0.0, 5.0, &[1.0, 1.0, 1.0], &options)
117            .expect("Solver failed in sensitivity");
118        result.y_final().unwrap()[2] // Return final z
119    };
120
121    let nominal_params = [params.sigma, params.rho, params.beta];
122    let param_names = ["σ", "ρ", "β"];
123
124    let sens_result = compute_sensitivities(solve_for_z, &nominal_params, &param_names, None);
125
126    println!("Sensitivity of z(t=5) to parameters:");
127    for s in &sens_result.sensitivities {
128        println!(
129            "  ∂z/∂{}: {:.4} (normalized: {:.4})",
130            s.name, s.coefficient, s.normalized
131        );
132    }
133    println!();
134
135    // Find most sensitive parameter
136    if let Some(most_sens) = sens_result.most_sensitive() {
137        println!(
138            "Most sensitive parameter: {} (|normalized| = {:.4})",
139            most_sens.name,
140            most_sens.normalized.abs()
141        );
142    }
143    println!();
144
145    // === Uncertainty Propagation ===
146    println!("=== Uncertainty Propagation ===\n");
147
148    // Assume 5% uncertainty on each parameter
149    let param_std = [0.5, 1.4, 0.133]; // 5% of nominal
150    let param_vars: Vec<f64> = param_std.iter().map(|s| s * s).collect();
151
152    let total_var = sens_result.propagate_uncertainty(&param_vars);
153    let total_std = total_var.sqrt();
154
155    println!("Parameter uncertainties:");
156    println!("  σ = {:.2} ± {:.3}", params.sigma, param_std[0]);
157    println!("  ρ = {:.2} ± {:.3}", params.rho, param_std[1]);
158    println!("  β = {:.4} ± {:.4}", params.beta, param_std[2]);
159    println!();
160
161    println!("Propagated uncertainty in z(t=5):");
162    println!("  z = {:.4} ± {:.4}", sens_result.output, total_std);
163    println!();
164
165    // Individual contributions
166    println!("Uncertainty contributions:");
167    for (i, s) in sens_result.sensitivities.iter().enumerate() {
168        let contribution = (s.coefficient * param_std[i]).abs();
169        let pct = 100.0 * contribution * contribution / total_var;
170        println!("  {}: {:.2}%", s.name, pct);
171    }
172    println!();
173
174    // === Ensemble of trajectories ===
175    println!("=== Ensemble with Perturbed Initial Conditions ===\n");
176
177    let n_samples = 5;
178    println!("Running {} trajectories with perturbed ICs...", n_samples);
179
180    let perturbations = [
181        [1.0, 1.0, 1.0],
182        [1.1, 1.0, 1.0],
183        [1.0, 1.1, 1.0],
184        [0.9, 1.0, 1.0],
185        [1.0, 0.9, 1.0],
186    ];
187
188    for ic in perturbations.iter() {
189        let result = DoPri5::solve(&problem, 0.0, 10.0, ic, &options).expect("Solver failed");
190        let y_end = result.y_final().unwrap();
191        println!(
192            "  IC [{:.1}, {:.1}, {:.1}] → z(10) = {:.4}",
193            ic[0], ic[1], ic[2], y_end[2]
194        );
195    }
196    println!();
197
198    println!("Note: The Lorenz system is chaotic - small perturbations in ICs");
199    println!("lead to exponentially diverging trajectories over time.");
200    println!();
201
202    println!("=== Done ===");
203}