1use crate::drift::action::{DriftAction, DriftEvent};
27use crate::drift::detector::DriftDetector;
28use crate::drift::strategy::DriftStrategy;
29use crate::error::RillError;
30use crate::traits::OnlineRegressor;
31
32const DEFAULT_MAX_EVENTS: usize = 1000;
34
35#[derive(Debug, Clone)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64pub struct DriftAwareModel<M, D, A>
65where
66 D: DriftDetector,
67 A: DriftStrategy,
68{
69 model: M,
70 detector: D,
71 strategy: A,
72 events: Vec<DriftEvent>,
73 max_events: usize,
74 samples_seen: u64,
75 last_action: Option<DriftAction>,
76}
77
78impl<M, D, A> DriftAwareModel<M, D, A>
79where
80 M: OnlineRegressor,
81 D: DriftDetector,
82 A: DriftStrategy,
83{
84 pub fn new(model: M, detector: D, strategy: A) -> Self {
87 Self {
88 model,
89 detector,
90 strategy,
91 events: Vec::new(),
92 max_events: DEFAULT_MAX_EVENTS,
93 samples_seen: 0,
94 last_action: None,
95 }
96 }
97
98 pub fn with_max_events(
102 model: M,
103 detector: D,
104 strategy: A,
105 max_events: usize,
106 ) -> Result<Self, RillError> {
107 if max_events == 0 {
108 return Err(RillError::InvalidCapacity(max_events));
109 }
110 Ok(Self {
111 model,
112 detector,
113 strategy,
114 events: Vec::with_capacity(max_events),
115 max_events,
116 samples_seen: 0,
117 last_action: None,
118 })
119 }
120
121 pub fn predict(&self, features: &[f64]) -> Result<f64, RillError> {
126 self.model.predict(features)
127 }
128
129 pub fn learn(&mut self, features: &[f64], target: f64) -> Result<(), RillError> {
139 let prediction = self.model.predict(features)?;
140 let error = (target - prediction).abs();
141 let level = self.detector.update(error)?;
142 let action = self.strategy.decide(level, self.samples_seen);
143
144 if level.is_change() {
145 let event =
146 DriftEvent::new(self.samples_seen, level, action, self.detector.last_value());
147 self.events.push(event);
148 while self.events.len() > self.max_events {
149 self.events.remove(0);
150 }
151 }
152
153 if action == DriftAction::ResetModel {
154 self.model.reset();
155 }
156 self.last_action = Some(action);
157
158 self.model.learn(features, target)?;
159 self.samples_seen += 1;
160 Ok(())
161 }
162
163 pub const fn model(&self) -> &M {
165 &self.model
166 }
167
168 pub const fn detector(&self) -> &D {
170 &self.detector
171 }
172
173 pub const fn strategy(&self) -> &A {
175 &self.strategy
176 }
177
178 pub fn events(&self) -> &[DriftEvent] {
183 &self.events
184 }
185
186 pub const fn last_action(&self) -> Option<DriftAction> {
189 self.last_action
190 }
191
192 pub const fn samples_seen(&self) -> u64 {
194 self.samples_seen
195 }
196
197 pub const fn max_events(&self) -> usize {
199 self.max_events
200 }
201
202 pub fn reset(&mut self) {
207 self.model.reset();
208 self.detector.reset();
209 self.events.clear();
210 self.samples_seen = 0;
211 self.last_action = None;
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::drift::detector::DriftLevel;
219 use crate::drift::page_hinkley::PageHinkley;
220 use crate::drift::strategy::StaticStrategy;
221 use crate::models::{BaselineConfig, MeanRegressor};
222
223 fn build(
225 strategy: StaticStrategy,
226 ) -> DriftAwareModel<MeanRegressor, PageHinkley, StaticStrategy> {
227 let model = MeanRegressor::new(BaselineConfig::default()).unwrap();
228 let detector = PageHinkley::default();
229 DriftAwareModel::new(model, detector, strategy)
230 }
231
232 #[test]
233 fn predict_delegates_to_model() {
234 let aware = build(StaticStrategy::default());
235 let p = aware.predict(&[]).unwrap();
237 assert!((p - 0.0).abs() < 1e-12);
238 }
239
240 #[test]
241 fn learn_feeds_error_to_detector() {
242 let mut aware = build(StaticStrategy::default());
243 assert_eq!(aware.detector().samples_seen(), 0);
244 aware.learn(&[], 1.0).unwrap();
245 assert_eq!(aware.detector().samples_seen(), 1);
246 aware.learn(&[], 2.0).unwrap();
247 assert_eq!(aware.detector().samples_seen(), 2);
248 }
249
250 #[test]
251 fn samples_seen_tracks_learn_calls() {
252 let mut aware = build(StaticStrategy::default());
253 assert_eq!(aware.samples_seen(), 0);
254 for i in 0..10u64 {
255 aware.learn(&[], i as f64).unwrap();
256 }
257 assert_eq!(aware.samples_seen(), 10);
258 }
259
260 #[test]
261 fn last_action_updated_after_learn() {
262 let mut aware = build(StaticStrategy::default());
263 assert_eq!(aware.last_action(), None);
264 aware.learn(&[], 1.0).unwrap();
265 assert_eq!(aware.last_action(), Some(DriftAction::NotifyOnly));
266 }
267
268 #[test]
269 fn default_strategy_does_not_reset_model() {
270 let mut aware = build(StaticStrategy::default());
271 for i in 0..20 {
273 aware.learn(&[], i as f64).unwrap();
274 }
275 assert!(aware.model().samples_seen() > 0);
277 let before = aware.model().samples_seen();
278 aware.learn(&[], 100.0).unwrap();
279 assert_eq!(aware.model().samples_seen(), before + 1);
280 }
281
282 #[test]
283 fn reset_model_action_calls_model_reset() {
284 let strategy = StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::ResetModel);
286 let mut aware = build(strategy);
287
288 for _ in 0..30 {
290 aware.learn(&[], 1.0).unwrap();
291 }
292 assert!(aware.model().samples_seen() > 0);
293
294 let mut reset_happened = false;
298 for _ in 0..100 {
299 aware.learn(&[], 100.0).unwrap();
300 if aware.model().samples_seen() < aware.samples_seen() {
303 reset_happened = true;
304 break;
305 }
306 }
307 assert!(
308 reset_happened,
309 "ResetModel action should have reset the model"
310 );
311 assert!(!aware.events().is_empty());
313 }
314
315 #[test]
316 fn detects_drift_and_records_event() {
317 let strategy = StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::NotifyOnly);
318 let mut aware = build(strategy);
319
320 for _ in 0..50 {
322 aware.learn(&[], 1.0).unwrap();
323 }
324 assert!(aware.events().is_empty());
325
326 let mut drift_recorded = false;
330 for _ in 0..100 {
331 aware.learn(&[], 50.0).unwrap();
332 if let Some(last) = aware.events().last() {
333 if last.level == DriftLevel::Drift {
334 drift_recorded = true;
335 break;
336 }
337 }
338 }
339 assert!(drift_recorded, "a drift event should have been recorded");
340 let last_event = aware.events().last().unwrap();
341 assert_eq!(last_event.level, DriftLevel::Drift);
342 }
343
344 #[test]
345 fn replace_with_baseline_action_recorded() {
346 let strategy =
348 StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::ReplaceWithBaseline);
349 let mut aware = build(strategy);
350
351 for _ in 0..50 {
353 aware.learn(&[], 1.0).unwrap();
354 }
355 let mut seen_action = false;
358 for _ in 0..200 {
359 aware.learn(&[], 100.0).unwrap();
360 if let Some(action) = aware.last_action() {
361 if action == DriftAction::ReplaceWithBaseline {
362 seen_action = true;
363 break;
364 }
365 }
366 }
367 assert!(
368 seen_action,
369 "ReplaceWithBaseline action should have been recorded"
370 );
371 }
372
373 #[test]
374 fn increase_adaptation_rate_action_recorded() {
375 let strategy =
376 StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::IncreaseAdaptationRate);
377 let mut aware = build(strategy);
378
379 for _ in 0..50 {
381 aware.learn(&[], 1.0).unwrap();
382 }
383 let mut seen_action = false;
385 for _ in 0..200 {
386 aware.learn(&[], 100.0).unwrap();
387 if let Some(action) = aware.last_action() {
388 if action == DriftAction::IncreaseAdaptationRate {
389 seen_action = true;
390 break;
391 }
392 }
393 }
394 assert!(
395 seen_action,
396 "IncreaseAdaptationRate action should have been recorded"
397 );
398 }
399
400 #[test]
401 fn events_bounded_by_max_events() {
402 let strategy = StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::NotifyOnly);
403 let model = MeanRegressor::new(BaselineConfig::default()).unwrap();
404 let detector = PageHinkley::default();
405 let mut aware = DriftAwareModel::with_max_events(model, detector, strategy, 3).unwrap();
406 assert_eq!(aware.max_events(), 3);
407
408 for i in 0..500 {
411 let target = if i % 50 < 25 { 0.0 } else { 100.0 };
412 aware.learn(&[], target).unwrap();
413 }
414 assert!(aware.events().len() <= 3);
416 }
417
418 #[test]
419 fn reset_clears_all_state() {
420 let mut aware = build(StaticStrategy::default());
421 for i in 0..20 {
422 aware.learn(&[], i as f64).unwrap();
423 }
424 assert_eq!(aware.samples_seen(), 20);
425 assert!(aware.last_action().is_some());
426
427 aware.reset();
428 assert_eq!(aware.samples_seen(), 0);
429 assert_eq!(aware.last_action(), None);
430 assert!(aware.events().is_empty());
431 assert_eq!(aware.model().samples_seen(), 0);
432 assert_eq!(aware.detector().samples_seen(), 0);
433 }
434
435 #[test]
436 fn rejects_zero_max_events() {
437 let model = MeanRegressor::new(BaselineConfig::default()).unwrap();
438 let detector = PageHinkley::default();
439 let strategy = StaticStrategy::default();
440 let result = DriftAwareModel::with_max_events(model, detector, strategy, 0);
441 assert!(result.is_err());
442 }
443
444 #[test]
445 fn model_detector_strategy_accessors() {
446 let strategy = StaticStrategy::new(DriftAction::ReduceConfidence, DriftAction::ResetModel);
447 let aware = build(strategy);
448 assert_eq!(
449 aware.strategy().decide(DriftLevel::Warning, 0),
450 DriftAction::ReduceConfidence
451 );
452 assert_eq!(
453 aware.strategy().decide(DriftLevel::Drift, 0),
454 DriftAction::ResetModel
455 );
456 assert_eq!(aware.detector().samples_seen(), 0);
458 assert_eq!(aware.model().samples_seen(), 0);
460 }
461
462 #[cfg(feature = "serde")]
463 #[test]
464 fn serde_roundtrip() {
465 let strategy = StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::ResetModel);
466 let model = MeanRegressor::new(BaselineConfig::default()).unwrap();
467 let detector = PageHinkley::default();
468 let mut aware = DriftAwareModel::new(model, detector, strategy);
469 for i in 0..10 {
471 aware.learn(&[], i as f64).unwrap();
472 }
473
474 let json = serde_json::to_string(&aware).unwrap();
475 let restored: DriftAwareModel<MeanRegressor, PageHinkley, StaticStrategy> =
476 serde_json::from_str(&json).unwrap();
477 assert_eq!(restored.samples_seen(), aware.samples_seen());
478 assert_eq!(restored.last_action(), aware.last_action());
479 assert_eq!(restored.events().len(), aware.events().len());
480 }
481}