Skip to main content

ruv_neural_decoder/
threshold_decoder.rs

1//! Threshold-based topology decoder for cognitive state classification.
2
3use std::collections::HashMap;
4
5use ruv_neural_core::topology::{CognitiveState, TopologyMetrics};
6use serde::{Deserialize, Serialize};
7
8/// Decode cognitive states from topology metrics using learned thresholds.
9///
10/// Each cognitive state is associated with expected ranges for key topology
11/// metrics (mincut, modularity, efficiency, entropy). The decoder scores
12/// each candidate state by how well the input metrics fall within the
13/// expected ranges.
14pub struct ThresholdDecoder {
15    thresholds: HashMap<CognitiveState, TopologyThreshold>,
16}
17
18/// Threshold ranges for topology metrics associated with a cognitive state.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct TopologyThreshold {
21    /// Expected range for global minimum cut value.
22    pub mincut_range: (f64, f64),
23    /// Expected range for modularity.
24    pub modularity_range: (f64, f64),
25    /// Expected range for global efficiency.
26    pub efficiency_range: (f64, f64),
27    /// Expected range for graph entropy.
28    pub entropy_range: (f64, f64),
29}
30
31impl TopologyThreshold {
32    /// Score how well a set of metrics matches this threshold.
33    ///
34    /// Returns a value in `[0, 1]` where 1.0 means all metrics fall within
35    /// the expected ranges.
36    fn score(&self, metrics: &TopologyMetrics) -> f64 {
37        let scores = [
38            range_score(metrics.global_mincut, self.mincut_range),
39            range_score(metrics.modularity, self.modularity_range),
40            range_score(metrics.global_efficiency, self.efficiency_range),
41            range_score(metrics.graph_entropy, self.entropy_range),
42        ];
43        scores.iter().sum::<f64>() / scores.len() as f64
44    }
45}
46
47impl ThresholdDecoder {
48    /// Create a new threshold decoder with no thresholds defined.
49    pub fn new() -> Self {
50        Self {
51            thresholds: HashMap::new(),
52        }
53    }
54
55    /// Set the threshold for a specific cognitive state.
56    pub fn set_threshold(&mut self, state: CognitiveState, threshold: TopologyThreshold) {
57        self.thresholds.insert(state, threshold);
58    }
59
60    /// Learn thresholds from labeled topology data.
61    ///
62    /// For each cognitive state present in the data, computes the min/max
63    /// range of each metric with a 10% margin.
64    pub fn learn_thresholds(&mut self, labeled_data: &[(TopologyMetrics, CognitiveState)]) {
65        // Group metrics by state.
66        let mut grouped: HashMap<CognitiveState, Vec<&TopologyMetrics>> = HashMap::new();
67        for (metrics, state) in labeled_data {
68            grouped.entry(*state).or_default().push(metrics);
69        }
70
71        for (state, metrics_vec) in grouped {
72            if metrics_vec.is_empty() {
73                continue;
74            }
75
76            let mincut_range = compute_range(metrics_vec.iter().map(|m| m.global_mincut));
77            let modularity_range = compute_range(metrics_vec.iter().map(|m| m.modularity));
78            let efficiency_range =
79                compute_range(metrics_vec.iter().map(|m| m.global_efficiency));
80            let entropy_range = compute_range(metrics_vec.iter().map(|m| m.graph_entropy));
81
82            self.thresholds.insert(
83                state,
84                TopologyThreshold {
85                    mincut_range,
86                    modularity_range,
87                    efficiency_range,
88                    entropy_range,
89                },
90            );
91        }
92    }
93
94    /// Decode the cognitive state from topology metrics.
95    ///
96    /// Returns the best-matching state and a confidence score in `[0, 1]`.
97    /// If no thresholds are defined, returns `(Unknown, 0.0)`.
98    pub fn decode(&self, metrics: &TopologyMetrics) -> (CognitiveState, f64) {
99        if self.thresholds.is_empty() {
100            return (CognitiveState::Unknown, 0.0);
101        }
102
103        let mut best_state = CognitiveState::Unknown;
104        let mut best_score = -1.0_f64;
105
106        for (state, threshold) in &self.thresholds {
107            let score = threshold.score(metrics);
108            if score > best_score {
109                best_score = score;
110                best_state = *state;
111            }
112        }
113
114        (best_state, best_score.clamp(0.0, 1.0))
115    }
116
117    /// Number of states with defined thresholds.
118    pub fn num_states(&self) -> usize {
119        self.thresholds.len()
120    }
121}
122
123impl Default for ThresholdDecoder {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129/// Compute the range (min, max) from an iterator of values, with a 10% margin.
130fn compute_range(values: impl Iterator<Item = f64>) -> (f64, f64) {
131    let vals: Vec<f64> = values.collect();
132    if vals.is_empty() {
133        return (0.0, 0.0);
134    }
135
136    let min = vals.iter().cloned().fold(f64::INFINITY, f64::min);
137    let max = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
138    let margin = (max - min).abs() * 0.1;
139
140    (min - margin, max + margin)
141}
142
143/// Score how well a value falls within a range.
144///
145/// Returns 1.0 if within range, decays toward 0.0 as the value moves
146/// further outside.
147fn range_score(value: f64, (lo, hi): (f64, f64)) -> f64 {
148    if value >= lo && value <= hi {
149        return 1.0;
150    }
151    let range_width = (hi - lo).abs().max(1e-10);
152    if value < lo {
153        let distance = lo - value;
154        (-distance / range_width).exp()
155    } else {
156        let distance = value - hi;
157        (-distance / range_width).exp()
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    fn make_metrics(mincut: f64, modularity: f64, efficiency: f64, entropy: f64) -> TopologyMetrics {
166        TopologyMetrics {
167            global_mincut: mincut,
168            modularity,
169            global_efficiency: efficiency,
170            local_efficiency: 0.0,
171            graph_entropy: entropy,
172            fiedler_value: 0.0,
173            num_modules: 4,
174            timestamp: 0.0,
175        }
176    }
177
178    #[test]
179    fn test_learn_thresholds() {
180        let mut decoder = ThresholdDecoder::new();
181        let data = vec![
182            (make_metrics(5.0, 0.4, 0.3, 2.0), CognitiveState::Rest),
183            (make_metrics(5.5, 0.45, 0.32, 2.1), CognitiveState::Rest),
184            (make_metrics(5.2, 0.42, 0.31, 2.05), CognitiveState::Rest),
185            (make_metrics(8.0, 0.6, 0.5, 3.0), CognitiveState::Focused),
186            (make_metrics(8.5, 0.65, 0.52, 3.1), CognitiveState::Focused),
187        ];
188
189        decoder.learn_thresholds(&data);
190        assert_eq!(decoder.num_states(), 2);
191
192        // Query with Rest-like metrics.
193        let (state, confidence) = decoder.decode(&make_metrics(5.1, 0.41, 0.31, 2.03));
194        assert_eq!(state, CognitiveState::Rest);
195        assert!(confidence > 0.5);
196    }
197
198    #[test]
199    fn test_set_threshold() {
200        let mut decoder = ThresholdDecoder::new();
201        decoder.set_threshold(
202            CognitiveState::Rest,
203            TopologyThreshold {
204                mincut_range: (4.0, 6.0),
205                modularity_range: (0.3, 0.5),
206                efficiency_range: (0.2, 0.4),
207                entropy_range: (1.5, 2.5),
208            },
209        );
210
211        let (state, confidence) = decoder.decode(&make_metrics(5.0, 0.4, 0.3, 2.0));
212        assert_eq!(state, CognitiveState::Rest);
213        assert!((confidence - 1.0).abs() < 1e-10);
214    }
215
216    #[test]
217    fn test_empty_decoder_returns_unknown() {
218        let decoder = ThresholdDecoder::new();
219        let (state, confidence) = decoder.decode(&make_metrics(5.0, 0.4, 0.3, 2.0));
220        assert_eq!(state, CognitiveState::Unknown);
221        assert!((confidence - 0.0).abs() < 1e-10);
222    }
223
224    #[test]
225    fn test_confidence_in_range() {
226        let mut decoder = ThresholdDecoder::new();
227        decoder.set_threshold(
228            CognitiveState::Focused,
229            TopologyThreshold {
230                mincut_range: (7.0, 9.0),
231                modularity_range: (0.5, 0.7),
232                efficiency_range: (0.4, 0.6),
233                entropy_range: (2.5, 3.5),
234            },
235        );
236        // Query outside all ranges.
237        let (_, confidence) = decoder.decode(&make_metrics(0.0, 0.0, 0.0, 0.0));
238        assert!(confidence >= 0.0 && confidence <= 1.0);
239    }
240}