1use numra::ode::{DoPri5, OdeProblem, Solver, SolverOptions};
20use numra::uncertainty::{compute_sensitivities, Uncertain};
21
22struct 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 let params = LorenzParams::default();
44
45 let x0 = Uncertain::from_std(1.0, 0.1); let y0 = Uncertain::from_std(1.0, 0.1); let z0 = Uncertain::from_std(1.0, 0.1); 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 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 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 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 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 println!("Trajectory has {} points", result.t.len());
96 println!();
97
98 println!("=== Sensitivity Analysis ===\n");
100
101 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); dydt[1] = x * (p[1] - z) - y_val; dydt[2] = x * y_val - p[2] * z; },
110 0.0,
111 5.0, 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] };
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, ¶m_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 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 println!("=== Uncertainty Propagation ===\n");
147
148 let param_std = [0.5, 1.4, 0.133]; let param_vars: Vec<f64> = param_std.iter().map(|s| s * s).collect();
151
152 let total_var = sens_result.propagate_uncertainty(¶m_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 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 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}