quantrs2_device/noise_modeling_scirs2/
temporal.rs

1//! Temporal correlation noise modeling using SciRS2
2//!
3//! This module provides temporal correlation analysis of quantum noise,
4//! including autoregressive models, long-term memory analysis, and change point detection.
5
6use crate::{DeviceError, DeviceResult};
7use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
8use std::collections::HashMap;
9
10/// Temporal correlation model
11#[derive(Debug, Clone)]
12pub struct TemporalNoiseModel {
13    /// Autoregressive models for each noise source
14    pub ar_models: HashMap<String, ARModel>,
15    /// Long-term memory characteristics
16    pub long_memory: HashMap<String, LongMemoryModel>,
17    /// Non-stationary analysis
18    pub nonstationarity: HashMap<String, NonstationarityAnalysis>,
19    /// Change point detection
20    pub change_points: HashMap<String, Vec<ChangePoint>>,
21    /// Temporal clustering
22    pub temporal_clusters: HashMap<String, TemporalClusters>,
23}
24
25/// Autoregressive model
26#[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/// Long-term memory model
37#[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/// Non-stationarity analysis
47#[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/// Change point detection
57#[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/// Types of changes detected
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum ChangeType {
69    Mean,
70    Variance,
71    Distribution,
72    Correlation,
73}
74
75/// Temporal clustering
76#[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/// Cluster statistics
85#[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
94/// Temporal analysis engine
95pub struct TemporalAnalyzer {
96    max_ar_order: usize,
97    change_detection_sensitivity: f64,
98    clustering_algorithm: ClusteringAlgorithm,
99}
100
101/// Clustering algorithms for temporal analysis
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum ClusteringAlgorithm {
104    KMeans,
105    HierarchicalClustering,
106    DBSCAN,
107    GaussianMixture,
108}
109
110impl TemporalAnalyzer {
111    /// Create a new temporal analyzer
112    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    /// Fit autoregressive model to temporal data
121    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        // Find optimal AR order using AIC/BIC criteria
137        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    /// Analyze long-term memory characteristics
173    pub fn analyze_long_memory(&self, data: &ArrayView2<f64>) -> DeviceResult<LongMemoryModel> {
174        let flat_data: Vec<f64> = data.iter().copied().collect();
175
176        // Estimate Hurst exponent using R/S analysis
177        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        // Calculate confidence interval (simplified)
183        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    /// Test for non-stationarity
195    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        // Augmented Dickey-Fuller test (simplified)
213        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; // Critical value at 5% significance
218
219        // Simple trend detection
220        let trend_components = self.extract_trend(&flat_data)?;
221
222        // Change point detection
223        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    /// Detect change points in the data
235    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, // Would use actual timestamps in practice
247                    change_magnitude,
248                    change_type: ChangeType::Mean, // Simplified - would detect actual type
249                    confidence: 0.95,              // Would calculate actual confidence
250                });
251            }
252        }
253
254        Ok(change_points)
255    }
256
257    /// Cluster temporal patterns
258    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        // Simple k-means clustering (k=3)
275        let k = 3.min(n / 2);
276        let (cluster_labels, cluster_centers) = self.simple_kmeans(&flat_data, k)?;
277
278        // Calculate cluster statistics
279        let cluster_statistics =
280            self.calculate_cluster_statistics(&flat_data, &cluster_labels, k)?;
281
282        // Calculate transition matrix
283        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    // Helper methods
294
295    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        // Yule-Walker equations (simplified)
306        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        // Simple AR(1) estimation
318        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        // Estimate noise variance
333        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        // R/S analysis
435        let mean = data.iter().sum::<f64>() / n as f64;
436
437        // Calculate cumulative deviations
438        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        // Calculate range
444        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        // Calculate standard deviation
453        let variance = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
454        let std_dev = variance.sqrt();
455
456        // R/S ratio
457        let rs_ratio = if std_dev > 0.0 { range / std_dev } else { 1.0 };
458
459        // Hurst exponent estimation
460        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); // Default to stationary
473        }
474
475        // Simple Dickey-Fuller test
476        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        // Calculate regression coefficient
496        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        // Simple t-statistic (should be proper t-test)
506        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        // Simple linear trend
520        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        // Initialize centroids
606        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        // Simple k-means iterations
617        for _iter in 0..10 {
618            // Assign points to clusters
619            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            // Update centroids
634            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; // Simplified - would use actual time durations
670
671            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        // Count transitions
712        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        // Normalize rows
721        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            // Generate AR(1) process
751            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        // Test with random walk (should have H ≈ 0.5)
774        let test_data = Array2::from_shape_fn((50, 1), |(i, _)| {
775            (i as f64).sqrt() // Simple trend
776        });
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        // Create data with a clear change point
795        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        // Should detect the change point around index 25
804        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        // Create data with distinct patterns
815        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        // Create trending data (non-stationary)
834        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}