1use super::config::*;
4use crate::error::{MLError, Result};
5use scirs2_core::ndarray::{s, Array1, Array2, Axis};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::f64::consts::PI;
9
10#[derive(Debug, Clone)]
12pub struct QuantumSeasonalDecomposer {
13 config: SeasonalityConfig,
15
16 seasonal_circuits: HashMap<String, Vec<f64>>,
18
19 trend_extractor: QuantumTrendExtractor,
21
22 residual_analyzer: QuantumResidualAnalyzer,
24
25 decomposition_cache: DecompositionCache,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct QuantumTrendExtractor {
32 smoothing_param: f64,
34
35 trend_circuit: Vec<f64>,
37
38 changepoint_detector: Option<QuantumChangepointDetector>,
40
41 trend_parameters: TrendParameters,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct QuantumChangepointDetector {
48 threshold: f64,
50
51 detection_circuit: Vec<f64>,
53
54 changepoints: Vec<ChangePoint>,
56
57 algorithm: ChangepointAlgorithm,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ChangePoint {
64 pub time_index: usize,
66
67 pub confidence: f64,
69
70 pub change_type: ChangeType,
72
73 pub magnitude: f64,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub enum ChangeType {
80 TrendChange,
81 LevelShift,
82 VarianceChange,
83 SeasonalityChange,
84 QuantumPhaseTransition,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub enum ChangepointAlgorithm {
90 CUSUM,
91 PELT,
92 BinarySegmentation,
93 QuantumDetection,
94 HybridQuantumClassical,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct TrendParameters {
100 pub linear_coeff: f64,
102
103 pub quadratic_coeff: f64,
105
106 pub exp_growth_rate: f64,
108
109 pub quantum_params: Array1<f64>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct QuantumResidualAnalyzer {
116 analysis_circuit: Vec<f64>,
118
119 anomaly_threshold: f64,
121
122 anomalies: Vec<AnomalyPoint>,
124
125 residual_stats: ResidualStatistics,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct AnomalyPoint {
132 pub timestamp: usize,
134
135 pub value: f64,
137
138 pub anomaly_score: f64,
140
141 pub anomaly_type: AnomalyType,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ResidualStatistics {
148 pub mean: f64,
150
151 pub std: f64,
153
154 pub skewness: f64,
156
157 pub kurtosis: f64,
159
160 pub autocorrelation: Array1<f64>,
162
163 pub quantum_coherence: f64,
165}
166
167#[derive(Debug, Clone)]
169pub struct DecompositionCache {
170 cache: HashMap<String, DecompositionResult>,
172
173 hits: usize,
175
176 misses: usize,
178
179 max_size: usize,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct DecompositionResult {
186 pub original: Array2<f64>,
188
189 pub trend: Array1<f64>,
191
192 pub seasonal: HashMap<String, Array1<f64>>,
194
195 pub residual: Array1<f64>,
197
198 pub quality_metrics: DecompositionQuality,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct DecompositionQuality {
205 pub explained_variance: f64,
207
208 pub reconstruction_error: f64,
210
211 pub seasonal_strength: HashMap<String, f64>,
213
214 pub trend_strength: f64,
216
217 pub quantum_effectiveness: f64,
219}
220
221#[derive(Debug, Clone)]
223pub struct SeasonalComponentExtractor {
224 period: usize,
226
227 name: String,
229
230 quantum_circuit: Vec<f64>,
232
233 method: SeasonalExtractionMethod,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub enum SeasonalExtractionMethod {
240 MovingAverage,
241 FourierDecomposition,
242 STL,
243 QuantumFourier,
244 WaveletDecomposition,
245 HybridQuantumClassical,
246}
247
248impl QuantumSeasonalDecomposer {
249 pub fn new(config: SeasonalityConfig, num_qubits: usize) -> Result<Self> {
251 let mut seasonal_circuits = HashMap::new();
252
253 if let Some(daily) = config.daily {
255 seasonal_circuits.insert(
256 "daily".to_string(),
257 Self::create_seasonal_circuit(daily, num_qubits)?,
258 );
259 }
260
261 if let Some(weekly) = config.weekly {
262 seasonal_circuits.insert(
263 "weekly".to_string(),
264 Self::create_seasonal_circuit(weekly, num_qubits)?,
265 );
266 }
267
268 if let Some(monthly) = config.monthly {
269 seasonal_circuits.insert(
270 "monthly".to_string(),
271 Self::create_seasonal_circuit(monthly, num_qubits)?,
272 );
273 }
274
275 if let Some(yearly) = config.yearly {
276 seasonal_circuits.insert(
277 "yearly".to_string(),
278 Self::create_seasonal_circuit(yearly, num_qubits)?,
279 );
280 }
281
282 for (i, &period) in config.custom_periods.iter().enumerate() {
284 let name = format!("custom_{}", i);
285 seasonal_circuits.insert(name, Self::create_seasonal_circuit(period, num_qubits)?);
286 }
287
288 let trend_extractor = QuantumTrendExtractor::new(0.1, num_qubits)?;
289 let residual_analyzer = QuantumResidualAnalyzer::new(num_qubits)?;
290 let decomposition_cache = DecompositionCache::new(100);
291
292 Ok(Self {
293 config,
294 seasonal_circuits,
295 trend_extractor,
296 residual_analyzer,
297 decomposition_cache,
298 })
299 }
300
301 fn create_seasonal_circuit(period: usize, num_qubits: usize) -> Result<Vec<f64>> {
303 let mut circuit = Vec::new();
304
305 for i in 0..num_qubits {
307 let frequency = 2.0 * PI * i as f64 / period as f64;
308 circuit.push(frequency);
309 }
310
311 for i in 0..num_qubits {
313 let phase = PI * i as f64 / num_qubits as f64;
314 circuit.push(phase);
315 }
316
317 Ok(circuit)
318 }
319
320 pub fn decompose(
322 &mut self,
323 data: &Array2<f64>,
324 ) -> Result<(Array2<f64>, Option<Array1<f64>>, Option<Array1<f64>>)> {
325 let cache_key = self.generate_cache_key(data);
327 if let Some(cached_result) = self.decomposition_cache.get(&cache_key) {
328 let cached_result = cached_result.clone();
329 return self.convert_cached_result(&cached_result);
330 }
331
332 let decomposition = self.full_decomposition(data)?;
334
335 self.decomposition_cache
337 .insert(cache_key, decomposition.clone());
338
339 let detrended = &decomposition.original - &decomposition.trend.clone().insert_axis(Axis(1));
341 let deseasonalized =
342 self.remove_seasonal_components(&detrended, &decomposition.seasonal)?;
343
344 Ok((
345 deseasonalized,
346 Some(decomposition.trend),
347 self.combine_seasonal_components(&decomposition.seasonal),
348 ))
349 }
350
351 fn full_decomposition(&mut self, data: &Array2<f64>) -> Result<DecompositionResult> {
353 let trend = self.trend_extractor.extract_trend(data)?;
355
356 let detrended = data - &trend.clone().insert_axis(Axis(1));
358
359 let seasonal = self.extract_all_seasonal_components(&detrended)?;
361
362 let mut residual_data = detrended.clone();
364 for (_, seasonal_component) in &seasonal {
365 residual_data = &residual_data - &seasonal_component.clone().insert_axis(Axis(1));
366 }
367 let residual = residual_data.column(0).to_owned();
368
369 self.residual_analyzer.analyze_residuals(&residual)?;
371
372 let quality_metrics = self.calculate_quality_metrics(data, &trend, &seasonal, &residual)?;
374
375 Ok(DecompositionResult {
376 original: data.clone(),
377 trend,
378 seasonal,
379 residual,
380 quality_metrics,
381 })
382 }
383
384 fn extract_all_seasonal_components(
386 &self,
387 data: &Array2<f64>,
388 ) -> Result<HashMap<String, Array1<f64>>> {
389 let mut seasonal_components = HashMap::new();
390
391 for (component_name, circuit) in &self.seasonal_circuits {
392 let component = self.extract_seasonal_component(data, component_name, circuit)?;
393 seasonal_components.insert(component_name.clone(), component);
394 }
395
396 Ok(seasonal_components)
397 }
398
399 fn extract_seasonal_component(
401 &self,
402 data: &Array2<f64>,
403 name: &str,
404 circuit: &[f64],
405 ) -> Result<Array1<f64>> {
406 let n_samples = data.nrows();
407 let mut seasonal_component = Array1::zeros(n_samples);
408
409 let period = self.get_period_for_component(name);
411
412 if let Some(period) = period {
413 for t in 0..n_samples {
415 let mut seasonal_value = 0.0;
416
417 let phase = 2.0 * PI * (t % period) as f64 / period as f64;
419 seasonal_value += phase.sin();
420
421 if !circuit.is_empty() {
423 let circuit_idx = t % circuit.len();
424 let quantum_phase = circuit[circuit_idx] * phase;
425 seasonal_value += 0.1 * quantum_phase.cos(); }
427
428 seasonal_component[t] = seasonal_value;
429 }
430
431 self.normalize_seasonal_component(&mut seasonal_component);
433 }
434
435 Ok(seasonal_component)
436 }
437
438 fn get_period_for_component(&self, name: &str) -> Option<usize> {
440 match name {
441 "daily" => self.config.daily,
442 "weekly" => self.config.weekly,
443 "monthly" => self.config.monthly,
444 "yearly" => self.config.yearly,
445 custom_name if custom_name.starts_with("custom_") => {
446 if let Ok(index) = custom_name[7..].parse::<usize>() {
447 self.config.custom_periods.get(index).copied()
448 } else {
449 None
450 }
451 }
452 _ => None,
453 }
454 }
455
456 fn normalize_seasonal_component(&self, component: &mut Array1<f64>) {
458 let mean = component.mean().unwrap_or(0.0);
459 for value in component.iter_mut() {
460 *value -= mean; }
462 }
463
464 fn remove_seasonal_components(
466 &self,
467 data: &Array2<f64>,
468 seasonal: &HashMap<String, Array1<f64>>,
469 ) -> Result<Array2<f64>> {
470 let mut result = data.clone();
471
472 for (_, component) in seasonal {
473 result = &result - &component.clone().insert_axis(Axis(1));
474 }
475
476 Ok(result)
477 }
478
479 fn combine_seasonal_components(
481 &self,
482 seasonal: &HashMap<String, Array1<f64>>,
483 ) -> Option<Array1<f64>> {
484 if seasonal.is_empty() {
485 return None;
486 }
487
488 let first_component = seasonal.values().next()?;
489 let mut combined = Array1::zeros(first_component.len());
490
491 for component in seasonal.values() {
492 combined = combined + component;
493 }
494
495 Some(combined)
496 }
497
498 fn calculate_quality_metrics(
500 &self,
501 original: &Array2<f64>,
502 trend: &Array1<f64>,
503 seasonal: &HashMap<String, Array1<f64>>,
504 residual: &Array1<f64>,
505 ) -> Result<DecompositionQuality> {
506 let original_var = original.var(0.0);
508 let residual_var = residual.var(0.0);
509 let explained_variance = 1.0 - (residual_var / original_var.max(1e-10));
510
511 let mut reconstructed = trend.clone().insert_axis(Axis(1));
513 for component in seasonal.values() {
514 reconstructed = reconstructed + component.clone().insert_axis(Axis(1));
515 }
516 reconstructed = reconstructed + residual.clone().insert_axis(Axis(1));
517
518 let reconstruction_error = (original - &reconstructed)
519 .mapv(|x| x * x)
520 .mean()
521 .unwrap_or(0.0)
522 .sqrt();
523
524 let mut seasonal_strength = HashMap::new();
526 for (name, component) in seasonal {
527 let strength = component.var(0.0) / original_var.max(1e-10);
528 seasonal_strength.insert(name.clone(), strength);
529 }
530
531 let trend_strength = trend.var(0.0) / original_var.max(1e-10);
533
534 let quantum_effectiveness = 0.8; Ok(DecompositionQuality {
538 explained_variance,
539 reconstruction_error,
540 seasonal_strength,
541 trend_strength,
542 quantum_effectiveness,
543 })
544 }
545
546 fn generate_cache_key(&self, data: &Array2<f64>) -> String {
548 format!("{}x{}_{:.3}", data.nrows(), data.ncols(), data.sum())
550 }
551
552 fn convert_cached_result(
554 &self,
555 result: &DecompositionResult,
556 ) -> Result<(Array2<f64>, Option<Array1<f64>>, Option<Array1<f64>>)> {
557 let detrended = &result.original - &result.trend.clone().insert_axis(Axis(1));
558 let deseasonalized = self.remove_seasonal_components(&detrended, &result.seasonal)?;
559 let combined_seasonal = self.combine_seasonal_components(&result.seasonal);
560
561 Ok((
562 deseasonalized,
563 Some(result.trend.clone()),
564 combined_seasonal,
565 ))
566 }
567
568 pub fn get_quality_metrics(&self) -> Option<&DecompositionQuality> {
570 None
572 }
573
574 pub fn get_changepoints(&self) -> Vec<&ChangePoint> {
576 if let Some(ref detector) = self.trend_extractor.changepoint_detector {
577 detector.changepoints.iter().collect()
578 } else {
579 Vec::new()
580 }
581 }
582
583 pub fn get_anomalies(&self) -> &[AnomalyPoint] {
585 &self.residual_analyzer.anomalies
586 }
587}
588
589impl QuantumTrendExtractor {
590 pub fn new(smoothing_param: f64, num_qubits: usize) -> Result<Self> {
592 let mut trend_circuit = Vec::new();
593
594 for i in 0..num_qubits {
596 trend_circuit.push(smoothing_param * (i + 1) as f64);
597 }
598
599 for i in 0..num_qubits {
601 trend_circuit.push(PI * i as f64 / num_qubits as f64);
602 }
603
604 let changepoint_detector = Some(QuantumChangepointDetector::new(0.05, num_qubits)?);
605
606 let trend_parameters = TrendParameters {
607 linear_coeff: 0.0,
608 quadratic_coeff: 0.0,
609 exp_growth_rate: 0.0,
610 quantum_params: Array1::zeros(num_qubits),
611 };
612
613 Ok(Self {
614 smoothing_param,
615 trend_circuit,
616 changepoint_detector,
617 trend_parameters,
618 })
619 }
620
621 pub fn extract_trend(&mut self, data: &Array2<f64>) -> Result<Array1<f64>> {
623 let n_samples = data.nrows();
624 let mut trend = Array1::zeros(n_samples);
625
626 for t in 0..n_samples {
628 let mut trend_value = 0.0;
629
630 let window_size = (n_samples / 10).max(3).min(21); let start = t.saturating_sub(window_size / 2);
633 let end = (t + window_size / 2 + 1).min(n_samples);
634
635 let window_sum: f64 = data.slice(s![start..end, 0]).sum();
636 trend_value = window_sum / (end - start) as f64;
637
638 if !self.trend_circuit.is_empty() {
640 let circuit_idx = t % self.trend_circuit.len();
641 let quantum_factor = self.trend_circuit[circuit_idx];
642 let quantum_phase = quantum_factor * trend_value * PI;
643 trend_value += 0.05 * quantum_phase.sin(); }
645
646 trend[t] = trend_value;
647 }
648
649 if let Some(ref mut detector) = self.changepoint_detector {
651 detector.detect_changepoints(&trend)?;
652 }
653
654 self.fit_trend_parameters(&trend)?;
656
657 Ok(trend)
658 }
659
660 fn fit_trend_parameters(&mut self, trend: &Array1<f64>) -> Result<()> {
662 let n = trend.len();
663 if n < 3 {
664 return Ok(()); }
666
667 let mut sum_t = 0.0;
669 let mut sum_y = 0.0;
670 let mut sum_ty = 0.0;
671 let mut sum_t2 = 0.0;
672
673 for (t, &y) in trend.iter().enumerate() {
674 let t_val = t as f64;
675 sum_t += t_val;
676 sum_y += y;
677 sum_ty += t_val * y;
678 sum_t2 += t_val * t_val;
679 }
680
681 let n_f = n as f64;
682 let denominator = n_f * sum_t2 - sum_t * sum_t;
683
684 if denominator.abs() > 1e-10 {
685 self.trend_parameters.linear_coeff = (n_f * sum_ty - sum_t * sum_y) / denominator;
686 }
688
689 Ok(())
690 }
691
692 pub fn get_trend_parameters(&self) -> &TrendParameters {
694 &self.trend_parameters
695 }
696}
697
698impl QuantumChangepointDetector {
699 pub fn new(threshold: f64, num_qubits: usize) -> Result<Self> {
701 let mut detection_circuit = Vec::new();
702
703 for i in 0..num_qubits {
705 detection_circuit.push(1.0); detection_circuit.push(PI * i as f64 / num_qubits as f64); }
708
709 Ok(Self {
710 threshold,
711 detection_circuit,
712 changepoints: Vec::new(),
713 algorithm: ChangepointAlgorithm::QuantumDetection,
714 })
715 }
716
717 pub fn detect_changepoints(&mut self, data: &Array1<f64>) -> Result<()> {
719 self.changepoints.clear();
720
721 match self.algorithm {
722 ChangepointAlgorithm::QuantumDetection => self.quantum_detection(data)?,
723 ChangepointAlgorithm::CUSUM => self.cusum_detection(data)?,
724 _ => {
725 self.quantum_detection(data)?
727 }
728 }
729
730 Ok(())
731 }
732
733 fn quantum_detection(&mut self, data: &Array1<f64>) -> Result<()> {
735 let n = data.len();
736 if n < 10 {
737 return Ok(()); }
739
740 let window_size = n / 10;
741
742 for t in window_size..(n - window_size) {
743 let before_window = data.slice(s![t.saturating_sub(window_size)..t]);
745 let after_window = data.slice(s![t..t + window_size]);
746
747 let mean_before = before_window.mean().unwrap_or(0.0);
748 let mean_after = after_window.mean().unwrap_or(0.0);
749 let std_before = before_window.std(1.0);
750 let std_after = after_window.std(1.0);
751
752 let mean_change = (mean_after - mean_before).abs() / (std_before + std_after + 1e-10);
754 let var_change = (std_after - std_before).abs() / (std_before + 1e-10);
755
756 let quantum_factor = if !self.detection_circuit.is_empty() {
758 let circuit_idx = t % self.detection_circuit.len();
759 let phase = self.detection_circuit[circuit_idx] * mean_change;
760 1.0 + 0.1 * phase.sin()
761 } else {
762 1.0
763 };
764
765 let change_score = (mean_change + var_change) * quantum_factor;
766
767 if change_score > self.threshold {
768 let changepoint = ChangePoint {
769 time_index: t,
770 confidence: change_score.min(1.0),
771 change_type: if mean_change > var_change {
772 ChangeType::LevelShift
773 } else {
774 ChangeType::VarianceChange
775 },
776 magnitude: change_score,
777 };
778
779 self.changepoints.push(changepoint);
780 }
781 }
782
783 Ok(())
784 }
785
786 fn cusum_detection(&mut self, data: &Array1<f64>) -> Result<()> {
788 let n = data.len();
789 let mean = data.mean().unwrap_or(0.0);
790 let std = data.std(1.0);
791
792 let mut cusum_pos = 0.0;
793 let mut cusum_neg = 0.0;
794 let threshold = 4.0 * std; for (t, &value) in data.iter().enumerate() {
797 let deviation = value - mean;
798
799 cusum_pos = (cusum_pos + deviation).max(0.0);
800 cusum_neg = (cusum_neg - deviation).max(0.0);
801
802 if cusum_pos > threshold || cusum_neg > threshold {
803 let changepoint = ChangePoint {
804 time_index: t,
805 confidence: (cusum_pos.max(cusum_neg) / threshold).min(1.0),
806 change_type: ChangeType::TrendChange,
807 magnitude: cusum_pos.max(cusum_neg),
808 };
809
810 self.changepoints.push(changepoint);
811
812 cusum_pos = 0.0;
814 cusum_neg = 0.0;
815 }
816 }
817
818 Ok(())
819 }
820
821 pub fn get_changepoints(&self) -> &[ChangePoint] {
823 &self.changepoints
824 }
825}
826
827impl QuantumResidualAnalyzer {
828 pub fn new(num_qubits: usize) -> Result<Self> {
830 let mut analysis_circuit = Vec::new();
831
832 for i in 0..num_qubits {
834 analysis_circuit.push(1.0); analysis_circuit.push(PI * i as f64 / num_qubits as f64); }
837
838 let residual_stats = ResidualStatistics {
839 mean: 0.0,
840 std: 1.0,
841 skewness: 0.0,
842 kurtosis: 3.0,
843 autocorrelation: Array1::zeros(10),
844 quantum_coherence: 0.0,
845 };
846
847 Ok(Self {
848 analysis_circuit,
849 anomaly_threshold: 2.0,
850 anomalies: Vec::new(),
851 residual_stats,
852 })
853 }
854
855 pub fn analyze_residuals(&mut self, residuals: &Array1<f64>) -> Result<()> {
857 self.update_residual_statistics(residuals)?;
859
860 self.detect_anomalies(residuals)?;
862
863 Ok(())
864 }
865
866 fn update_residual_statistics(&mut self, residuals: &Array1<f64>) -> Result<()> {
868 let n = residuals.len() as f64;
869
870 self.residual_stats.mean = residuals.mean().unwrap_or(0.0);
872 self.residual_stats.std = residuals.std(1.0);
873
874 let mean = self.residual_stats.mean;
876 let mut skewness_sum = 0.0;
877 let mut kurtosis_sum = 0.0;
878
879 for &value in residuals.iter() {
880 let deviation = value - mean;
881 skewness_sum += deviation.powi(3);
882 kurtosis_sum += deviation.powi(4);
883 }
884
885 let std_cubed = self.residual_stats.std.powi(3);
886 let std_fourth = self.residual_stats.std.powi(4);
887
888 if std_cubed > 1e-10 {
889 self.residual_stats.skewness = skewness_sum / (n * std_cubed);
890 }
891
892 if std_fourth > 1e-10 {
893 self.residual_stats.kurtosis = kurtosis_sum / (n * std_fourth);
894 }
895
896 self.calculate_autocorrelation(residuals)?;
898
899 self.residual_stats.quantum_coherence = self.calculate_quantum_coherence(residuals)?;
901
902 Ok(())
903 }
904
905 fn calculate_autocorrelation(&mut self, residuals: &Array1<f64>) -> Result<()> {
907 let n = residuals.len();
908 let max_lag = 10.min(n / 4);
909 let mut autocorr = Array1::zeros(max_lag);
910
911 let mean = residuals.mean().unwrap_or(0.0);
912 let variance = residuals.var(1.0);
913
914 for lag in 0..max_lag {
915 let mut sum = 0.0;
916 let mut count = 0;
917
918 for t in lag..n {
919 sum += (residuals[t] - mean) * (residuals[t - lag] - mean);
920 count += 1;
921 }
922
923 if count > 0 && variance > 1e-10 {
924 autocorr[lag] = sum / (count as f64 * variance);
925 }
926 }
927
928 self.residual_stats.autocorrelation = autocorr;
929 Ok(())
930 }
931
932 fn calculate_quantum_coherence(&self, residuals: &Array1<f64>) -> Result<f64> {
934 let n = residuals.len();
936 let n_bins = 10;
937
938 let min_val = residuals.iter().fold(f64::INFINITY, |a, &b| a.min(b));
939 let max_val = residuals.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
940 let range = max_val - min_val;
941
942 if range < 1e-10 {
943 return Ok(0.0);
944 }
945
946 let mut bin_counts = vec![0; n_bins];
947 for &value in residuals.iter() {
948 let bin_idx = ((value - min_val) / range * (n_bins - 1) as f64) as usize;
949 let bin_idx = bin_idx.min(n_bins - 1);
950 bin_counts[bin_idx] += 1;
951 }
952
953 let mut entropy = 0.0;
954 for &count in &bin_counts {
955 if count > 0 {
956 let prob = count as f64 / n as f64;
957 entropy -= prob * prob.ln();
958 }
959 }
960
961 let max_entropy = (n_bins as f64).ln();
963 let normalized_entropy = entropy / max_entropy;
964
965 Ok(1.0 - normalized_entropy) }
967
968 fn detect_anomalies(&mut self, residuals: &Array1<f64>) -> Result<()> {
970 self.anomalies.clear();
971
972 let mean = self.residual_stats.mean;
973 let std = self.residual_stats.std;
974 let threshold = self.anomaly_threshold * std;
975
976 for (t, &value) in residuals.iter().enumerate() {
977 let deviation = (value - mean).abs();
978
979 if deviation > threshold {
980 let anomaly_score = deviation / std;
981
982 let anomaly_type = if anomaly_score > 4.0 {
984 AnomalyType::Point
985 } else if self.is_contextual_anomaly(t, residuals) {
986 AnomalyType::Contextual
987 } else {
988 AnomalyType::Point
989 };
990
991 let anomaly = AnomalyPoint {
992 timestamp: t,
993 value,
994 anomaly_score,
995 anomaly_type,
996 };
997
998 self.anomalies.push(anomaly);
999 }
1000 }
1001
1002 Ok(())
1003 }
1004
1005 fn is_contextual_anomaly(&self, index: usize, residuals: &Array1<f64>) -> bool {
1007 let window_size = 5;
1008 let start = index.saturating_sub(window_size);
1009 let end = (index + window_size + 1).min(residuals.len());
1010
1011 if end - start < 3 {
1012 return false;
1013 }
1014
1015 let window = residuals.slice(s![start..end]);
1016 let local_mean = window.mean().unwrap_or(0.0);
1017 let local_std = window.std(1.0);
1018
1019 let global_mean = self.residual_stats.mean;
1020 let global_std = self.residual_stats.std;
1021
1022 let mean_diff = (local_mean - global_mean).abs() / global_std.max(1e-10);
1024 let std_ratio = local_std / global_std.max(1e-10);
1025
1026 mean_diff > 1.0 || std_ratio > 2.0 || std_ratio < 0.5
1027 }
1028
1029 pub fn get_statistics(&self) -> &ResidualStatistics {
1031 &self.residual_stats
1032 }
1033
1034 pub fn get_anomalies(&self) -> &[AnomalyPoint] {
1036 &self.anomalies
1037 }
1038}
1039
1040impl DecompositionCache {
1041 pub fn new(max_size: usize) -> Self {
1043 Self {
1044 cache: HashMap::new(),
1045 hits: 0,
1046 misses: 0,
1047 max_size,
1048 }
1049 }
1050
1051 pub fn get(&mut self, key: &str) -> Option<&DecompositionResult> {
1053 if let Some(result) = self.cache.get(key) {
1054 self.hits += 1;
1055 Some(result)
1056 } else {
1057 self.misses += 1;
1058 None
1059 }
1060 }
1061
1062 pub fn insert(&mut self, key: String, result: DecompositionResult) {
1064 if self.cache.len() >= self.max_size {
1065 if let Some(random_key) = self.cache.keys().next().cloned() {
1067 self.cache.remove(&random_key);
1068 }
1069 }
1070
1071 self.cache.insert(key, result);
1072 }
1073
1074 pub fn get_stats(&self) -> (usize, usize, f64) {
1076 let total = self.hits + self.misses;
1077 let hit_rate = if total > 0 {
1078 self.hits as f64 / total as f64
1079 } else {
1080 0.0
1081 };
1082 (self.hits, self.misses, hit_rate)
1083 }
1084}