1use std::collections::HashMap;
4
5use ruv_neural_core::topology::{CognitiveState, TopologyMetrics};
6use serde::{Deserialize, Serialize};
7
8pub struct ThresholdDecoder {
15 thresholds: HashMap<CognitiveState, TopologyThreshold>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct TopologyThreshold {
21 pub mincut_range: (f64, f64),
23 pub modularity_range: (f64, f64),
25 pub efficiency_range: (f64, f64),
27 pub entropy_range: (f64, f64),
29}
30
31impl TopologyThreshold {
32 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 pub fn new() -> Self {
50 Self {
51 thresholds: HashMap::new(),
52 }
53 }
54
55 pub fn set_threshold(&mut self, state: CognitiveState, threshold: TopologyThreshold) {
57 self.thresholds.insert(state, threshold);
58 }
59
60 pub fn learn_thresholds(&mut self, labeled_data: &[(TopologyMetrics, CognitiveState)]) {
65 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 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 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
129fn 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
143fn 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 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 let (_, confidence) = decoder.decode(&make_metrics(0.0, 0.0, 0.0, 0.0));
238 assert!(confidence >= 0.0 && confidence <= 1.0);
239 }
240}