Skip to main content

ruvector_coherence/
metrics.rs

1//! Core coherence metrics for attention mechanism evaluation.
2
3use serde::{Deserialize, Serialize};
4
5/// Result of comparing baseline vs. gated attention outputs.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct DeltaMetric {
8    pub coherence_delta: f64,
9    pub decision_flips: usize,
10    pub path_length_change: f64,
11}
12
13/// Measures the rate of contradictory outputs (negative dot product) between pairs.
14pub fn contradiction_rate(predictions: &[Vec<f32>], references: &[Vec<f32>]) -> f64 {
15    if predictions.is_empty() || references.is_empty() {
16        return 0.0;
17    }
18    let n = predictions.len().min(references.len());
19    let contradictions = predictions[..n]
20        .iter()
21        .zip(&references[..n])
22        .filter(|(p, r)| {
23            p.iter().zip(r.iter()).map(|(a, b)| *a as f64 * *b as f64).sum::<f64>() < 0.0
24        })
25        .count();
26    contradictions as f64 / n as f64
27}
28
29/// Mean pairwise cosine similarity between consecutive output vectors.
30pub fn entailment_consistency(outputs: &[Vec<f32>]) -> f64 {
31    if outputs.len() < 2 {
32        return 1.0;
33    }
34    let pairs = outputs.len() - 1;
35    let total: f64 = (0..pairs).map(|i| cosine(&outputs[i], &outputs[i + 1])).sum();
36    total / pairs as f64
37}
38
39/// Computes the behavioral delta between baseline and gated attention outputs.
40pub fn delta_behavior(baseline_outputs: &[f32], gated_outputs: &[f32]) -> DeltaMetric {
41    let n = baseline_outputs.len().min(gated_outputs.len());
42    if n == 0 {
43        return DeltaMetric { coherence_delta: 0.0, decision_flips: 0, path_length_change: 0.0 };
44    }
45    let (bl, gl) = (&baseline_outputs[..n], &gated_outputs[..n]);
46    let coherence_delta = cosine(bl, gl) - 1.0;
47    let decision_flips = bl.iter().zip(gl).filter(|(b, g)| b.is_sign_positive() != g.is_sign_positive()).count();
48    let bn = l2_norm(bl);
49    let path_length_change = if bn > f64::EPSILON { l2_norm(gl) / bn - 1.0 } else { 0.0 };
50    DeltaMetric { coherence_delta, decision_flips, path_length_change }
51}
52
53fn cosine(a: &[f32], b: &[f32]) -> f64 {
54    let dot: f64 = a.iter().zip(b).map(|(x, y)| *x as f64 * *y as f64).sum();
55    let denom = l2_norm(a) * l2_norm(b);
56    if denom < f64::EPSILON { 0.0 } else { dot / denom }
57}
58
59fn l2_norm(v: &[f32]) -> f64 {
60    v.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt()
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn contradiction_rate_boundaries() {
69        let preds = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
70        assert_eq!(contradiction_rate(&preds, &[vec![1.0, 1.0], vec![1.0, 1.0]]), 0.0);
71        assert_eq!(contradiction_rate(&preds, &[vec![-1.0, -1.0], vec![-1.0, -1.0]]), 1.0);
72        assert_eq!(contradiction_rate(&[], &[]), 0.0);
73    }
74
75    #[test]
76    fn entailment_consistency_cases() {
77        let identical = vec![vec![1.0, 0.0]; 3];
78        assert!((entailment_consistency(&identical) - 1.0).abs() < 1e-10);
79        assert_eq!(entailment_consistency(&[vec![1.0]]), 1.0);
80        let ortho = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
81        assert!(entailment_consistency(&ortho).abs() < 1e-10);
82    }
83
84    #[test]
85    fn delta_behavior_cases() {
86        let v = vec![1.0, 2.0, 3.0];
87        let d = delta_behavior(&v, &v);
88        assert!(d.coherence_delta.abs() < 1e-10);
89        assert_eq!(d.decision_flips, 0);
90
91        let d2 = delta_behavior(&[1.0, -1.0, 1.0], &[-1.0, 1.0, 1.0]);
92        assert_eq!(d2.decision_flips, 2);
93
94        let d3 = delta_behavior(&[], &[]);
95        assert_eq!(d3.decision_flips, 0);
96    }
97}