1use crate::math::u64_f64;
7#[cfg(not(any(feature = "std", test)))]
10use crate::math::FloatExt;
11
12pub const DEFAULT_WINDOW: usize = 100;
18
19#[derive(Clone, Debug)]
27pub struct StreamingMedian {
28 buffer: [f64; DEFAULT_WINDOW],
30 sorted: [f64; DEFAULT_WINDOW],
32 pos: usize,
34 count: usize,
36}
37
38impl StreamingMedian {
39 pub const WINDOW: usize = DEFAULT_WINDOW;
41
42 #[must_use]
44 pub const fn new() -> Self {
45 Self {
46 buffer: [0.0; DEFAULT_WINDOW],
47 sorted: [0.0; DEFAULT_WINDOW],
48 pos: 0,
49 count: 0,
50 }
51 }
52
53 #[inline]
55 fn binary_search_insert(&self, value: f64, len: usize) -> usize {
56 let mut left = 0;
57 let mut right = len;
58 while left < right {
59 let mid = left + (right - left) / 2;
60 if self.sorted[mid] < value {
61 left = mid + 1;
62 } else {
63 right = mid;
64 }
65 }
66 left
67 }
68
69 #[inline]
71 fn binary_search_remove(&self, value: f64, len: usize) -> Option<usize> {
72 let mut left = 0;
73 let mut right = len;
74 while left < right {
75 let mid = left + (right - left) / 2;
76 if self.sorted[mid] < value {
77 left = mid + 1;
78 } else if self.sorted[mid] > value {
79 right = mid;
80 } else {
81 return Some(mid);
82 }
83 }
84 if left < len && (self.sorted[left] - value).abs() < f64::EPSILON {
86 return Some(left);
87 }
88 if left > 0 && (self.sorted[left - 1] - value).abs() < f64::EPSILON {
89 return Some(left - 1);
90 }
91 None
92 }
93
94 #[inline]
96 pub fn push(&mut self, value: f64) {
97 if self.count < DEFAULT_WINDOW {
98 let insert_pos = self.binary_search_insert(value, self.count);
100
101 if insert_pos < self.count {
103 self.sorted
104 .copy_within(insert_pos..self.count, insert_pos + 1);
105 }
106 self.sorted[insert_pos] = value;
107
108 self.buffer[self.pos] = value;
109 self.pos = (self.pos + 1) % DEFAULT_WINDOW;
110 self.count += 1;
111 } else {
112 let old_value = self.buffer[self.pos];
114
115 if let Some(remove_pos) = self.binary_search_remove(old_value, self.count) {
117 if remove_pos < self.count - 1 {
119 self.sorted
120 .copy_within(remove_pos + 1..self.count, remove_pos);
121 }
122 }
123
124 let insert_pos = self.binary_search_insert(value, self.count - 1);
126
127 if insert_pos < self.count - 1 {
129 self.sorted
130 .copy_within(insert_pos..self.count - 1, insert_pos + 1);
131 }
132 self.sorted[insert_pos] = value;
133
134 self.buffer[self.pos] = value;
136 self.pos = (self.pos + 1) % DEFAULT_WINDOW;
137 }
138 }
139
140 #[inline]
142 pub fn median(&mut self) -> f64 {
143 if self.count == 0 {
144 return 0.0;
145 }
146
147 let mid = self.count / 2;
148 if self.count.is_multiple_of(2) {
149 f64::midpoint(self.sorted[mid - 1], self.sorted[mid])
150 } else {
151 self.sorted[mid]
152 }
153 }
154
155 #[inline]
157 #[must_use]
158 pub const fn count(&self) -> usize {
159 self.count
160 }
161
162 #[inline]
164 #[must_use]
165 pub const fn is_full(&self) -> bool {
166 self.count >= DEFAULT_WINDOW
167 }
168
169 pub const fn clear(&mut self) {
171 self.buffer = [0.0; DEFAULT_WINDOW];
172 self.sorted = [0.0; DEFAULT_WINDOW];
173 self.pos = 0;
174 self.count = 0;
175 }
176}
177
178impl Default for StreamingMedian {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184#[derive(Clone, Debug)]
215pub struct MadDetector {
216 values_median: StreamingMedian,
218 deviations_median: StreamingMedian,
220 recent_values: [f64; DEFAULT_WINDOW],
222 pos: usize,
224 count: usize,
226 threshold_k: f64,
228 cached_median: f64,
230 cached_mad: f64,
232 cache_valid: bool,
234}
235
236impl MadDetector {
237 const MAD_SCALE: f64 = 1.4826;
239
240 #[must_use]
245 pub const fn new(threshold_k: f64) -> Self {
246 Self {
247 values_median: StreamingMedian::new(),
248 deviations_median: StreamingMedian::new(),
249 recent_values: [0.0; DEFAULT_WINDOW],
250 pos: 0,
251 count: 0,
252 threshold_k,
253 cached_median: 0.0,
254 cached_mad: 0.0,
255 cache_valid: false,
256 }
257 }
258
259 pub fn observe(&mut self, value: f64) {
261 self.recent_values[self.pos] = value;
263 self.pos = (self.pos + 1) % DEFAULT_WINDOW;
264 if self.count < DEFAULT_WINDOW {
265 self.count += 1;
266 }
267
268 self.values_median.push(value);
270
271 self.cache_valid = false;
273 }
274
275 fn update_cache(&mut self) {
277 if self.cache_valid {
278 return;
279 }
280
281 self.cached_median = self.values_median.median();
283
284 self.deviations_median.clear();
286 for i in 0..self.count {
287 let dev = (self.recent_values[i] - self.cached_median).abs();
288 self.deviations_median.push(dev);
289 }
290 self.cached_mad = self.deviations_median.median();
291
292 self.cache_valid = true;
293 }
294
295 pub fn is_anomaly(&mut self, value: f64) -> bool {
297 if self.count < 3 {
298 return false;
300 }
301
302 self.update_cache();
303
304 if self.cached_mad < 1e-10 {
306 return (value - self.cached_median).abs() > 1e-10;
308 }
309
310 let deviation = (value - self.cached_median).abs();
311 let threshold = self.threshold_k * self.cached_mad * Self::MAD_SCALE;
312
313 deviation > threshold
314 }
315
316 pub fn anomaly_score(&mut self, value: f64) -> f64 {
320 if self.count < 3 {
321 return 0.0;
322 }
323
324 self.update_cache();
325
326 if self.cached_mad < 1e-10 {
327 return if (value - self.cached_median).abs() > 1e-10 {
328 f64::INFINITY
329 } else {
330 0.0
331 };
332 }
333
334 let inv_mad_scale = 1.0 / (self.cached_mad * Self::MAD_SCALE);
335 (value - self.cached_median).abs() * inv_mad_scale
336 }
337
338 pub fn median(&mut self) -> f64 {
340 self.update_cache();
341 self.cached_median
342 }
343
344 pub fn mad(&mut self) -> f64 {
346 self.update_cache();
347 self.cached_mad
348 }
349
350 #[inline]
352 #[must_use]
353 pub const fn threshold_k(&self) -> f64 {
354 self.threshold_k
355 }
356
357 #[inline]
359 pub const fn set_threshold_k(&mut self, k: f64) {
360 self.threshold_k = k;
361 }
362
363 #[inline]
365 #[must_use]
366 pub const fn count(&self) -> usize {
367 self.count
368 }
369
370 pub const fn clear(&mut self) {
372 self.values_median.clear();
373 self.deviations_median.clear();
374 self.recent_values = [0.0; DEFAULT_WINDOW];
375 self.pos = 0;
376 self.count = 0;
377 self.cache_valid = false;
378 }
379}
380
381#[derive(Clone, Debug)]
406pub struct EwmaDetector {
407 alpha: f64,
409 ewma: f64,
411 ewma_var: f64,
413 threshold_k: f64,
415 initialized: bool,
417 count: u64,
419}
420
421impl EwmaDetector {
422 #[must_use]
443 pub const fn new(alpha: f64, threshold_k: f64) -> Self {
444 assert!(
445 alpha > 0.0 && alpha <= 1.0,
446 "EwmaDetector alpha must be in (0.0, 1.0]"
447 );
448 Self {
449 alpha,
450 ewma: 0.0,
451 ewma_var: 0.0,
452 threshold_k,
453 initialized: false,
454 count: 0,
455 }
456 }
457
458 pub fn observe(&mut self, value: f64) {
460 self.count += 1;
461
462 if !self.initialized {
463 self.ewma = value;
464 self.ewma_var = 0.0;
465 self.initialized = true;
466 return;
467 }
468
469 let deviation = value - self.ewma;
471 self.ewma += self.alpha * deviation;
472
473 self.ewma_var =
475 (1.0 - self.alpha) * (self.alpha * deviation).mul_add(deviation, self.ewma_var);
476 }
477
478 #[must_use]
480 pub fn is_anomaly(&self, value: f64) -> bool {
481 if !self.initialized || self.count < 3 {
482 return false;
483 }
484
485 let std_dev = self.ewma_var.sqrt();
486 if std_dev < 1e-10 {
487 return (value - self.ewma).abs() > 1e-10;
488 }
489
490 let deviation = (value - self.ewma).abs();
491 deviation > self.threshold_k * std_dev
492 }
493
494 #[must_use]
503 pub fn anomaly_score(&self, value: f64) -> f64 {
504 if !self.initialized {
505 return 0.0;
506 }
507
508 let std_dev = self.ewma_var.sqrt();
509 if std_dev < 1e-10 {
510 return if (value - self.ewma).abs() > 1e-10 {
511 f64::INFINITY
512 } else {
513 0.0
514 };
515 }
516
517 let inv_std = 1.0 / std_dev;
518 (value - self.ewma).abs() * inv_std
519 }
520
521 #[inline]
523 #[must_use]
524 pub const fn ewma(&self) -> f64 {
525 self.ewma
526 }
527
528 #[inline]
530 #[must_use]
531 pub fn std_dev(&self) -> f64 {
532 self.ewma_var.sqrt()
533 }
534
535 #[inline]
537 #[must_use]
538 pub const fn alpha(&self) -> f64 {
539 self.alpha
540 }
541
542 #[inline]
549 pub const fn set_alpha(&mut self, alpha: f64) {
550 assert!(
551 alpha > 0.0 && alpha <= 1.0,
552 "EwmaDetector alpha must be in (0.0, 1.0]"
553 );
554 self.alpha = alpha;
555 }
556
557 #[inline]
559 #[must_use]
560 pub const fn threshold_k(&self) -> f64 {
561 self.threshold_k
562 }
563
564 #[inline]
566 pub const fn set_threshold_k(&mut self, k: f64) {
567 self.threshold_k = k;
568 }
569
570 #[inline]
572 #[must_use]
573 pub const fn count(&self) -> u64 {
574 self.count
575 }
576
577 pub const fn reset(&mut self) {
579 self.ewma = 0.0;
580 self.ewma_var = 0.0;
581 self.initialized = false;
582 self.count = 0;
583 }
584}
585
586#[derive(Clone, Debug)]
595pub struct ZScoreDetector {
596 mean: f64,
598 m2: f64,
600 count: u64,
602 threshold_k: f64,
604}
605
606impl ZScoreDetector {
607 #[must_use]
609 pub const fn new(threshold_k: f64) -> Self {
610 Self {
611 mean: 0.0,
612 m2: 0.0,
613 count: 0,
614 threshold_k,
615 }
616 }
617
618 pub fn observe(&mut self, value: f64) {
620 self.count += 1;
621 let inv_count = 1.0 / u64_f64(self.count);
622 let delta = value - self.mean;
623 self.mean += delta * inv_count;
624 let delta2 = value - self.mean;
625 self.m2 += delta * delta2;
626 }
627
628 #[inline]
630 #[must_use]
631 pub fn variance(&self) -> f64 {
632 if self.count < 2 {
633 0.0
634 } else {
635 self.m2 / u64_f64(self.count - 1)
636 }
637 }
638
639 #[inline]
641 #[must_use]
642 pub fn std_dev(&self) -> f64 {
643 self.variance().sqrt()
644 }
645
646 #[must_use]
648 pub fn is_anomaly(&self, value: f64) -> bool {
649 if self.count < 3 {
650 return false;
651 }
652
653 let std_dev = self.std_dev();
654 if std_dev < 1e-10 {
655 return (value - self.mean).abs() > 1e-10;
656 }
657
658 let inv_std = 1.0 / std_dev;
659 let z_score = (value - self.mean).abs() * inv_std;
660 z_score > self.threshold_k
661 }
662
663 #[must_use]
665 pub fn z_score(&self, value: f64) -> f64 {
666 let std_dev = self.std_dev();
667 if std_dev < 1e-10 {
668 return if (value - self.mean).abs() > 1e-10 {
669 f64::INFINITY
670 } else {
671 0.0
672 };
673 }
674 let inv_std = 1.0 / std_dev;
675 (value - self.mean) * inv_std
676 }
677
678 #[inline]
680 #[must_use]
681 pub const fn mean(&self) -> f64 {
682 self.mean
683 }
684
685 #[inline]
687 #[must_use]
688 pub const fn count(&self) -> u64 {
689 self.count
690 }
691
692 pub const fn reset(&mut self) {
694 self.mean = 0.0;
695 self.m2 = 0.0;
696 self.count = 0;
697 }
698}
699
700#[derive(Clone, Debug)]
706pub struct AnomalyEvent {
707 pub value: f64,
709 pub score: f64,
711 pub expected: f64,
713 pub timestamp: u64,
715 pub metric_id: u64,
717}
718
719pub trait AnomalyCallback {
721 fn on_anomaly(&mut self, event: AnomalyEvent);
723}
724
725#[derive(Clone, Debug)]
733pub struct CompositeDetector {
734 pub mad: MadDetector,
736 pub ewma: EwmaDetector,
738 pub zscore: ZScoreDetector,
740 pub require_consensus: bool,
742}
743
744impl CompositeDetector {
745 #[must_use]
747 pub const fn new() -> Self {
748 Self {
749 mad: MadDetector::new(3.0),
750 ewma: EwmaDetector::new(0.1, 3.0),
751 zscore: ZScoreDetector::new(3.0),
752 require_consensus: false,
753 }
754 }
755
756 #[must_use]
762 pub const fn with_thresholds(mad_k: f64, ewma_alpha: f64, ewma_k: f64, zscore_k: f64) -> Self {
763 Self {
764 mad: MadDetector::new(mad_k),
765 ewma: EwmaDetector::new(ewma_alpha, ewma_k),
766 zscore: ZScoreDetector::new(zscore_k),
767 require_consensus: false,
768 }
769 }
770
771 pub fn observe(&mut self, value: f64) {
773 self.mad.observe(value);
774 self.ewma.observe(value);
775 self.zscore.observe(value);
776 }
777
778 pub fn is_anomaly(&mut self, value: f64) -> bool {
780 let mad_anomaly = self.mad.is_anomaly(value);
781 let ewma_anomaly = self.ewma.is_anomaly(value);
782 let zscore_anomaly = self.zscore.is_anomaly(value);
783
784 if self.require_consensus {
785 mad_anomaly && ewma_anomaly && zscore_anomaly
787 } else {
788 mad_anomaly || ewma_anomaly || zscore_anomaly
790 }
791 }
792
793 pub fn anomaly_score(&mut self, value: f64) -> f64 {
795 let mad_score = self.mad.anomaly_score(value);
796 let ewma_score = self.ewma.anomaly_score(value);
797 let zscore_score = self.zscore.z_score(value).abs();
798
799 mad_score.max(ewma_score).max(zscore_score)
800 }
801
802 #[inline]
804 #[must_use]
805 pub const fn count(&self) -> u64 {
806 self.zscore.count()
807 }
808
809 pub const fn reset(&mut self) {
811 self.mad.clear();
812 self.ewma.reset();
813 self.zscore.reset();
814 }
815}
816
817impl Default for CompositeDetector {
818 fn default() -> Self {
819 Self::new()
820 }
821}
822
823#[cfg(test)]
828#[allow(clippy::float_cmp)]
829mod tests {
830 use super::*;
831 use crate::math::usize_f64;
832
833 #[test]
834 fn test_streaming_median() {
835 let mut sm = StreamingMedian::new();
836
837 for v in [5.0, 2.0, 8.0, 1.0, 9.0] {
838 sm.push(v);
839 }
840
841 let median = sm.median();
842 assert_eq!(median, 5.0); }
844
845 #[test]
846 fn test_streaming_median_even() {
847 let mut sm = StreamingMedian::new();
848
849 for v in [1.0, 2.0, 3.0, 4.0] {
850 sm.push(v);
851 }
852
853 let median = sm.median();
854 assert_eq!(median, 2.5); }
856
857 #[test]
858 fn test_mad_detector_normal() {
859 let mut detector = MadDetector::new(3.0);
860
861 for v in [10.0, 10.5, 9.5, 10.2, 9.8, 10.1, 9.9, 10.3, 9.7, 10.4] {
863 detector.observe(v);
864 }
865
866 assert!(!detector.is_anomaly(10.0));
868 assert!(!detector.is_anomaly(10.5));
869 assert!(!detector.is_anomaly(9.5));
870
871 assert!(detector.is_anomaly(100.0));
873 assert!(detector.is_anomaly(-50.0));
874 }
875
876 #[test]
877 fn test_ewma_detector() {
878 let mut detector = EwmaDetector::new(0.1, 3.0);
879
880 for i in 0..100 {
882 let value = 10.0 + f64::from(i % 3) - 1.0; detector.observe(value);
884 }
885
886 assert!((detector.ewma() - 10.0).abs() < 1.0);
888
889 assert!(!detector.is_anomaly(10.0));
891 assert!(!detector.is_anomaly(11.0));
892
893 assert!(detector.is_anomaly(50.0));
895 }
896
897 #[test]
898 fn test_zscore_detector() {
899 let mut detector = ZScoreDetector::new(3.0);
900
901 for v in [10.0, 11.0, 9.0, 10.5, 9.5, 10.2, 9.8, 10.1, 9.9, 10.3] {
903 detector.observe(v);
904 }
905
906 assert!((detector.mean() - 10.03).abs() < 0.01);
907
908 assert!(!detector.is_anomaly(10.0));
910
911 assert!(detector.is_anomaly(100.0));
913 }
914
915 #[test]
916 fn test_composite_detector() {
917 let mut detector = CompositeDetector::new();
918
919 for v in [10.0, 10.5, 9.5, 10.2, 9.8, 10.1, 9.9, 10.3, 9.7, 10.4] {
921 detector.observe(v);
922 }
923
924 assert!(!detector.is_anomaly(10.0));
926
927 assert!(detector.is_anomaly(100.0));
929
930 let score = detector.anomaly_score(100.0);
931 assert!(score > 3.0);
932 }
933
934 #[test]
935 fn test_mad_detector_constant() {
936 let mut detector = MadDetector::new(3.0);
937
938 for _ in 0..10 {
940 detector.observe(5.0);
941 }
942
943 assert!(!detector.is_anomaly(5.0));
945
946 assert!(detector.is_anomaly(5.1));
948 }
949
950 #[test]
951 fn test_streaming_median_default() {
952 let sm = StreamingMedian::default();
953 assert_eq!(sm.count(), 0);
954 assert!(!sm.is_full());
955 }
956
957 #[test]
958 fn test_streaming_median_empty() {
959 let mut sm = StreamingMedian::new();
960 assert_eq!(sm.median(), 0.0);
961 }
962
963 #[test]
964 fn test_streaming_median_clear() {
965 let mut sm = StreamingMedian::new();
966 sm.push(1.0);
967 sm.push(2.0);
968 assert_eq!(sm.count(), 2);
969 sm.clear();
970 assert_eq!(sm.count(), 0);
971 }
972
973 #[test]
974 fn test_streaming_median_full() {
975 let mut sm = StreamingMedian::new();
976 for i in 0..DEFAULT_WINDOW {
977 sm.push(usize_f64(i));
978 }
979 assert!(sm.is_full());
980 assert_eq!(sm.count(), DEFAULT_WINDOW);
981 }
982
983 #[test]
984 fn test_streaming_median_wrap() {
985 let mut sm = StreamingMedian::new();
986 for i in 0..(DEFAULT_WINDOW + 10) {
988 sm.push(usize_f64(i));
989 }
990 assert_eq!(sm.count(), DEFAULT_WINDOW);
991 let median = sm.median();
992 assert!(median > 0.0);
993 }
994
995 #[test]
996 fn test_mad_detector_insufficient_data() {
997 let mut detector = MadDetector::new(3.0);
998 detector.observe(10.0);
999 detector.observe(20.0);
1000 assert!(!detector.is_anomaly(1000.0));
1002 }
1003
1004 #[test]
1005 fn test_mad_detector_count() {
1006 let mut detector = MadDetector::new(3.0);
1007 assert_eq!(detector.count(), 0);
1008 detector.observe(1.0);
1009 assert_eq!(detector.count(), 1);
1010 }
1011
1012 #[test]
1013 fn test_mad_detector_threshold_k() {
1014 let detector = MadDetector::new(5.0);
1015 assert!((detector.threshold_k() - 5.0).abs() < f64::EPSILON);
1016 }
1017
1018 #[test]
1019 fn test_mad_detector_set_threshold() {
1020 let mut detector = MadDetector::new(3.0);
1021 detector.set_threshold_k(5.0);
1022 assert!((detector.threshold_k() - 5.0).abs() < f64::EPSILON);
1023 }
1024
1025 #[test]
1026 fn test_mad_detector_clear() {
1027 let mut detector = MadDetector::new(3.0);
1028 for v in [10.0, 11.0, 9.0, 10.5] {
1029 detector.observe(v);
1030 }
1031 detector.clear();
1032 assert_eq!(detector.count(), 0);
1033 }
1034
1035 #[test]
1036 fn test_mad_detector_anomaly_score() {
1037 let mut detector = MadDetector::new(3.0);
1038 for v in [10.0, 10.5, 9.5, 10.2, 9.8] {
1039 detector.observe(v);
1040 }
1041 let score_normal = detector.anomaly_score(10.0);
1042 let score_extreme = detector.anomaly_score(100.0);
1043 assert!(score_extreme > score_normal);
1044 }
1045
1046 #[test]
1047 fn test_mad_detector_median_and_mad() {
1048 let mut detector = MadDetector::new(3.0);
1049 for v in [1.0, 2.0, 3.0, 4.0, 5.0] {
1050 detector.observe(v);
1051 }
1052 let median = detector.median();
1053 assert!((median - 3.0).abs() < 0.01);
1054 let mad = detector.mad();
1055 assert!(mad > 0.0);
1056 }
1057
1058 #[test]
1059 fn test_ewma_detector_not_initialized() {
1060 let detector = EwmaDetector::new(0.1, 3.0);
1061 assert!(!detector.is_anomaly(100.0));
1062 assert_eq!(detector.anomaly_score(100.0), 0.0);
1063 }
1064
1065 #[test]
1066 fn test_ewma_detector_count() {
1067 let mut detector = EwmaDetector::new(0.1, 3.0);
1068 assert_eq!(detector.count(), 0);
1069 detector.observe(10.0);
1070 assert_eq!(detector.count(), 1);
1071 }
1072
1073 #[test]
1074 fn test_ewma_detector_alpha() {
1075 let detector = EwmaDetector::new(0.2, 3.0);
1076 assert!((detector.alpha() - 0.2).abs() < f64::EPSILON);
1077 }
1078
1079 #[test]
1080 fn test_ewma_detector_set_alpha() {
1081 let mut detector = EwmaDetector::new(0.1, 3.0);
1082 detector.set_alpha(0.5);
1083 assert!((detector.alpha() - 0.5).abs() < f64::EPSILON);
1084 }
1085
1086 #[test]
1087 fn test_ewma_detector_set_threshold() {
1088 let mut detector = EwmaDetector::new(0.1, 3.0);
1089 detector.set_threshold_k(5.0);
1090 assert!((detector.threshold_k() - 5.0).abs() < f64::EPSILON);
1091 }
1092
1093 #[test]
1094 fn test_ewma_detector_std_dev() {
1095 let mut detector = EwmaDetector::new(0.1, 3.0);
1096 for v in [10.0, 20.0, 30.0, 40.0, 50.0] {
1097 detector.observe(v);
1098 }
1099 assert!(detector.std_dev() > 0.0);
1100 }
1101
1102 #[test]
1103 fn test_ewma_detector_reset() {
1104 let mut detector = EwmaDetector::new(0.1, 3.0);
1105 detector.observe(10.0);
1106 detector.observe(20.0);
1107 detector.reset();
1108 assert_eq!(detector.count(), 0);
1109 }
1110
1111 #[test]
1112 fn test_zscore_detector_insufficient_data() {
1113 let mut detector = ZScoreDetector::new(3.0);
1114 detector.observe(10.0);
1115 assert!(!detector.is_anomaly(1000.0));
1116 }
1117
1118 #[test]
1119 fn test_zscore_detector_variance() {
1120 let mut detector = ZScoreDetector::new(3.0);
1121 for v in [10.0, 20.0, 30.0] {
1122 detector.observe(v);
1123 }
1124 assert!(detector.variance() > 0.0);
1125 assert!(detector.std_dev() > 0.0);
1126 }
1127
1128 #[test]
1129 fn test_zscore_detector_z_score() {
1130 let mut detector = ZScoreDetector::new(3.0);
1131 for v in [10.0, 10.0, 10.0, 10.0, 10.0] {
1132 detector.observe(v);
1133 }
1134 let z = detector.z_score(10.0);
1136 assert!(z.abs() < f64::EPSILON || z == 0.0);
1137 }
1138
1139 #[test]
1140 fn test_zscore_detector_count() {
1141 let mut detector = ZScoreDetector::new(3.0);
1142 detector.observe(1.0);
1143 detector.observe(2.0);
1144 assert_eq!(detector.count(), 2);
1145 }
1146
1147 #[test]
1148 fn test_zscore_detector_reset() {
1149 let mut detector = ZScoreDetector::new(3.0);
1150 detector.observe(1.0);
1151 detector.observe(2.0);
1152 detector.reset();
1153 assert_eq!(detector.count(), 0);
1154 assert_eq!(detector.mean(), 0.0);
1155 }
1156
1157 #[test]
1158 fn test_composite_detector_default() {
1159 let detector = CompositeDetector::default();
1160 assert_eq!(detector.count(), 0);
1161 }
1162
1163 #[test]
1164 fn test_composite_detector_with_thresholds() {
1165 let detector = CompositeDetector::with_thresholds(2.0, 0.2, 2.5, 2.0);
1166 assert!((detector.mad.threshold_k() - 2.0).abs() < f64::EPSILON);
1167 }
1168
1169 #[test]
1170 fn test_composite_detector_consensus() {
1171 let mut detector = CompositeDetector::new();
1172 detector.require_consensus = true;
1173 for v in [10.0, 10.5, 9.5, 10.2, 9.8, 10.1, 9.9] {
1174 detector.observe(v);
1175 }
1176 assert!(!detector.is_anomaly(10.0));
1178 }
1179
1180 #[test]
1181 fn test_composite_detector_reset() {
1182 let mut detector = CompositeDetector::new();
1183 detector.observe(10.0);
1184 detector.observe(20.0);
1185 detector.reset();
1186 assert_eq!(detector.count(), 0);
1187 }
1188
1189 #[test]
1190 fn test_anomaly_event_clone() {
1191 let event = AnomalyEvent {
1192 value: 100.0,
1193 score: 5.0,
1194 expected: 10.0,
1195 timestamp: 12345,
1196 metric_id: 1,
1197 };
1198 let cloned = event;
1199 assert_eq!(cloned.value, 100.0);
1200 assert_eq!(cloned.metric_id, 1);
1201 }
1202
1203 #[cfg(feature = "std")]
1206 #[test]
1207 fn test_anomaly_event_debug() {
1208 let event = AnomalyEvent {
1209 value: 100.0,
1210 score: 5.0,
1211 expected: 10.0,
1212 timestamp: 0,
1213 metric_id: 0,
1214 };
1215 let s = format!("{event:?}");
1216 assert!(s.contains("value"));
1217 }
1218}