1const fn non_negative(k: f64) -> f64 {
33 if k < 0.0 {
34 0.0
35 } else {
36 k
37 }
38}
39
40pub const DEFAULT_WINDOW: usize = 100;
42
43#[derive(Clone, Debug)]
51pub struct StreamingMedian {
52 buffer: [f64; DEFAULT_WINDOW],
54 sorted: [f64; DEFAULT_WINDOW],
56 pos: usize,
58 count: usize,
60}
61
62impl StreamingMedian {
63 pub const WINDOW: usize = DEFAULT_WINDOW;
65
66 #[must_use]
68 pub const fn new() -> Self {
69 Self {
70 buffer: [0.0; DEFAULT_WINDOW],
71 sorted: [0.0; DEFAULT_WINDOW],
72 pos: 0,
73 count: 0,
74 }
75 }
76
77 #[inline]
79 fn binary_search_insert(&self, value: f64, len: usize) -> usize {
80 let mut left = 0;
81 let mut right = len;
82 while left < right {
83 let mid = left + (right - left) / 2;
84 if self.sorted[mid] < value {
85 left = mid + 1;
86 } else {
87 right = mid;
88 }
89 }
90 left
91 }
92
93 #[inline]
95 fn binary_search_remove(&self, value: f64, len: usize) -> Option<usize> {
96 let mut left = 0;
97 let mut right = len;
98 while left < right {
99 let mid = left + (right - left) / 2;
100 if self.sorted[mid] < value {
101 left = mid + 1;
102 } else if self.sorted[mid] > value {
103 right = mid;
104 } else {
105 return Some(mid);
106 }
107 }
108 if left < len && (self.sorted[left] - value).abs() < f64::EPSILON {
110 return Some(left);
111 }
112 if left > 0 && (self.sorted[left - 1] - value).abs() < f64::EPSILON {
113 return Some(left - 1);
114 }
115 None
116 }
117
118 #[inline]
126 pub fn push(&mut self, value: f64) {
127 if value.is_nan() {
128 return;
129 }
130 if self.count < DEFAULT_WINDOW {
131 let insert_pos = self.binary_search_insert(value, self.count);
133
134 if insert_pos < self.count {
136 self.sorted
137 .copy_within(insert_pos..self.count, insert_pos + 1);
138 }
139 self.sorted[insert_pos] = value;
140
141 self.buffer[self.pos] = value;
142 self.pos = (self.pos + 1) % DEFAULT_WINDOW;
143 self.count += 1;
144 } else {
145 let old_value = self.buffer[self.pos];
147
148 if let Some(remove_pos) = self.binary_search_remove(old_value, self.count) {
150 if remove_pos < self.count - 1 {
152 self.sorted
153 .copy_within(remove_pos + 1..self.count, remove_pos);
154 }
155 }
156
157 let insert_pos = self.binary_search_insert(value, self.count - 1);
159
160 if insert_pos < self.count - 1 {
162 self.sorted
163 .copy_within(insert_pos..self.count - 1, insert_pos + 1);
164 }
165 self.sorted[insert_pos] = value;
166
167 self.buffer[self.pos] = value;
169 self.pos = (self.pos + 1) % DEFAULT_WINDOW;
170 }
171 }
172
173 #[inline]
175 pub fn median(&mut self) -> f64 {
176 if self.count == 0 {
177 return 0.0;
178 }
179
180 let mid = self.count / 2;
181 if self.count % 2 == 0 {
182 (self.sorted[mid - 1] + self.sorted[mid]) / 2.0
183 } else {
184 self.sorted[mid]
185 }
186 }
187
188 #[inline]
190 #[must_use]
191 pub const fn count(&self) -> usize {
192 self.count
193 }
194
195 #[inline]
197 #[must_use]
198 pub const fn is_full(&self) -> bool {
199 self.count >= DEFAULT_WINDOW
200 }
201
202 pub fn clear(&mut self) {
204 self.buffer = [0.0; DEFAULT_WINDOW];
205 self.sorted = [0.0; DEFAULT_WINDOW];
206 self.pos = 0;
207 self.count = 0;
208 }
209}
210
211impl Default for StreamingMedian {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217#[derive(Clone, Debug)]
248pub struct MadDetector {
249 values_median: StreamingMedian,
251 deviations_median: StreamingMedian,
253 recent_values: [f64; DEFAULT_WINDOW],
255 pos: usize,
257 count: usize,
259 threshold_k: f64,
261 cached_median: f64,
263 cached_mad: f64,
265 cache_valid: bool,
267}
268
269impl MadDetector {
270 const MAD_SCALE: f64 = 1.4826;
272
273 #[must_use]
280 pub const fn new(threshold_k: f64) -> Self {
281 let threshold_k = non_negative(threshold_k);
282 Self {
283 values_median: StreamingMedian::new(),
284 deviations_median: StreamingMedian::new(),
285 recent_values: [0.0; DEFAULT_WINDOW],
286 pos: 0,
287 count: 0,
288 threshold_k,
289 cached_median: 0.0,
290 cached_mad: 0.0,
291 cache_valid: false,
292 }
293 }
294
295 pub fn observe(&mut self, value: f64) {
302 if value.is_nan() {
303 return;
304 }
305 self.recent_values[self.pos] = value;
307 self.pos = (self.pos + 1) % DEFAULT_WINDOW;
308 if self.count < DEFAULT_WINDOW {
309 self.count += 1;
310 }
311
312 self.values_median.push(value);
314
315 self.cache_valid = false;
317 }
318
319 fn update_cache(&mut self) {
321 if self.cache_valid {
322 return;
323 }
324
325 self.cached_median = self.values_median.median();
327
328 self.deviations_median.clear();
330 for i in 0..self.count {
331 let dev = (self.recent_values[i] - self.cached_median).abs();
332 self.deviations_median.push(dev);
333 }
334 self.cached_mad = self.deviations_median.median();
335
336 self.cache_valid = true;
337 }
338
339 pub fn is_anomaly(&mut self, value: f64) -> bool {
341 if self.count < 3 {
342 return false;
344 }
345
346 self.update_cache();
347
348 if self.cached_mad < 1e-10 {
350 return (value - self.cached_median).abs() > 1e-10;
352 }
353
354 let deviation = (value - self.cached_median).abs();
355 let threshold = self.threshold_k * self.cached_mad * Self::MAD_SCALE;
356
357 deviation > threshold
358 }
359
360 pub fn anomaly_score(&mut self, value: f64) -> f64 {
364 if self.count < 3 {
365 return 0.0;
366 }
367
368 self.update_cache();
369
370 if self.cached_mad < 1e-10 {
371 return if (value - self.cached_median).abs() > 1e-10 {
372 f64::INFINITY
373 } else {
374 0.0
375 };
376 }
377
378 (value - self.cached_median).abs() / (self.cached_mad * Self::MAD_SCALE)
379 }
380
381 pub fn median(&mut self) -> f64 {
383 self.update_cache();
384 self.cached_median
385 }
386
387 pub fn mad(&mut self) -> f64 {
389 self.update_cache();
390 self.cached_mad
391 }
392
393 #[inline]
395 #[must_use]
396 pub const fn threshold_k(&self) -> f64 {
397 self.threshold_k
398 }
399
400 #[inline]
402 pub fn set_threshold_k(&mut self, k: f64) {
403 self.threshold_k = non_negative(k);
404 }
405
406 #[inline]
408 #[must_use]
409 pub const fn count(&self) -> usize {
410 self.count
411 }
412
413 pub fn clear(&mut self) {
415 self.values_median.clear();
416 self.deviations_median.clear();
417 self.recent_values = [0.0; DEFAULT_WINDOW];
418 self.pos = 0;
419 self.count = 0;
420 self.cache_valid = false;
421 }
422}
423
424#[derive(Clone, Debug)]
449pub struct EwmaDetector {
450 alpha: f64,
452 ewma: f64,
454 ewma_var: f64,
456 threshold_k: f64,
458 initialized: bool,
460 count: u64,
462}
463
464impl EwmaDetector {
465 #[must_use]
472 pub fn new(alpha: f64, threshold_k: f64) -> Self {
473 Self {
474 alpha: alpha.clamp(0.001, 1.0),
475 ewma: 0.0,
476 ewma_var: 0.0,
477 threshold_k: non_negative(threshold_k),
478 initialized: false,
479 count: 0,
480 }
481 }
482
483 pub fn observe(&mut self, value: f64) {
491 if value.is_nan() {
492 return;
493 }
494 self.count += 1;
495
496 if !self.initialized {
497 self.ewma = value;
498 self.ewma_var = 0.0;
499 self.initialized = true;
500 return;
501 }
502
503 let deviation = value - self.ewma;
505 self.ewma += self.alpha * deviation;
506
507 self.ewma_var =
509 (1.0 - self.alpha) * (self.alpha * deviation).mul_add(deviation, self.ewma_var);
510 }
511
512 #[must_use]
514 pub fn is_anomaly(&self, value: f64) -> bool {
515 if !self.initialized || self.count < 3 {
516 return false;
517 }
518
519 let std_dev = self.ewma_var.sqrt();
520 if std_dev < 1e-10 {
521 return (value - self.ewma).abs() > 1e-10;
522 }
523
524 let deviation = (value - self.ewma).abs();
525 deviation > self.threshold_k * std_dev
526 }
527
528 #[must_use]
530 pub fn anomaly_score(&self, value: f64) -> f64 {
531 if !self.initialized || self.count < 3 {
534 return 0.0;
535 }
536
537 let std_dev = self.ewma_var.sqrt();
538 if std_dev < 1e-10 {
539 return if (value - self.ewma).abs() > 1e-10 {
540 f64::INFINITY
541 } else {
542 0.0
543 };
544 }
545
546 (value - self.ewma).abs() / std_dev
547 }
548
549 #[inline]
551 #[must_use]
552 pub const fn ewma(&self) -> f64 {
553 self.ewma
554 }
555
556 #[inline]
558 #[must_use]
559 pub fn std_dev(&self) -> f64 {
560 self.ewma_var.sqrt()
561 }
562
563 #[inline]
565 #[must_use]
566 pub const fn alpha(&self) -> f64 {
567 self.alpha
568 }
569
570 #[inline]
572 pub fn set_alpha(&mut self, alpha: f64) {
573 self.alpha = alpha.clamp(0.001, 1.0);
574 }
575
576 #[inline]
578 #[must_use]
579 pub const fn threshold_k(&self) -> f64 {
580 self.threshold_k
581 }
582
583 #[inline]
585 pub fn set_threshold_k(&mut self, k: f64) {
586 self.threshold_k = non_negative(k);
587 }
588
589 #[inline]
591 #[must_use]
592 pub const fn count(&self) -> u64 {
593 self.count
594 }
595
596 pub fn reset(&mut self) {
598 self.ewma = 0.0;
599 self.ewma_var = 0.0;
600 self.initialized = false;
601 self.count = 0;
602 }
603}
604
605#[derive(Clone, Debug)]
614pub struct ZScoreDetector {
615 mean: f64,
617 m2: f64,
619 count: u64,
621 threshold_k: f64,
623}
624
625impl ZScoreDetector {
626 #[must_use]
628 pub const fn new(threshold_k: f64) -> Self {
629 Self {
630 mean: 0.0,
631 m2: 0.0,
632 count: 0,
633 threshold_k: non_negative(threshold_k),
634 }
635 }
636
637 pub fn observe(&mut self, value: f64) {
645 if value.is_nan() {
646 return;
647 }
648 self.count += 1;
649 let delta = value - self.mean;
650 self.mean += delta / self.count as f64;
651 let delta2 = value - self.mean;
652 self.m2 += delta * delta2;
653 }
654
655 #[inline]
657 #[must_use]
658 pub fn variance(&self) -> f64 {
659 if self.count < 2 {
660 0.0
661 } else {
662 self.m2 / (self.count - 1) as f64
663 }
664 }
665
666 #[inline]
668 #[must_use]
669 pub fn std_dev(&self) -> f64 {
670 self.variance().sqrt()
671 }
672
673 #[must_use]
675 pub fn is_anomaly(&self, value: f64) -> bool {
676 if self.count < 3 {
677 return false;
678 }
679
680 let std_dev = self.std_dev();
681 if std_dev < 1e-10 {
682 return (value - self.mean).abs() > 1e-10;
683 }
684
685 let z_score = (value - self.mean).abs() / std_dev;
686 z_score > self.threshold_k
687 }
688
689 #[must_use]
691 pub fn z_score(&self, value: f64) -> f64 {
692 if self.count < 3 {
695 return 0.0;
696 }
697 let std_dev = self.std_dev();
698 if std_dev < 1e-10 {
699 return if (value - self.mean).abs() > 1e-10 {
700 f64::INFINITY
701 } else {
702 0.0
703 };
704 }
705 (value - self.mean) / std_dev
706 }
707
708 #[inline]
710 #[must_use]
711 pub const fn mean(&self) -> f64 {
712 self.mean
713 }
714
715 #[inline]
717 #[must_use]
718 pub const fn count(&self) -> u64 {
719 self.count
720 }
721
722 pub fn reset(&mut self) {
724 self.mean = 0.0;
725 self.m2 = 0.0;
726 self.count = 0;
727 }
728}
729
730#[derive(Clone, Debug)]
736pub struct AnomalyEvent {
737 pub value: f64,
739 pub score: f64,
741 pub expected: f64,
743 pub timestamp: u64,
745 pub metric_id: u64,
747}
748
749pub trait AnomalyCallback {
751 fn on_anomaly(&mut self, event: AnomalyEvent);
753}
754
755#[derive(Clone, Debug)]
763pub struct CompositeDetector {
764 pub mad: MadDetector,
766 pub ewma: EwmaDetector,
768 pub zscore: ZScoreDetector,
770 pub require_consensus: bool,
772}
773
774impl CompositeDetector {
775 #[must_use]
777 pub fn new() -> Self {
778 Self {
779 mad: MadDetector::new(3.0),
780 ewma: EwmaDetector::new(0.1, 3.0),
781 zscore: ZScoreDetector::new(3.0),
782 require_consensus: false,
783 }
784 }
785
786 #[must_use]
788 pub fn with_thresholds(mad_k: f64, ewma_alpha: f64, ewma_k: f64, zscore_k: f64) -> Self {
789 Self {
790 mad: MadDetector::new(mad_k),
791 ewma: EwmaDetector::new(ewma_alpha, ewma_k),
792 zscore: ZScoreDetector::new(zscore_k),
793 require_consensus: false,
794 }
795 }
796
797 pub fn observe(&mut self, value: f64) {
799 self.mad.observe(value);
800 self.ewma.observe(value);
801 self.zscore.observe(value);
802 }
803
804 pub fn is_anomaly(&mut self, value: f64) -> bool {
806 let mad_anomaly = self.mad.is_anomaly(value);
807 let ewma_anomaly = self.ewma.is_anomaly(value);
808 let zscore_anomaly = self.zscore.is_anomaly(value);
809
810 if self.require_consensus {
811 mad_anomaly && ewma_anomaly && zscore_anomaly
813 } else {
814 mad_anomaly || ewma_anomaly || zscore_anomaly
816 }
817 }
818
819 pub fn anomaly_score(&mut self, value: f64) -> f64 {
821 let mad_score = self.mad.anomaly_score(value);
822 let ewma_score = self.ewma.anomaly_score(value);
823 let zscore_score = self.zscore.z_score(value).abs();
824
825 crate::det_math::max64(crate::det_math::max64(mad_score, ewma_score), zscore_score)
826 }
827
828 #[inline]
830 #[must_use]
831 pub const fn count(&self) -> u64 {
832 self.zscore.count()
833 }
834
835 pub fn reset(&mut self) {
837 self.mad.clear();
838 self.ewma.reset();
839 self.zscore.reset();
840 }
841
842 pub fn observe_with_callback<C: AnomalyCallback>(
857 &mut self,
858 value: f64,
859 timestamp: u64,
860 metric_id: u64,
861 callback: &mut C,
862 ) -> bool {
863 let flagged = self.is_anomaly(value);
864 if flagged {
865 let score = self.anomaly_score(value);
866 let expected = self.mad.median();
867 callback.on_anomaly(AnomalyEvent {
868 value,
869 score,
870 expected,
871 timestamp,
872 metric_id,
873 });
874 }
875 self.observe(value);
876 flagged
877 }
878}
879
880impl Default for CompositeDetector {
881 fn default() -> Self {
882 Self::new()
883 }
884}
885
886#[cfg(test)]
891#[allow(clippy::float_cmp)]
892mod tests {
893 use super::*;
894
895 #[test]
896 fn test_streaming_median() {
897 let mut sm = StreamingMedian::new();
898
899 for v in [5.0, 2.0, 8.0, 1.0, 9.0] {
900 sm.push(v);
901 }
902
903 let median = sm.median();
904 assert_eq!(median, 5.0); }
906
907 #[test]
908 fn test_streaming_median_even() {
909 let mut sm = StreamingMedian::new();
910
911 for v in [1.0, 2.0, 3.0, 4.0] {
912 sm.push(v);
913 }
914
915 let median = sm.median();
916 assert_eq!(median, 2.5); }
918
919 #[test]
920 fn test_mad_detector_normal() {
921 let mut detector = MadDetector::new(3.0);
922
923 for v in [10.0, 10.5, 9.5, 10.2, 9.8, 10.1, 9.9, 10.3, 9.7, 10.4] {
925 detector.observe(v);
926 }
927
928 assert!(!detector.is_anomaly(10.0));
930 assert!(!detector.is_anomaly(10.5));
931 assert!(!detector.is_anomaly(9.5));
932
933 assert!(detector.is_anomaly(100.0));
935 assert!(detector.is_anomaly(-50.0));
936 }
937
938 #[test]
939 fn test_ewma_detector() {
940 let mut detector = EwmaDetector::new(0.1, 3.0);
941
942 for i in 0..100 {
944 let value = 10.0 + (i % 3) as f64 - 1.0; detector.observe(value);
946 }
947
948 assert!((detector.ewma() - 10.0).abs() < 1.0);
950
951 assert!(!detector.is_anomaly(10.0));
953 assert!(!detector.is_anomaly(11.0));
954
955 assert!(detector.is_anomaly(50.0));
957 }
958
959 #[test]
960 fn test_zscore_detector() {
961 let mut detector = ZScoreDetector::new(3.0);
962
963 for v in [10.0, 11.0, 9.0, 10.5, 9.5, 10.2, 9.8, 10.1, 9.9, 10.3] {
965 detector.observe(v);
966 }
967
968 assert!((detector.mean() - 10.03).abs() < 0.01);
969
970 assert!(!detector.is_anomaly(10.0));
972
973 assert!(detector.is_anomaly(100.0));
975 }
976
977 #[test]
978 fn test_composite_detector() {
979 let mut detector = CompositeDetector::new();
980
981 for v in [10.0, 10.5, 9.5, 10.2, 9.8, 10.1, 9.9, 10.3, 9.7, 10.4] {
983 detector.observe(v);
984 }
985
986 assert!(!detector.is_anomaly(10.0));
988
989 assert!(detector.is_anomaly(100.0));
991
992 let score = detector.anomaly_score(100.0);
993 assert!(score > 3.0);
994 }
995
996 #[test]
997 fn test_mad_detector_constant() {
998 let mut detector = MadDetector::new(3.0);
999
1000 for _ in 0..10 {
1002 detector.observe(5.0);
1003 }
1004
1005 assert!(!detector.is_anomaly(5.0));
1007
1008 assert!(detector.is_anomaly(5.1));
1010 }
1011
1012 #[test]
1013 fn ewma_set_alpha_clamps_and_flips_detection() {
1014 let mut d = EwmaDetector::new(0.1, 3.0);
1016 d.set_alpha(5.0);
1017 assert!((d.alpha() - 1.0).abs() < 1e-12, "alpha={}", d.alpha());
1018 d.set_alpha(0.0);
1019 assert!((d.alpha() - 0.001).abs() < 1e-12, "alpha={}", d.alpha());
1020 d.set_alpha(0.5);
1021 assert!((d.alpha() - 0.5).abs() < 1e-12, "alpha={}", d.alpha());
1022
1023 let mut slow = EwmaDetector::new(0.1, 3.0);
1031 slow.set_alpha(0.001);
1032 for v in [10.0, 10.0, 100.0] {
1033 slow.observe(v);
1034 }
1035 assert!((slow.ewma() - 10.09).abs() < 1e-9, "ewma={}", slow.ewma());
1036 assert!(
1037 (slow.std_dev() - 8.0919_f64.sqrt()).abs() < 1e-9,
1038 "std={}",
1039 slow.std_dev()
1040 );
1041 assert!(slow.is_anomaly(100.0));
1042 assert!(!slow.is_anomaly(10.0));
1043
1044 let mut fast = EwmaDetector::new(0.1, 3.0);
1047 fast.set_alpha(1.0);
1048 for v in [10.0, 10.0, 100.0] {
1049 fast.observe(v);
1050 }
1051 assert!((fast.ewma() - 100.0).abs() < 1e-12, "ewma={}", fast.ewma());
1052 assert!(fast.std_dev().abs() < 1e-12, "std={}", fast.std_dev());
1053 assert!(!fast.is_anomaly(100.0));
1054 assert!(fast.is_anomaly(10.0));
1055 }
1056
1057 #[test]
1058 fn ewma_set_threshold_k_changes_verdict_at_fixed_score() {
1059 let mut d = EwmaDetector::new(0.5, 3.0);
1063 for v in [10.0, 10.0, 12.0] {
1064 d.observe(v);
1065 }
1066 assert!((d.std_dev() - 1.0).abs() < 1e-12, "std={}", d.std_dev());
1067 assert!((d.anomaly_score(13.0) - 2.0).abs() < 1e-12);
1068
1069 assert!(!d.is_anomaly(13.0));
1071 d.set_threshold_k(1.5);
1073 assert!((d.threshold_k() - 1.5).abs() < 1e-12);
1074 assert!(d.is_anomaly(13.0));
1075 assert!((d.anomaly_score(13.0) - 2.0).abs() < 1e-12);
1077 d.set_threshold_k(2.0);
1079 assert!(!d.is_anomaly(13.0));
1080 }
1081
1082 #[test]
1083 fn mad_set_threshold_k_changes_verdict_at_fixed_score() {
1084 let mut d = MadDetector::new(3.0);
1087 for v in 1..=9 {
1088 d.observe(f64::from(v));
1089 }
1090 assert!((d.median() - 5.0).abs() < 1e-12, "median={}", d.median());
1091 assert!((d.mad() - 2.0).abs() < 1e-12, "mad={}", d.mad());
1092 let score13 = 8.0 / (2.0 * 1.4826);
1094 assert!((d.anomaly_score(13.0) - score13).abs() < 1e-12);
1095
1096 assert!(d.is_anomaly(15.0));
1098 assert!(!d.is_anomaly(13.0));
1099
1100 d.set_threshold_k(2.0);
1102 assert!((d.threshold_k() - 2.0).abs() < 1e-12);
1103 assert!(d.is_anomaly(13.0));
1104 assert!((d.anomaly_score(13.0) - score13).abs() < 1e-12);
1105
1106 d.set_threshold_k(4.0);
1108 assert!(!d.is_anomaly(15.0));
1109 assert!(!d.is_anomaly(13.0));
1110 }
1111
1112 #[test]
1113 fn composite_with_thresholds_propagates_each_parameter() {
1114 let c = CompositeDetector::with_thresholds(2.5, 0.25, 4.5, 1.0);
1115 assert!((c.mad.threshold_k() - 2.5).abs() < 1e-12);
1116 assert!((c.ewma.alpha() - 0.25).abs() < 1e-12);
1117 assert!((c.ewma.threshold_k() - 4.5).abs() < 1e-12);
1118 assert!(!c.require_consensus);
1119 assert_eq!(c.count(), 0);
1120
1121 let obs = [10.0, 11.0, 9.0, 10.0, 11.0, 9.0];
1128 let mut loose = CompositeDetector::with_thresholds(1e6, 0.1, 1e6, 100.0);
1129 let mut tight = CompositeDetector::with_thresholds(1e6, 0.1, 1e6, 1.0);
1130 for &v in &obs {
1131 loose.observe(v);
1132 tight.observe(v);
1133 }
1134 assert_eq!(loose.count(), 6);
1135 assert!((tight.zscore.mean() - 10.0).abs() < 1e-12);
1136 assert!((tight.zscore.variance() - 0.8).abs() < 1e-12);
1137 assert!(!loose.mad.is_anomaly(12.0));
1139 assert!(!loose.ewma.is_anomaly(12.0));
1140 assert!(!tight.mad.is_anomaly(12.0));
1141 assert!(!tight.ewma.is_anomaly(12.0));
1142 assert!(!loose.is_anomaly(12.0));
1144 assert!(tight.is_anomaly(12.0));
1145 assert!(tight.zscore.is_anomaly(12.0));
1146 assert!(!loose.zscore.is_anomaly(12.0));
1147 }
1148}