ruvector_coherence/
metrics.rs1use serde::{Deserialize, Serialize};
4
5#[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
13pub 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()
24 .zip(r.iter())
25 .map(|(a, b)| *a as f64 * *b as f64)
26 .sum::<f64>()
27 < 0.0
28 })
29 .count();
30 contradictions as f64 / n as f64
31}
32
33pub fn entailment_consistency(outputs: &[Vec<f32>]) -> f64 {
35 if outputs.len() < 2 {
36 return 1.0;
37 }
38 let pairs = outputs.len() - 1;
39 let total: f64 = (0..pairs)
40 .map(|i| cosine(&outputs[i], &outputs[i + 1]))
41 .sum();
42 total / pairs as f64
43}
44
45pub fn delta_behavior(baseline_outputs: &[f32], gated_outputs: &[f32]) -> DeltaMetric {
47 let n = baseline_outputs.len().min(gated_outputs.len());
48 if n == 0 {
49 return DeltaMetric {
50 coherence_delta: 0.0,
51 decision_flips: 0,
52 path_length_change: 0.0,
53 };
54 }
55 let (bl, gl) = (&baseline_outputs[..n], &gated_outputs[..n]);
56 let coherence_delta = cosine(bl, gl) - 1.0;
57 let decision_flips = bl
58 .iter()
59 .zip(gl)
60 .filter(|(b, g)| b.is_sign_positive() != g.is_sign_positive())
61 .count();
62 let bn = l2_norm(bl);
63 let path_length_change = if bn > f64::EPSILON {
64 l2_norm(gl) / bn - 1.0
65 } else {
66 0.0
67 };
68 DeltaMetric {
69 coherence_delta,
70 decision_flips,
71 path_length_change,
72 }
73}
74
75fn cosine(a: &[f32], b: &[f32]) -> f64 {
76 let dot: f64 = a.iter().zip(b).map(|(x, y)| *x as f64 * *y as f64).sum();
77 let denom = l2_norm(a) * l2_norm(b);
78 if denom < f64::EPSILON {
79 0.0
80 } else {
81 dot / denom
82 }
83}
84
85fn l2_norm(v: &[f32]) -> f64 {
86 v.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt()
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn contradiction_rate_boundaries() {
95 let preds = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
96 assert_eq!(
97 contradiction_rate(&preds, &[vec![1.0, 1.0], vec![1.0, 1.0]]),
98 0.0
99 );
100 assert_eq!(
101 contradiction_rate(&preds, &[vec![-1.0, -1.0], vec![-1.0, -1.0]]),
102 1.0
103 );
104 assert_eq!(contradiction_rate(&[], &[]), 0.0);
105 }
106
107 #[test]
108 fn entailment_consistency_cases() {
109 let identical = vec![vec![1.0, 0.0]; 3];
110 assert!((entailment_consistency(&identical) - 1.0).abs() < 1e-10);
111 assert_eq!(entailment_consistency(&[vec![1.0]]), 1.0);
112 let ortho = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
113 assert!(entailment_consistency(&ortho).abs() < 1e-10);
114 }
115
116 #[test]
117 fn delta_behavior_cases() {
118 let v = vec![1.0, 2.0, 3.0];
119 let d = delta_behavior(&v, &v);
120 assert!(d.coherence_delta.abs() < 1e-10);
121 assert_eq!(d.decision_flips, 0);
122
123 let d2 = delta_behavior(&[1.0, -1.0, 1.0], &[-1.0, 1.0, 1.0]);
124 assert_eq!(d2.decision_flips, 2);
125
126 let d3 = delta_behavior(&[], &[]);
127 assert_eq!(d3.decision_flips, 0);
128 }
129}