qualia_core_db/solvers/learning/active/
uncertainty.rs1use super::{argsort_desc, ActiveError};
6use crate::solvers::statistics::information::entropy;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Strategy {
12 LeastConfidence,
14 Margin,
16 Entropy,
18}
19
20fn top_two(row: &[f64]) -> (f64, f64) {
21 let mut a = f64::NEG_INFINITY; let mut b = f64::NEG_INFINITY; for &p in row {
24 if p > a {
25 b = a;
26 a = p;
27 } else if p > b {
28 b = p;
29 }
30 }
31 if b.is_infinite() {
32 b = 0.0;
33 }
34 (a, b)
35}
36
37pub fn row_score(row: &[f64], strategy: Strategy) -> Result<f64, ActiveError> {
39 if row.is_empty() {
40 return Err(ActiveError::InvalidDimension);
41 }
42 Ok(match strategy {
43 Strategy::LeastConfidence => {
44 let (top, _) = top_two(row);
45 1.0 - top
46 }
47 Strategy::Margin => {
48 let (p1, p2) = top_two(row);
49 1.0 - (p1 - p2)
50 }
51 Strategy::Entropy => entropy(row).ok_or(ActiveError::InvalidDimension)?,
52 })
53}
54
55pub fn score(probs: &[Vec<f64>], strategy: Strategy) -> Result<Vec<f64>, ActiveError> {
57 if probs.is_empty() {
58 return Err(ActiveError::InsufficientData);
59 }
60 let n_classes = probs[0].len();
61 if n_classes == 0 || probs.iter().any(|r| r.len() != n_classes) {
62 return Err(ActiveError::InvalidDimension);
63 }
64 probs.iter().map(|r| row_score(r, strategy)).collect()
65}
66
67pub fn rank_informative(probs: &[Vec<f64>], strategy: Strategy) -> Result<Vec<usize>, ActiveError> {
69 Ok(argsort_desc(&score(probs, strategy)?))
70}
71
72pub fn most_informative(probs: &[Vec<f64>], strategy: Strategy) -> Result<usize, ActiveError> {
74 rank_informative(probs, strategy)?
75 .first()
76 .copied()
77 .ok_or(ActiveError::InsufficientData)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 const EPS: f64 = 1e-9;
84
85 #[test]
86 fn least_confidence_picks_the_flat_distribution() {
87 let probs = vec![vec![0.9, 0.1], vec![0.5, 0.5]];
89 let s = score(&probs, Strategy::LeastConfidence).unwrap();
90 assert!((s[0] - 0.1).abs() < EPS);
91 assert!((s[1] - 0.5).abs() < EPS);
92 assert_eq!(
93 most_informative(&probs, Strategy::LeastConfidence).unwrap(),
94 1
95 );
96 }
97
98 #[test]
99 fn margin_uses_top_two_gap() {
100 let probs = vec![vec![0.5, 0.3, 0.2], vec![0.4, 0.4, 0.2]];
102 let s = score(&probs, Strategy::Margin).unwrap();
103 assert!((s[0] - 0.8).abs() < EPS);
104 assert!((s[1] - 1.0).abs() < EPS);
105 assert_eq!(most_informative(&probs, Strategy::Margin).unwrap(), 1);
106 }
107
108 #[test]
109 fn entropy_ranks_diffuse_highest() {
110 let probs = vec![vec![1.0, 0.0], vec![0.5, 0.5]];
111 let r = rank_informative(&probs, Strategy::Entropy).unwrap();
112 assert_eq!(r[0], 1); let s = score(&probs, Strategy::Entropy).unwrap();
114 assert!((s[0]).abs() < EPS); }
116
117 #[test]
118 fn fails_closed_on_empty_and_ragged() {
119 assert_eq!(
120 score(&[], Strategy::Entropy).unwrap_err(),
121 ActiveError::InsufficientData
122 );
123 let ragged = vec![vec![0.5, 0.5], vec![1.0]];
124 assert_eq!(
125 score(&ragged, Strategy::Entropy).unwrap_err(),
126 ActiveError::InvalidDimension
127 );
128 }
129}