chronix_analytics/anomaly/
storage.rs1use serde::{Deserialize, Serialize};
6
7pub trait DetectorStore: Serialize + for<'de> Deserialize<'de> {
9 fn to_bytes(&self) -> Result<Vec<u8>, DetectorStorageError> {
11 postcard::to_stdvec(self).map_err(|e| DetectorStorageError::Serialize(e.to_string()))
12 }
13
14 fn from_bytes(bytes: &[u8]) -> Result<Self, DetectorStorageError>
16 where
17 Self: Sized,
18 {
19 postcard::from_bytes(bytes).map_err(|e| DetectorStorageError::Deserialize(e.to_string()))
20 }
21}
22
23#[derive(Debug, thiserror::Error)]
25pub enum DetectorStorageError {
26 #[error("serialization failed: {0}")]
27 Serialize(String),
28 #[error("deserialization failed: {0}")]
29 Deserialize(String),
30}
31
32impl DetectorStore for crate::anomaly::ZScoreDetector {}
35impl DetectorStore for crate::anomaly::ModifiedZScoreDetector {}
36impl DetectorStore for crate::anomaly::IqrDetector {}
37impl DetectorStore for crate::anomaly::MovingAverageResidualDetector {}
38impl DetectorStore for crate::anomaly::DynamicThresholdDetector {}
39impl DetectorStore for crate::anomaly::ForecastResidualDetector {}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45 use crate::anomaly::{AnomalyDetector, ZScoreDetector};
46
47 #[test]
48 fn zscore_round_trip() {
49 let ts: Vec<i64> = (0..100).map(|i| i * 1_000_000_000).collect();
50 let vals: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64 * 0.1).sin()).collect();
51
52 let mut det = ZScoreDetector::new(Some(3.0));
53 det.fit(&ts, &vals).unwrap();
54
55 let bytes = det.to_bytes().unwrap();
56 let mut restored = ZScoreDetector::from_bytes(&bytes).unwrap();
57
58 let orig = det.detect_point(999, 500.0).unwrap();
60 let rest = restored.detect_point(999, 500.0).unwrap();
61 assert_eq!(orig.is_anomaly, rest.is_anomaly);
62 assert!((orig.score - rest.score).abs() < 1e-10);
63 }
64
65 #[test]
66 fn iqr_round_trip() {
67 let ts: Vec<i64> = (0..100).map(|i| i * 1_000_000_000).collect();
68 let vals: Vec<f64> = (0..100).map(|i| i as f64).collect();
69
70 let mut det = crate::anomaly::IqrDetector::new(Some(1.5));
71 det.fit(&ts, &vals).unwrap();
72
73 let bytes = det.to_bytes().unwrap();
74 let mut restored = crate::anomaly::IqrDetector::from_bytes(&bytes).unwrap();
75
76 let orig = det.detect_point(999, 500.0).unwrap();
77 let rest = restored.detect_point(999, 500.0).unwrap();
78 assert_eq!(orig.is_anomaly, rest.is_anomaly);
79 }
80}