#![forbid(unsafe_code)]
use serde::{Deserialize, Serialize};
use crate::monte_carlo::{Distribution, McConfig, MonteCarloSimulator};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SensitivityIndex {
pub variable_index: usize,
pub first_order: f64,
pub total_order: f64,
pub label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SensitivityResult {
pub indices: Vec<SensitivityIndex>,
pub n_samples: usize,
pub total_variance: f64,
}
impl SensitivityResult {
#[must_use]
pub fn most_influential(&self) -> Option<&SensitivityIndex> {
self.indices.iter().max_by(|a, b| {
a.total_order
.partial_cmp(&b.total_order)
.unwrap_or(std::cmp::Ordering::Equal)
})
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"n_samples": self.n_samples,
"total_variance": self.total_variance,
"indices": self.indices.iter().map(|i| serde_json::json!({
"variable_index": i.variable_index,
"label": i.label,
"first_order": i.first_order,
"total_order": i.total_order,
})).collect::<Vec<_>>(),
})
}
}
pub struct SensitivityAnalyzer {
n_samples: usize,
seed: u64,
}
impl Default for SensitivityAnalyzer {
fn default() -> Self {
Self {
n_samples: 5000,
seed: 42,
}
}
}
impl std::fmt::Debug for SensitivityAnalyzer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SensitivityAnalyzer")
.field("n_samples", &self.n_samples)
.finish_non_exhaustive()
}
}
impl SensitivityAnalyzer {
#[must_use]
pub const fn new(n_samples: usize, seed: u64) -> Self {
Self { n_samples, seed }
}
#[must_use]
pub fn analyze<F>(&self, distributions: &[Distribution], model: F) -> SensitivityResult
where
F: Fn(&[f64]) -> f64,
{
let n_vars = distributions.len();
if n_vars == 0 {
return SensitivityResult {
indices: Vec::new(),
n_samples: 0,
total_variance: 0.0,
};
}
let mut sim = MonteCarloSimulator::new(McConfig {
n_samples: self.n_samples,
seed: self.seed,
quasi_mc: false,
});
let total_result = sim.simulate(distributions, |inputs| model(inputs));
let total_variance = total_result.std_dev * total_result.std_dev;
let mut indices = Vec::with_capacity(n_vars);
for i in 0..n_vars {
let means: Vec<f64> = distributions.iter().map(Distribution::mean).collect();
let single_dist = vec![distributions[i].clone()];
let mut sim_i = MonteCarloSimulator::new(McConfig {
n_samples: self.n_samples,
seed: self.seed.wrapping_add((i + 1) as u64),
quasi_mc: false,
});
let var_result = sim_i.simulate(&single_dist, |inputs| {
let mut full_inputs = means.clone();
full_inputs[i] = inputs[0];
model(&full_inputs)
});
let var_variance = var_result.std_dev * var_result.std_dev;
let first_order = if total_variance > 1e-10 {
var_variance / total_variance
} else {
0.0
};
let total_order = first_order.min(1.0);
indices.push(SensitivityIndex {
variable_index: i,
first_order,
total_order,
label: format!("var_{i}"),
});
}
SensitivityResult {
indices,
n_samples: self.n_samples,
total_variance,
}
}
#[must_use]
pub fn analyze_with_labels<F>(
&self,
distributions: &[Distribution],
labels: &[String],
model: F,
) -> SensitivityResult
where
F: Fn(&[f64]) -> f64,
{
let mut result = self.analyze(distributions, model);
for (i, label) in labels.iter().enumerate() {
if i < result.indices.len() {
result.indices[i].label.clone_from(label);
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn analyze_single_dominant_variable() {
let analyzer = SensitivityAnalyzer::new(1000, 42);
let dists = vec![
Distribution::Uniform {
min: 0.0,
max: 10.0,
},
Distribution::Constant(5.0),
];
let result = analyzer.analyze(&dists, |inputs| inputs[0]);
assert_eq!(result.indices.len(), 2);
assert!(result.indices[0].first_order > 0.5);
assert!(result.indices[1].first_order < 0.1);
}
#[test]
fn analyze_equal_variables() {
let analyzer = SensitivityAnalyzer::new(2000, 42);
let dists = vec![
Distribution::Uniform { min: 0.0, max: 1.0 },
Distribution::Uniform { min: 0.0, max: 1.0 },
];
let result = analyzer.analyze(&dists, |inputs| inputs[0] + inputs[1]);
assert_eq!(result.indices.len(), 2);
assert!((result.indices[0].first_order - result.indices[1].first_order).abs() < 0.3);
}
#[test]
fn analyze_empty() {
let analyzer = SensitivityAnalyzer::default();
let result = analyzer.analyze(&[], |_| 0.0);
assert_eq!(result.indices.len(), 0);
assert_eq!(result.n_samples, 0);
}
#[test]
fn most_influential() {
let analyzer = SensitivityAnalyzer::new(1000, 42);
let dists = vec![
Distribution::Constant(5.0),
Distribution::Uniform {
min: 0.0,
max: 10.0,
},
];
let result = analyzer.analyze(&dists, |inputs| inputs[1]);
let most = result.most_influential();
assert!(most.is_some());
assert_eq!(most.unwrap().variable_index, 1);
}
#[test]
fn analyze_with_labels() {
let analyzer = SensitivityAnalyzer::new(500, 42);
let dists = vec![
Distribution::Uniform { min: 0.0, max: 1.0 },
Distribution::Uniform { min: 0.0, max: 1.0 },
];
let labels = vec!["cpu_load".to_string(), "memory".to_string()];
let result = analyzer.analyze_with_labels(&dists, &labels, |inputs| inputs[0] + inputs[1]);
assert_eq!(result.indices[0].label, "cpu_load");
assert_eq!(result.indices[1].label, "memory");
}
#[test]
fn result_to_json() {
let result = SensitivityResult {
indices: vec![SensitivityIndex {
variable_index: 0,
first_order: 0.8,
total_order: 0.9,
label: "test".to_string(),
}],
n_samples: 1000,
total_variance: 2.5,
};
let json = result.to_json();
assert_eq!(json["n_samples"], 1000);
assert_eq!(json["total_variance"], 2.5);
}
#[test]
fn total_variance_computed() {
let analyzer = SensitivityAnalyzer::new(2000, 42);
let dists = vec![Distribution::Uniform {
min: 0.0,
max: 10.0,
}];
let result = analyzer.analyze(&dists, |inputs| inputs[0]);
assert!(result.total_variance > 5.0 && result.total_variance < 12.0);
}
#[test]
fn constant_model_zero_variance() {
let analyzer = SensitivityAnalyzer::new(500, 42);
let dists = vec![Distribution::Uniform { min: 0.0, max: 1.0 }];
let result = analyzer.analyze(&dists, |_| 42.0);
assert!(result.total_variance < 0.001);
assert!(result.indices[0].first_order < 0.001);
}
}