1use crate::{DeviceError, DeviceResult};
7use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone)]
12pub struct TemporalNoiseModel {
13 pub ar_models: HashMap<String, ARModel>,
15 pub long_memory: HashMap<String, LongMemoryModel>,
17 pub nonstationarity: HashMap<String, NonstationarityAnalysis>,
19 pub change_points: HashMap<String, Vec<ChangePoint>>,
21 pub temporal_clusters: HashMap<String, TemporalClusters>,
23}
24
25#[derive(Debug, Clone)]
27pub struct ARModel {
28 pub order: usize,
29 pub coefficients: Array1<f64>,
30 pub noise_variance: f64,
31 pub aic: f64,
32 pub bic: f64,
33 pub prediction_error: f64,
34}
35
36#[derive(Debug, Clone)]
38pub struct LongMemoryModel {
39 pub hurst_exponent: f64,
40 pub fractal_dimension: f64,
41 pub long_range_dependence: bool,
42 pub memory_parameter: f64,
43 pub confidence_interval: (f64, f64),
44}
45
46#[derive(Debug, Clone)]
48pub struct NonstationarityAnalysis {
49 pub is_stationary: bool,
50 pub test_statistics: HashMap<String, f64>,
51 pub change_point_locations: Vec<usize>,
52 pub trend_components: Array1<f64>,
53 pub seasonal_components: Option<Array1<f64>>,
54}
55
56#[derive(Debug, Clone)]
58pub struct ChangePoint {
59 pub location: usize,
60 pub timestamp: f64,
61 pub change_magnitude: f64,
62 pub change_type: ChangeType,
63 pub confidence: f64,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum ChangeType {
69 Mean,
70 Variance,
71 Distribution,
72 Correlation,
73}
74
75#[derive(Debug, Clone)]
77pub struct TemporalClusters {
78 pub cluster_labels: Array1<usize>,
79 pub cluster_centers: Array2<f64>,
80 pub cluster_statistics: Vec<ClusterStatistics>,
81 pub temporal_transitions: Array2<f64>,
82}
83
84#[derive(Debug, Clone)]
86pub struct ClusterStatistics {
87 pub cluster_id: usize,
88 pub size: usize,
89 pub duration: f64,
90 pub stability: f64,
91 pub characteristics: HashMap<String, f64>,
92}
93
94pub struct TemporalAnalyzer {
96 max_ar_order: usize,
97 change_detection_sensitivity: f64,
98 clustering_algorithm: ClusteringAlgorithm,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum ClusteringAlgorithm {
104 KMeans,
105 HierarchicalClustering,
106 DBSCAN,
107 GaussianMixture,
108}
109
110impl TemporalAnalyzer {
111 pub const fn new(max_ar_order: usize, change_detection_sensitivity: f64) -> Self {
113 Self {
114 max_ar_order,
115 change_detection_sensitivity,
116 clustering_algorithm: ClusteringAlgorithm::KMeans,
117 }
118 }
119
120 pub fn fit_ar_model(&self, data: &ArrayView2<f64>) -> DeviceResult<ARModel> {
122 let flat_data: Vec<f64> = data.iter().copied().collect();
123 let n = flat_data.len();
124
125 if n < self.max_ar_order + 1 {
126 return Ok(ARModel {
127 order: 0,
128 coefficients: Array1::zeros(0),
129 noise_variance: 1.0,
130 aic: f64::INFINITY,
131 bic: f64::INFINITY,
132 prediction_error: 1.0,
133 });
134 }
135
136 let mut best_order = 1;
138 let mut best_aic = f64::INFINITY;
139 let mut best_coefficients = Array1::zeros(1);
140 let mut best_noise_variance = 1.0;
141
142 for order in 1..=self.max_ar_order.min(n / 4) {
143 let (coefficients, noise_variance) = self.estimate_ar_parameters(&flat_data, order)?;
144 let aic = self.calculate_aic(&flat_data, &coefficients, noise_variance, order);
145
146 if aic < best_aic {
147 best_aic = aic;
148 best_order = order;
149 best_coefficients = coefficients;
150 best_noise_variance = noise_variance;
151 }
152 }
153
154 let bic = self.calculate_bic(
155 &flat_data,
156 &best_coefficients,
157 best_noise_variance,
158 best_order,
159 );
160 let prediction_error = self.calculate_prediction_error(&flat_data, &best_coefficients)?;
161
162 Ok(ARModel {
163 order: best_order,
164 coefficients: best_coefficients,
165 noise_variance: best_noise_variance,
166 aic: best_aic,
167 bic,
168 prediction_error,
169 })
170 }
171
172 pub fn analyze_long_memory(&self, data: &ArrayView2<f64>) -> DeviceResult<LongMemoryModel> {
174 let flat_data: Vec<f64> = data.iter().copied().collect();
175
176 let hurst_exponent = self.estimate_hurst_exponent(&flat_data)?;
178 let fractal_dimension = 2.0 - hurst_exponent;
179 let long_range_dependence = hurst_exponent > 0.5;
180 let memory_parameter = hurst_exponent - 0.5;
181
182 let confidence_interval = (hurst_exponent - 0.1, hurst_exponent + 0.1);
184
185 Ok(LongMemoryModel {
186 hurst_exponent,
187 fractal_dimension,
188 long_range_dependence,
189 memory_parameter,
190 confidence_interval,
191 })
192 }
193
194 pub fn test_nonstationarity(
196 &self,
197 data: &ArrayView2<f64>,
198 ) -> DeviceResult<NonstationarityAnalysis> {
199 let flat_data: Vec<f64> = data.iter().copied().collect();
200 let n = flat_data.len();
201
202 if n < 10 {
203 return Ok(NonstationarityAnalysis {
204 is_stationary: true,
205 test_statistics: HashMap::new(),
206 change_point_locations: vec![],
207 trend_components: Array1::zeros(n),
208 seasonal_components: None,
209 });
210 }
211
212 let mut test_statistics = HashMap::new();
214 let adf_statistic = self.augmented_dickey_fuller_test(&flat_data)?;
215 test_statistics.insert("ADF".to_string(), adf_statistic);
216
217 let is_stationary = adf_statistic < -2.86; let trend_components = self.extract_trend(&flat_data)?;
221
222 let change_point_locations = self.detect_change_points_simple(&flat_data)?;
224
225 Ok(NonstationarityAnalysis {
226 is_stationary,
227 test_statistics,
228 change_point_locations,
229 trend_components,
230 seasonal_components: None,
231 })
232 }
233
234 pub fn detect_change_points(&self, data: &ArrayView2<f64>) -> DeviceResult<Vec<ChangePoint>> {
236 let flat_data: Vec<f64> = data.iter().copied().collect();
237 let change_point_locations = self.detect_change_points_simple(&flat_data)?;
238
239 let mut change_points = Vec::new();
240 for &location in &change_point_locations {
241 if location < flat_data.len() {
242 let change_magnitude = self.estimate_change_magnitude(&flat_data, location)?;
243
244 change_points.push(ChangePoint {
245 location,
246 timestamp: location as f64, change_magnitude,
248 change_type: ChangeType::Mean, confidence: 0.95, });
251 }
252 }
253
254 Ok(change_points)
255 }
256
257 pub fn cluster_temporal_patterns(
259 &self,
260 data: &ArrayView2<f64>,
261 ) -> DeviceResult<TemporalClusters> {
262 let flat_data: Vec<f64> = data.iter().copied().collect();
263 let n = flat_data.len();
264
265 if n < 4 {
266 return Ok(TemporalClusters {
267 cluster_labels: Array1::zeros(n),
268 cluster_centers: Array2::zeros((1, 2)),
269 cluster_statistics: vec![],
270 temporal_transitions: Array2::zeros((1, 1)),
271 });
272 }
273
274 let k = 3.min(n / 2);
276 let (cluster_labels, cluster_centers) = self.simple_kmeans(&flat_data, k)?;
277
278 let cluster_statistics =
280 self.calculate_cluster_statistics(&flat_data, &cluster_labels, k)?;
281
282 let temporal_transitions = self.calculate_transition_matrix(&cluster_labels, k)?;
284
285 Ok(TemporalClusters {
286 cluster_labels: Array1::from_vec(cluster_labels),
287 cluster_centers,
288 cluster_statistics,
289 temporal_transitions,
290 })
291 }
292
293 fn estimate_ar_parameters(
296 &self,
297 data: &[f64],
298 order: usize,
299 ) -> DeviceResult<(Array1<f64>, f64)> {
300 let n = data.len();
301 if n <= order {
302 return Ok((Array1::zeros(order), 1.0));
303 }
304
305 let mut coefficients = Array1::zeros(order);
307 let mut sum_y = 0.0;
308 let mut sum_yy = 0.0;
309
310 for i in order..n {
311 sum_y += data[i];
312 sum_yy += data[i] * data[i];
313 }
314
315 let mean_y = sum_y / (n - order) as f64;
316
317 if order >= 1 && n > order + 1 {
319 let mut numerator = 0.0;
320 let mut denominator = 0.0;
321
322 for i in order..n - 1 {
323 numerator += (data[i] - mean_y) * (data[i + 1] - mean_y);
324 denominator += (data[i] - mean_y).powi(2);
325 }
326
327 if denominator > 1e-10 {
328 coefficients[0] = numerator / denominator;
329 }
330 }
331
332 let mut residual_sum = 0.0;
334 let mut count = 0;
335
336 for i in order..n {
337 let mut predicted = mean_y;
338 for j in 0..order.min(coefficients.len()) {
339 if i > j {
340 predicted += coefficients[j] * (data[i - j - 1] - mean_y);
341 }
342 }
343 residual_sum += (data[i] - predicted).powi(2);
344 count += 1;
345 }
346
347 let noise_variance = if count > 0 {
348 residual_sum / count as f64
349 } else {
350 1.0
351 };
352
353 Ok((coefficients, noise_variance))
354 }
355
356 fn calculate_aic(
357 &self,
358 data: &[f64],
359 coefficients: &Array1<f64>,
360 noise_variance: f64,
361 order: usize,
362 ) -> f64 {
363 let n = data.len() as f64;
364 let k = order as f64;
365
366 if noise_variance <= 0.0 {
367 return f64::INFINITY;
368 }
369
370 let log_likelihood =
371 -0.5 * n * (noise_variance.ln() + 1.0 + (2.0 * std::f64::consts::PI).ln());
372 2.0f64.mul_add(k, -(2.0 * log_likelihood))
373 }
374
375 fn calculate_bic(
376 &self,
377 data: &[f64],
378 coefficients: &Array1<f64>,
379 noise_variance: f64,
380 order: usize,
381 ) -> f64 {
382 let n = data.len() as f64;
383 let k = order as f64;
384
385 if noise_variance <= 0.0 {
386 return f64::INFINITY;
387 }
388
389 let log_likelihood =
390 -0.5 * n * (noise_variance.ln() + 1.0 + (2.0 * std::f64::consts::PI).ln());
391 k.mul_add(n.ln(), -(2.0 * log_likelihood))
392 }
393
394 fn calculate_prediction_error(
395 &self,
396 data: &[f64],
397 coefficients: &Array1<f64>,
398 ) -> DeviceResult<f64> {
399 let n = data.len();
400 let order = coefficients.len();
401
402 if n <= order {
403 return Ok(1.0);
404 }
405
406 let mean = data.iter().sum::<f64>() / n as f64;
407 let mut error_sum = 0.0;
408 let mut count = 0;
409
410 for i in order..n {
411 let mut prediction = mean;
412 for j in 0..order {
413 if i > j {
414 prediction += coefficients[j] * (data[i - j - 1] - mean);
415 }
416 }
417 error_sum += (data[i] - prediction).powi(2);
418 count += 1;
419 }
420
421 Ok(if count > 0 {
422 (error_sum / count as f64).sqrt()
423 } else {
424 1.0
425 })
426 }
427
428 fn estimate_hurst_exponent(&self, data: &[f64]) -> DeviceResult<f64> {
429 let n = data.len();
430 if n < 4 {
431 return Ok(0.5);
432 }
433
434 let mean = data.iter().sum::<f64>() / n as f64;
436
437 let mut cumulative_deviations = vec![0.0; n];
439 for i in 1..n {
440 cumulative_deviations[i] = cumulative_deviations[i - 1] + (data[i] - mean);
441 }
442
443 let max_deviation = cumulative_deviations
445 .iter()
446 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
447 let min_deviation = cumulative_deviations
448 .iter()
449 .fold(f64::INFINITY, |a, &b| a.min(b));
450 let range = max_deviation - min_deviation;
451
452 let variance = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
454 let std_dev = variance.sqrt();
455
456 let rs_ratio = if std_dev > 0.0 { range / std_dev } else { 1.0 };
458
459 let hurst = if rs_ratio > 0.0 {
461 rs_ratio.log(n as f64)
462 } else {
463 0.5
464 };
465
466 Ok(hurst.clamp(0.0, 1.0))
467 }
468
469 fn augmented_dickey_fuller_test(&self, data: &[f64]) -> DeviceResult<f64> {
470 let n = data.len();
471 if n < 3 {
472 return Ok(-3.0); }
474
475 let mut sum_diff = 0.0;
477 let mut sum_lag = 0.0;
478 let mut sum_diff_lag = 0.0;
479 let mut sum_lag_sq = 0.0;
480
481 for i in 1..n {
482 let diff = data[i] - data[i - 1];
483 let lag = data[i - 1];
484
485 sum_diff += diff;
486 sum_lag += lag;
487 sum_diff_lag += diff * lag;
488 sum_lag_sq += lag * lag;
489 }
490
491 let n_obs = (n - 1) as f64;
492 let mean_diff = sum_diff / n_obs;
493 let mean_lag = sum_lag / n_obs;
494
495 let numerator = (n_obs * mean_diff).mul_add(-mean_lag, sum_diff_lag);
497 let denominator = (n_obs * mean_lag).mul_add(-mean_lag, sum_lag_sq);
498
499 let beta = if denominator.abs() > 1e-10 {
500 numerator / denominator
501 } else {
502 0.0
503 };
504
505 let t_stat = if beta.abs() < 1.0 { beta * 10.0 } else { -3.0 };
507
508 Ok(t_stat)
509 }
510
511 fn extract_trend(&self, data: &[f64]) -> DeviceResult<Array1<f64>> {
512 let n = data.len();
513 let mut trend = Array1::zeros(n);
514
515 if n < 2 {
516 return Ok(trend);
517 }
518
519 let x_mean = (n - 1) as f64 / 2.0;
521 let y_mean = data.iter().sum::<f64>() / n as f64;
522
523 let mut numerator = 0.0;
524 let mut denominator = 0.0;
525
526 for (i, &y) in data.iter().enumerate() {
527 let x = i as f64;
528 numerator += (x - x_mean) * (y - y_mean);
529 denominator += (x - x_mean).powi(2);
530 }
531
532 let slope = if denominator > 1e-10 {
533 numerator / denominator
534 } else {
535 0.0
536 };
537 let intercept = y_mean - slope * x_mean;
538
539 for i in 0..n {
540 trend[i] = intercept + slope * i as f64;
541 }
542
543 Ok(trend)
544 }
545
546 fn detect_change_points_simple(&self, data: &[f64]) -> DeviceResult<Vec<usize>> {
547 let n = data.len();
548 let mut change_points = Vec::new();
549
550 if n < 6 {
551 return Ok(change_points);
552 }
553
554 let window_size = (n / 10).max(3);
555
556 for i in window_size..(n - window_size) {
557 let before_mean = data[i - window_size..i].iter().sum::<f64>() / window_size as f64;
558 let after_mean = data[i..i + window_size].iter().sum::<f64>() / window_size as f64;
559
560 let change_magnitude = (after_mean - before_mean).abs();
561 let threshold = self.change_detection_sensitivity
562 * data.iter().map(|&x| x.abs()).sum::<f64>()
563 / n as f64;
564
565 if change_magnitude > threshold {
566 change_points.push(i);
567 }
568 }
569
570 Ok(change_points)
571 }
572
573 fn estimate_change_magnitude(&self, data: &[f64], location: usize) -> DeviceResult<f64> {
574 let n = data.len();
575 if location >= n || location == 0 {
576 return Ok(0.0);
577 }
578
579 let window_size = (n / 20).max(2).min(location).min(n - location);
580
581 let before_start = location.saturating_sub(window_size);
582 let after_end = (location + window_size).min(n);
583
584 let before_mean = if before_start < location {
585 data[before_start..location].iter().sum::<f64>() / (location - before_start) as f64
586 } else {
587 data[location]
588 };
589
590 let after_mean = if location < after_end {
591 data[location..after_end].iter().sum::<f64>() / (after_end - location) as f64
592 } else {
593 data[location]
594 };
595
596 Ok((after_mean - before_mean).abs())
597 }
598
599 fn simple_kmeans(&self, data: &[f64], k: usize) -> DeviceResult<(Vec<usize>, Array2<f64>)> {
600 let n = data.len();
601 if k == 0 || n == 0 {
602 return Ok((vec![], Array2::zeros((0, 1))));
603 }
604
605 let mut centroids = Array2::zeros((k, 1));
607 let data_min = data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
608 let data_max = data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
609
610 for i in 0..k {
611 centroids[[i, 0]] = (i as f64 / k as f64).mul_add(data_max - data_min, data_min);
612 }
613
614 let mut labels = vec![0; n];
615
616 for _iter in 0..10 {
618 for (i, &point) in data.iter().enumerate() {
620 let mut best_cluster = 0;
621 let mut min_distance = f64::INFINITY;
622
623 for cluster in 0..k {
624 let distance = (point - centroids[[cluster, 0]]).abs();
625 if distance < min_distance {
626 min_distance = distance;
627 best_cluster = cluster;
628 }
629 }
630 labels[i] = best_cluster;
631 }
632
633 for cluster in 0..k {
635 let cluster_points: Vec<f64> = data
636 .iter()
637 .enumerate()
638 .filter(|(i, _)| labels[*i] == cluster)
639 .map(|(_, &value)| value)
640 .collect();
641
642 if !cluster_points.is_empty() {
643 centroids[[cluster, 0]] =
644 cluster_points.iter().sum::<f64>() / cluster_points.len() as f64;
645 }
646 }
647 }
648
649 Ok((labels, centroids))
650 }
651
652 fn calculate_cluster_statistics(
653 &self,
654 data: &[f64],
655 labels: &[usize],
656 k: usize,
657 ) -> DeviceResult<Vec<ClusterStatistics>> {
658 let mut statistics = Vec::new();
659
660 for cluster_id in 0..k {
661 let cluster_data: Vec<f64> = data
662 .iter()
663 .enumerate()
664 .filter(|(i, _)| labels[*i] == cluster_id)
665 .map(|(_, &value)| value)
666 .collect();
667
668 let size = cluster_data.len();
669 let duration = size as f64; let stability = if size > 1 {
672 let mean = cluster_data.iter().sum::<f64>() / size as f64;
673 let variance = cluster_data
674 .iter()
675 .map(|&x| (x - mean).powi(2))
676 .sum::<f64>()
677 / size as f64;
678 1.0 / (1.0 + variance.sqrt())
679 } else {
680 1.0
681 };
682
683 let mut characteristics = HashMap::new();
684 if !cluster_data.is_empty() {
685 characteristics.insert(
686 "mean".to_string(),
687 cluster_data.iter().sum::<f64>() / size as f64,
688 );
689 characteristics.insert("size".to_string(), size as f64);
690 }
691
692 statistics.push(ClusterStatistics {
693 cluster_id,
694 size,
695 duration,
696 stability,
697 characteristics,
698 });
699 }
700
701 Ok(statistics)
702 }
703
704 fn calculate_transition_matrix(&self, labels: &[usize], k: usize) -> DeviceResult<Array2<f64>> {
705 let mut transitions = Array2::zeros((k, k));
706
707 if labels.len() < 2 {
708 return Ok(transitions);
709 }
710
711 for i in 0..labels.len() - 1 {
713 let from = labels[i];
714 let to = labels[i + 1];
715 if from < k && to < k {
716 transitions[[from, to]] += 1.0;
717 }
718 }
719
720 for i in 0..k {
722 let row_sum: f64 = (0..k).map(|j| transitions[[i, j]]).sum();
723 if row_sum > 0.0 {
724 for j in 0..k {
725 transitions[[i, j]] /= row_sum;
726 }
727 }
728 }
729
730 Ok(transitions)
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use scirs2_core::ndarray::Array2;
738
739 #[test]
740 fn test_temporal_analyzer_creation() {
741 let analyzer = TemporalAnalyzer::new(5, 0.1);
742 assert_eq!(analyzer.max_ar_order, 5);
743 assert_eq!(analyzer.change_detection_sensitivity, 0.1);
744 }
745
746 #[test]
747 fn test_ar_model_fitting() {
748 let analyzer = TemporalAnalyzer::new(3, 0.1);
749 let test_data = Array2::from_shape_fn((100, 1), |(i, _)| {
750 if i == 0 {
752 0.0
753 } else {
754 0.5 * (i as f64) + 0.1 * (i as f64).sin()
755 }
756 });
757
758 let ar_model = analyzer
759 .fit_ar_model(&test_data.view())
760 .expect("AR model fitting should succeed");
761
762 assert!(ar_model.order > 0);
763 assert!(!ar_model.coefficients.is_empty());
764 assert!(ar_model.noise_variance > 0.0);
765 assert!(ar_model.aic.is_finite());
766 assert!(ar_model.bic.is_finite());
767 }
768
769 #[test]
770 fn test_hurst_exponent_estimation() {
771 let analyzer = TemporalAnalyzer::new(3, 0.1);
772
773 let test_data = Array2::from_shape_fn((50, 1), |(i, _)| {
775 (i as f64).sqrt() });
777
778 let long_memory = analyzer
779 .analyze_long_memory(&test_data.view())
780 .expect("Long memory analysis should succeed");
781
782 assert!(long_memory.hurst_exponent >= 0.0);
783 assert!(long_memory.hurst_exponent <= 1.0);
784 assert_eq!(
785 long_memory.fractal_dimension,
786 2.0 - long_memory.hurst_exponent
787 );
788 }
789
790 #[test]
791 fn test_change_point_detection() {
792 let analyzer = TemporalAnalyzer::new(3, 0.5);
793
794 let mut test_data_vec = vec![1.0; 25];
796 test_data_vec.extend(vec![5.0; 25]);
797 let test_data = Array2::from_shape_fn((50, 1), |(i, _)| test_data_vec[i]);
798
799 let change_points = analyzer
800 .detect_change_points(&test_data.view())
801 .expect("Change point detection should succeed");
802
803 assert!(!change_points.is_empty());
805 assert!(change_points
806 .iter()
807 .any(|cp| cp.location > 20 && cp.location < 30));
808 }
809
810 #[test]
811 fn test_temporal_clustering() {
812 let analyzer = TemporalAnalyzer::new(3, 0.1);
813
814 let test_data = Array2::from_shape_fn((20, 1), |(i, _)| if i < 10 { 1.0 } else { 5.0 });
816
817 let clusters = analyzer
818 .cluster_temporal_patterns(&test_data.view())
819 .expect("Temporal clustering should succeed");
820
821 assert_eq!(clusters.cluster_labels.len(), 20);
822 assert!(clusters.cluster_centers.nrows() > 0);
823 assert_eq!(
824 clusters.temporal_transitions.nrows(),
825 clusters.temporal_transitions.ncols()
826 );
827 }
828
829 #[test]
830 fn test_nonstationarity_analysis() {
831 let analyzer = TemporalAnalyzer::new(3, 0.1);
832
833 let test_data = Array2::from_shape_fn((30, 1), |(i, _)| i as f64);
835
836 let nonstationarity = analyzer
837 .test_nonstationarity(&test_data.view())
838 .expect("Nonstationarity test should succeed");
839
840 assert!(nonstationarity.test_statistics.contains_key("ADF"));
841 assert_eq!(nonstationarity.trend_components.len(), 30);
842 }
843}