#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
pub struct ConformalDetector {
sorted_scores: Vec<f64>,
}
impl ConformalDetector {
pub fn new() -> Self {
ConformalDetector { sorted_scores: Vec::new() }
}
pub fn calibrate(&mut self, scores: &[f64]) {
self.sorted_scores = scores.to_vec();
self.sorted_scores.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
}
pub fn p_value(&self, score: f64) -> f64 {
if self.sorted_scores.is_empty() {
return 1.0;
}
let n = self.sorted_scores.len();
let rank = match self.sorted_scores.binary_search_by(|v| {
v.partial_cmp(&score).unwrap_or(core::cmp::Ordering::Equal)
}) {
Ok(i) => n - i,
Err(i) => n - i,
};
(rank as f64 + 1.0) / (n as f64 + 1.0)
}
pub fn is_anomalous(&self, score: f64, alpha: f64) -> bool {
self.p_value(score) < alpha
}
pub fn confidence(&self, score: f64) -> f64 {
1.0 - self.p_value(score)
}
pub fn threshold_at(&self, alpha: f64) -> f64 {
if self.sorted_scores.is_empty() {
return f64::INFINITY;
}
let idx = ((1.0 - alpha) * self.sorted_scores.len() as f64) as usize;
self.sorted_scores[idx.min(self.sorted_scores.len() - 1)]
}
}
impl Default for ConformalDetector {
fn default() -> Self { Self::new() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn p_value_ranks_correctly() {
let mut det = ConformalDetector::new();
det.calibrate(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
assert!(det.p_value(10.5) < 0.1);
let p = det.p_value(5.5);
assert!(p > 0.3 && p < 0.6, "p = {}", p);
assert!(det.p_value(0.5) > 0.9);
}
#[test]
fn coverage_guarantee() {
let mut det = ConformalDetector::new();
let cal: Vec<f64> = (0..1000).map(|i| (i as f64 * 0.1).sin().abs()).collect();
det.calibrate(&cal);
let false_alarms: usize = cal.iter()
.filter(|&&s| det.is_anomalous(s, 0.05))
.count();
let rate = false_alarms as f64 / cal.len() as f64;
assert!(rate < 0.08, "false alarm rate {} should be near 5%", rate);
}
#[test]
fn threshold_at_matches_p_value() {
let mut det = ConformalDetector::new();
det.calibrate(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
let thr = det.threshold_at(0.05);
assert!(det.p_value(thr + 0.1) < 0.15);
}
}