ruvector_sparse_inference/precision/
policy.rs

1//! Graduation Policy - Rules for lane transitions
2//!
3//! Implements the control theory for when signals should move between precision lanes.
4
5use super::lanes::{PrecisionLane, LaneConfig};
6use serde::{Deserialize, Serialize};
7
8/// Metrics used for graduation decisions
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct GraduationMetrics {
11    /// Novelty score (0.0 to 1.0) - how different from recent patterns
12    pub novelty: f32,
13
14    /// Drift score (0.0 to 1.0) - how much the signal has drifted
15    pub drift: f32,
16
17    /// Number of steps drift has persisted
18    pub drift_steps: usize,
19
20    /// Confidence score (0.0 to 1.0)
21    pub confidence: f32,
22
23    /// Stability score (0.0 to 1.0) - inverse of variance
24    pub stability: f32,
25
26    /// Number of stable steps
27    pub stable_steps: usize,
28
29    /// Velocity (rate of change)
30    pub velocity: f32,
31
32    /// Active set size (number of active neurons)
33    pub active_set_size: usize,
34
35    /// Uncertainty score (0.0 to 1.0)
36    pub uncertainty: f32,
37
38    /// Current cost usage (0.0 to 1.0)
39    pub cost_usage: f32,
40
41    /// Whether action is needed
42    pub action_needed: bool,
43}
44
45impl GraduationMetrics {
46    /// Create new metrics with default values
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Update metrics with a new observation
52    pub fn update(&mut self, observation: &ObservationMetrics, ema_alpha: f32) {
53        // Exponential moving average for smooth updates
54        self.novelty = ema_alpha * observation.novelty + (1.0 - ema_alpha) * self.novelty;
55        self.drift = ema_alpha * observation.drift + (1.0 - ema_alpha) * self.drift;
56        self.confidence = ema_alpha * observation.confidence + (1.0 - ema_alpha) * self.confidence;
57        self.stability = ema_alpha * observation.stability + (1.0 - ema_alpha) * self.stability;
58        self.velocity = ema_alpha * observation.velocity + (1.0 - ema_alpha) * self.velocity;
59        self.uncertainty = ema_alpha * observation.uncertainty + (1.0 - ema_alpha) * self.uncertainty;
60
61        self.active_set_size = observation.active_set_size;
62        self.action_needed = observation.action_needed;
63
64        // Update drift persistence
65        if observation.drift > 0.1 {
66            self.drift_steps += 1;
67        } else {
68            self.drift_steps = 0;
69        }
70
71        // Update stability persistence
72        if observation.stability > 0.8 {
73            self.stable_steps += 1;
74        } else {
75            self.stable_steps = 0;
76        }
77    }
78}
79
80/// Raw observation metrics from a single step
81#[derive(Debug, Clone, Default)]
82pub struct ObservationMetrics {
83    pub novelty: f32,
84    pub drift: f32,
85    pub confidence: f32,
86    pub stability: f32,
87    pub velocity: f32,
88    pub uncertainty: f32,
89    pub active_set_size: usize,
90    pub action_needed: bool,
91}
92
93/// Decision from graduation policy
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum GraduationDecision {
96    /// Stay in current lane
97    Stay,
98    /// Escalate to higher precision lane
99    Escalate(PrecisionLane),
100    /// Demote to lower precision lane
101    Demote(PrecisionLane),
102}
103
104/// Graduation policy for lane transitions
105#[derive(Debug, Clone)]
106pub struct GraduationPolicy {
107    /// Current precision lane
108    pub current_lane: PrecisionLane,
109    /// Configuration
110    pub config: LaneConfig,
111    /// Accumulated metrics
112    pub metrics: GraduationMetrics,
113    /// EMA smoothing factor
114    pub ema_alpha: f32,
115}
116
117impl GraduationPolicy {
118    /// Create a new graduation policy
119    pub fn new(initial_lane: PrecisionLane, config: LaneConfig) -> Self {
120        Self {
121            current_lane: initial_lane,
122            config,
123            metrics: GraduationMetrics::new(),
124            ema_alpha: 0.3,
125        }
126    }
127
128    /// Evaluate and return graduation decision
129    pub fn evaluate(&mut self, observation: &ObservationMetrics) -> GraduationDecision {
130        // Update metrics
131        self.metrics.update(observation, self.ema_alpha);
132
133        // Check for escalation
134        if self.should_escalate() {
135            if let Some(next_lane) = self.next_higher_lane() {
136                return GraduationDecision::Escalate(next_lane);
137            }
138        }
139
140        // Check for demotion
141        if self.should_demote() {
142            if let Some(prev_lane) = self.next_lower_lane() {
143                return GraduationDecision::Demote(prev_lane);
144            }
145        }
146
147        GraduationDecision::Stay
148    }
149
150    /// Apply a graduation decision
151    pub fn apply_decision(&mut self, decision: GraduationDecision) {
152        match decision {
153            GraduationDecision::Stay => {}
154            GraduationDecision::Escalate(lane) | GraduationDecision::Demote(lane) => {
155                self.current_lane = lane;
156                // Reset stability counters on lane change
157                self.metrics.stable_steps = 0;
158                self.metrics.drift_steps = 0;
159            }
160        }
161    }
162
163    /// Check if escalation conditions are met
164    fn should_escalate(&self) -> bool {
165        // Escalate when:
166        // 1. Novelty exceeds threshold
167        let novelty_trigger = self.metrics.novelty > self.config.novelty_threshold;
168
169        // 2. Drift persists
170        let drift_trigger = self.metrics.drift_steps >= self.config.drift_persistence_threshold;
171
172        // 3. Confidence and stability pass
173        let quality_pass = self.metrics.confidence >= self.config.confidence_threshold
174            && self.metrics.stability >= 0.5;
175
176        // 4. Cost budget allows
177        let budget_allows = self.metrics.cost_usage < self.config.escalation_budget;
178
179        // Escalate if any trigger fires AND quality/budget conditions are met
180        (novelty_trigger || drift_trigger) && quality_pass && budget_allows
181    }
182
183    /// Check if demotion conditions are met
184    fn should_demote(&self) -> bool {
185        // Demote when:
186        // 1. Stability returns
187        let stability_returned = self.metrics.stable_steps >= self.config.min_stability_steps;
188
189        // 2. Velocity stalls
190        let velocity_stalled = self.metrics.velocity.abs() < 0.01;
191
192        // 3. Active set shrinks (not using the precision)
193        let active_set_shrunk = self.metrics.active_set_size < 10;
194
195        // 4. High uncertainty but no action needed
196        let uncertain_idle = self.metrics.uncertainty > 0.7 && !self.metrics.action_needed;
197
198        // Demote if stability AND (velocity stall OR active shrink OR uncertain idle)
199        stability_returned && (velocity_stalled || active_set_shrunk || uncertain_idle)
200    }
201
202    /// Get the next higher precision lane
203    fn next_higher_lane(&self) -> Option<PrecisionLane> {
204        match self.current_lane {
205            PrecisionLane::Bit3 => Some(PrecisionLane::Bit5),
206            PrecisionLane::Bit5 => Some(PrecisionLane::Bit7),
207            PrecisionLane::Bit7 => Some(PrecisionLane::Float32),
208            PrecisionLane::Float32 => None,
209        }
210    }
211
212    /// Get the next lower precision lane
213    fn next_lower_lane(&self) -> Option<PrecisionLane> {
214        match self.current_lane {
215            PrecisionLane::Bit3 => None,
216            PrecisionLane::Bit5 => Some(PrecisionLane::Bit3),
217            PrecisionLane::Bit7 => Some(PrecisionLane::Bit5),
218            PrecisionLane::Float32 => Some(PrecisionLane::Bit7),
219        }
220    }
221}
222
223/// Event processor with precision lane awareness
224pub struct LanedEventProcessor {
225    /// Graduation policy
226    policy: GraduationPolicy,
227    /// Event counter
228    event_count: usize,
229}
230
231impl LanedEventProcessor {
232    /// Create a new event processor
233    pub fn new(config: LaneConfig) -> Self {
234        Self {
235            policy: GraduationPolicy::new(config.default_lane, config),
236            event_count: 0,
237        }
238    }
239
240    /// Process an event through the appropriate precision lane
241    pub fn process_event(&mut self, event: &Event) -> ProcessResult {
242        self.event_count += 1;
243
244        // 3-bit reflex check (always runs first)
245        let reflex_result = self.reflex_3bit(event);
246        if !reflex_result.boundary_crossed {
247            return ProcessResult::Reflexed(reflex_result);
248        }
249
250        // 5-bit embedding update (event-driven)
251        let embed_result = self.embed_5bit(event);
252
253        // Check for graduation to 7-bit
254        let observation = self.compute_observation(&reflex_result, &embed_result);
255        let decision = self.policy.evaluate(&observation);
256
257        if matches!(decision, GraduationDecision::Escalate(PrecisionLane::Bit7))
258            || self.policy.current_lane == PrecisionLane::Bit7
259        {
260            // 7-bit reasoning
261            let reason_result = self.reason_7bit(event, &embed_result);
262            self.policy.apply_decision(decision);
263            return ProcessResult::Reasoned(reason_result);
264        }
265
266        self.policy.apply_decision(decision);
267        ProcessResult::Embedded(embed_result)
268    }
269
270    fn reflex_3bit(&self, _event: &Event) -> ReflexResult {
271        // 3-bit reflex processing
272        ReflexResult {
273            boundary_crossed: true, // Simplified
274            health_ok: true,
275            anomaly_detected: false,
276        }
277    }
278
279    fn embed_5bit(&self, _event: &Event) -> EmbedResult {
280        // 5-bit embedding update
281        EmbedResult {
282            embedding_delta: vec![0.0; 64],
283            drift_detected: false,
284        }
285    }
286
287    fn reason_7bit(&self, _event: &Event, _embed: &EmbedResult) -> ReasonResult {
288        // 7-bit reasoning
289        ReasonResult {
290            should_write_memory: false,
291            summary: String::new(),
292            actions: Vec::new(),
293        }
294    }
295
296    fn compute_observation(&self, _reflex: &ReflexResult, _embed: &EmbedResult) -> ObservationMetrics {
297        ObservationMetrics::default()
298    }
299
300    /// Get current lane
301    pub fn current_lane(&self) -> PrecisionLane {
302        self.policy.current_lane
303    }
304}
305
306/// Simple event type for processing
307#[derive(Debug, Clone)]
308pub struct Event {
309    pub data: Vec<f32>,
310    pub timestamp: u64,
311}
312
313/// Result of 3-bit reflex processing
314#[derive(Debug, Clone)]
315pub struct ReflexResult {
316    pub boundary_crossed: bool,
317    pub health_ok: bool,
318    pub anomaly_detected: bool,
319}
320
321/// Result of 5-bit embedding
322#[derive(Debug, Clone)]
323pub struct EmbedResult {
324    pub embedding_delta: Vec<f32>,
325    pub drift_detected: bool,
326}
327
328/// Result of 7-bit reasoning
329#[derive(Debug, Clone)]
330pub struct ReasonResult {
331    pub should_write_memory: bool,
332    pub summary: String,
333    pub actions: Vec<String>,
334}
335
336/// Overall processing result
337#[derive(Debug)]
338pub enum ProcessResult {
339    Reflexed(ReflexResult),
340    Embedded(EmbedResult),
341    Reasoned(ReasonResult),
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_graduation_policy_creation() {
350        let config = LaneConfig::default();
351        let policy = GraduationPolicy::new(PrecisionLane::Bit5, config);
352
353        assert_eq!(policy.current_lane, PrecisionLane::Bit5);
354    }
355
356    #[test]
357    fn test_escalation_on_novelty() {
358        let config = LaneConfig {
359            novelty_threshold: 0.3,
360            confidence_threshold: 0.5,
361            ..Default::default()
362        };
363        let mut policy = GraduationPolicy::new(PrecisionLane::Bit5, config);
364        // Set higher EMA alpha for faster response in tests
365        policy.ema_alpha = 1.0;
366
367        // High novelty, good confidence (use high values to overcome any thresholds)
368        let observation = ObservationMetrics {
369            novelty: 0.9,
370            confidence: 0.9,
371            stability: 0.6,
372            ..Default::default()
373        };
374
375        let decision = policy.evaluate(&observation);
376        assert!(matches!(decision, GraduationDecision::Escalate(PrecisionLane::Bit7)));
377    }
378
379    #[test]
380    fn test_demotion_on_stability() {
381        let mut config = LaneConfig::default();
382        config.min_stability_steps = 2;
383
384        let mut policy = GraduationPolicy::new(PrecisionLane::Bit7, config);
385
386        // Build up stable steps
387        for _ in 0..5 {
388            let observation = ObservationMetrics {
389                stability: 0.9,
390                velocity: 0.001,
391                active_set_size: 5,
392                ..Default::default()
393            };
394            policy.evaluate(&observation);
395        }
396
397        let observation = ObservationMetrics {
398            stability: 0.9,
399            velocity: 0.001,
400            active_set_size: 5,
401            ..Default::default()
402        };
403
404        let decision = policy.evaluate(&observation);
405        assert!(matches!(decision, GraduationDecision::Demote(PrecisionLane::Bit5)));
406    }
407}