use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Uncertainty {
pub contradiction_rate: f32,
pub single_source_fraction: f32,
pub staleness: f32,
pub trust_exclusion_fraction: f32,
}
impl Uncertainty {
pub fn score(&self) -> f32 {
(self.contradiction_rate * 0.4
+ self.single_source_fraction * 0.25
+ self.staleness * 0.15
+ self.trust_exclusion_fraction * 0.2)
.clamp(0.0, 1.0)
}
}
impl Default for Uncertainty {
fn default() -> Self {
Self {
contradiction_rate: 0.0,
single_source_fraction: 0.0,
staleness: 0.0,
trust_exclusion_fraction: 0.0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct UncertaintyInput {
pub total_beliefs: usize,
pub contradicted_beliefs: usize,
pub single_source_beliefs: usize,
pub untrusted_beliefs: usize,
pub max_episode_age_days: f64,
}
pub fn compute(input: &UncertaintyInput) -> Uncertainty {
let total = input.total_beliefs.max(1) as f32;
let staleness = (input.max_episode_age_days / 365.0).clamp(0.0, 1.0) as f32;
Uncertainty {
contradiction_rate: (input.contradicted_beliefs as f32 / total).clamp(0.0, 1.0),
single_source_fraction: (input.single_source_beliefs as f32 / total).clamp(0.0, 1.0),
staleness,
trust_exclusion_fraction: (input.untrusted_beliefs as f32 / total).clamp(0.0, 1.0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_group_is_zero_uncertainty() {
let u = compute(&UncertaintyInput::default());
assert_eq!(u, Uncertainty::default());
assert_eq!(u.score(), 0.0);
}
#[test]
fn all_contradicted_gives_max_contradiction_rate() {
let input = UncertaintyInput {
total_beliefs: 10,
contradicted_beliefs: 10,
..Default::default()
};
let u = compute(&input);
assert!((u.contradiction_rate - 1.0).abs() < f32::EPSILON);
assert!(u.score() > 0.35); }
#[test]
fn score_weights_contradictions_above_single_source() {
let contradictions_only = compute(&UncertaintyInput {
total_beliefs: 10,
contradicted_beliefs: 5,
..Default::default()
});
let single_source_only = compute(&UncertaintyInput {
total_beliefs: 10,
single_source_beliefs: 5,
..Default::default()
});
assert!(contradictions_only.score() > single_source_only.score());
}
#[test]
fn staleness_normalises_to_year() {
let fresh = compute(&UncertaintyInput {
max_episode_age_days: 0.0,
..Default::default()
});
let half_year = compute(&UncertaintyInput {
max_episode_age_days: 182.0,
..Default::default()
});
let one_year = compute(&UncertaintyInput {
max_episode_age_days: 365.0,
..Default::default()
});
let two_years = compute(&UncertaintyInput {
max_episode_age_days: 730.0,
..Default::default()
});
assert!(fresh.staleness < half_year.staleness);
assert!((one_year.staleness - 1.0).abs() < 0.01);
assert!((two_years.staleness - 1.0).abs() < f32::EPSILON); }
#[test]
fn compute_is_pure() {
let input = UncertaintyInput {
total_beliefs: 8,
contradicted_beliefs: 2,
single_source_beliefs: 3,
untrusted_beliefs: 1,
max_episode_age_days: 100.0,
};
let u1 = compute(&input);
let u2 = compute(&input);
assert_eq!(u1, u2);
}
#[test]
fn score_is_in_unit_interval() {
for cr in [0.0f32, 0.25, 0.5, 0.75, 1.0] {
for ss in [0.0f32, 0.5, 1.0] {
for st in [0.0f32, 0.5, 1.0] {
for te in [0.0f32, 0.5, 1.0] {
let u = Uncertainty {
contradiction_rate: cr,
single_source_fraction: ss,
staleness: st,
trust_exclusion_fraction: te,
};
let s = u.score();
assert!(
(0.0..=1.0).contains(&s),
"score {s} out of range for cr={cr} ss={ss} st={st} te={te}"
);
}
}
}
}
}
}