1#![forbid(unsafe_code)]
8
9use serde::{Deserialize, Serialize};
10
11use crate::monte_carlo::{Distribution, McConfig, MonteCarloSimulator};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SensitivityIndex {
18 pub variable_index: usize,
20 pub first_order: f64,
22 pub total_order: f64,
24 pub label: String,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct SensitivityResult {
33 pub indices: Vec<SensitivityIndex>,
35 pub n_samples: usize,
37 pub total_variance: f64,
39}
40
41impl SensitivityResult {
42 #[must_use]
44 pub fn most_influential(&self) -> Option<&SensitivityIndex> {
45 self.indices.iter().max_by(|a, b| {
46 a.total_order
47 .partial_cmp(&b.total_order)
48 .unwrap_or(std::cmp::Ordering::Equal)
49 })
50 }
51
52 #[must_use]
54 pub fn to_json(&self) -> serde_json::Value {
55 serde_json::json!({
56 "n_samples": self.n_samples,
57 "total_variance": self.total_variance,
58 "indices": self.indices.iter().map(|i| serde_json::json!({
59 "variable_index": i.variable_index,
60 "label": i.label,
61 "first_order": i.first_order,
62 "total_order": i.total_order,
63 })).collect::<Vec<_>>(),
64 })
65 }
66}
67
68pub struct SensitivityAnalyzer {
72 n_samples: usize,
73 seed: u64,
74}
75
76impl Default for SensitivityAnalyzer {
77 fn default() -> Self {
78 Self {
79 n_samples: 5000,
80 seed: 42,
81 }
82 }
83}
84
85impl std::fmt::Debug for SensitivityAnalyzer {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("SensitivityAnalyzer")
88 .field("n_samples", &self.n_samples)
89 .finish_non_exhaustive()
90 }
91}
92
93impl SensitivityAnalyzer {
94 #[must_use]
96 pub const fn new(n_samples: usize, seed: u64) -> Self {
97 Self { n_samples, seed }
98 }
99
100 #[must_use]
106 pub fn analyze<F>(&self, distributions: &[Distribution], model: F) -> SensitivityResult
107 where
108 F: Fn(&[f64]) -> f64,
109 {
110 let n_vars = distributions.len();
111 if n_vars == 0 {
112 return SensitivityResult {
113 indices: Vec::new(),
114 n_samples: 0,
115 total_variance: 0.0,
116 };
117 }
118
119 let mut sim = MonteCarloSimulator::new(McConfig {
121 n_samples: self.n_samples,
122 seed: self.seed,
123 quasi_mc: false,
124 });
125 let total_result = sim.simulate(distributions, |inputs| model(inputs));
126 let total_variance = total_result.std_dev * total_result.std_dev;
127
128 let mut indices = Vec::with_capacity(n_vars);
130 for i in 0..n_vars {
131 let means: Vec<f64> = distributions.iter().map(Distribution::mean).collect();
133 let single_dist = vec![distributions[i].clone()];
134
135 let mut sim_i = MonteCarloSimulator::new(McConfig {
136 n_samples: self.n_samples,
137 seed: self.seed.wrapping_add((i + 1) as u64),
138 quasi_mc: false,
139 });
140
141 let var_result = sim_i.simulate(&single_dist, |inputs| {
142 let mut full_inputs = means.clone();
143 full_inputs[i] = inputs[0];
144 model(&full_inputs)
145 });
146
147 let var_variance = var_result.std_dev * var_result.std_dev;
148 let first_order = if total_variance > 1e-10 {
149 var_variance / total_variance
150 } else {
151 0.0
152 };
153
154 let total_order = first_order.min(1.0);
158
159 indices.push(SensitivityIndex {
160 variable_index: i,
161 first_order,
162 total_order,
163 label: format!("var_{i}"),
164 });
165 }
166
167 SensitivityResult {
168 indices,
169 n_samples: self.n_samples,
170 total_variance,
171 }
172 }
173
174 #[must_use]
176 pub fn analyze_with_labels<F>(
177 &self,
178 distributions: &[Distribution],
179 labels: &[String],
180 model: F,
181 ) -> SensitivityResult
182 where
183 F: Fn(&[f64]) -> f64,
184 {
185 let mut result = self.analyze(distributions, model);
186 for (i, label) in labels.iter().enumerate() {
187 if i < result.indices.len() {
188 result.indices[i].label.clone_from(label);
189 }
190 }
191 result
192 }
193}
194
195#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn analyze_single_dominant_variable() {
203 let analyzer = SensitivityAnalyzer::new(1000, 42);
204 let dists = vec![
205 Distribution::Uniform {
206 min: 0.0,
207 max: 10.0,
208 },
209 Distribution::Constant(5.0),
210 ];
211 let result = analyzer.analyze(&dists, |inputs| inputs[0]);
213 assert_eq!(result.indices.len(), 2);
214 assert!(result.indices[0].first_order > 0.5);
216 assert!(result.indices[1].first_order < 0.1);
218 }
219
220 #[test]
221 fn analyze_equal_variables() {
222 let analyzer = SensitivityAnalyzer::new(2000, 42);
223 let dists = vec![
224 Distribution::Uniform { min: 0.0, max: 1.0 },
225 Distribution::Uniform { min: 0.0, max: 1.0 },
226 ];
227 let result = analyzer.analyze(&dists, |inputs| inputs[0] + inputs[1]);
229 assert_eq!(result.indices.len(), 2);
230 assert!((result.indices[0].first_order - result.indices[1].first_order).abs() < 0.3);
232 }
233
234 #[test]
235 fn analyze_empty() {
236 let analyzer = SensitivityAnalyzer::default();
237 let result = analyzer.analyze(&[], |_| 0.0);
238 assert_eq!(result.indices.len(), 0);
239 assert_eq!(result.n_samples, 0);
240 }
241
242 #[test]
243 fn most_influential() {
244 let analyzer = SensitivityAnalyzer::new(1000, 42);
245 let dists = vec![
246 Distribution::Constant(5.0),
247 Distribution::Uniform {
248 min: 0.0,
249 max: 10.0,
250 },
251 ];
252 let result = analyzer.analyze(&dists, |inputs| inputs[1]);
253 let most = result.most_influential();
254 assert!(most.is_some());
255 assert_eq!(most.unwrap().variable_index, 1);
256 }
257
258 #[test]
259 fn analyze_with_labels() {
260 let analyzer = SensitivityAnalyzer::new(500, 42);
261 let dists = vec![
262 Distribution::Uniform { min: 0.0, max: 1.0 },
263 Distribution::Uniform { min: 0.0, max: 1.0 },
264 ];
265 let labels = vec!["cpu_load".to_string(), "memory".to_string()];
266 let result = analyzer.analyze_with_labels(&dists, &labels, |inputs| inputs[0] + inputs[1]);
267 assert_eq!(result.indices[0].label, "cpu_load");
268 assert_eq!(result.indices[1].label, "memory");
269 }
270
271 #[test]
272 fn result_to_json() {
273 let result = SensitivityResult {
274 indices: vec![SensitivityIndex {
275 variable_index: 0,
276 first_order: 0.8,
277 total_order: 0.9,
278 label: "test".to_string(),
279 }],
280 n_samples: 1000,
281 total_variance: 2.5,
282 };
283 let json = result.to_json();
284 assert_eq!(json["n_samples"], 1000);
285 assert_eq!(json["total_variance"], 2.5);
286 }
287
288 #[test]
289 fn total_variance_computed() {
290 let analyzer = SensitivityAnalyzer::new(2000, 42);
291 let dists = vec![Distribution::Uniform {
292 min: 0.0,
293 max: 10.0,
294 }];
295 let result = analyzer.analyze(&dists, |inputs| inputs[0]);
296 assert!(result.total_variance > 5.0 && result.total_variance < 12.0);
298 }
299
300 #[test]
301 fn constant_model_zero_variance() {
302 let analyzer = SensitivityAnalyzer::new(500, 42);
303 let dists = vec![Distribution::Uniform { min: 0.0, max: 1.0 }];
304 let result = analyzer.analyze(&dists, |_| 42.0);
305 assert!(result.total_variance < 0.001);
306 assert!(result.indices[0].first_order < 0.001);
307 }
308}