1use crate::drift::detector::DriftLevel;
17use crate::error::{RillError, 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 = factor * self.weighted_sum + value;
104 self.weight_total = factor * self.weight_total + 1.0;
105 }
106 }
107 self.last_time = Some(time);
108 Ok(())
109 }
110
111 pub fn value(&self) -> Option<f64> {
113 if self.weight_total > 0.0 {
114 Some(self.weighted_sum / self.weight_total)
115 } else {
116 None
117 }
118 }
119
120 pub const fn weight_total(&self) -> f64 {
122 self.weight_total
123 }
124
125 pub const fn last_time(&self) -> Option<f64> {
127 self.last_time
128 }
129
130 pub fn reset(&mut self) {
132 self.weighted_sum = 0.0;
133 self.weight_total = 0.0;
134 self.last_time = None;
135 }
136}
137
138#[derive(Debug, Clone)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
166pub struct LearningRateScheduler {
167 base_lr: f64,
168 warning_multiplier: f64,
169 drift_multiplier: f64,
170 current_state: DriftLevel,
171}
172
173impl LearningRateScheduler {
174 pub fn new(
181 base_lr: f64,
182 warning_multiplier: f64,
183 drift_multiplier: f64,
184 ) -> Result<Self, RillError> {
185 ensure_finite("base_lr", base_lr)?;
186 ensure_finite("warning_multiplier", warning_multiplier)?;
187 ensure_finite("drift_multiplier", drift_multiplier)?;
188 if base_lr <= 0.0 {
189 return Err(RillError::InvalidLearningRate(base_lr));
190 }
191 if warning_multiplier < 1.0 {
192 return Err(RillError::InvalidParameter {
193 name: "warning_multiplier",
194 value: warning_multiplier,
195 });
196 }
197 if drift_multiplier < warning_multiplier {
198 return Err(RillError::InvalidParameter {
199 name: "drift_multiplier",
200 value: drift_multiplier,
201 });
202 }
203 Ok(Self {
204 base_lr,
205 warning_multiplier,
206 drift_multiplier,
207 current_state: DriftLevel::None,
208 })
209 }
210
211 pub const fn base_lr(&self) -> f64 {
213 self.base_lr
214 }
215
216 pub const fn warning_multiplier(&self) -> f64 {
218 self.warning_multiplier
219 }
220
221 pub const fn drift_multiplier(&self) -> f64 {
223 self.drift_multiplier
224 }
225
226 pub const fn current_state(&self) -> DriftLevel {
228 self.current_state
229 }
230
231 pub fn on_drift_level(&mut self, level: DriftLevel) {
233 self.current_state = level;
234 }
235
236 pub fn current_lr(&self) -> f64 {
238 match self.current_state {
239 DriftLevel::None => self.base_lr,
240 DriftLevel::Warning => self.base_lr * self.warning_multiplier,
241 DriftLevel::Drift => self.base_lr * self.drift_multiplier,
242 }
243 }
244
245 pub fn reset(&mut self) {
247 self.current_state = DriftLevel::None;
248 }
249}
250
251impl Default for LearningRateScheduler {
252 fn default() -> Self {
253 Self::new(0.01, 2.0, 5.0).expect("default config is valid")
254 }
255}
256
257#[derive(Debug, Clone)]
284#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
285pub struct FixedWindowBuffer {
286 buffer: Vec<f64>,
287 capacity: usize,
288 head: usize,
289 len: usize,
290}
291
292impl FixedWindowBuffer {
293 pub fn new(capacity: usize) -> Result<Self, RillError> {
297 if capacity == 0 {
298 return Err(RillError::InvalidCapacity(capacity));
299 }
300 Ok(Self {
301 buffer: vec![0.0; capacity],
302 capacity,
303 head: 0,
304 len: 0,
305 })
306 }
307
308 pub const fn capacity(&self) -> usize {
310 self.capacity
311 }
312
313 pub const fn len(&self) -> usize {
315 self.len
316 }
317
318 pub const fn is_empty(&self) -> bool {
320 self.len == 0
321 }
322
323 pub const fn is_full(&self) -> bool {
325 self.len == self.capacity
326 }
327
328 pub fn push(&mut self, value: f64) -> Result<(), RillError> {
332 ensure_finite("value", value)?;
333 self.buffer[self.head] = value;
334 self.head = (self.head + 1) % self.capacity;
335 if self.len < self.capacity {
336 self.len += 1;
337 }
338 Ok(())
339 }
340
341 pub fn mean(&self) -> Option<f64> {
343 if self.len == 0 {
344 return None;
345 }
346 let sum: f64 = self.iter().sum();
347 Some(sum / self.len as f64)
348 }
349
350 pub fn iter(&self) -> impl Iterator<Item = &f64> {
352 let start = if self.is_full() { self.head } else { 0 };
353 let len = self.len;
354 let cap = self.capacity;
355 (0..len).map(move |i| &self.buffer[(start + i) % cap])
356 }
357
358 pub fn reset(&mut self) {
360 self.head = 0;
361 self.len = 0;
362 }
363}
364
365#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
376 fn tdm_first_sample_seeds_mean() {
377 let mut m = TimeDecayedMean::new(0.1).unwrap();
378 m.update(0.0, 10.0).unwrap();
379 assert!((m.value().unwrap() - 10.0).abs() < 1e-12);
380 }
381
382 #[test]
383 fn tdm_decay_weights_old_samples() {
384 let mut m = TimeDecayedMean::new(1.0).unwrap();
385 m.update(0.0, 100.0).unwrap();
386 m.update(10.0, 1.0).unwrap();
387 let v = m.value().unwrap();
390 assert!(
391 (v - 1.0).abs() < 0.01,
392 "recent sample should dominate, got {}",
393 v
394 );
395 }
396
397 #[test]
398 fn tdm_value_correct() {
399 let mut m = TimeDecayedMean::new(0.5).unwrap();
400 m.update(0.0, 10.0).unwrap();
401 m.update(1.0, 20.0).unwrap();
402 let v = m.value().unwrap();
407 assert!((v - 16.22).abs() < 0.1, "expected ~16.22, got {}", v);
408 }
409
410 #[test]
411 fn tdm_reset_clears_state() {
412 let mut m = TimeDecayedMean::new(0.1).unwrap();
413 m.update(0.0, 10.0).unwrap();
414 m.update(1.0, 20.0).unwrap();
415 assert!(m.value().is_some());
416 m.reset();
417 assert!(m.value().is_none());
418 assert_eq!(m.weight_total(), 0.0);
419 assert_eq!(m.last_time(), None);
420 }
421
422 #[test]
423 fn tdm_rejects_invalid_decay() {
424 assert!(TimeDecayedMean::new(0.0).is_err());
425 assert!(TimeDecayedMean::new(-1.0).is_err());
426 assert!(TimeDecayedMean::new(f64::NAN).is_err());
427 assert!(TimeDecayedMean::new(f64::INFINITY).is_err());
428 }
429
430 #[test]
431 fn tdm_rejects_non_finite() {
432 let mut m = TimeDecayedMean::new(0.1).unwrap();
433 assert!(m.update(f64::NAN, 1.0).is_err());
434 assert!(m.update(1.0, f64::NAN).is_err());
435 assert!(m.update(f64::INFINITY, 1.0).is_err());
436 assert!(m.update(1.0, f64::INFINITY).is_err());
437 }
438
439 #[test]
440 fn tdm_rejects_negative_dt() {
441 let mut m = TimeDecayedMean::new(0.1).unwrap();
442 m.update(5.0, 10.0).unwrap();
443 assert!(m.update(3.0, 20.0).is_err());
444 }
445
446 #[test]
447 fn tdm_equal_time_no_decay() {
448 let mut m = TimeDecayedMean::new(1.0).unwrap();
449 m.update(0.0, 10.0).unwrap();
450 m.update(0.0, 20.0).unwrap();
451 assert!((m.value().unwrap() - 15.0).abs() < 1e-12);
453 }
454
455 #[cfg(feature = "serde")]
456 #[test]
457 fn tdm_serde_roundtrip() {
458 let mut m = TimeDecayedMean::new(0.5).unwrap();
459 m.update(0.0, 10.0).unwrap();
460 m.update(1.0, 20.0).unwrap();
461 let json = serde_json::to_string(&m).unwrap();
462 let restored: TimeDecayedMean = serde_json::from_str(&json).unwrap();
463 assert!((restored.decay() - 0.5).abs() < 1e-12);
464 assert!((restored.value().unwrap() - m.value().unwrap()).abs() < 1e-12);
465 }
466
467 #[test]
470 fn lrs_default_lr() {
471 let sched = LearningRateScheduler::default();
472 assert!((sched.current_lr() - 0.01).abs() < 1e-12);
473 assert_eq!(sched.current_state(), DriftLevel::None);
474 }
475
476 #[test]
477 fn lrs_warning_increases_lr() {
478 let mut sched = LearningRateScheduler::new(0.05, 2.0, 5.0).unwrap();
479 sched.on_drift_level(DriftLevel::Warning);
480 assert!((sched.current_lr() - 0.10).abs() < 1e-12);
481 }
482
483 #[test]
484 fn lrs_drift_increases_more() {
485 let mut sched = LearningRateScheduler::new(0.05, 2.0, 5.0).unwrap();
486 sched.on_drift_level(DriftLevel::Drift);
487 assert!((sched.current_lr() - 0.25).abs() < 1e-12);
488 }
489
490 #[test]
491 fn lrs_reset_to_base() {
492 let mut sched = LearningRateScheduler::new(0.05, 2.0, 5.0).unwrap();
493 sched.on_drift_level(DriftLevel::Drift);
494 sched.reset();
495 assert_eq!(sched.current_state(), DriftLevel::None);
496 assert!((sched.current_lr() - 0.05).abs() < 1e-12);
497 }
498
499 #[test]
500 fn lrs_rejects_invalid_config() {
501 assert!(LearningRateScheduler::new(0.0, 2.0, 5.0).is_err());
503 assert!(LearningRateScheduler::new(-1.0, 2.0, 5.0).is_err());
504 assert!(LearningRateScheduler::new(0.01, 0.5, 5.0).is_err());
506 assert!(LearningRateScheduler::new(0.01, 3.0, 2.0).is_err());
508 assert!(LearningRateScheduler::new(f64::NAN, 2.0, 5.0).is_err());
510 }
511
512 #[cfg(feature = "serde")]
513 #[test]
514 fn lrs_serde_roundtrip() {
515 let mut sched = LearningRateScheduler::new(0.02, 3.0, 7.0).unwrap();
516 sched.on_drift_level(DriftLevel::Warning);
517 let json = serde_json::to_string(&sched).unwrap();
518 let restored: LearningRateScheduler = serde_json::from_str(&json).unwrap();
519 assert!((restored.base_lr() - 0.02).abs() < 1e-12);
520 assert!((restored.warning_multiplier() - 3.0).abs() < 1e-12);
521 assert!((restored.drift_multiplier() - 7.0).abs() < 1e-12);
522 assert_eq!(restored.current_state(), DriftLevel::Warning);
523 assert!((restored.current_lr() - 0.06).abs() < 1e-12);
524 }
525
526 #[test]
529 fn fwb_push_below_capacity() {
530 let mut buf = FixedWindowBuffer::new(5).unwrap();
531 buf.push(1.0).unwrap();
532 buf.push(2.0).unwrap();
533 buf.push(3.0).unwrap();
534 assert_eq!(buf.len(), 3);
535 assert!(!buf.is_full());
536 assert!(!buf.is_empty());
537 let collected: Vec<f64> = buf.iter().copied().collect();
538 assert_eq!(collected, vec![1.0, 2.0, 3.0]);
539 }
540
541 #[test]
542 fn fwb_push_overwrites_oldest() {
543 let mut buf = FixedWindowBuffer::new(3).unwrap();
544 buf.push(1.0).unwrap();
545 buf.push(2.0).unwrap();
546 buf.push(3.0).unwrap();
547 assert!(buf.is_full());
548 buf.push(4.0).unwrap();
549 let collected: Vec<f64> = buf.iter().copied().collect();
551 assert_eq!(collected, vec![2.0, 3.0, 4.0]);
552 assert_eq!(buf.len(), 3);
553 }
554
555 #[test]
556 fn fwb_mean_correct() {
557 let mut buf = FixedWindowBuffer::new(4).unwrap();
558 buf.push(1.0).unwrap();
559 buf.push(2.0).unwrap();
560 buf.push(3.0).unwrap();
561 buf.push(4.0).unwrap();
562 assert_eq!(buf.mean(), Some(2.5));
563 buf.push(10.0).unwrap(); assert_eq!(buf.mean(), Some((2.0 + 3.0 + 4.0 + 10.0) / 4.0));
565 }
566
567 #[test]
568 fn fwb_iter_returns_in_order() {
569 let mut buf = FixedWindowBuffer::new(3).unwrap();
570 for v in &[10.0, 20.0, 30.0, 40.0, 50.0] {
571 buf.push(*v).unwrap();
572 }
573 let collected: Vec<f64> = buf.iter().copied().collect();
575 assert_eq!(collected, vec![30.0, 40.0, 50.0]);
576 }
577
578 #[test]
579 fn fwb_empty_buffer_mean_none() {
580 let buf = FixedWindowBuffer::new(3).unwrap();
581 assert_eq!(buf.mean(), None);
582 assert!(buf.is_empty());
583 assert!(!buf.is_full());
584 }
585
586 #[test]
587 fn fwb_rejects_zero_capacity() {
588 assert!(FixedWindowBuffer::new(0).is_err());
589 }
590
591 #[test]
592 fn fwb_rejects_non_finite() {
593 let mut buf = FixedWindowBuffer::new(3).unwrap();
594 assert!(buf.push(f64::NAN).is_err());
595 assert!(buf.push(f64::INFINITY).is_err());
596 assert!(buf.push(f64::NEG_INFINITY).is_err());
597 assert_eq!(buf.len(), 0);
598 }
599
600 #[test]
601 fn fwb_reset_clears() {
602 let mut buf = FixedWindowBuffer::new(3).unwrap();
603 buf.push(1.0).unwrap();
604 buf.push(2.0).unwrap();
605 buf.reset();
606 assert_eq!(buf.len(), 0);
607 assert!(buf.is_empty());
608 assert_eq!(buf.mean(), None);
609 }
610
611 #[test]
612 fn fwb_wrap_around_multiple_times() {
613 let mut buf = FixedWindowBuffer::new(2).unwrap();
614 for i in 1..=10 {
615 buf.push(i as f64).unwrap();
616 }
617 assert_eq!(buf.len(), 2);
618 assert!(buf.is_full());
619 let collected: Vec<f64> = buf.iter().copied().collect();
620 assert_eq!(collected, vec![9.0, 10.0]);
621 }
622
623 #[cfg(feature = "serde")]
624 #[test]
625 fn fwb_serde_roundtrip() {
626 let mut buf = FixedWindowBuffer::new(3).unwrap();
627 buf.push(1.0).unwrap();
628 buf.push(2.0).unwrap();
629 buf.push(3.0).unwrap();
630 buf.push(4.0).unwrap(); let json = serde_json::to_string(&buf).unwrap();
632 let restored: FixedWindowBuffer = serde_json::from_str(&json).unwrap();
633 assert_eq!(restored.capacity(), 3);
634 assert_eq!(restored.len(), 3);
635 assert!(restored.is_full());
636 let collected: Vec<f64> = restored.iter().copied().collect();
637 assert_eq!(collected, vec![2.0, 3.0, 4.0]);
638 }
639}