Skip to main content

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}