use crate::error::{TlBackendError, TlBackendResult};
use crate::Scirs2Tensor;
use scirs2_core::ndarray::{ArrayD, Axis, IxDyn};
#[derive(Debug, Clone)]
pub struct UncertaintyEstimate {
pub mean: Scirs2Tensor,
pub variance: Scirs2Tensor,
pub std_dev: Scirs2Tensor,
pub lower_ci: Scirs2Tensor,
pub upper_ci: Scirs2Tensor,
pub confidence_level: f64,
}
#[derive(Debug, Clone)]
pub struct MonteCarloEstimator {
pub confidence_level: f64,
}
impl Default for MonteCarloEstimator {
fn default() -> Self {
Self {
confidence_level: 0.95,
}
}
}
impl MonteCarloEstimator {
pub fn estimate(&self, samples: &[Scirs2Tensor]) -> TlBackendResult<UncertaintyEstimate> {
if samples.is_empty() {
return Err(TlBackendError::InvalidOperation(
"MonteCarloEstimator::estimate: samples slice must not be empty".to_string(),
));
}
let ref_shape = samples[0].shape().to_vec();
for (i, s) in samples.iter().enumerate().skip(1) {
if s.shape() != ref_shape.as_slice() {
return Err(TlBackendError::InvalidOperation(format!(
"MonteCarloEstimator::estimate: sample {i} has shape {:?}, expected {:?}",
s.shape(),
ref_shape
)));
}
}
let n = samples.len() as f64;
let n_elems = ref_shape.iter().product::<usize>();
let mut mean_data = vec![0.0_f64; n_elems];
for sample in samples.iter() {
for (acc, &v) in mean_data.iter_mut().zip(sample.iter()) {
*acc += v;
}
}
for acc in mean_data.iter_mut() {
*acc /= n;
}
let mut var_data = vec![0.0_f64; n_elems];
for sample in samples.iter() {
for ((acc, &v), &m) in var_data.iter_mut().zip(sample.iter()).zip(mean_data.iter()) {
*acc += (v - m).powi(2);
}
}
for acc in var_data.iter_mut() {
*acc /= n;
}
let std_data: Vec<f64> = var_data.iter().map(|&v| v.sqrt()).collect();
let alpha = 1.0 - self.confidence_level;
let lo_quantile = alpha / 2.0;
let hi_quantile = 1.0 - alpha / 2.0;
let mut lower_data = vec![0.0_f64; n_elems];
let mut upper_data = vec![0.0_f64; n_elems];
let mut elem_values: Vec<f64> = Vec::with_capacity(samples.len());
for elem_idx in 0..n_elems {
elem_values.clear();
for sample in samples.iter() {
elem_values.push(sample.iter().nth(elem_idx).copied().unwrap_or(f64::NAN));
}
elem_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
lower_data[elem_idx] = quantile_sorted(&elem_values, lo_quantile);
upper_data[elem_idx] = quantile_sorted(&elem_values, hi_quantile);
}
let dyn_shape = IxDyn(&ref_shape);
let mean = ArrayD::from_shape_vec(dyn_shape.clone(), mean_data)
.map_err(|e| TlBackendError::Internal(e.to_string()))?;
let variance = ArrayD::from_shape_vec(dyn_shape.clone(), var_data)
.map_err(|e| TlBackendError::Internal(e.to_string()))?;
let std_dev = ArrayD::from_shape_vec(dyn_shape.clone(), std_data)
.map_err(|e| TlBackendError::Internal(e.to_string()))?;
let lower_ci = ArrayD::from_shape_vec(dyn_shape.clone(), lower_data)
.map_err(|e| TlBackendError::Internal(e.to_string()))?;
let upper_ci = ArrayD::from_shape_vec(dyn_shape, upper_data)
.map_err(|e| TlBackendError::Internal(e.to_string()))?;
Ok(UncertaintyEstimate {
mean,
variance,
std_dev,
lower_ci,
upper_ci,
confidence_level: self.confidence_level,
})
}
}
fn quantile_sorted(sorted: &[f64], q: f64) -> f64 {
let n = sorted.len();
if n == 0 {
return f64::NAN;
}
if n == 1 {
return sorted[0];
}
let q = q.clamp(0.0, 1.0);
let virtual_idx = q * (n - 1) as f64;
let lo = virtual_idx.floor() as usize;
let hi = virtual_idx.ceil() as usize;
if lo == hi {
sorted[lo]
} else {
let frac = virtual_idx - lo as f64;
sorted[lo] * (1.0 - frac) + sorted[hi] * frac
}
}
pub fn predictive_entropy(probs: &Scirs2Tensor, axis: usize) -> TlBackendResult<Scirs2Tensor> {
let ndim = probs.ndim();
if ndim == 0 {
return Err(TlBackendError::InvalidOperation(
"predictive_entropy: probs must have at least one dimension".to_string(),
));
}
if axis >= ndim {
return Err(TlBackendError::InvalidOperation(format!(
"predictive_entropy: axis {axis} out of range for {ndim}-D tensor"
)));
}
const EPS: f64 = 1e-10;
let term = probs.mapv(|p| {
let p_safe = (p + EPS).max(EPS);
-p * p_safe.ln()
});
Ok(term.sum_axis(Axis(axis)))
}
pub fn bald_epistemic_uncertainty(
ensemble: &[Scirs2Tensor],
axis: usize,
) -> TlBackendResult<Scirs2Tensor> {
if ensemble.is_empty() {
return Err(TlBackendError::InvalidOperation(
"bald_epistemic_uncertainty: ensemble must not be empty".to_string(),
));
}
let ref_shape = ensemble[0].shape().to_vec();
for (i, m) in ensemble.iter().enumerate().skip(1) {
if m.shape() != ref_shape.as_slice() {
return Err(TlBackendError::InvalidOperation(format!(
"bald_epistemic_uncertainty: member {i} has shape {:?}, expected {:?}",
m.shape(),
ref_shape
)));
}
}
let n_members = ensemble.len() as f64;
let mean_probs: Scirs2Tensor = ensemble
.iter()
.fold(ArrayD::<f64>::zeros(IxDyn(&ref_shape)), |acc, m| acc + m)
.mapv(|v| v / n_members);
let entropy_of_mean = predictive_entropy(&mean_probs, axis)?;
let member_entropies: Vec<Scirs2Tensor> = ensemble
.iter()
.map(|m| predictive_entropy(m, axis))
.collect::<TlBackendResult<Vec<_>>>()?;
let mean_entropy: Scirs2Tensor = {
let entropy_shape = member_entropies[0].shape().to_vec();
member_entropies
.iter()
.fold(ArrayD::<f64>::zeros(IxDyn(&entropy_shape)), |acc, h| {
acc + h
})
.mapv(|v| v / n_members)
};
Ok(entropy_of_mean - mean_entropy)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{ArrayD, IxDyn};
#[test]
fn estimator_basic_statistics() {
let s0 =
ArrayD::from_shape_vec(IxDyn(&[2, 2]), vec![1.0, 3.0, 5.0, 7.0]).expect("shape ok");
let s1 =
ArrayD::from_shape_vec(IxDyn(&[2, 2]), vec![3.0, 5.0, 7.0, 9.0]).expect("shape ok");
let samples = vec![s0, s1];
let est = MonteCarloEstimator::default();
let ue = est.estimate(&samples).expect("estimate failed");
let expected_mean = [2.0, 4.0, 6.0, 8.0];
for (&got, &exp) in ue.mean.iter().zip(expected_mean.iter()) {
assert!((got - exp).abs() < 1e-12, "mean: got {got}, expected {exp}");
}
for &v in ue.variance.iter() {
assert!((v - 1.0).abs() < 1e-12, "variance: got {v}, expected 1.0");
}
}
#[test]
fn predictive_entropy_uniform() {
let p = 0.25_f64;
let probs = ArrayD::from_elem(IxDyn(&[1, 4]), p);
let h = predictive_entropy(&probs, 1).expect("entropy failed");
let expected = -(4.0 * p * (p + 1e-10_f64).ln());
for &v in h.iter() {
assert!(
(v - expected).abs() < 1e-6,
"H_uniform: got {v}, expected ≈ {expected}"
);
}
}
#[test]
fn predictive_entropy_certain() {
let probs = ArrayD::from_shape_vec(IxDyn(&[1, 3]), vec![1.0, 0.0, 0.0]).expect("shape ok");
let h = predictive_entropy(&probs, 1).expect("entropy failed");
for &v in h.iter() {
assert!(v.abs() < 1e-8, "H_certain: got {v}, expected ≈ 0");
}
}
#[test]
fn bald_identical_ensemble() {
let probs = ArrayD::from_shape_vec(IxDyn(&[2, 3]), vec![0.2, 0.5, 0.3, 0.1, 0.8, 0.1])
.expect("shape ok");
let ensemble = vec![probs.clone(), probs.clone(), probs.clone()];
let bald = bald_epistemic_uncertainty(&ensemble, 1).expect("bald failed");
for &v in bald.iter() {
assert!(
v.abs() < 1e-10,
"identical ensemble: bald {v} should be ≈ 0"
);
}
}
#[test]
fn bald_diverse_ensemble() {
let m0 = ArrayD::from_shape_vec(IxDyn(&[1, 2]), vec![1.0, 0.0]).expect("shape ok");
let m1 = ArrayD::from_shape_vec(IxDyn(&[1, 2]), vec![0.0, 1.0]).expect("shape ok");
let ensemble = vec![m0, m1];
let bald = bald_epistemic_uncertainty(&ensemble, 1).expect("bald failed");
for &v in bald.iter() {
assert!(v > 0.5, "diverse ensemble: bald {v} should be > 0.5");
}
}
}