Skip to main content

qualia_core_db/solvers/learning/active/
density.rs

1//! Information density — weight raw uncertainty by how *representative* a point is, so
2//! the query strategy is not lured into labelling unrepresentative outliers (which are
3//! uncertain but teach the model little about the bulk of the data).
4//!
5//! `density_i = uncertainty_i · ( mean_j similarity(i, j) )^β`. With `β = 0` this
6//! reduces to plain uncertainty; larger `β` favours points in dense regions.
7
8use super::{argsort_desc, ActiveError};
9
10/// Cosine similarity of two equal-length feature vectors, in `[-1, 1]`. Zero vectors
11/// have undefined direction → similarity `0`.
12pub fn cosine_similarity(a: &[f64], b: &[f64]) -> Result<f64, ActiveError> {
13    if a.len() != b.len() || a.is_empty() {
14        return Err(ActiveError::InvalidDimension);
15    }
16    let mut dot = 0.0;
17    let mut na = 0.0;
18    let mut nb = 0.0;
19    for i in 0..a.len() {
20        dot += a[i] * b[i];
21        na += a[i] * a[i];
22        nb += b[i] * b[i];
23    }
24    if na <= 0.0 || nb <= 0.0 {
25        return Ok(0.0);
26    }
27    Ok(dot / (na.sqrt() * nb.sqrt()))
28}
29
30/// Mean representativeness of each point: the average similarity of point `i` to all
31/// *other* points in the pool. `features` is `n_samples × n_features`.
32pub fn representativeness(features: &[Vec<f64>]) -> Result<Vec<f64>, ActiveError> {
33    let n = features.len();
34    if n < 2 {
35        return Err(ActiveError::InsufficientData);
36    }
37    let mut rep = vec![0.0; n];
38    for i in 0..n {
39        let mut sum = 0.0;
40        for j in 0..n {
41            if i != j {
42                sum += cosine_similarity(&features[i], &features[j])?;
43            }
44        }
45        rep[i] = sum / (n - 1) as f64;
46    }
47    Ok(rep)
48}
49
50/// Information-density scores: `uncertainty_i · representativeness_i^β`. Lengths of
51/// `uncertainty` and `features` must match. Representativeness is clamped to `≥ 0`
52/// before exponentiation (negative mean-similarity points get no density bonus).
53pub fn information_density(
54    uncertainty: &[f64],
55    features: &[Vec<f64>],
56    beta: f64,
57) -> Result<Vec<f64>, ActiveError> {
58    if uncertainty.len() != features.len() {
59        return Err(ActiveError::InvalidDimension);
60    }
61    let rep = representativeness(features)?;
62    Ok(uncertainty
63        .iter()
64        .zip(rep.iter())
65        .map(|(&u, &r)| u * r.max(0.0).powf(beta))
66        .collect())
67}
68
69/// Rank pool indices by information density, most-informative first.
70pub fn rank_by_density(
71    uncertainty: &[f64],
72    features: &[Vec<f64>],
73    beta: f64,
74) -> Result<Vec<usize>, ActiveError> {
75    Ok(argsort_desc(&information_density(
76        uncertainty,
77        features,
78        beta,
79    )?))
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    const EPS: f64 = 1e-9;
86
87    #[test]
88    fn cosine_basic() {
89        assert!((cosine_similarity(&[1.0, 0.0], &[1.0, 0.0]).unwrap() - 1.0).abs() < EPS);
90        assert!(cosine_similarity(&[1.0, 0.0], &[0.0, 1.0]).unwrap().abs() < EPS);
91        assert!((cosine_similarity(&[1.0, 0.0], &[-1.0, 0.0]).unwrap() + 1.0).abs() < EPS);
92        assert!(cosine_similarity(&[0.0, 0.0], &[1.0, 1.0]).unwrap().abs() < EPS);
93        // zero vector
94    }
95
96    #[test]
97    fn density_demotes_an_uncertain_outlier() {
98        // Three points: 0 and 1 form a dense cluster, 2 is an outlier. All equally
99        // uncertain. Density should rank the cluster members above the outlier.
100        let features = vec![vec![1.0, 0.0], vec![0.96, 0.28], vec![-1.0, 0.0]];
101        let uncertainty = vec![0.5, 0.5, 0.5];
102        let ranked = rank_by_density(&uncertainty, &features, 1.0).unwrap();
103        assert_ne!(ranked[0], 2, "the outlier must not be the top query");
104        assert_eq!(
105            ranked[2], 2,
106            "the outlier ranks last under density weighting"
107        );
108    }
109
110    #[test]
111    fn beta_zero_is_plain_uncertainty() {
112        let features = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
113        let uncertainty = vec![0.3, 0.9];
114        let d = information_density(&uncertainty, &features, 0.0).unwrap();
115        // r^0 = 1 → density == uncertainty.
116        assert!((d[0] - 0.3).abs() < EPS && (d[1] - 0.9).abs() < EPS);
117    }
118
119    #[test]
120    fn fails_closed_on_mismatch() {
121        let features = vec![vec![1.0], vec![1.0]];
122        assert_eq!(
123            information_density(&[0.5], &features, 1.0).unwrap_err(),
124            ActiveError::InvalidDimension
125        );
126    }
127}