1use super::config::*;
8use super::optimizer::{Adaptation, AdaptationType, StreamingDataPoint};
9
10use crate::utils::scalar_or;
11use scirs2_core::numeric::Float;
12use std::collections::{HashMap, VecDeque};
13use std::time::{Duration, Instant};
14
15pub struct AnomalyDetector<A: Float + Send + Sync> {
17 config: AnomalyConfig,
19 statistical_detectors: HashMap<String, Box<dyn StatisticalAnomalyDetector<A>>>,
21 ml_detectors: HashMap<String, Box<dyn MLAnomalyDetector<A>>>,
23 ensemble_detector: EnsembleAnomalyDetector<A>,
25 threshold_manager: AdaptiveThresholdManager<A>,
27 anomaly_history: VecDeque<AnomalyEvent<A>>,
29 false_positive_tracker: FalsePositiveTracker<A>,
31 response_system: AnomalyResponseSystem<A>,
33 recent_points: VecDeque<StreamingDataPoint<A>>,
40 recent_capacity: usize,
42 context_performance_metrics: Vec<A>,
47 context_resource_usage: Vec<A>,
48 context_drift_indicators: Vec<A>,
49 recent_scores: VecDeque<A>,
52 points_since_recalibration: usize,
54}
55
56#[derive(Debug, Clone)]
58pub struct AnomalyEvent<A: Float + Send + Sync> {
59 pub id: u64,
61 pub timestamp: Instant,
63 pub anomaly_type: AnomalyType,
65 pub severity: AnomalySeverity,
67 pub confidence: A,
69 pub data_point: StreamingDataPoint<A>,
71 pub detector_name: String,
73 pub anomaly_score: A,
75 pub context: AnomalyContext<A>,
77 pub response_actions: Vec<String>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash)]
83pub enum AnomalyType {
84 StatisticalOutlier,
86 PatternChange,
88 TemporalAnomaly,
90 SpatialAnomaly,
92 ContextualAnomaly,
94 CollectiveAnomaly,
96 PointAnomaly,
98 DataQualityAnomaly,
100 PerformanceAnomaly,
102 Custom(String),
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
108pub enum AnomalySeverity {
109 Low,
111 Medium,
113 High,
115 Critical,
117}
118
119#[derive(Debug, Clone)]
121pub struct AnomalyContext<A: Float + Send + Sync> {
122 pub recent_statistics: DataStatistics<A>,
124 pub performance_metrics: Vec<A>,
126 pub resource_usage: Vec<A>,
128 pub drift_indicators: Vec<A>,
130 pub time_since_last_anomaly: Duration,
132}
133
134#[derive(Debug, Clone)]
136pub struct DataStatistics<A: Float + Send + Sync> {
137 pub means: Vec<A>,
139 pub std_devs: Vec<A>,
141 pub min_values: Vec<A>,
143 pub max_values: Vec<A>,
145 pub medians: Vec<A>,
147 pub skewness: Vec<A>,
149 pub kurtosis: Vec<A>,
151}
152
153pub trait StatisticalAnomalyDetector<A: Float + Send + Sync>: Send + Sync {
155 fn detect_anomaly(
157 &mut self,
158 data_point: &StreamingDataPoint<A>,
159 ) -> Result<AnomalyDetectionResult<A>, String>;
160
161 fn update(&mut self, data_point: &StreamingDataPoint<A>) -> Result<(), String>;
163
164 fn reset(&mut self);
166
167 fn name(&self) -> String;
169
170 fn get_threshold(&self) -> A;
172
173 fn set_threshold(&mut self, threshold: A);
175}
176
177pub trait MLAnomalyDetector<A: Float + Send + Sync>: Send + Sync {
179 fn detect_anomaly(
181 &mut self,
182 data_point: &StreamingDataPoint<A>,
183 ) -> Result<AnomalyDetectionResult<A>, String>;
184
185 fn train(&mut self, training_data: &[StreamingDataPoint<A>]) -> Result<(), String>;
187
188 fn update_incremental(&mut self, data_point: &StreamingDataPoint<A>) -> Result<(), String>;
190
191 fn record_outcome(&mut self, predicted_anomaly: bool, was_true_anomaly: bool);
197
198 fn get_performance_metrics(&self) -> Result<MLModelMetrics<A>, String>;
204
205 fn name(&self) -> String;
207}
208
209pub use super::anomaly_scoring::DetectionCounters;
213
214#[derive(Debug, Clone)]
216pub struct AnomalyDetectionResult<A: Float + Send + Sync> {
217 pub is_anomaly: bool,
219 pub anomaly_score: A,
221 pub confidence: A,
223 pub anomaly_type: Option<AnomalyType>,
225 pub severity: AnomalySeverity,
227 pub metadata: HashMap<String, A>,
229}
230
231#[derive(Debug, Clone)]
233pub struct MLModelMetrics<A: Float + Send + Sync> {
234 pub accuracy: A,
236 pub precision: A,
238 pub recall: A,
240 pub f1_score: A,
242 pub auc_roc: Option<A>,
252 pub false_positive_rate: A,
254 pub training_time: Duration,
256 pub inference_time: Duration,
258}
259
260pub use super::anomaly_ensemble::{
263 EnsembleAnomalyDetector, EnsembleConfig, EnsembleVotingStrategy,
264};
265
266#[derive(Debug, Clone)]
268pub struct DetectorPerformance<A: Float + Send + Sync> {
269 pub recent_accuracy: A,
271 pub historical_accuracy: A,
273 pub false_positive_rate: A,
275 pub false_negative_rate: A,
277 pub detection_latency: Duration,
279 pub reliability_score: A,
281}
282
283pub struct AdaptiveThresholdManager<A: Float + Send + Sync> {
285 thresholds: HashMap<String, A>,
287 threshold_bounds: HashMap<String, (A, A)>,
289}
290
291#[derive(Debug, Clone)]
293pub enum ThresholdAdaptationStrategy {
294 Fixed,
296 PerformanceBased,
298 QuantileBased { quantile: f64 },
300 ROCOptimized,
302 PROptimized,
304 FPRControlled { target_fpr: f64 },
306 DistributionAdaptive,
308}
309
310#[derive(Debug, Clone)]
312pub struct ThresholdPerformanceFeedback<A: Float + Send + Sync> {
313 pub detector_name: String,
315 pub threshold: A,
317 pub true_positives: usize,
319 pub false_positives: usize,
321 pub true_negatives: usize,
323 pub false_negatives: usize,
325 pub timestamp: Instant,
327}
328
329#[derive(Debug, Clone)]
331pub struct ThresholdAdaptationParams<A: Float + Send + Sync> {
332 pub learning_rate: A,
334 pub momentum: A,
336 pub min_change: A,
338 pub max_change: A,
340 pub adaptation_frequency: usize,
342}
343
344pub struct FalsePositiveTracker<A: Float + Send + Sync> {
346 false_positives: VecDeque<FalsePositiveEvent<A>>,
348 fp_rate_calculator: FPRateCalculator<A>,
350}
351
352#[derive(Debug, Clone)]
354pub struct FalsePositiveEvent<A: Float + Send + Sync> {
355 pub timestamp: Instant,
357 pub data_point: StreamingDataPoint<A>,
359 pub detector_name: String,
361 pub anomaly_score: A,
363 pub context: AnomalyContext<A>,
365}
366
367pub struct FPRateCalculator<A: Float + Send + Sync> {
369 recent_results: VecDeque<DetectionResult>,
371 window_size: usize,
373 current_fp_rate: A,
375}
376
377#[derive(Debug, Clone)]
379pub struct DetectionResult {
380 pub timestamp: Instant,
382 pub anomaly_detected: bool,
384 pub ground_truth: Option<bool>,
386 pub detector_name: String,
388}
389
390#[derive(Debug, Clone)]
392pub struct FalsePositivePatterns<A: Float + Send + Sync> {
393 pub temporal_patterns: Vec<TemporalPattern>,
395 pub feature_patterns: HashMap<String, A>,
397 pub context_patterns: Vec<ContextPattern<A>>,
399 pub detector_patterns: HashMap<String, Vec<A>>,
401}
402
403#[derive(Debug, Clone)]
405pub struct TemporalPattern {
406 pub pattern_type: TemporalPatternType,
408 pub strength: f64,
410 pub period: Option<Duration>,
412 pub confidence: f64,
414}
415
416#[derive(Debug, Clone)]
418pub enum TemporalPatternType {
419 Periodic,
421 TimeSpecific,
423 Burst,
425 Trend,
427}
428
429#[derive(Debug, Clone)]
431pub struct ContextPattern<A: Float + Send + Sync> {
432 pub context_features: Vec<A>,
434 pub frequency: usize,
436 pub reliability: A,
438}
439
440#[derive(Debug, Clone)]
442pub enum FPMitigationStrategy {
443 ThresholdAdjustment,
445 FeatureAdjustment,
447 EnsembleReweighting,
449 ContextFiltering,
451 TemporalFiltering,
453 ModelRetraining,
455}
456
457pub struct AnomalyResponseSystem<A: Float + Send + Sync> {
459 response_strategies: HashMap<AnomalyType, Vec<ResponseAction>>,
461 response_executor: ResponseExecutor<A>,
463 next_response_id: u64,
465 log_entries: VecDeque<String>,
467 alert_entries: VecDeque<String>,
469 quarantined_points: VecDeque<StreamingDataPoint<A>>,
471 pending_threshold_adjustment: Option<f64>,
474 monitoring_level: u32,
476}
477
478const RECENT_POINT_WINDOW: usize = 512;
480
481const RESPONSE_HISTORY_CAPACITY: usize = 1000;
483
484const QUARANTINE_CAPACITY: usize = 256;
486
487#[derive(Debug, Clone)]
489pub enum ResponseAction {
490 Log,
492 Alert,
494 Quarantine,
496 ModelAdjustment,
498 IncreaseMonitoring,
500 TriggerRecovery,
502 Custom(String),
504}
505
506pub struct ResponseExecutor<A: Float + Send + Sync> {
508 pending_responses: VecDeque<PendingResponse<A>>,
510 execution_history: VecDeque<ResponseExecution<A>>,
512 resource_limits: ResponseResourceLimits,
514}
515
516#[derive(Debug, Clone)]
518pub struct PendingResponse<A: Float + Send + Sync> {
519 pub id: u64,
521 pub anomaly_event: AnomalyEvent<A>,
523 pub action: ResponseAction,
525 pub priority: ResponsePriority,
527 pub scheduled_time: Instant,
529 pub timeout: Duration,
531}
532
533#[derive(Debug, Clone)]
535pub struct ResponseExecution<A: Float + Send + Sync> {
536 pub id: u64,
538 pub response: PendingResponse<A>,
540 pub start_time: Instant,
542 pub duration: Duration,
544 pub success: bool,
546 pub error_message: Option<String>,
548 pub resources_consumed: HashMap<String, A>,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
554pub enum ResponsePriority {
555 Low = 0,
557 Normal = 1,
559 High = 2,
561 Critical = 3,
563}
564
565#[derive(Debug, Clone)]
567pub struct ResponseResourceLimits {
568 pub max_concurrent_responses: usize,
570 pub max_cpu_usage: f64,
572 pub max_memory_usage: usize,
574 pub max_execution_time: Duration,
576}
577
578#[derive(Debug, Clone)]
580pub struct EffectivenessMetrics<A: Float + Send + Sync> {
581 pub success_rate: A,
583 pub avg_response_time: Duration,
585 pub resolution_rate: A,
587 pub false_alarm_reduction: A,
589 pub cost_benefit_ratio: A,
591}
592
593#[derive(Debug, Clone)]
595pub struct ResponseOutcome<A: Float + Send + Sync> {
596 pub execution: ResponseExecution<A>,
598 pub outcome: OutcomeMeasurement<A>,
600 pub follow_up_required: bool,
602 pub lessons_learned: Vec<String>,
604}
605
606#[derive(Debug, Clone)]
608pub struct OutcomeMeasurement<A: Float + Send + Sync> {
609 pub issue_resolved: bool,
611 pub time_to_resolution: Duration,
613 pub performance_impact: A,
615 pub side_effects: Vec<String>,
617 pub effectiveness_score: A,
619}
620
621#[derive(Debug, Clone)]
623pub struct TrendAnalysis<A: Float + Send + Sync> {
624 pub trend_direction: TrendDirection,
626 pub trend_magnitude: A,
628 pub trend_confidence: A,
630 pub trend_stability: A,
632}
633
634#[derive(Debug, Clone, PartialEq, Eq)]
636pub enum TrendDirection {
637 Improving,
639 Declining,
641 Stable,
643 Oscillating,
645}
646
647#[derive(Debug, Clone)]
649pub struct EscalationRule<A: Float + Send + Sync> {
650 pub name: String,
652 pub conditions: Vec<EscalationCondition<A>>,
654 pub actions: Vec<EscalationAction>,
656 pub priority: EscalationPriority,
658}
659
660#[derive(Debug, Clone)]
662pub struct EscalationCondition<A: Float + Send + Sync> {
663 pub condition_type: EscalationConditionType,
665 pub threshold: A,
667 pub time_window: Duration,
669}
670
671#[derive(Debug, Clone)]
673pub enum EscalationConditionType {
674 MultipleAnomalies,
676 HighSeverity,
678 ResponseFailure,
680 PerformanceDegradation,
682 ResourceExhaustion,
684}
685
686#[derive(Debug, Clone)]
688pub enum EscalationAction {
689 NotifyAdmin,
691 EmergencyProtocol,
693 SystemShutdown,
695 ActivateBackup,
697 IncreaseResources,
699}
700
701#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
703pub enum EscalationPriority {
704 Normal = 0,
706 Urgent = 1,
708 Emergency = 2,
710}
711
712impl<A: Float + Default + Clone + std::iter::Sum + Send + Sync + 'static> AnomalyDetector<A> {
713 pub fn new(config: &StreamingConfig) -> Result<Self, String> {
715 let anomaly_config = config.anomaly_config.clone();
716
717 let mut statistical_detectors: HashMap<String, Box<dyn StatisticalAnomalyDetector<A>>> =
718 HashMap::new();
719 let mut ml_detectors: HashMap<String, Box<dyn MLAnomalyDetector<A>>> = HashMap::new();
720
721 statistical_detectors.insert(
723 "zscore".to_string(),
724 Box::new(super::anomaly_statistical::ZScoreDetector::new(
725 anomaly_config.threshold,
726 )?),
727 );
728 statistical_detectors.insert(
729 "iqr".to_string(),
730 Box::new(super::anomaly_statistical::IQRDetector::new(
731 anomaly_config.threshold,
732 )?),
733 );
734
735 match anomaly_config.detection_method {
737 AnomalyDetectionMethod::IsolationForest => {
738 ml_detectors.insert(
739 "isolation_forest".to_string(),
740 Box::new(super::anomaly_ml::IsolationForestDetector::new()?),
741 );
742 }
743 AnomalyDetectionMethod::OneClassSVM => {
744 ml_detectors.insert(
745 "one_class_svm".to_string(),
746 Box::new(super::anomaly_ml::OneClassSvmDetector::new()?),
747 );
748 }
749 AnomalyDetectionMethod::LocalOutlierFactor => {
750 ml_detectors.insert(
751 "lof".to_string(),
752 Box::new(super::anomaly_ml::LofDetector::new()?),
753 );
754 }
755 _ => {
756 }
758 }
759
760 let ensemble_detector = EnsembleAnomalyDetector::new(EnsembleVotingStrategy::Weighted)?;
761 let threshold_manager = AdaptiveThresholdManager::new()?;
768 let false_positive_tracker = FalsePositiveTracker::new();
769 let response_system = AnomalyResponseSystem::new(&anomaly_config.response_strategy)?;
770
771 let recent_capacity = RECENT_POINT_WINDOW;
772 Ok(Self {
773 config: anomaly_config,
774 statistical_detectors,
775 ml_detectors,
776 ensemble_detector,
777 threshold_manager,
778 anomaly_history: VecDeque::with_capacity(10000),
779 false_positive_tracker,
780 response_system,
781 recent_points: VecDeque::with_capacity(recent_capacity),
782 recent_capacity,
783 context_performance_metrics: Vec::new(),
784 context_resource_usage: Vec::new(),
785 context_drift_indicators: Vec::new(),
786 recent_scores: VecDeque::with_capacity(recent_capacity),
787 points_since_recalibration: 0,
788 })
789 }
790
791 fn recalibrate_thresholds(&mut self) -> Result<(), String> {
806 if !self.config.enable_adaptive_threshold {
807 return Ok(());
808 }
809 let window = self.config.window_size.max(1);
810 if self.points_since_recalibration < window || self.recent_scores.len() < window {
811 return Ok(());
812 }
813 self.points_since_recalibration = 0;
814
815 let contamination = self.config.contamination_rate;
816 if !(contamination > 0.0 && contamination < 1.0) {
817 return Err(format!(
818 "AnomalyConfig::contamination_rate must be in (0, 1), got {contamination}"
819 ));
820 }
821
822 let mut scores: Vec<A> = self.recent_scores.iter().copied().collect();
823 scores.sort_by(crate::utils::total_order);
824 let rank = ((1.0 - contamination) * scores.len() as f64).floor() as usize;
827 let index = rank.min(scores.len().saturating_sub(1));
828 let Some(&target) = scores.get(index) else {
829 return Ok(());
830 };
831
832 for (name, detector) in &mut self.statistical_detectors {
833 let bounded = match self.threshold_manager.threshold_bounds.get(name) {
834 Some(&(low, high)) => target.max(low).min(high),
835 None => target,
836 };
837 detector.set_threshold(bounded);
838 self.threshold_manager
839 .thresholds
840 .insert(name.clone(), bounded);
841 }
842 Ok(())
843 }
844
845 #[cfg(test)]
847 pub(crate) fn calibrated_threshold_names_for_test(&self) -> Vec<A> {
848 self.threshold_manager
849 .thresholds
850 .values()
851 .copied()
852 .collect()
853 }
854
855 pub fn calibrated_threshold(&self, detector_name: &str) -> Option<A> {
858 self.threshold_manager
859 .thresholds
860 .get(detector_name)
861 .copied()
862 }
863
864 pub fn update_context_signals(
870 &mut self,
871 performance_metrics: Vec<A>,
872 resource_usage: Vec<A>,
873 drift_indicators: Vec<A>,
874 ) {
875 self.context_performance_metrics = performance_metrics;
876 self.context_resource_usage = resource_usage;
877 self.context_drift_indicators = drift_indicators;
878 }
879
880 pub fn record_detection_outcome(&mut self, predicted_anomaly: bool, was_true_anomaly: bool) {
887 for detector in self.ml_detectors.values_mut() {
888 detector.record_outcome(predicted_anomaly, was_true_anomaly);
889 }
890 self.ensemble_detector.record_outcome(was_true_anomaly);
894 self.false_positive_tracker
895 .record_outcome(predicted_anomaly, was_true_anomaly);
896
897 if predicted_anomaly && !was_true_anomaly {
900 if let Some(event) = self.anomaly_history.back().cloned() {
901 self.false_positive_tracker.record_false_positive(&event);
902 }
903 }
904 }
905
906 pub fn confirmed_false_positive_count(&self) -> usize {
908 self.false_positive_tracker.confirmed_false_positive_count()
909 }
910
911 pub fn set_ensemble_voting_strategy(&mut self, strategy: EnsembleVotingStrategy) {
918 self.ensemble_detector.set_voting_strategy(strategy);
919 }
920
921 pub fn set_detector_weight(&mut self, detector_name: &str, weight: A) {
926 self.ensemble_detector
927 .set_detector_weight(detector_name, weight);
928 }
929
930 pub fn detector_balanced_accuracy(&self, detector_name: &str) -> Option<f64> {
933 self.ensemble_detector
934 .detector_balanced_accuracy(detector_name)
935 }
936
937 pub fn response_execution_count(&self) -> usize {
939 self.response_system.execution_count()
940 }
941
942 pub fn response_log_entry_count(&self) -> usize {
944 self.response_system.log_entry_count()
945 }
946
947 pub fn response_alert_entry_count(&self) -> usize {
949 self.response_system.alert_entry_count()
950 }
951
952 pub fn quarantined_point_count(&self) -> usize {
954 self.response_system.quarantined_count()
955 }
956
957 pub fn monitoring_level(&self) -> u32 {
959 self.response_system.monitoring_level()
960 }
961
962 pub fn ml_performance_metrics(&self) -> HashMap<String, Result<MLModelMetrics<A>, String>> {
966 self.ml_detectors
967 .iter()
968 .map(|(name, detector)| (name.clone(), detector.get_performance_metrics()))
969 .collect()
970 }
971
972 pub fn recent_window_len(&self) -> usize {
974 self.recent_points.len()
975 }
976
977 pub fn build_context_for_test(
982 &self,
983 data_point: &StreamingDataPoint<A>,
984 ) -> Result<AnomalyContext<A>, String> {
985 self.create_anomaly_context(data_point)
986 }
987
988 fn remember_point(&mut self, data_point: &StreamingDataPoint<A>) {
990 if self.recent_points.len() >= self.recent_capacity {
991 self.recent_points.pop_front();
992 }
993 self.recent_points.push_back(data_point.clone());
994 }
995
996 pub fn detect_anomaly(&mut self, data_point: &StreamingDataPoint<A>) -> Result<bool, String> {
998 let mut detection_results = HashMap::new();
999
1000 for (name, detector) in &mut self.statistical_detectors {
1002 let result = detector.detect_anomaly(data_point)?;
1003 detection_results.insert(name.clone(), result);
1004 }
1005
1006 for (name, detector) in &mut self.ml_detectors {
1008 let result = detector.detect_anomaly(data_point)?;
1009 detection_results.insert(name.clone(), result);
1010 }
1011
1012 let ensemble_result = self.ensemble_detector.combine_results(detection_results)?;
1014
1015 self.remember_point(data_point);
1019
1020 if self.recent_scores.len() >= self.recent_capacity {
1022 self.recent_scores.pop_front();
1023 }
1024 self.recent_scores.push_back(ensemble_result.anomaly_score);
1025 self.points_since_recalibration = self.points_since_recalibration.saturating_add(1);
1026 self.recalibrate_thresholds()?;
1027
1028 if ensemble_result.is_anomaly {
1030 let mut anomaly_event = AnomalyEvent {
1032 id: self.generate_event_id(),
1033 timestamp: Instant::now(),
1034 anomaly_type: ensemble_result
1035 .anomaly_type
1036 .as_ref()
1037 .cloned()
1038 .unwrap_or(AnomalyType::StatisticalOutlier),
1039 severity: ensemble_result.severity.clone(),
1040 confidence: ensemble_result.confidence,
1041 data_point: data_point.clone(),
1042 detector_name: "ensemble".to_string(),
1043 anomaly_score: ensemble_result.anomaly_score,
1044 context: self.create_anomaly_context(data_point)?,
1045 response_actions: Vec::new(),
1046 };
1047
1048 anomaly_event.response_actions =
1052 self.response_system.trigger_response(&anomaly_event)?;
1053
1054 let pending_adjustment = self
1057 .response_system
1058 .take_pending_threshold_adjustment()
1059 .filter(|_| self.config.enable_adaptive_threshold);
1062 if let Some(adjustment) = pending_adjustment {
1063 let magnitude = A::from(adjustment).ok_or_else(|| {
1064 format!("threshold adjustment {adjustment} is not representable")
1065 })?;
1066 for detector in self.statistical_detectors.values_mut() {
1067 let updated = detector.get_threshold() + magnitude;
1068 detector.set_threshold(updated);
1069 }
1070 self.ensemble_detector.adjust_sensitivity(magnitude)?;
1071 }
1072
1073 self.record_anomaly(anomaly_event)?;
1075
1076 return Ok(true);
1077 }
1078
1079 for detector in self.statistical_detectors.values_mut() {
1081 detector.update(data_point)?;
1082 }
1083
1084 for detector in self.ml_detectors.values_mut() {
1085 detector.update_incremental(data_point)?;
1086 }
1087
1088 Ok(false)
1089 }
1090
1091 fn generate_event_id(&self) -> u64 {
1093 self.anomaly_history.len() as u64 + 1
1094 }
1095
1096 fn create_anomaly_context(
1098 &self,
1099 data_point: &StreamingDataPoint<A>,
1100 ) -> Result<AnomalyContext<A>, String> {
1101 let recent_statistics = self.calculate_recent_statistics(data_point)?;
1103
1104 let time_since_last_anomaly = match self.anomaly_history.back() {
1108 Some(last_anomaly) => last_anomaly.timestamp.elapsed(),
1109 None => self
1110 .recent_points
1111 .front()
1112 .map(|point| point.timestamp.elapsed())
1113 .unwrap_or(Duration::ZERO),
1114 };
1115
1116 Ok(AnomalyContext {
1117 recent_statistics,
1118 performance_metrics: self.context_performance_metrics.clone(),
1123 resource_usage: self.context_resource_usage.clone(),
1124 drift_indicators: self.context_drift_indicators.clone(),
1125 time_since_last_anomaly,
1126 })
1127 }
1128
1129 fn calculate_recent_statistics(
1138 &self,
1139 data_point: &StreamingDataPoint<A>,
1140 ) -> Result<DataStatistics<A>, String> {
1141 let feature_count = self
1142 .recent_points
1143 .iter()
1144 .map(|point| point.features.len())
1145 .chain(std::iter::once(data_point.features.len()))
1146 .max()
1147 .unwrap_or(0);
1148
1149 let mut means = Vec::with_capacity(feature_count);
1150 let mut std_devs = Vec::with_capacity(feature_count);
1151 let mut min_values = Vec::with_capacity(feature_count);
1152 let mut max_values = Vec::with_capacity(feature_count);
1153 let mut medians = Vec::with_capacity(feature_count);
1154 let mut skewness = Vec::with_capacity(feature_count);
1155 let mut kurtosis = Vec::with_capacity(feature_count);
1156
1157 for index in 0..feature_count {
1158 let mut column: Vec<A> = self
1159 .recent_points
1160 .iter()
1161 .filter_map(|point| point.features.get(index).copied())
1162 .collect();
1163 if let Some(value) = data_point.features.get(index) {
1164 column.push(*value);
1165 }
1166
1167 if column.is_empty() {
1168 means.push(A::zero());
1169 std_devs.push(A::zero());
1170 min_values.push(A::zero());
1171 max_values.push(A::zero());
1172 medians.push(A::zero());
1173 skewness.push(A::zero());
1174 kurtosis.push(A::zero());
1175 continue;
1176 }
1177
1178 let count = A::from(column.len())
1179 .ok_or_else(|| format!("sample count {} is not representable", column.len()))?;
1180 let mean = column.iter().fold(A::zero(), |acc, &v| acc + v) / count;
1181 let variance = column
1182 .iter()
1183 .fold(A::zero(), |acc, &v| acc + (v - mean) * (v - mean))
1184 / count;
1185 let std_dev = variance.sqrt();
1186
1187 let minimum = column
1188 .iter()
1189 .copied()
1190 .reduce(|a, b| {
1191 if super::statistics::total_order(&b, &a) == std::cmp::Ordering::Less {
1192 b
1193 } else {
1194 a
1195 }
1196 })
1197 .unwrap_or_else(A::zero);
1198 let maximum = column
1199 .iter()
1200 .copied()
1201 .reduce(|a, b| {
1202 if super::statistics::total_order(&b, &a) == std::cmp::Ordering::Greater {
1203 b
1204 } else {
1205 a
1206 }
1207 })
1208 .unwrap_or_else(A::zero);
1209 let median = super::statistics::median_in_place(&mut column).unwrap_or(mean);
1210
1211 let (skew, kurt) = if std_dev > A::zero() {
1216 let mut third = A::zero();
1217 let mut fourth = A::zero();
1218 for &value in column.iter() {
1219 let z = (value - mean) / std_dev;
1220 let z2 = z * z;
1221 third = third + z2 * z;
1222 fourth = fourth + z2 * z2;
1223 }
1224 let three = A::from(3.0).ok_or_else(|| "3.0 is not representable".to_string())?;
1225 (third / count, fourth / count - three)
1227 } else {
1228 (A::zero(), A::zero())
1229 };
1230
1231 means.push(mean);
1232 std_devs.push(std_dev);
1233 min_values.push(minimum);
1234 max_values.push(maximum);
1235 medians.push(median);
1236 skewness.push(skew);
1237 kurtosis.push(kurt);
1238 }
1239
1240 Ok(DataStatistics {
1241 means,
1242 std_devs,
1243 min_values,
1244 max_values,
1245 medians,
1246 skewness,
1247 kurtosis,
1248 })
1249 }
1250
1251 fn record_anomaly(&mut self, anomaly_event: AnomalyEvent<A>) -> Result<(), String> {
1253 if self.anomaly_history.len() >= 10000 {
1254 self.anomaly_history.pop_front();
1255 }
1256 self.anomaly_history.push_back(anomaly_event);
1257 Ok(())
1258 }
1259
1260 pub fn apply_adaptation(&mut self, adaptation: &Adaptation<A>) -> Result<(), String> {
1262 if adaptation.adaptation_type == AdaptationType::AnomalyDetection {
1263 let threshold_adjustment = adaptation.magnitude;
1265
1266 for detector in self.statistical_detectors.values_mut() {
1267 let current_threshold = detector.get_threshold();
1268 let new_threshold = current_threshold + threshold_adjustment;
1269 detector.set_threshold(new_threshold);
1270 }
1271
1272 self.ensemble_detector
1274 .adjust_sensitivity(threshold_adjustment)?;
1275 }
1276
1277 Ok(())
1278 }
1279
1280 pub fn get_recent_anomalies(&self, count: usize) -> Vec<&AnomalyEvent<A>> {
1282 self.anomaly_history.iter().rev().take(count).collect()
1283 }
1284
1285 pub fn get_diagnostics(&self) -> AnomalyDiagnostics {
1287 AnomalyDiagnostics {
1288 total_anomalies: self.anomaly_history.len(),
1289 recent_anomaly_rate: self.calculate_recent_anomaly_rate(),
1290 false_positive_rate: self.false_positive_tracker.get_current_fp_rate(),
1291 detector_count: self.statistical_detectors.len() + self.ml_detectors.len(),
1292 response_success_rate: self.response_system.get_success_rate(),
1293 response_executions: self.response_system.execution_count(),
1294 recent_window_len: self.recent_points.len(),
1295 }
1296 }
1297
1298 fn calculate_recent_anomaly_rate(&self) -> f64 {
1300 let recent_window = Duration::from_secs(3600); let now = Instant::now();
1304
1305 let recent_count = self
1306 .anomaly_history
1307 .iter()
1308 .filter(|event| now.duration_since(event.timestamp) <= recent_window)
1309 .count();
1310
1311 recent_count as f64 / recent_window.as_secs_f64() }
1313}
1314
1315impl<A: Float + Default + Clone + Send + Sync + Send + Sync> AdaptiveThresholdManager<A> {
1329 fn new() -> Result<Self, String> {
1330 Ok(Self {
1331 thresholds: HashMap::new(),
1332 threshold_bounds: HashMap::new(),
1333 })
1334 }
1335}
1336
1337impl<A: Float + Default + Clone + Send + Sync + Send + Sync> FalsePositiveTracker<A> {
1338 fn new() -> Self {
1339 Self {
1340 false_positives: VecDeque::with_capacity(1000),
1341 fp_rate_calculator: FPRateCalculator {
1342 recent_results: VecDeque::with_capacity(1000),
1343 window_size: 1000,
1344 current_fp_rate: scalar_or(0.05, A::zero()),
1345 },
1346 }
1347 }
1348
1349 fn get_current_fp_rate(&self) -> Option<f64> {
1356 if self.fp_rate_calculator.recent_results.is_empty() {
1357 return None;
1358 }
1359 self.fp_rate_calculator.current_fp_rate.to_f64()
1360 }
1361
1362 fn record_outcome(&mut self, predicted_anomaly: bool, was_true_anomaly: bool) {
1365 let calculator = &mut self.fp_rate_calculator;
1366 if calculator.recent_results.len() >= calculator.window_size {
1367 calculator.recent_results.pop_front();
1368 }
1369 calculator.recent_results.push_back(DetectionResult {
1370 timestamp: Instant::now(),
1371 anomaly_detected: predicted_anomaly,
1372 ground_truth: Some(was_true_anomaly),
1373 detector_name: "ensemble".to_string(),
1374 });
1375
1376 let negatives = calculator
1379 .recent_results
1380 .iter()
1381 .filter(|result| result.ground_truth == Some(false))
1382 .count();
1383 if negatives > 0 {
1384 let false_positives = calculator
1385 .recent_results
1386 .iter()
1387 .filter(|result| result.anomaly_detected && result.ground_truth == Some(false))
1388 .count();
1389 if let Some(rate) = A::from(false_positives as f64 / negatives as f64) {
1390 calculator.current_fp_rate = rate;
1391 }
1392 }
1393 }
1394
1395 fn record_false_positive(&mut self, event: &AnomalyEvent<A>) {
1397 if self.false_positives.len() >= RESPONSE_HISTORY_CAPACITY {
1398 self.false_positives.pop_front();
1399 }
1400 self.false_positives.push_back(FalsePositiveEvent {
1401 timestamp: event.timestamp,
1402 data_point: event.data_point.clone(),
1403 detector_name: event.detector_name.clone(),
1404 anomaly_score: event.anomaly_score,
1405 context: event.context.clone(),
1406 });
1407 }
1408
1409 fn confirmed_false_positive_count(&self) -> usize {
1411 self.false_positives.len()
1412 }
1413}
1414
1415impl<A: Float + Default + Clone + Send + Sync + Send + Sync> AnomalyResponseSystem<A> {
1416 fn new(response_strategy: &AnomalyResponseStrategy) -> Result<Self, String> {
1417 let mut response_strategies = HashMap::new();
1418
1419 match response_strategy {
1421 AnomalyResponseStrategy::Ignore => {
1422 response_strategies
1423 .insert(AnomalyType::StatisticalOutlier, vec![ResponseAction::Log]);
1424 }
1425 AnomalyResponseStrategy::Filter => {
1426 response_strategies.insert(
1427 AnomalyType::StatisticalOutlier,
1428 vec![ResponseAction::Quarantine],
1429 );
1430 }
1431 AnomalyResponseStrategy::Adaptive => {
1432 response_strategies.insert(
1433 AnomalyType::StatisticalOutlier,
1434 vec![ResponseAction::Log, ResponseAction::ModelAdjustment],
1435 );
1436 }
1437 _ => {
1438 response_strategies
1439 .insert(AnomalyType::StatisticalOutlier, vec![ResponseAction::Alert]);
1440 }
1441 }
1442
1443 Ok(Self {
1444 response_strategies,
1445 response_executor: ResponseExecutor {
1446 pending_responses: VecDeque::new(),
1447 execution_history: VecDeque::with_capacity(1000),
1448 resource_limits: ResponseResourceLimits {
1449 max_concurrent_responses: 10,
1450 max_cpu_usage: 0.2,
1451 max_memory_usage: 100 * 1024 * 1024, max_execution_time: Duration::from_secs(60),
1453 },
1454 },
1455 next_response_id: 0,
1456 log_entries: VecDeque::with_capacity(RESPONSE_HISTORY_CAPACITY),
1457 alert_entries: VecDeque::with_capacity(RESPONSE_HISTORY_CAPACITY),
1458 quarantined_points: VecDeque::with_capacity(QUARANTINE_CAPACITY),
1459 pending_threshold_adjustment: None,
1460 monitoring_level: 0,
1461 })
1462 }
1463
1464 fn trigger_response(&mut self, event: &AnomalyEvent<A>) -> Result<Vec<String>, String> {
1473 let actions = self
1474 .response_strategies
1475 .get(&event.anomaly_type)
1476 .or_else(|| {
1477 self.response_strategies
1478 .get(&AnomalyType::StatisticalOutlier)
1479 })
1480 .cloned()
1481 .unwrap_or_default();
1482
1483 if actions.is_empty() {
1484 return Ok(Vec::new());
1485 }
1486
1487 let priority = match event.severity {
1488 AnomalySeverity::Critical => ResponsePriority::Critical,
1489 AnomalySeverity::High => ResponsePriority::High,
1490 AnomalySeverity::Medium => ResponsePriority::Normal,
1491 AnomalySeverity::Low => ResponsePriority::Low,
1492 };
1493 let timeout = self.response_executor.resource_limits.max_execution_time;
1494
1495 for action in actions {
1496 if self.response_executor.pending_responses.len()
1497 >= self
1498 .response_executor
1499 .resource_limits
1500 .max_concurrent_responses
1501 {
1502 break;
1505 }
1506 self.next_response_id += 1;
1507 self.response_executor
1508 .pending_responses
1509 .push_back(PendingResponse {
1510 id: self.next_response_id,
1511 anomaly_event: event.clone(),
1512 action,
1513 priority: priority.clone(),
1514 scheduled_time: Instant::now(),
1515 timeout,
1516 });
1517 }
1518
1519 self.execute_pending_responses()
1520 }
1521
1522 fn execute_pending_responses(&mut self) -> Result<Vec<String>, String> {
1524 self.response_executor
1527 .pending_responses
1528 .make_contiguous()
1529 .sort_by(|a, b| b.priority.cmp(&a.priority));
1530
1531 let mut executed = Vec::new();
1532 while let Some(pending) = self.response_executor.pending_responses.pop_front() {
1533 let started = Instant::now();
1534 let outcome = self.perform_action(&pending);
1535 let duration = started.elapsed();
1536
1537 let (success, error_message) = match &outcome {
1538 Ok(()) => (true, None),
1539 Err(reason) => (false, Some(reason.clone())),
1540 };
1541 if success {
1542 executed.push(format!("{:?}", pending.action));
1543 }
1544
1545 let mut resources_consumed = HashMap::new();
1546 if let Some(millis) = A::from(duration.as_secs_f64() * 1000.0) {
1547 resources_consumed.insert("execution_time_ms".to_string(), millis);
1548 }
1549
1550 let execution = ResponseExecution {
1551 id: pending.id,
1552 response: pending,
1553 start_time: started,
1554 duration,
1555 success,
1556 error_message,
1557 resources_consumed,
1558 };
1559
1560 if self.response_executor.execution_history.len() >= RESPONSE_HISTORY_CAPACITY {
1561 self.response_executor.execution_history.pop_front();
1562 }
1563 self.response_executor
1564 .execution_history
1565 .push_back(execution);
1566 }
1567
1568 Ok(executed)
1569 }
1570
1571 fn perform_action(&mut self, pending: &PendingResponse<A>) -> Result<(), String> {
1573 match &pending.action {
1574 ResponseAction::Log => {
1575 self.push_bounded(
1576 ResponseChannel::Log,
1577 format!(
1578 "anomaly {} type={:?} severity={:?} score={:?}",
1579 pending.anomaly_event.id,
1580 pending.anomaly_event.anomaly_type,
1581 pending.anomaly_event.severity,
1582 pending.anomaly_event.anomaly_score.to_f64()
1583 ),
1584 );
1585 Ok(())
1586 }
1587 ResponseAction::Alert => {
1588 self.push_bounded(
1589 ResponseChannel::Alert,
1590 format!(
1591 "ALERT: anomaly {} severity={:?}",
1592 pending.anomaly_event.id, pending.anomaly_event.severity
1593 ),
1594 );
1595 Ok(())
1596 }
1597 ResponseAction::Quarantine => {
1598 if self.quarantined_points.len() >= QUARANTINE_CAPACITY {
1599 self.quarantined_points.pop_front();
1600 }
1601 self.quarantined_points
1602 .push_back(pending.anomaly_event.data_point.clone());
1603 Ok(())
1604 }
1605 ResponseAction::ModelAdjustment => {
1606 let step = match pending.anomaly_event.severity {
1611 AnomalySeverity::Critical => 0.20,
1612 AnomalySeverity::High => 0.10,
1613 AnomalySeverity::Medium => 0.05,
1614 AnomalySeverity::Low => 0.01,
1615 };
1616 self.pending_threshold_adjustment =
1617 Some(self.pending_threshold_adjustment.unwrap_or(0.0) + step);
1618 Ok(())
1619 }
1620 ResponseAction::IncreaseMonitoring => {
1621 self.monitoring_level = self.monitoring_level.saturating_add(1);
1622 Ok(())
1623 }
1624 ResponseAction::TriggerRecovery => Err(
1625 "no recovery procedure is registered with this response system; \
1626 a recovery handler must be installed before this action can run"
1627 .to_string(),
1628 ),
1629 ResponseAction::Custom(name) => Err(format!(
1630 "no handler is registered for custom response action '{name}'"
1631 )),
1632 }
1633 }
1634
1635 fn push_bounded(&mut self, channel: ResponseChannel, message: String) {
1636 let sink = match channel {
1637 ResponseChannel::Log => &mut self.log_entries,
1638 ResponseChannel::Alert => &mut self.alert_entries,
1639 };
1640 if sink.len() >= RESPONSE_HISTORY_CAPACITY {
1641 sink.pop_front();
1642 }
1643 sink.push_back(message);
1644 }
1645
1646 fn take_pending_threshold_adjustment(&mut self) -> Option<f64> {
1648 self.pending_threshold_adjustment.take()
1649 }
1650
1651 fn get_success_rate(&self) -> Option<f64> {
1658 let history = &self.response_executor.execution_history;
1659 if history.is_empty() {
1660 return None;
1661 }
1662 let successes = history.iter().filter(|execution| execution.success).count();
1663 Some(successes as f64 / history.len() as f64)
1664 }
1665
1666 fn log_entry_count(&self) -> usize {
1668 self.log_entries.len()
1669 }
1670
1671 fn alert_entry_count(&self) -> usize {
1673 self.alert_entries.len()
1674 }
1675
1676 fn quarantined_count(&self) -> usize {
1678 self.quarantined_points.len()
1679 }
1680
1681 fn monitoring_level(&self) -> u32 {
1683 self.monitoring_level
1684 }
1685
1686 fn execution_count(&self) -> usize {
1688 self.response_executor.execution_history.len()
1689 }
1690}
1691
1692#[derive(Debug, Clone, Copy)]
1694enum ResponseChannel {
1695 Log,
1696 Alert,
1697}
1698
1699#[derive(Debug, Clone)]
1701pub struct AnomalyDiagnostics {
1702 pub total_anomalies: usize,
1704 pub recent_anomaly_rate: f64,
1706 pub false_positive_rate: Option<f64>,
1709 pub detector_count: usize,
1711 pub response_success_rate: Option<f64>,
1714 pub response_executions: usize,
1716 pub recent_window_len: usize,
1718}
1719
1720#[cfg(test)]
1721#[path = "anomaly_detection_regression_tests.rs"]
1722mod regression_tests;