Skip to main content

wm_selfmodel/
confidence.rs

1//! Confidence calibration — overall system confidence from metrics and forecast accuracy.
2
3use crate::metrics::MetricKind;
4
5/// Confidence calibrator — computes overall system confidence (0.0–1.0)
6/// from current metric values and forecast accuracy.
7///
8/// Confidence is used by the dispatch pipeline:
9/// - <0.5 → conservative mode (prefer cached results, avoid risky operations)
10/// - ≥0.5 → normal mode
11///
12/// The calibrator weights metrics by importance:
13/// - Error rate (30%) — high weight, directly impacts reliability
14/// - Coherence (20%) — cognitive stability
15/// - CPU load (15%) — resource headroom
16/// - Memory pressure (15%) — resource headroom
17/// - Latency (10%) — responsiveness
18/// - Swap/disk I/O (10%) — I/O health
19pub struct ConfidenceCalibrator {
20    /// Last computed confidence.
21    last_confidence: f32,
22    /// Smoothing factor for confidence (0.0–1.0).
23    /// Higher = faster adaptation to new values.
24    smoothing: f32,
25}
26
27impl ConfidenceCalibrator {
28    /// Create a new calibrator with default smoothing (0.2).
29    #[must_use]
30    pub const fn new() -> Self {
31        Self {
32            last_confidence: 0.5,
33            smoothing: 0.2,
34        }
35    }
36
37    /// Create with custom smoothing factor.
38    #[must_use]
39    pub const fn with_smoothing(smoothing: f32) -> Self {
40        Self {
41            last_confidence: 0.5,
42            smoothing: smoothing.clamp(0.0, 1.0),
43        }
44    }
45
46    /// Update the calibrator with current metric values and forecast accuracy.
47    pub fn update(&mut self, metrics: &[(MetricKind, f32)], forecast_accuracy: f32) {
48        let raw = Self::compute_raw(metrics, forecast_accuracy);
49        // Exponential smoothing to avoid sudden jumps
50        self.last_confidence = self
51            .smoothing
52            .mul_add(raw, (1.0 - self.smoothing) * self.last_confidence);
53    }
54
55    /// Get the current confidence value (0.0–1.0).
56    #[must_use]
57    pub const fn confidence(&self) -> f32 {
58        self.last_confidence.clamp(0.0, 1.0)
59    }
60
61    /// Compute raw confidence from metrics and forecast accuracy.
62    /// This is the instantaneous value before smoothing.
63    fn compute_raw(metrics: &[(MetricKind, f32)], forecast_accuracy: f32) -> f32 {
64        let mut weighted_sum = 0.0_f32;
65        let mut total_weight = 0.0_f32;
66
67        for &(kind, value) in metrics {
68            let (score, weight) = Self::metric_score(kind, value);
69            weighted_sum = score.mul_add(weight, weighted_sum);
70            total_weight += weight;
71        }
72
73        // Include forecast accuracy (20% weight)
74        weighted_sum = forecast_accuracy.mul_add(0.2, weighted_sum);
75        total_weight += 0.2;
76
77        if total_weight < f32::EPSILON {
78            return 0.5;
79        }
80
81        (weighted_sum / total_weight).clamp(0.0, 1.0)
82    }
83
84    /// Score a single metric (0.0–1.0) and return its weight.
85    fn metric_score(kind: MetricKind, value: f32) -> (f32, f32) {
86        match kind {
87            MetricKind::ErrorRate => {
88                // 0.0 errors = 1.0 confidence, 0.3+ errors = 0.0
89                let score = (1.0 - value / 0.3).clamp(0.0, 1.0);
90                (score, 0.30)
91            }
92            MetricKind::Coherence => {
93                // Coherence is already 0.0–1.0, higher is better
94                (value.clamp(0.0, 1.0), 0.20)
95            }
96            MetricKind::CpuLoad => {
97                // 0.0 load = 1.0, 1.0 load = 0.0
98                (1.0 - value.clamp(0.0, 1.0), 0.15)
99            }
100            MetricKind::MemoryPressure => (1.0 - value.clamp(0.0, 1.0), 0.15),
101            MetricKind::Latency => {
102                // 0ms = 1.0, 50ms+ = 0.0
103                let score = (1.0 - value / 50.0).clamp(0.0, 1.0);
104                (score, 0.10)
105            }
106            MetricKind::SwapUsage => (1.0 - value.clamp(0.0, 1.0), 0.05),
107            MetricKind::DiskIo => (1.0 - value.clamp(0.0, 1.0), 0.05),
108            // Cognitive metrics — higher is better (except variance)
109            MetricKind::ImaginationQuality => (value.clamp(0.0, 1.0), 0.10),
110            MetricKind::ResearchOutput => (value.clamp(0.0, 1.0), 0.08),
111            MetricKind::ScenarioConfidence => (value.clamp(0.0, 1.0), 0.10),
112            MetricKind::SimulationVariance => (1.0 - value.clamp(0.0, 1.0), 0.05),
113            MetricKind::ConformalCoverage => (value.clamp(0.0, 1.0), 0.05),
114            MetricKind::BrierScore => (1.0 - value.clamp(0.0, 1.0), 0.05),
115        }
116    }
117
118    /// Whether the system is in conservative mode (confidence < 0.5).
119    #[must_use]
120    pub fn is_conservative(&self) -> bool {
121        self.last_confidence < 0.5
122    }
123
124    /// Current calibrator state `(last_confidence, smoothing)` for persistence.
125    #[must_use]
126    pub const fn state(&self) -> (f32, f32) {
127        (self.last_confidence, self.smoothing)
128    }
129
130    /// Restore calibrator state from persisted values.
131    pub const fn restore_state(&mut self, last_confidence: f32, smoothing: f32) {
132        self.last_confidence = last_confidence.clamp(0.0, 1.0);
133        self.smoothing = smoothing.clamp(0.0, 1.0);
134    }
135}
136
137impl Default for ConfidenceCalibrator {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn calibrator_default_confidence() {
149        let cal = ConfidenceCalibrator::new();
150        assert_eq!(cal.confidence(), 0.5);
151        assert!(!cal.is_conservative());
152    }
153
154    #[test]
155    fn calibrator_perfect_metrics() {
156        let mut cal = ConfidenceCalibrator::with_smoothing(1.0);
157        let metrics = vec![
158            (MetricKind::ErrorRate, 0.0),
159            (MetricKind::Coherence, 1.0),
160            (MetricKind::CpuLoad, 0.0),
161            (MetricKind::MemoryPressure, 0.0),
162            (MetricKind::Latency, 0.0),
163        ];
164        cal.update(&metrics, 1.0);
165        assert!(cal.confidence() > 0.7);
166        assert!(!cal.is_conservative());
167    }
168
169    #[test]
170    fn calibrator_terrible_metrics() {
171        let mut cal = ConfidenceCalibrator::new();
172        let metrics = vec![
173            (MetricKind::ErrorRate, 0.5),
174            (MetricKind::Coherence, 0.1),
175            (MetricKind::CpuLoad, 0.95),
176            (MetricKind::MemoryPressure, 0.9),
177            (MetricKind::Latency, 60.0),
178        ];
179        cal.update(&metrics, 0.1);
180        // With smoothing=0.2, first update won't drop to 0 immediately
181        assert!(cal.confidence() < 0.5);
182        assert!(cal.is_conservative());
183    }
184
185    #[test]
186    fn calibrator_smoothing_prevents_jumps() {
187        let mut cal = ConfidenceCalibrator::with_smoothing(0.2);
188        // Start at 0.5, update with perfect metrics
189        let good = vec![
190            (MetricKind::ErrorRate, 0.0),
191            (MetricKind::Coherence, 1.0),
192            (MetricKind::CpuLoad, 0.0),
193        ];
194        cal.update(&good, 1.0);
195        let after_one = cal.confidence();
196        // Should have moved up but not to 1.0
197        assert!(after_one > 0.5 && after_one < 0.95);
198
199        // Second update should move further
200        cal.update(&good, 1.0);
201        let after_two = cal.confidence();
202        assert!(after_two > after_one);
203    }
204
205    #[test]
206    fn calibrator_no_smoothing() {
207        let mut cal = ConfidenceCalibrator::with_smoothing(1.0);
208        let metrics = vec![
209            (MetricKind::ErrorRate, 0.0),
210            (MetricKind::Coherence, 1.0),
211            (MetricKind::CpuLoad, 0.0),
212        ];
213        cal.update(&metrics, 1.0);
214        // With smoothing=1.0, should jump directly to raw value
215        assert!(cal.confidence() > 0.8);
216    }
217
218    #[test]
219    fn calibrator_empty_metrics() {
220        let mut cal = ConfidenceCalibrator::new();
221        cal.update(&[], 0.5);
222        // Only forecast accuracy contributes (0.5 * 0.2 / 0.2 = 0.5)
223        assert!((cal.confidence() - 0.5).abs() < 0.1);
224    }
225
226    #[test]
227    fn calibrator_error_rate_dominates() {
228        let mut cal = ConfidenceCalibrator::with_smoothing(1.0);
229        // High error rate with good everything else
230        let metrics = vec![
231            (MetricKind::ErrorRate, 0.5),
232            (MetricKind::Coherence, 1.0),
233            (MetricKind::CpuLoad, 0.0),
234            (MetricKind::MemoryPressure, 0.0),
235            (MetricKind::Latency, 0.0),
236        ];
237        cal.update(&metrics, 1.0);
238        // Error rate has 30% weight — 0.5 error rate → score 0.0 for that 30%
239        // This should pull confidence down significantly
240        assert!(cal.confidence() < 0.8);
241    }
242
243    #[test]
244    fn calibrator_is_conservative_threshold() {
245        let mut cal = ConfidenceCalibrator::with_smoothing(1.0);
246        let metrics = vec![
247            (MetricKind::ErrorRate, 0.3),
248            (MetricKind::Coherence, 0.2),
249            (MetricKind::CpuLoad, 0.8),
250        ];
251        cal.update(&metrics, 0.3);
252        assert!(cal.is_conservative());
253    }
254
255    #[test]
256    fn calibrator_clamps_to_valid_range() {
257        let mut cal = ConfidenceCalibrator::with_smoothing(1.0);
258        // Extreme values shouldn't produce out-of-range confidence
259        let metrics = vec![
260            (MetricKind::ErrorRate, 100.0), // Way above normal range
261            (MetricKind::CpuLoad, 10.0),    // Way above 1.0
262        ];
263        cal.update(&metrics, 0.0);
264        assert!(cal.confidence() >= 0.0 && cal.confidence() <= 1.0);
265    }
266
267    #[test]
268    fn calibrator_with_smoothing_clamped() {
269        let cal = ConfidenceCalibrator::with_smoothing(5.0);
270        // Smoothing should be clamped to 1.0
271        // With smoothing=1.0, first update jumps to raw value
272        assert_eq!(cal.confidence(), 0.5); // Initial value
273    }
274
275    #[test]
276    fn calibrator_latency_scoring() {
277        // 0ms → 1.0, 25ms → 0.5, 50ms+ → 0.0
278        let mut cal = ConfidenceCalibrator::with_smoothing(1.0);
279        cal.update(&[(MetricKind::Latency, 0.0)], 1.0);
280        // Only latency (0.10) + forecast_accuracy (0.20) = 0.30 total weight
281        // latency score = 1.0, accuracy = 1.0 → 1.0
282        assert!(cal.confidence() > 0.9);
283
284        cal.update(&[(MetricKind::Latency, 50.0)], 1.0);
285        // latency score = 0.0, accuracy = 1.0 → (0*0.1 + 1.0*0.2) / 0.3 = 0.667
286        assert!(cal.confidence() < 0.9 && cal.confidence() > 0.5);
287    }
288}