1use crate::drift::detector::{DriftDetector, DriftLevel};
26use crate::error::{RillError, ensure_finite};
27
28#[derive(Debug, Clone)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub struct AdwinConfig {
32 pub delta: f64,
35
36 pub warning_delta: f64,
39
40 pub max_window: usize,
44
45 pub min_samples: u64,
48}
49
50impl Default for AdwinConfig {
51 fn default() -> Self {
52 Self {
53 delta: 0.002,
54 warning_delta: 0.01,
55 max_window: 1000,
56 min_samples: 10,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95pub struct Adwin {
96 config: AdwinConfig,
97 window: std::collections::VecDeque<f64>,
98 total: f64,
99 samples: u64,
100 current_level: DriftLevel,
101}
102
103impl Adwin {
104 pub fn new(config: AdwinConfig) -> Result<Self, RillError> {
112 ensure_finite("delta", config.delta)?;
113 if config.delta <= 0.0 || config.delta >= 1.0 {
114 return Err(RillError::InvalidSignificanceLevel(config.delta));
115 }
116 ensure_finite("warning_delta", config.warning_delta)?;
117 if config.warning_delta <= 0.0 || config.warning_delta >= 1.0 {
118 return Err(RillError::InvalidSignificanceLevel(config.warning_delta));
119 }
120 if config.warning_delta < config.delta {
121 return Err(RillError::InvalidParameter {
122 name: "warning_delta",
123 value: config.warning_delta,
124 });
125 }
126 if config.max_window == 0 {
127 return Err(RillError::InvalidCapacity(config.max_window));
128 }
129 if config.min_samples == 0 {
130 return Err(RillError::InvalidParameter {
131 name: "min_samples",
132 value: 0.0,
133 });
134 }
135 Ok(Self {
136 window: std::collections::VecDeque::with_capacity(config.max_window),
137 config,
138 total: 0.0,
139 samples: 0,
140 current_level: DriftLevel::None,
141 })
142 }
143
144 pub fn window_size(&self) -> usize {
146 self.window.len()
147 }
148
149 pub fn window_mean(&self) -> f64 {
151 if self.window.is_empty() {
152 0.0
153 } else {
154 self.total / self.window.len() as f64
155 }
156 }
157
158 pub const fn config(&self) -> &AdwinConfig {
160 &self.config
161 }
162
163 fn hoeffding_bound(n0: f64, n1: f64, n: u64, delta: f64) -> f64 {
166 let m = n0 * n1 / (n0 + n1);
167 let ln_n = (n as f64).ln().max(1.0);
168 let delta_eff = delta / ln_n;
169 (1.0 / (2.0 * m) * (4.0 / delta_eff).ln()).sqrt()
170 }
171
172 fn check_splits(&self) -> Option<(usize, DriftLevel, f64)> {
176 let n = self.window.len();
177 if n < 2 {
178 return None;
179 }
180 let mut prefix = Vec::with_capacity(n + 1);
182 prefix.push(0.0_f64);
183 let mut acc = 0.0;
184 for &v in &self.window {
185 acc += v;
186 prefix.push(acc);
187 }
188 let total = prefix[n];
189 let n_total = n as u64;
190
191 let mut best_split: Option<(usize, DriftLevel, f64)> = None;
192 for (k, &sum0) in prefix.iter().enumerate().take(n).skip(1) {
194 let n0 = k as f64;
195 let n1 = (n - k) as f64;
196 let sum1 = total - sum0;
197 let mean0 = sum0 / n0;
198 let mean1 = sum1 / n1;
199 let diff = (mean0 - mean1).abs();
200
201 let eps_drift = Self::hoeffding_bound(n0, n1, n_total, self.config.delta);
203 if diff > eps_drift {
204 return Some((k, DriftLevel::Drift, diff));
205 }
206 let eps_warn = Self::hoeffding_bound(n0, n1, n_total, self.config.warning_delta);
208 if diff > eps_warn && best_split.is_none() {
209 best_split = Some((k, DriftLevel::Warning, diff));
210 }
211 }
212 best_split
213 }
214
215 fn trim_front(&mut self, count: usize) {
217 for _ in 0..count {
218 if let Some(v) = self.window.pop_front() {
219 self.total -= v;
220 }
221 }
222 }
223}
224
225impl Default for Adwin {
226 fn default() -> Self {
227 Self::new(AdwinConfig::default()).expect("default config is valid")
228 }
229}
230
231impl DriftDetector for Adwin {
232 fn update(&mut self, value: f64) -> Result<DriftLevel, RillError> {
233 ensure_finite("value", value)?;
234 self.samples += 1;
235 self.window.push_back(value);
237 self.total += value;
238 if self.window.len() > self.config.max_window
240 && let Some(v) = self.window.pop_front()
241 {
242 self.total -= v;
243 }
244 if self.samples < self.config.min_samples || self.window.len() < 2 {
246 self.current_level = DriftLevel::None;
247 return Ok(DriftLevel::None);
248 }
249 if let Some((split, level, _diff)) = self.check_splits() {
251 if level == DriftLevel::Drift {
254 self.trim_front(split);
255 }
256 self.current_level = level;
257 } else {
258 self.current_level = DriftLevel::None;
259 }
260 Ok(self.current_level)
261 }
262
263 fn detected(&self) -> bool {
264 self.current_level == DriftLevel::Drift
265 }
266
267 fn warning(&self) -> bool {
268 self.current_level == DriftLevel::Warning
269 }
270
271 fn level(&self) -> DriftLevel {
272 self.current_level
273 }
274
275 fn samples_seen(&self) -> u64 {
276 self.samples
277 }
278
279 fn reset(&mut self) {
280 self.window.clear();
281 self.total = 0.0;
282 self.samples = 0;
283 self.current_level = DriftLevel::None;
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 fn next_unit(seed: &mut u64) -> f64 {
293 *seed = seed
294 .wrapping_mul(6364136223846793005)
295 .wrapping_add(1442695040888963407);
296 ((*seed >> 11) as f64) / ((1u64 << 53) as f64)
297 }
298
299 #[test]
300 fn default_config_is_valid() {
301 let adwin = Adwin::default();
302 assert_eq!(adwin.samples_seen(), 0);
303 assert_eq!(adwin.level(), DriftLevel::None);
304 assert_eq!(adwin.window_size(), 0);
305 }
306
307 #[test]
308 fn detects_sudden_mean_shift() {
309 let mut adwin = Adwin::new(AdwinConfig {
310 delta: 0.05,
311 warning_delta: 0.1,
312 max_window: 500,
313 min_samples: 5,
314 })
315 .unwrap();
316 let mut seed = 42u64;
318 for _ in 0..100 {
319 let noise = 0.1 * (next_unit(&mut seed) - 0.5);
320 adwin.update(noise).unwrap();
321 }
322 assert_eq!(adwin.level(), DriftLevel::None);
323 let mut detected = false;
325 for _ in 0..200 {
326 let noise = 0.1 * (next_unit(&mut seed) - 0.5);
327 let level = adwin.update(5.0 + noise).unwrap();
328 if level == DriftLevel::Drift {
329 detected = true;
330 break;
331 }
332 }
333 assert!(detected, "ADWIN should detect the sudden mean shift");
334 }
335
336 #[test]
337 fn no_false_positive_on_stable_stream() {
338 let mut adwin = Adwin::new(AdwinConfig {
339 delta: 0.002,
340 warning_delta: 0.01,
341 max_window: 500,
342 min_samples: 10,
343 })
344 .unwrap();
345 let mut seed = 7u64;
346 for _ in 0..2000 {
347 let noise = 0.5 * (next_unit(&mut seed) - 0.5);
348 adwin.update(noise).unwrap();
349 }
350 assert!(
351 !adwin.detected(),
352 "false positive: drift reported on stable stream"
353 );
354 }
355
356 #[test]
357 fn detects_gradual_drift() {
358 let mut adwin = Adwin::new(AdwinConfig {
359 delta: 0.05,
360 warning_delta: 0.1,
361 max_window: 300,
362 min_samples: 5,
363 })
364 .unwrap();
365 let mut seed = 99u64;
367 let mut detected = false;
368 for i in 0..500 {
369 let mean = (i as f64 / 100.0).min(5.0);
370 let noise = 0.1 * (next_unit(&mut seed) - 0.5);
371 let level = adwin.update(mean + noise).unwrap();
372 if level == DriftLevel::Drift {
373 detected = true;
374 break;
375 }
376 }
377 assert!(detected, "ADWIN should detect gradual drift");
378 }
379
380 #[test]
381 fn window_trims_after_drift() {
382 let mut adwin = Adwin::new(AdwinConfig {
383 delta: 0.05,
384 warning_delta: 0.1,
385 max_window: 500,
386 min_samples: 5,
387 })
388 .unwrap();
389 for _ in 0..100 {
391 adwin.update(0.0).unwrap();
392 }
393 let size_before = adwin.window_size();
394 assert!(size_before > 0);
395 let mut trimmed = false;
397 for _ in 0..200 {
398 adwin.update(10.0).unwrap();
399 if adwin.detected() {
400 if adwin.window_size() < size_before + 200 {
404 trimmed = true;
405 break;
406 }
407 }
408 }
409 assert!(trimmed, "window should be trimmed after drift");
410 }
411
412 #[test]
413 fn max_window_enforced() {
414 let mut adwin = Adwin::new(AdwinConfig {
415 max_window: 50,
416 ..Default::default()
417 })
418 .unwrap();
419 for i in 0..200u64 {
420 adwin.update(i as f64).unwrap();
421 }
422 assert!(
425 adwin.window_size() <= 50,
426 "window should not exceed max_window, got {}",
427 adwin.window_size()
428 );
429 }
430
431 #[test]
432 fn min_samples_gates_detection() {
433 let mut adwin = Adwin::new(AdwinConfig {
434 delta: 0.5,
435 warning_delta: 0.5,
436 max_window: 100,
437 min_samples: 50,
438 })
439 .unwrap();
440 for _ in 0..48 {
442 adwin.update(0.0).unwrap();
443 }
444 adwin.update(100.0).unwrap();
446 assert_eq!(adwin.level(), DriftLevel::None);
447 let mut detected = false;
451 for _ in 0..50 {
452 let level = adwin.update(100.0).unwrap();
453 if level.is_change() {
454 detected = true;
455 }
456 }
457 assert!(detected, "should have detected drift after min_samples");
458 }
459
460 #[test]
461 fn reset_clears_state() {
462 let mut adwin = Adwin::default();
463 for _ in 0..50 {
464 adwin.update(1.0).unwrap();
465 }
466 assert!(adwin.window_size() > 0);
467 adwin.reset();
468 assert_eq!(adwin.window_size(), 0);
469 assert_eq!(adwin.samples_seen(), 0);
470 assert_eq!(adwin.level(), DriftLevel::None);
471 assert_eq!(adwin.window_mean(), 0.0);
472 }
473
474 #[test]
475 fn rejects_non_finite_input() {
476 let mut adwin = Adwin::default();
477 assert!(adwin.update(f64::NAN).is_err());
478 assert!(adwin.update(f64::INFINITY).is_err());
479 assert!(adwin.update(f64::NEG_INFINITY).is_err());
480 assert_eq!(adwin.samples_seen(), 0);
481 assert_eq!(adwin.window_size(), 0);
482 }
483
484 #[test]
485 fn rejects_invalid_config() {
486 assert!(
488 Adwin::new(AdwinConfig {
489 delta: 0.0,
490 ..Default::default()
491 })
492 .is_err()
493 );
494 assert!(
495 Adwin::new(AdwinConfig {
496 delta: 1.0,
497 ..Default::default()
498 })
499 .is_err()
500 );
501 assert!(
503 Adwin::new(AdwinConfig {
504 delta: 0.05,
505 warning_delta: 0.01,
506 ..Default::default()
507 })
508 .is_err()
509 );
510 assert!(
512 Adwin::new(AdwinConfig {
513 max_window: 0,
514 ..Default::default()
515 })
516 .is_err()
517 );
518 assert!(
520 Adwin::new(AdwinConfig {
521 min_samples: 0,
522 ..Default::default()
523 })
524 .is_err()
525 );
526 }
527
528 #[test]
529 fn window_mean_correct() {
530 let mut adwin = Adwin::new(AdwinConfig {
531 max_window: 100,
532 min_samples: 11, ..Default::default()
534 })
535 .unwrap();
536 for i in 1..=10 {
537 adwin.update(i as f64).unwrap();
538 }
539 assert!((adwin.window_mean() - 5.5).abs() < 1e-9);
541 }
542
543 #[test]
544 fn hoeffding_bound_decreases_with_more_data() {
545 let b1 = Adwin::hoeffding_bound(5.0, 5.0, 10, 0.01);
547 let b2 = Adwin::hoeffding_bound(50.0, 50.0, 100, 0.01);
548 assert!(
549 b2 < b1,
550 "bound should decrease with more data: {} vs {}",
551 b2,
552 b1
553 );
554 }
555
556 #[cfg(feature = "serde")]
557 #[test]
558 fn serde_roundtrip() {
559 let mut adwin = Adwin::new(AdwinConfig {
560 delta: 0.01,
561 warning_delta: 0.05,
562 max_window: 200,
563 min_samples: 5,
564 })
565 .unwrap();
566 for i in 0..50 {
567 adwin.update(i as f64 * 0.1).unwrap();
568 }
569 let json = serde_json::to_string(&adwin).unwrap();
570 let restored: Adwin = serde_json::from_str(&json).unwrap();
571 assert_eq!(restored.samples_seen(), 50);
572 assert_eq!(restored.window_size(), adwin.window_size());
573 assert!((restored.window_mean() - adwin.window_mean()).abs() < 1e-12);
574 assert_eq!(restored.level(), adwin.level());
575 }
576}