ruvector_sparse_inference/pi/
drift.rs

1//! π-based drift detection for quantization honesty
2//!
3//! Because π cannot be represented exactly at any finite precision, it is
4//! perfect for detecting distortion. If you:
5//!
6//! 1. Project a signal through a π-based transform
7//! 2. Quantize
8//! 3. Dequantize
9//! 4. Project back
10//!
11//! Then measure error growth over time, you get a **quantization honesty signal**.
12//!
13//! If error grows faster than expected:
14//! - Precision is too low
15//! - Accumulation is biased
16//! - Or hardware is misbehaving
17//!
18//! This pairs beautifully with min-cut stability metrics.
19
20use crate::precision::PrecisionLane;
21use std::f32::consts::PI;
22
23/// Expected drift rate per lane (empirically calibrated)
24const DRIFT_RATE_3BIT: f32 = 0.15;  // High drift expected
25const DRIFT_RATE_5BIT: f32 = 0.05;  // Moderate drift
26const DRIFT_RATE_7BIT: f32 = 0.01;  // Low drift
27const DRIFT_RATE_FLOAT: f32 = 0.0001; // Minimal drift
28
29/// Drift detector using π transforms
30#[derive(Debug, Clone)]
31pub struct DriftDetector {
32    /// Precision lane being monitored
33    lane: PrecisionLane,
34    /// Accumulated error
35    accumulated_error: f32,
36    /// Number of samples processed
37    sample_count: usize,
38    /// Error history (ring buffer)
39    error_history: Vec<f32>,
40    /// History index
41    history_idx: usize,
42    /// Expected drift rate for this lane
43    expected_drift_rate: f32,
44    /// π reference signal
45    pi_reference: f32,
46    /// Escalation threshold
47    escalation_threshold: f32,
48}
49
50impl DriftDetector {
51    /// Create a new drift detector for a precision lane
52    pub fn new(lane: PrecisionLane) -> Self {
53        let expected_drift_rate = match lane {
54            PrecisionLane::Bit3 => DRIFT_RATE_3BIT,
55            PrecisionLane::Bit5 => DRIFT_RATE_5BIT,
56            PrecisionLane::Bit7 => DRIFT_RATE_7BIT,
57            PrecisionLane::Float32 => DRIFT_RATE_FLOAT,
58        };
59
60        Self {
61            lane,
62            accumulated_error: 0.0,
63            sample_count: 0,
64            error_history: vec![0.0; 64], // Rolling window
65            history_idx: 0,
66            expected_drift_rate,
67            pi_reference: PI,
68            escalation_threshold: expected_drift_rate * 3.0, // 3x expected = escalate
69        }
70    }
71
72    /// Check quantization honesty between original and quantized values
73    pub fn check(&mut self, original: &[f32], quantized: &[f32]) -> QuantizationHonesty {
74        assert_eq!(original.len(), quantized.len());
75
76        // Apply π transform to both
77        let pi_original: Vec<f32> = original.iter()
78            .map(|&x| self.pi_transform(x))
79            .collect();
80        let pi_quantized: Vec<f32> = quantized.iter()
81            .map(|&x| self.pi_transform(x))
82            .collect();
83
84        // Compute error after π projection
85        let error = self.compute_error(&pi_original, &pi_quantized);
86        self.update(error);
87
88        // Check if error is within expected bounds
89        let ratio = error / self.expected_drift_rate.max(0.0001);
90        let is_honest = ratio < 2.0;
91        let should_escalate = ratio > 3.0;
92
93        QuantizationHonesty {
94            error,
95            expected_error: self.expected_drift_rate,
96            ratio,
97            is_honest,
98            should_escalate,
99            sample_count: self.sample_count,
100        }
101    }
102
103    /// π transform: project value through π-based trigonometric function
104    fn pi_transform(&self, value: f32) -> f32 {
105        // Use both sin and cos to capture full information
106        let angle = value * self.pi_reference;
107        angle.sin() + angle.cos() * 0.5
108    }
109
110    /// Inverse π transform (approximate)
111    fn inverse_pi_transform(&self, transformed: f32) -> f32 {
112        // This is lossy by design - the difference measures drift
113        let angle = transformed.atan2(1.0);
114        angle / self.pi_reference
115    }
116
117    /// Compute mean squared error between transformed vectors
118    fn compute_error(&self, a: &[f32], b: &[f32]) -> f32 {
119        if a.is_empty() {
120            return 0.0;
121        }
122
123        let mse: f32 = a.iter()
124            .zip(b.iter())
125            .map(|(&x, &y)| (x - y).powi(2))
126            .sum::<f32>() / a.len() as f32;
127
128        mse.sqrt()
129    }
130
131    /// Update drift tracking with new error sample
132    pub fn update(&mut self, error: f32) {
133        self.accumulated_error += error;
134        self.sample_count += 1;
135
136        // Update rolling history
137        self.error_history[self.history_idx] = error;
138        self.history_idx = (self.history_idx + 1) % self.error_history.len();
139    }
140
141    /// Get drift report
142    pub fn report(&self) -> DriftReport {
143        let mean_error = if self.sample_count > 0 {
144            self.accumulated_error / self.sample_count as f32
145        } else {
146            0.0
147        };
148
149        // Compute trend from history
150        let trend = self.compute_trend();
151
152        // Check if drift is accelerating
153        let is_accelerating = trend > self.expected_drift_rate * 0.1;
154
155        DriftReport {
156            mean_error,
157            accumulated_error: self.accumulated_error,
158            sample_count: self.sample_count,
159            trend,
160            is_accelerating,
161            should_escalate: mean_error > self.escalation_threshold,
162            lane: self.lane,
163        }
164    }
165
166    /// Compute error trend (slope of recent errors)
167    fn compute_trend(&self) -> f32 {
168        if self.sample_count < 2 {
169            return 0.0;
170        }
171
172        let n = self.error_history.len().min(self.sample_count);
173        if n < 2 {
174            return 0.0;
175        }
176
177        // Simple linear regression on recent errors
178        let mut sum_x = 0.0f32;
179        let mut sum_y = 0.0f32;
180        let mut sum_xy = 0.0f32;
181        let mut sum_xx = 0.0f32;
182
183        for i in 0..n {
184            let x = i as f32;
185            let y = self.error_history[i];
186            sum_x += x;
187            sum_y += y;
188            sum_xy += x * y;
189            sum_xx += x * x;
190        }
191
192        let n_f = n as f32;
193        let denominator = n_f * sum_xx - sum_x * sum_x;
194        if denominator.abs() < 1e-10 {
195            return 0.0;
196        }
197
198        (n_f * sum_xy - sum_x * sum_y) / denominator
199    }
200
201    /// Reset drift tracking
202    pub fn reset(&mut self) {
203        self.accumulated_error = 0.0;
204        self.sample_count = 0;
205        self.error_history.fill(0.0);
206        self.history_idx = 0;
207    }
208
209    /// Run π checksum on a signal (deterministic honesty test)
210    pub fn pi_checksum(&self, signal: &[f32]) -> f32 {
211        if signal.is_empty() {
212            return 0.0;
213        }
214
215        // Accumulate through π transform
216        let mut checksum = 0.0f32;
217        for (i, &val) in signal.iter().enumerate() {
218            let pi_phase = (i as f32 + 1.0) * PI / signal.len() as f32;
219            checksum += val * pi_phase.sin();
220        }
221
222        checksum / signal.len() as f32
223    }
224
225    /// Verify π checksum after quantization
226    pub fn verify_checksum(&self, original: &[f32], quantized: &[f32]) -> bool {
227        let orig_checksum = self.pi_checksum(original);
228        let quant_checksum = self.pi_checksum(quantized);
229
230        let error = (orig_checksum - quant_checksum).abs();
231        error < self.expected_drift_rate
232    }
233}
234
235/// Quantization honesty result
236#[derive(Debug, Clone, Copy)]
237pub struct QuantizationHonesty {
238    /// Actual error measured
239    pub error: f32,
240    /// Expected error for this precision lane
241    pub expected_error: f32,
242    /// Ratio of actual to expected (>1 = worse than expected)
243    pub ratio: f32,
244    /// Is the quantization honest (within 2x expected)?
245    pub is_honest: bool,
246    /// Should we escalate to higher precision?
247    pub should_escalate: bool,
248    /// Number of samples in this measurement
249    pub sample_count: usize,
250}
251
252/// Drift report summary
253#[derive(Debug, Clone)]
254pub struct DriftReport {
255    /// Mean error over all samples
256    pub mean_error: f32,
257    /// Total accumulated error
258    pub accumulated_error: f32,
259    /// Number of samples processed
260    pub sample_count: usize,
261    /// Error trend (positive = getting worse)
262    pub trend: f32,
263    /// Is drift accelerating?
264    pub is_accelerating: bool,
265    /// Should escalate precision lane?
266    pub should_escalate: bool,
267    /// Current precision lane
268    pub lane: PrecisionLane,
269}
270
271impl DriftReport {
272    /// Get severity level (0-3)
273    pub fn severity(&self) -> u8 {
274        if self.should_escalate {
275            3
276        } else if self.is_accelerating {
277            2
278        } else if self.mean_error > 0.05 {
279            1
280        } else {
281            0
282        }
283    }
284
285    /// Suggested next lane
286    pub fn suggested_lane(&self) -> Option<PrecisionLane> {
287        if self.should_escalate {
288            match self.lane {
289                PrecisionLane::Bit3 => Some(PrecisionLane::Bit5),
290                PrecisionLane::Bit5 => Some(PrecisionLane::Bit7),
291                PrecisionLane::Bit7 => Some(PrecisionLane::Float32),
292                PrecisionLane::Float32 => None,
293            }
294        } else {
295            None
296        }
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_drift_detector_creation() {
306        let detector = DriftDetector::new(PrecisionLane::Bit5);
307        assert_eq!(detector.sample_count, 0);
308    }
309
310    #[test]
311    fn test_pi_transform_deterministic() {
312        let detector = DriftDetector::new(PrecisionLane::Bit5);
313        let v1 = detector.pi_transform(0.5);
314        let v2 = detector.pi_transform(0.5);
315        assert_eq!(v1, v2);
316    }
317
318    #[test]
319    fn test_honesty_check_identical() {
320        let mut detector = DriftDetector::new(PrecisionLane::Bit7);
321        let values = vec![0.1, 0.2, 0.3, 0.4, 0.5];
322        let honesty = detector.check(&values, &values);
323        assert!(honesty.error < 0.001);
324        assert!(honesty.is_honest);
325    }
326
327    #[test]
328    fn test_honesty_check_with_error() {
329        let mut detector = DriftDetector::new(PrecisionLane::Bit3);
330        let original = vec![0.1, 0.2, 0.3, 0.4, 0.5];
331        let quantized = vec![0.15, 0.25, 0.35, 0.45, 0.55]; // 0.05 error each
332        let honesty = detector.check(&original, &quantized);
333        assert!(honesty.error > 0.0);
334    }
335
336    #[test]
337    fn test_drift_report() {
338        let mut detector = DriftDetector::new(PrecisionLane::Bit5);
339        detector.update(0.01);
340        detector.update(0.02);
341        detector.update(0.03);
342
343        let report = detector.report();
344        assert_eq!(report.sample_count, 3);
345        assert!(report.mean_error > 0.0);
346    }
347
348    #[test]
349    fn test_pi_checksum() {
350        let detector = DriftDetector::new(PrecisionLane::Bit5);
351        let signal = vec![1.0, 2.0, 3.0, 4.0, 5.0];
352        let checksum = detector.pi_checksum(&signal);
353        assert!(checksum.is_finite());
354
355        // Deterministic
356        assert_eq!(detector.pi_checksum(&signal), checksum);
357    }
358
359    #[test]
360    fn test_verify_checksum() {
361        let detector = DriftDetector::new(PrecisionLane::Bit7);
362        let original = vec![1.0, 2.0, 3.0, 4.0, 5.0];
363        let nearly_same = vec![1.001, 2.001, 3.001, 4.001, 5.001];
364        assert!(detector.verify_checksum(&original, &nearly_same));
365    }
366
367    #[test]
368    fn test_severity_levels() {
369        let report = DriftReport {
370            mean_error: 0.5,
371            accumulated_error: 1.0,
372            sample_count: 2,
373            trend: 0.1,
374            is_accelerating: true,
375            should_escalate: true,
376            lane: PrecisionLane::Bit3,
377        };
378        assert_eq!(report.severity(), 3);
379        assert_eq!(report.suggested_lane(), Some(PrecisionLane::Bit5));
380    }
381}