1use crate::drift::detector::DriftLevel;
17use crate::error::{RillError, checked_finite_add, ensure_finite};
18
19#[derive(Debug, Clone)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub struct TimeDecayedMean {
49 decay: f64,
50 weighted_sum: f64,
51 weight_total: f64,
52 last_time: Option<f64>,
53}
54
55impl TimeDecayedMean {
56 pub fn new(decay: f64) -> Result<Self, RillError> {
61 ensure_finite("decay", decay)?;
62 if decay <= 0.0 {
63 return Err(RillError::InvalidParameter {
64 name: "decay",
65 value: decay,
66 });
67 }
68 Ok(Self {
69 decay,
70 weighted_sum: 0.0,
71 weight_total: 0.0,
72 last_time: None,
73 })
74 }
75
76 pub const fn decay(&self) -> f64 {
78 self.decay
79 }
80
81 pub fn update(&mut self, time: f64, value: f64) -> Result<(), RillError> {
86 ensure_finite("time", time)?;
87 ensure_finite("value", value)?;
88 match self.last_time {
89 None => {
90 self.weighted_sum = value;
92 self.weight_total = 1.0;
93 }
94 Some(prev) => {
95 if time < prev {
96 return Err(RillError::InvalidParameter {
97 name: "time",
98 value: time,
99 });
100 }
101 let dt = time - prev;
102 let factor = (-self.decay * dt).exp();
103 self.weighted_sum =
104 checked_finite_add(factor * self.weighted_sum, value, "weighted_sum")?;
105 self.weight_total =
106 checked_finite_add(factor * self.weight_total, 1.0, "weight_total")?;
107 }
108 }
109 self.last_time = Some(time);
110 Ok(())
111 }
112
113 pub fn value(&self) -> Option<f64> {
115 if self.weight_total > 0.0 {
116 Some(self.weighted_sum / self.weight_total)
117 } else {
118 None
119 }
120 }
121
122 pub const fn weight_total(&self) -> f64 {
124 self.weight_total
125 }
126
127 pub const fn last_time(&self) -> Option<f64> {
129 self.last_time
130 }
131
132 pub fn reset(&mut self) {
134 self.weighted_sum = 0.0;
135 self.weight_total = 0.0;
136 self.last_time = None;
137 }
138}
139
140#[derive(Debug, Clone)]
167#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
168pub struct LearningRateScheduler {
169 base_lr: f64,
170 warning_multiplier: f64,
171 drift_multiplier: f64,
172 current_state: DriftLevel,
173}
174
175impl LearningRateScheduler {
176 pub fn new(
183 base_lr: f64,
184 warning_multiplier: f64,
185 drift_multiplier: f64,
186 ) -> Result<Self, RillError> {
187 ensure_finite("base_lr", base_lr)?;
188 ensure_finite("warning_multiplier", warning_multiplier)?;
189 ensure_finite("drift_multiplier", drift_multiplier)?;
190 if base_lr <= 0.0 {
191 return Err(RillError::InvalidLearningRate(base_lr));
192 }
193 if warning_multiplier < 1.0 {
194 return Err(RillError::InvalidParameter {
195 name: "warning_multiplier",
196 value: warning_multiplier,
197 });
198 }
199 if drift_multiplier < warning_multiplier {
200 return Err(RillError::InvalidParameter {
201 name: "drift_multiplier",
202 value: drift_multiplier,
203 });
204 }
205 Ok(Self {
206 base_lr,
207 warning_multiplier,
208 drift_multiplier,
209 current_state: DriftLevel::None,
210 })
211 }
212
213 pub const fn base_lr(&self) -> f64 {
215 self.base_lr
216 }
217
218 pub const fn warning_multiplier(&self) -> f64 {
220 self.warning_multiplier
221 }
222
223 pub const fn drift_multiplier(&self) -> f64 {
225 self.drift_multiplier
226 }
227
228 pub const fn current_state(&self) -> DriftLevel {
230 self.current_state
231 }
232
233 pub fn on_drift_level(&mut self, level: DriftLevel) {
235 self.current_state = level;
236 }
237
238 pub fn current_lr(&self) -> f64 {
240 match self.current_state {
241 DriftLevel::None => self.base_lr,
242 DriftLevel::Warning => self.base_lr * self.warning_multiplier,
243 DriftLevel::Drift => self.base_lr * self.drift_multiplier,
244 }
245 }
246
247 pub fn reset(&mut self) {
249 self.current_state = DriftLevel::None;
250 }
251}
252
253impl Default for LearningRateScheduler {
254 fn default() -> Self {
255 Self::new(0.01, 2.0, 5.0).expect("default config is valid")
256 }
257}
258
259#[derive(Debug, Clone)]
286#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
287pub struct FixedWindowBuffer {
288 buffer: Vec<f64>,
289 capacity: usize,
290 head: usize,
291 len: usize,
292}
293
294impl FixedWindowBuffer {
295 pub fn new(capacity: usize) -> Result<Self, RillError> {
299 if capacity == 0 {
300 return Err(RillError::InvalidCapacity(capacity));
301 }
302 Ok(Self {
303 buffer: vec![0.0; capacity],
304 capacity,
305 head: 0,
306 len: 0,
307 })
308 }
309
310 pub const fn capacity(&self) -> usize {
312 self.capacity
313 }
314
315 pub const fn len(&self) -> usize {
317 self.len
318 }
319
320 pub const fn is_empty(&self) -> bool {
322 self.len == 0
323 }
324
325 pub const fn is_full(&self) -> bool {
327 self.len == self.capacity
328 }
329
330 pub fn push(&mut self, value: f64) -> Result<(), RillError> {
334 ensure_finite("value", value)?;
335 self.buffer[self.head] = value;
336 self.head = (self.head + 1) % self.capacity;
337 if self.len < self.capacity {
338 self.len += 1;
339 }
340 Ok(())
341 }
342
343 pub fn mean(&self) -> Option<f64> {
345 if self.len == 0 {
346 return None;
347 }
348 let sum: f64 = self.iter().sum();
349 if !sum.is_finite() {
350 return None;
351 }
352 Some(sum / self.len as f64)
353 }
354
355 pub fn iter(&self) -> impl Iterator<Item = &f64> {
357 let start = if self.is_full() { self.head } else { 0 };
358 let len = self.len;
359 let cap = self.capacity;
360 (0..len).map(move |i| &self.buffer[(start + i) % cap])
361 }
362
363 pub fn reset(&mut self) {
365 self.head = 0;
366 self.len = 0;
367 }
368}
369
370#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
381 fn tdm_first_sample_seeds_mean() {
382 let mut m = TimeDecayedMean::new(0.1).unwrap();
383 m.update(0.0, 10.0).unwrap();
384 assert!((m.value().unwrap() - 10.0).abs() < 1e-12);
385 }
386
387 #[test]
388 fn tdm_decay_weights_old_samples() {
389 let mut m = TimeDecayedMean::new(1.0).unwrap();
390 m.update(0.0, 100.0).unwrap();
391 m.update(10.0, 1.0).unwrap();
392 let v = m.value().unwrap();
395 assert!(
396 (v - 1.0).abs() < 0.01,
397 "recent sample should dominate, got {}",
398 v
399 );
400 }
401
402 #[test]
403 fn tdm_value_correct() {
404 let mut m = TimeDecayedMean::new(0.5).unwrap();
405 m.update(0.0, 10.0).unwrap();
406 m.update(1.0, 20.0).unwrap();
407 let v = m.value().unwrap();
412 assert!((v - 16.22).abs() < 0.1, "expected ~16.22, got {}", v);
413 }
414
415 #[test]
416 fn tdm_reset_clears_state() {
417 let mut m = TimeDecayedMean::new(0.1).unwrap();
418 m.update(0.0, 10.0).unwrap();
419 m.update(1.0, 20.0).unwrap();
420 assert!(m.value().is_some());
421 m.reset();
422 assert!(m.value().is_none());
423 assert_eq!(m.weight_total(), 0.0);
424 assert_eq!(m.last_time(), None);
425 }
426
427 #[test]
428 fn tdm_rejects_invalid_decay() {
429 assert!(TimeDecayedMean::new(0.0).is_err());
430 assert!(TimeDecayedMean::new(-1.0).is_err());
431 assert!(TimeDecayedMean::new(f64::NAN).is_err());
432 assert!(TimeDecayedMean::new(f64::INFINITY).is_err());
433 }
434
435 #[test]
436 fn tdm_rejects_non_finite() {
437 let mut m = TimeDecayedMean::new(0.1).unwrap();
438 assert!(m.update(f64::NAN, 1.0).is_err());
439 assert!(m.update(1.0, f64::NAN).is_err());
440 assert!(m.update(f64::INFINITY, 1.0).is_err());
441 assert!(m.update(1.0, f64::INFINITY).is_err());
442 }
443
444 #[test]
445 fn tdm_rejects_negative_dt() {
446 let mut m = TimeDecayedMean::new(0.1).unwrap();
447 m.update(5.0, 10.0).unwrap();
448 assert!(m.update(3.0, 20.0).is_err());
449 }
450
451 #[test]
452 fn tdm_equal_time_no_decay() {
453 let mut m = TimeDecayedMean::new(1.0).unwrap();
454 m.update(0.0, 10.0).unwrap();
455 m.update(0.0, 20.0).unwrap();
456 assert!((m.value().unwrap() - 15.0).abs() < 1e-12);
458 }
459
460 #[cfg(feature = "serde")]
461 #[test]
462 fn tdm_serde_roundtrip() {
463 let mut m = TimeDecayedMean::new(0.5).unwrap();
464 m.update(0.0, 10.0).unwrap();
465 m.update(1.0, 20.0).unwrap();
466 let json = serde_json::to_string(&m).unwrap();
467 let restored: TimeDecayedMean = serde_json::from_str(&json).unwrap();
468 assert!((restored.decay() - 0.5).abs() < 1e-12);
469 assert!((restored.value().unwrap() - m.value().unwrap()).abs() < 1e-12);
470 }
471
472 #[test]
475 fn lrs_default_lr() {
476 let sched = LearningRateScheduler::default();
477 assert!((sched.current_lr() - 0.01).abs() < 1e-12);
478 assert_eq!(sched.current_state(), DriftLevel::None);
479 }
480
481 #[test]
482 fn lrs_warning_increases_lr() {
483 let mut sched = LearningRateScheduler::new(0.05, 2.0, 5.0).unwrap();
484 sched.on_drift_level(DriftLevel::Warning);
485 assert!((sched.current_lr() - 0.10).abs() < 1e-12);
486 }
487
488 #[test]
489 fn lrs_drift_increases_more() {
490 let mut sched = LearningRateScheduler::new(0.05, 2.0, 5.0).unwrap();
491 sched.on_drift_level(DriftLevel::Drift);
492 assert!((sched.current_lr() - 0.25).abs() < 1e-12);
493 }
494
495 #[test]
496 fn lrs_reset_to_base() {
497 let mut sched = LearningRateScheduler::new(0.05, 2.0, 5.0).unwrap();
498 sched.on_drift_level(DriftLevel::Drift);
499 sched.reset();
500 assert_eq!(sched.current_state(), DriftLevel::None);
501 assert!((sched.current_lr() - 0.05).abs() < 1e-12);
502 }
503
504 #[test]
505 fn lrs_rejects_invalid_config() {
506 assert!(LearningRateScheduler::new(0.0, 2.0, 5.0).is_err());
508 assert!(LearningRateScheduler::new(-1.0, 2.0, 5.0).is_err());
509 assert!(LearningRateScheduler::new(0.01, 0.5, 5.0).is_err());
511 assert!(LearningRateScheduler::new(0.01, 3.0, 2.0).is_err());
513 assert!(LearningRateScheduler::new(f64::NAN, 2.0, 5.0).is_err());
515 }
516
517 #[cfg(feature = "serde")]
518 #[test]
519 fn lrs_serde_roundtrip() {
520 let mut sched = LearningRateScheduler::new(0.02, 3.0, 7.0).unwrap();
521 sched.on_drift_level(DriftLevel::Warning);
522 let json = serde_json::to_string(&sched).unwrap();
523 let restored: LearningRateScheduler = serde_json::from_str(&json).unwrap();
524 assert!((restored.base_lr() - 0.02).abs() < 1e-12);
525 assert!((restored.warning_multiplier() - 3.0).abs() < 1e-12);
526 assert!((restored.drift_multiplier() - 7.0).abs() < 1e-12);
527 assert_eq!(restored.current_state(), DriftLevel::Warning);
528 assert!((restored.current_lr() - 0.06).abs() < 1e-12);
529 }
530
531 #[test]
534 fn fwb_push_below_capacity() {
535 let mut buf = FixedWindowBuffer::new(5).unwrap();
536 buf.push(1.0).unwrap();
537 buf.push(2.0).unwrap();
538 buf.push(3.0).unwrap();
539 assert_eq!(buf.len(), 3);
540 assert!(!buf.is_full());
541 assert!(!buf.is_empty());
542 let collected: Vec<f64> = buf.iter().copied().collect();
543 assert_eq!(collected, vec![1.0, 2.0, 3.0]);
544 }
545
546 #[test]
547 fn fwb_push_overwrites_oldest() {
548 let mut buf = FixedWindowBuffer::new(3).unwrap();
549 buf.push(1.0).unwrap();
550 buf.push(2.0).unwrap();
551 buf.push(3.0).unwrap();
552 assert!(buf.is_full());
553 buf.push(4.0).unwrap();
554 let collected: Vec<f64> = buf.iter().copied().collect();
556 assert_eq!(collected, vec![2.0, 3.0, 4.0]);
557 assert_eq!(buf.len(), 3);
558 }
559
560 #[test]
561 fn fwb_mean_correct() {
562 let mut buf = FixedWindowBuffer::new(4).unwrap();
563 buf.push(1.0).unwrap();
564 buf.push(2.0).unwrap();
565 buf.push(3.0).unwrap();
566 buf.push(4.0).unwrap();
567 assert_eq!(buf.mean(), Some(2.5));
568 buf.push(10.0).unwrap(); assert_eq!(buf.mean(), Some((2.0 + 3.0 + 4.0 + 10.0) / 4.0));
570 }
571
572 #[test]
573 fn fwb_iter_returns_in_order() {
574 let mut buf = FixedWindowBuffer::new(3).unwrap();
575 for v in &[10.0, 20.0, 30.0, 40.0, 50.0] {
576 buf.push(*v).unwrap();
577 }
578 let collected: Vec<f64> = buf.iter().copied().collect();
580 assert_eq!(collected, vec![30.0, 40.0, 50.0]);
581 }
582
583 #[test]
584 fn fwb_empty_buffer_mean_none() {
585 let buf = FixedWindowBuffer::new(3).unwrap();
586 assert_eq!(buf.mean(), None);
587 assert!(buf.is_empty());
588 assert!(!buf.is_full());
589 }
590
591 #[test]
592 fn fwb_rejects_zero_capacity() {
593 assert!(FixedWindowBuffer::new(0).is_err());
594 }
595
596 #[test]
597 fn fwb_rejects_non_finite() {
598 let mut buf = FixedWindowBuffer::new(3).unwrap();
599 assert!(buf.push(f64::NAN).is_err());
600 assert!(buf.push(f64::INFINITY).is_err());
601 assert!(buf.push(f64::NEG_INFINITY).is_err());
602 assert_eq!(buf.len(), 0);
603 }
604
605 #[test]
606 fn fwb_reset_clears() {
607 let mut buf = FixedWindowBuffer::new(3).unwrap();
608 buf.push(1.0).unwrap();
609 buf.push(2.0).unwrap();
610 buf.reset();
611 assert_eq!(buf.len(), 0);
612 assert!(buf.is_empty());
613 assert_eq!(buf.mean(), None);
614 }
615
616 #[test]
617 fn fwb_wrap_around_multiple_times() {
618 let mut buf = FixedWindowBuffer::new(2).unwrap();
619 for i in 1..=10 {
620 buf.push(i as f64).unwrap();
621 }
622 assert_eq!(buf.len(), 2);
623 assert!(buf.is_full());
624 let collected: Vec<f64> = buf.iter().copied().collect();
625 assert_eq!(collected, vec![9.0, 10.0]);
626 }
627
628 #[cfg(feature = "serde")]
629 #[test]
630 fn fwb_serde_roundtrip() {
631 let mut buf = FixedWindowBuffer::new(3).unwrap();
632 buf.push(1.0).unwrap();
633 buf.push(2.0).unwrap();
634 buf.push(3.0).unwrap();
635 buf.push(4.0).unwrap(); let json = serde_json::to_string(&buf).unwrap();
637 let restored: FixedWindowBuffer = serde_json::from_str(&json).unwrap();
638 assert_eq!(restored.capacity(), 3);
639 assert_eq!(restored.len(), 3);
640 assert!(restored.is_full());
641 let collected: Vec<f64> = restored.iter().copied().collect();
642 assert_eq!(collected, vec![2.0, 3.0, 4.0]);
643 }
644}