use crate::envelope::AdmissibilityEnvelope;
use crate::math;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CalibrationOutcome {
Ok(AdmissibilityEnvelope),
EmptyOrAllNonFinite,
NonFiniteStatistics,
WindowTooNoisy {
mean: f64,
std_dev: f64,
},
}
#[must_use]
pub fn calibrate(healthy_norms: &[f64]) -> CalibrationOutcome {
calibrate_with_gate(healthy_norms, 10.0)
}
#[must_use]
pub fn calibrate_with_gate(healthy_norms: &[f64], max_cv: f64) -> CalibrationOutcome {
debug_assert!(max_cv >= 0.0, "max_cv must be non-negative");
let Some(mean) = math::finite_mean(healthy_norms) else {
return CalibrationOutcome::EmptyOrAllNonFinite;
};
let Some(var) = math::finite_variance(healthy_norms) else {
return CalibrationOutcome::EmptyOrAllNonFinite;
};
let Some(std_dev) = math::sqrt_f64(var) else {
return CalibrationOutcome::NonFiniteStatistics;
};
if !mean.is_finite() || !std_dev.is_finite() {
return CalibrationOutcome::NonFiniteStatistics;
}
let abs_mean = math::abs_f64(mean);
let cv = if abs_mean > 1e-9 { std_dev / abs_mean } else { std_dev };
if cv > max_cv {
return CalibrationOutcome::WindowTooNoisy { mean, std_dev };
}
let rho = mean + 3.0 * std_dev;
debug_assert!(rho.is_finite(), "calibrated rho must be finite");
CalibrationOutcome::Ok(AdmissibilityEnvelope::new(rho))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_window_rejected() {
assert!(matches!(calibrate(&[]), CalibrationOutcome::EmptyOrAllNonFinite));
}
#[test]
fn all_nan_rejected() {
let xs = [f64::NAN; 10];
assert!(matches!(calibrate(&xs), CalibrationOutcome::EmptyOrAllNonFinite));
}
#[test]
fn clean_window_produces_envelope() {
let xs = [0.05_f64; 100];
match calibrate(&xs) {
CalibrationOutcome::Ok(env) => assert!((env.rho - 0.05).abs() < 1e-12),
other => panic!("expected Ok, got {other:?}"),
}
}
#[test]
fn very_noisy_window_rejected_by_default_gate() {
let xs = [0.01, -0.5, 0.5, -0.5, 0.5, -0.5, 0.5];
match calibrate(&xs) {
CalibrationOutcome::WindowTooNoisy { .. } => {}
other => panic!("expected WindowTooNoisy, got {other:?}"),
}
}
#[test]
fn noisy_window_accepted_with_relaxed_gate() {
let xs = [0.01, -0.5, 0.5, -0.5, 0.5, -0.5, 0.5];
match calibrate_with_gate(&xs, f64::INFINITY) {
CalibrationOutcome::Ok(env) => assert!(env.rho.is_finite()),
other => panic!("expected Ok with relaxed gate, got {other:?}"),
}
}
}