pub const AC_DRIFT_NO_DRIFT_SCORE: f32 = 0.0;
pub const AC_DRIFT_MIN_SAMPLES_FLOOR: usize = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DriftStatus {
NoDrift = 0,
Warning = 1,
Drift = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DriftVerdict {
Pass,
Fail,
}
#[must_use]
pub fn univariate_drift_score(mu_ref: f32, mu_cur: f32, sigma_ref: f32) -> f32 {
if !sigma_ref.is_finite() || sigma_ref <= 0.0 {
return f32::NAN;
}
if !mu_ref.is_finite() || !mu_cur.is_finite() {
return f32::NAN;
}
(mu_ref - mu_cur).abs() / sigma_ref
}
#[must_use]
pub fn classify_drift(score: f32, warn_threshold: f32, drift_threshold: f32) -> DriftStatus {
if !score.is_finite() || !warn_threshold.is_finite() || !drift_threshold.is_finite() {
return DriftStatus::NoDrift;
}
if !(0.0 < warn_threshold && warn_threshold < drift_threshold) {
return DriftStatus::NoDrift;
}
if score < warn_threshold {
DriftStatus::NoDrift
} else if score < drift_threshold {
DriftStatus::Warning
} else {
DriftStatus::Drift
}
}
#[must_use]
pub fn verdict_from_score_nonneg(score: f32) -> DriftVerdict {
if score.is_finite() && score >= 0.0 {
DriftVerdict::Pass
} else {
DriftVerdict::Fail
}
}
#[must_use]
pub fn verdict_from_status_monotone(
s_lo: f32,
s_hi: f32,
warn_threshold: f32,
drift_threshold: f32,
) -> DriftVerdict {
if !matches!(
s_lo.partial_cmp(&s_hi),
Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
) {
return DriftVerdict::Fail; }
let st_lo = classify_drift(s_lo, warn_threshold, drift_threshold);
let st_hi = classify_drift(s_hi, warn_threshold, drift_threshold);
if st_lo <= st_hi {
DriftVerdict::Pass
} else {
DriftVerdict::Fail
}
}
#[must_use]
pub fn verdict_from_min_samples_guard(
data_len: usize,
min_samples: usize,
actual_status: DriftStatus,
) -> DriftVerdict {
if min_samples < AC_DRIFT_MIN_SAMPLES_FLOOR {
return DriftVerdict::Fail;
}
if data_len < min_samples && actual_status != DriftStatus::NoDrift {
return DriftVerdict::Fail;
}
DriftVerdict::Pass
}
#[must_use]
pub fn verdict_from_identical_no_drift(
mu_ref: f32,
sigma_ref: f32,
warn_threshold: f32,
drift_threshold: f32,
) -> DriftVerdict {
let score = univariate_drift_score(mu_ref, mu_ref, sigma_ref);
if score != AC_DRIFT_NO_DRIFT_SCORE {
return DriftVerdict::Fail;
}
let status = classify_drift(score, warn_threshold, drift_threshold);
if status == DriftStatus::NoDrift {
DriftVerdict::Pass
} else {
DriftVerdict::Fail
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provenance_no_drift_score_is_zero() {
assert_eq!(AC_DRIFT_NO_DRIFT_SCORE, 0.0);
}
#[test]
fn provenance_min_samples_floor_is_1() {
assert_eq!(AC_DRIFT_MIN_SAMPLES_FLOOR, 1);
}
#[test]
fn provenance_status_ordering() {
assert!(DriftStatus::NoDrift < DriftStatus::Warning);
assert!(DriftStatus::Warning < DriftStatus::Drift);
}
#[test]
fn fdrift001_pass_zero_score() {
let v = verdict_from_score_nonneg(0.0);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift001_pass_positive_score() {
let v = verdict_from_score_nonneg(2.5);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift001_fail_negative_score() {
let v = verdict_from_score_nonneg(-0.0001);
assert_eq!(v, DriftVerdict::Fail);
}
#[test]
fn fdrift001_fail_nan() {
let v = verdict_from_score_nonneg(f32::NAN);
assert_eq!(v, DriftVerdict::Fail);
}
#[test]
fn fdrift001_fail_neg_infinity() {
let v = verdict_from_score_nonneg(f32::NEG_INFINITY);
assert_eq!(v, DriftVerdict::Fail);
}
#[test]
fn fdrift002_pass_strictly_increasing() {
let v = verdict_from_status_monotone(0.1, 1.5, 1.0, 2.0);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift002_pass_same_score_same_status() {
let v = verdict_from_status_monotone(0.5, 0.5, 1.0, 2.0);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift002_pass_no_drift_to_warning() {
let v = verdict_from_status_monotone(0.5, 1.5, 1.0, 2.0);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift002_pass_warning_to_drift() {
let v = verdict_from_status_monotone(1.5, 2.5, 1.0, 2.0);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift002_pass_no_drift_to_drift() {
let v = verdict_from_status_monotone(0.0, 5.0, 1.0, 2.0);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift002_fail_inverted_inputs() {
let v = verdict_from_status_monotone(1.5, 0.5, 1.0, 2.0);
assert_eq!(v, DriftVerdict::Fail);
}
#[test]
fn fdrift003_pass_below_min_with_no_drift() {
let v = verdict_from_min_samples_guard(5, 30, DriftStatus::NoDrift);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift003_fail_below_min_with_warning() {
let v = verdict_from_min_samples_guard(5, 30, DriftStatus::Warning);
assert_eq!(v, DriftVerdict::Fail);
}
#[test]
fn fdrift003_fail_below_min_with_drift() {
let v = verdict_from_min_samples_guard(5, 30, DriftStatus::Drift);
assert_eq!(v, DriftVerdict::Fail);
}
#[test]
fn fdrift003_pass_above_min_status_irrelevant() {
let v = verdict_from_min_samples_guard(100, 30, DriftStatus::Drift);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift003_fail_zero_min_samples() {
let v = verdict_from_min_samples_guard(0, 0, DriftStatus::NoDrift);
assert_eq!(v, DriftVerdict::Fail);
}
#[test]
fn fdrift004_pass_identical_means() {
let v = verdict_from_identical_no_drift(5.0, 1.0, 1.0, 2.0);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift004_pass_identical_negative_mean() {
let v = verdict_from_identical_no_drift(-3.0, 0.5, 1.0, 2.0);
assert_eq!(v, DriftVerdict::Pass);
}
#[test]
fn fdrift004_fail_invalid_sigma() {
let v = verdict_from_identical_no_drift(5.0, 0.0, 1.0, 2.0);
assert_eq!(v, DriftVerdict::Fail);
let v = verdict_from_identical_no_drift(5.0, -1.0, 1.0, 2.0);
assert_eq!(v, DriftVerdict::Fail);
}
#[test]
fn univariate_drift_zero_when_means_match() {
assert_eq!(univariate_drift_score(3.0, 3.0, 1.0), 0.0);
}
#[test]
fn univariate_drift_scales_with_shift() {
let s1 = univariate_drift_score(0.0, 1.0, 1.0);
let s2 = univariate_drift_score(0.0, 5.0, 1.0);
assert!(s2 > s1);
assert!(s1 > 0.0);
}
#[test]
fn classify_drift_three_regions() {
assert_eq!(classify_drift(0.0, 1.0, 2.0), DriftStatus::NoDrift);
assert_eq!(classify_drift(0.99, 1.0, 2.0), DriftStatus::NoDrift);
assert_eq!(classify_drift(1.0, 1.0, 2.0), DriftStatus::Warning);
assert_eq!(classify_drift(1.99, 1.0, 2.0), DriftStatus::Warning);
assert_eq!(classify_drift(2.0, 1.0, 2.0), DriftStatus::Drift);
assert_eq!(classify_drift(100.0, 1.0, 2.0), DriftStatus::Drift);
}
#[test]
fn classify_drift_invalid_thresholds_fail_closed() {
assert_eq!(classify_drift(5.0, 2.0, 1.0), DriftStatus::NoDrift);
assert_eq!(classify_drift(5.0, 1.0, 1.0), DriftStatus::NoDrift);
assert_eq!(classify_drift(5.0, -1.0, 2.0), DriftStatus::NoDrift);
}
#[test]
fn mutation_survey_002_score_sweep_monotone() {
let warn = 1.0_f32;
let drift = 2.0_f32;
let probes = [0.0_f32, 0.5, 0.99, 1.0, 1.5, 1.99, 2.0, 3.0, 5.0];
for &lo in &probes {
for &hi in &probes {
if lo <= hi {
let v = verdict_from_status_monotone(lo, hi, warn, drift);
assert_eq!(v, DriftVerdict::Pass, "lo={lo} hi={hi}");
}
}
}
}
#[test]
fn realistic_healthy_drift_passes_all_4() {
let v1 = verdict_from_score_nonneg(0.42);
let v2 = verdict_from_status_monotone(0.1, 2.5, 1.0, 2.0);
let v3 = verdict_from_min_samples_guard(5, 30, DriftStatus::NoDrift);
let v4 = verdict_from_identical_no_drift(5.0, 1.0, 1.0, 2.0);
assert_eq!(v1, DriftVerdict::Pass);
assert_eq!(v2, DriftVerdict::Pass);
assert_eq!(v3, DriftVerdict::Pass);
assert_eq!(v4, DriftVerdict::Pass);
}
#[test]
fn realistic_pre_fix_all_4_failures() {
let v1 = verdict_from_score_nonneg(-0.5);
let v2 = verdict_from_status_monotone(2.5, 0.5, 1.0, 2.0);
let v3 = verdict_from_min_samples_guard(5, 30, DriftStatus::Drift);
let v4 = verdict_from_identical_no_drift(5.0, -1.0, 1.0, 2.0);
assert_eq!(v1, DriftVerdict::Fail);
assert_eq!(v2, DriftVerdict::Fail);
assert_eq!(v3, DriftVerdict::Fail);
assert_eq!(v4, DriftVerdict::Fail);
}
}