irithyll_core/drift/mod.rs
1//! Concept drift detection algorithms.
2//!
3//! Drift detectors monitor a stream of error values and signal when the
4//! underlying distribution has changed, triggering tree replacement in SGBT.
5//!
6//! This is the `no_std`-compatible core module. The [`DriftDetector`] trait
7//! requires the `alloc` feature for `Box`-returning methods; the signal enum
8//! and state types are always available.
9
10#[cfg(feature = "alloc")]
11#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
12pub mod adwin;
13pub mod ddm;
14pub mod pht;
15
16// ---------------------------------------------------------------------------
17// DriftSignal
18// ---------------------------------------------------------------------------
19
20/// Signal emitted by a drift detector after observing a value.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub enum DriftSignal {
24 /// No significant change detected.
25 Stable,
26 /// Possible drift -- start training an alternate model.
27 Warning,
28 /// Confirmed drift -- replace the current model.
29 Drift,
30}
31
32impl core::fmt::Display for DriftSignal {
33 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34 match self {
35 Self::Stable => write!(f, "Stable"),
36 Self::Warning => write!(f, "Warning"),
37 Self::Drift => write!(f, "Drift"),
38 }
39 }
40}
41
42// ---------------------------------------------------------------------------
43// DriftDetectorState (requires alloc for Vec/String)
44// ---------------------------------------------------------------------------
45
46/// Serializable state for any drift detector variant.
47///
48/// Captures running statistics so that save/load cycles preserve accumulated
49/// warmup data instead of creating a fresh detector (which would cause
50/// spurious drift signals).
51#[cfg(feature = "alloc")]
52#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
53#[derive(Debug, Clone)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub enum DriftDetectorState {
56 /// Page-Hinkley Test accumulated state.
57 PageHinkley {
58 running_mean: f64,
59 sum_up: f64,
60 min_sum_up: f64,
61 sum_down: f64,
62 min_sum_down: f64,
63 count: u64,
64 },
65 /// ADWIN exponential histogram state.
66 Adwin {
67 rows: alloc::vec::Vec<alloc::vec::Vec<AdwinBucketState>>,
68 total: f64,
69 variance: f64,
70 count: u64,
71 width: u64,
72 },
73 /// DDM Welford running statistics state.
74 Ddm {
75 mean: f64,
76 m2: f64,
77 count: u64,
78 min_p_plus_s: f64,
79 min_s: f64,
80 },
81}
82
83/// Serializable snapshot of a bucket in ADWIN's exponential histogram.
84#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
85#[derive(Debug, Clone)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87pub struct AdwinBucketState {
88 pub total: f64,
89 pub variance: f64,
90 pub count: u64,
91}
92
93// ---------------------------------------------------------------------------
94// DriftDetector trait (requires alloc for Box)
95// ---------------------------------------------------------------------------
96
97/// A sequential drift detector that monitors a stream of values.
98#[cfg(feature = "alloc")]
99#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
100pub trait DriftDetector: Send + Sync + 'static {
101 /// Feed a new value and get the current drift signal.
102 fn update(&mut self, value: f64) -> DriftSignal;
103
104 /// Reset to initial state.
105 fn reset(&mut self);
106
107 /// Create a fresh detector with the same configuration but no state.
108 fn clone_fresh(&self) -> alloc::boxed::Box<dyn DriftDetector>;
109
110 /// Clone this detector including its internal state.
111 ///
112 /// Unlike [`clone_fresh`](Self::clone_fresh), this preserves accumulated
113 /// statistics (running means, counters, etc.), producing a true deep copy.
114 fn clone_boxed(&self) -> alloc::boxed::Box<dyn DriftDetector>;
115
116 /// Current estimated mean of the monitored stream.
117 fn estimated_mean(&self) -> f64;
118
119 /// Serialize the detector's internal state for model persistence.
120 ///
121 /// Returns `None` if the detector does not support state serialization.
122 /// Default implementation returns `None`.
123 fn serialize_state(&self) -> Option<DriftDetectorState> {
124 None
125 }
126
127 /// Restore the detector's internal state from a serialized snapshot.
128 ///
129 /// Returns `true` if the state was successfully restored. Default
130 /// implementation returns `false` (unsupported).
131 fn restore_state(&mut self, _state: &DriftDetectorState) -> bool {
132 false
133 }
134}