#[derive(Debug, Clone, PartialEq)]
pub enum PrecisionStatus {
MetGoal,
Near,
Unmet,
NoiseFloorHit,
}
#[derive(Debug, Clone)]
pub struct PrecisionAssessment {
pub max_std: f64,
pub mean_std: f64,
pub percentile_95_std: f64,
pub target_std: f64,
pub gap: f64,
pub status: PrecisionStatus,
}
pub fn percentile(values: &[f64], p: f64) -> f64 {
if values.is_empty() {
return 0.0;
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((p * (sorted.len() - 1) as f64).round() as usize).min(sorted.len() - 1);
sorted[idx]
}
pub fn assess_precision(posterior_std: &[f64], target_std: f64) -> PrecisionAssessment {
if posterior_std.is_empty() {
return PrecisionAssessment {
max_std: 0.0,
mean_std: 0.0,
percentile_95_std: 0.0,
target_std,
gap: if target_std > 0.0 {
-1.0
} else {
f64::INFINITY
},
status: PrecisionStatus::Unmet,
};
}
let max_std = posterior_std
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
let mean_std = posterior_std.iter().sum::<f64>() / posterior_std.len() as f64;
let percentile_95_std = percentile(posterior_std, 0.95);
let gap = if target_std > 0.0 {
(percentile_95_std - target_std) / target_std
} else {
f64::INFINITY
};
let status = if target_std > 0.0 && percentile_95_std <= target_std {
PrecisionStatus::MetGoal
} else if target_std > 0.0 && percentile_95_std <= 1.1 * target_std {
PrecisionStatus::Near
} else {
PrecisionStatus::Unmet
};
PrecisionAssessment {
max_std,
mean_std,
percentile_95_std,
target_std,
gap,
status,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_precision_goal_met() {
let stds = vec![0.005; 100];
let a = assess_precision(&stds, 0.01);
assert_eq!(a.status, PrecisionStatus::MetGoal);
assert!(a.gap < 0.0, "gap should be negative when goal met");
}
#[test]
fn test_precision_near() {
let mut stds = vec![0.01; 100];
stds[99] = 0.012; let stds: Vec<f64> = (0..95).map(|_| 0.01).chain((0..5).map(|_| 0.012)).collect();
let _a = assess_precision(&stds, 0.01);
let stds2: Vec<f64> = (0..80)
.map(|_| 0.01)
.chain((0..20).map(|_| 0.0108))
.collect();
let a2 = assess_precision(&stds2, 0.01);
assert!(
a2.status == PrecisionStatus::Near || a2.status == PrecisionStatus::MetGoal,
"expected Near or MetGoal, got {:?}",
a2.status
);
}
#[test]
fn test_precision_unmet() {
let stds = vec![0.05; 100]; let a = assess_precision(&stds, 0.01);
assert_eq!(a.status, PrecisionStatus::Unmet);
assert!(a.gap > 0.0, "gap should be positive when unmet");
}
#[test]
fn test_gap_calculation() {
let stds = vec![0.02; 100];
let a = assess_precision(&stds, 0.01);
let expected_gap = (0.02 - 0.01) / 0.01; assert!(
(a.gap - expected_gap).abs() < 1e-10,
"gap={}, expected={}",
a.gap,
expected_gap
);
}
#[test]
fn test_percentile_computation() {
let vals: Vec<f64> = (0..100).map(|i| i as f64 / 100.0).collect();
let p95 = percentile(&vals, 0.95);
assert!((p95 - 0.94).abs() < 1e-10, "p95={}, expected 0.94", p95);
}
#[test]
fn test_empty_stds_no_panic() {
let a = assess_precision(&[], 0.01);
assert_eq!(a.status, PrecisionStatus::Unmet);
}
#[test]
fn test_zero_target_std() {
let stds = vec![0.001; 10];
let a = assess_precision(&stds, 0.0);
assert_ne!(a.status, PrecisionStatus::MetGoal);
assert!(
a.gap.is_infinite(),
"gap should be infinite for target_std=0"
);
}
}