1use crate::diagnostics::prediction_interval::{ResidualInterval, ResidualIntervalConfig};
13use crate::diagnostics::training_summary::{TrainingSummary, TrainingSummaryConfig};
14use crate::diagnostics::warmup::{WarmupConfig, WarmupState, WarmupTracker};
15use crate::error::{RillError, ensure_finite};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum Confidence {
22 None,
24 Low,
26 Medium,
28 High,
30}
31
32impl Confidence {
33 pub const fn as_str(&self) -> &'static str {
37 match self {
38 Confidence::None => "none",
39 Confidence::Low => "low",
40 Confidence::Medium => "medium",
41 Confidence::High => "high",
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub struct PredictionReport {
50 prediction: f64,
51 lower_bound: Option<f64>,
52 upper_bound: Option<f64>,
53 confidence: Confidence,
54 samples_seen: u64,
55 recent_error: Option<f64>,
56 baseline_error: Option<f64>,
57 warmup_state: WarmupState,
58 beats_baseline: Option<bool>,
59}
60
61impl PredictionReport {
62 pub const fn prediction(&self) -> f64 {
64 self.prediction
65 }
66
67 pub const fn lower_bound(&self) -> Option<f64> {
69 self.lower_bound
70 }
71
72 pub const fn upper_bound(&self) -> Option<f64> {
74 self.upper_bound
75 }
76
77 pub const fn confidence(&self) -> Confidence {
79 self.confidence
80 }
81
82 pub const fn samples_seen(&self) -> u64 {
84 self.samples_seen
85 }
86
87 pub const fn recent_error(&self) -> Option<f64> {
89 self.recent_error
90 }
91
92 pub const fn baseline_error(&self) -> Option<f64> {
94 self.baseline_error
95 }
96
97 pub const fn warmup_state(&self) -> WarmupState {
99 self.warmup_state
100 }
101
102 pub const fn beats_baseline(&self) -> Option<bool> {
106 self.beats_baseline
107 }
108}
109
110#[derive(Debug, Clone)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub struct PredictionReporter {
135 interval: ResidualInterval,
136 warmup: WarmupTracker,
137 summary: TrainingSummary,
138}
139
140impl PredictionReporter {
141 pub fn new(
146 interval_config: ResidualIntervalConfig,
147 warmup_config: WarmupConfig,
148 summary_config: TrainingSummaryConfig,
149 ) -> Result<Self, RillError> {
150 Ok(Self {
151 interval: ResidualInterval::new(interval_config)?,
152 warmup: WarmupTracker::new(warmup_config)?,
153 summary: TrainingSummary::new(summary_config)?,
154 })
155 }
156
157 pub fn observe(&mut self, prediction: f64, truth: f64) -> Result<(), RillError> {
162 self.interval.observe(prediction, truth)?;
163 let error = (truth - prediction).abs();
164 self.warmup.observe_sample(Some(error))?;
165 self.summary.record_error(error)?;
166 self.summary.record_sample();
167 Ok(())
168 }
169
170 pub fn set_baseline(&mut self, baseline: f64) -> Result<(), RillError> {
174 self.warmup.set_baseline(baseline)?;
175 self.summary.set_baseline_error(baseline)?;
176 Ok(())
177 }
178
179 pub fn report(&self, prediction: f64) -> Result<PredictionReport, RillError> {
185 ensure_finite("prediction", prediction)?;
186
187 let (lower_bound, upper_bound) = match self.interval.interval(prediction) {
188 Ok(iv) => (Some(iv.lower()), Some(iv.upper())),
189 Err(RillError::InsufficientData) => (None, None),
190 Err(e) => return Err(e),
191 };
192
193 let warmup_state = self.warmup.state();
194 let beats_baseline = self.summary.beats_baseline();
195 let samples_seen = self.summary.total_samples();
196 let recent_error = self.summary.recent_error();
197 let baseline_error = self.summary.baseline_error();
198
199 let confidence = match warmup_state {
200 WarmupState::NoData => Confidence::None,
201 WarmupState::WarmingUp | WarmupState::Degraded => Confidence::Low,
202 WarmupState::Usable => Confidence::Medium,
203 WarmupState::Stable => {
204 if matches!(beats_baseline, Some(true)) {
205 Confidence::High
206 } else {
207 Confidence::Medium
208 }
209 }
210 };
211
212 Ok(PredictionReport {
213 prediction,
214 lower_bound,
215 upper_bound,
216 confidence,
217 samples_seen,
218 recent_error,
219 baseline_error,
220 warmup_state,
221 beats_baseline,
222 })
223 }
224
225 pub fn summary(&self) -> &TrainingSummary {
227 &self.summary
228 }
229
230 pub fn warmup_state(&self) -> WarmupState {
232 self.warmup.state()
233 }
234
235 pub fn recent_error(&self) -> Option<f64> {
237 self.summary.recent_error()
238 }
239
240 pub fn reset(&mut self) {
242 self.interval.reset();
243 self.warmup.reset();
244 self.summary.reset();
245 }
246}
247
248impl Default for PredictionReporter {
249 fn default() -> Self {
250 Self::new(
251 ResidualIntervalConfig::default(),
252 WarmupConfig::default(),
253 TrainingSummaryConfig::default(),
254 )
255 .expect("default configs are valid")
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn confidence_as_str() {
265 assert_eq!(Confidence::None.as_str(), "none");
266 assert_eq!(Confidence::Low.as_str(), "low");
267 assert_eq!(Confidence::Medium.as_str(), "medium");
268 assert_eq!(Confidence::High.as_str(), "high");
269 }
270
271 #[test]
272 fn default_reporter_no_data() {
273 let reporter = PredictionReporter::default();
274 let r = reporter.report(0.0).unwrap();
275 assert_eq!(r.prediction(), 0.0);
276 assert_eq!(r.lower_bound(), None);
277 assert_eq!(r.upper_bound(), None);
278 assert_eq!(r.confidence(), Confidence::None);
279 assert_eq!(r.warmup_state(), WarmupState::NoData);
280 assert_eq!(r.samples_seen(), 0);
281 assert_eq!(r.recent_error(), None);
282 assert_eq!(r.baseline_error(), None);
283 assert_eq!(r.beats_baseline(), None);
284 }
285
286 #[test]
287 fn observe_then_report() {
288 let mut reporter = PredictionReporter::default();
289 reporter.observe(10.0, 11.0).unwrap(); reporter.observe(10.0, 9.0).unwrap(); let r = reporter.report(10.0).unwrap();
292 assert_eq!(r.prediction(), 10.0);
293 assert!(r.lower_bound().is_some());
294 assert!(r.upper_bound().is_some());
295 assert!(r.lower_bound().unwrap() < 10.0);
296 assert!(r.upper_bound().unwrap() > 10.0);
297 assert_eq!(r.samples_seen(), 2);
298 assert!(r.recent_error().is_some());
299 }
300
301 #[test]
302 fn set_baseline_enables_comparison() {
303 let mut reporter = PredictionReporter::default();
304 reporter.observe(0.0, 1.0).unwrap();
305 let r = reporter.report(0.0).unwrap();
306 assert_eq!(r.beats_baseline(), None);
307 assert_eq!(r.baseline_error(), None);
308 reporter.set_baseline(2.0).unwrap();
309 let r = reporter.report(0.0).unwrap();
310 assert_eq!(r.baseline_error(), Some(2.0));
311 assert_eq!(r.beats_baseline(), Some(true)); }
313
314 #[test]
315 fn confidence_progression() {
316 let warmup_config = WarmupConfig {
317 warming_up_threshold: 2,
318 usable_threshold: 5,
319 stable_threshold: 10,
320 degraded_error_ratio: 2.0,
321 };
322 let summary_config = TrainingSummaryConfig { error_alpha: 1.0 };
323 let mut reporter = PredictionReporter::new(
324 ResidualIntervalConfig::default(),
325 warmup_config,
326 summary_config,
327 )
328 .unwrap();
329
330 let r = reporter.report(0.0).unwrap();
332 assert_eq!(r.warmup_state(), WarmupState::NoData);
333 assert_eq!(r.confidence(), Confidence::None);
334
335 reporter.observe(0.0, 0.5).unwrap();
337 let r = reporter.report(0.0).unwrap();
338 assert_eq!(r.warmup_state(), WarmupState::WarmingUp);
339 assert_eq!(r.confidence(), Confidence::Low);
340
341 for _ in 0..4 {
343 reporter.observe(0.0, 0.5).unwrap();
344 }
345 let r = reporter.report(0.0).unwrap();
346 assert_eq!(r.warmup_state(), WarmupState::Usable);
347 assert_eq!(r.confidence(), Confidence::Medium);
348
349 reporter.set_baseline(1.0).unwrap();
351 let r = reporter.report(0.0).unwrap();
352 assert_eq!(r.warmup_state(), WarmupState::Usable);
353 assert_eq!(r.confidence(), Confidence::Medium);
354
355 for _ in 0..5 {
357 reporter.observe(0.0, 0.5).unwrap();
358 }
359 let r = reporter.report(0.0).unwrap();
360 assert_eq!(r.warmup_state(), WarmupState::Stable);
361 assert_eq!(r.confidence(), Confidence::High);
362 }
363
364 #[test]
365 fn degraded_state() {
366 let mut reporter = PredictionReporter::default();
367 reporter.set_baseline(0.4).unwrap();
368 for _ in 0..5 {
370 reporter.observe(0.0, 1.0).unwrap();
371 }
372 let r = reporter.report(0.0).unwrap();
373 assert_eq!(r.warmup_state(), WarmupState::Degraded);
374 assert_eq!(r.confidence(), Confidence::Low);
375 }
376
377 #[test]
378 fn reset_clears_all() {
379 let mut reporter = PredictionReporter::default();
380 reporter.observe(10.0, 12.0).unwrap();
381 reporter.set_baseline(2.0).unwrap();
382 reporter.reset();
383 let r = reporter.report(0.0).unwrap();
384 assert_eq!(r.lower_bound(), None);
385 assert_eq!(r.upper_bound(), None);
386 assert_eq!(r.confidence(), Confidence::None);
387 assert_eq!(r.warmup_state(), WarmupState::NoData);
388 assert_eq!(r.samples_seen(), 0);
389 assert_eq!(r.recent_error(), None);
390 assert_eq!(r.baseline_error(), None);
391 assert_eq!(r.beats_baseline(), None);
392 }
393
394 #[test]
395 fn report_with_non_finite_prediction_errors() {
396 let mut reporter = PredictionReporter::default();
397 reporter.observe(0.0, 1.0).unwrap();
398 assert!(reporter.report(f64::NAN).is_err());
399 assert!(reporter.report(f64::INFINITY).is_err());
400 assert!(reporter.report(f64::NEG_INFINITY).is_err());
401 }
402
403 #[test]
404 fn observe_with_non_finite_rejected() {
405 let mut reporter = PredictionReporter::default();
406 assert!(reporter.observe(0.0, f64::NAN).is_err());
407 assert!(reporter.observe(0.0, f64::INFINITY).is_err());
408 assert!(reporter.observe(f64::NAN, 0.0).is_err());
409 let r = reporter.report(0.0).unwrap();
411 assert_eq!(r.samples_seen(), 0);
412 assert_eq!(r.warmup_state(), WarmupState::NoData);
413 }
414
415 #[test]
416 fn samples_seen_tracked() {
417 let mut reporter = PredictionReporter::default();
418 for i in 0..10 {
419 reporter.observe(0.0, i as f64).unwrap();
420 }
421 let r = reporter.report(0.0).unwrap();
422 assert_eq!(r.samples_seen(), 10);
423 }
424
425 #[cfg(feature = "serde")]
426 #[test]
427 fn serde_roundtrip() {
428 let mut reporter = PredictionReporter::default();
429 reporter.observe(10.0, 12.0).unwrap();
430 reporter.observe(10.0, 9.0).unwrap();
431 reporter.set_baseline(3.0).unwrap();
432
433 let json = serde_json::to_string(&reporter).unwrap();
434 let restored: PredictionReporter = serde_json::from_str(&json).unwrap();
435
436 let r = restored.report(10.0).unwrap();
437 assert_eq!(r.samples_seen(), 2);
438 assert_eq!(r.baseline_error(), Some(3.0));
439 assert!(r.beats_baseline().is_some());
440 }
441}