Skip to main content

trustformers_debug/model_diagnostics/
analytics.rs

1//! Advanced analytics for model behavior analysis.
2//!
3//! This module provides sophisticated analytical capabilities including
4//! clustering analysis, temporal dynamics monitoring, representation
5//! stability assessment, and multi-dimensional data visualization.
6// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
7// are retained for the data model, serialization completeness, and future consumers that
8// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
9#![allow(dead_code)]
10
11use anyhow::Result;
12use std::collections::{HashMap, VecDeque};
13
14use super::types::{
15    ActivationHeatmap, ClusteringResults, DriftInfo, HiddenStateAnalysis, LayerActivationStats,
16    ModelPerformanceMetrics, RepresentationStability, TemporalDynamics,
17};
18
19/// Advanced analytics engine for model behavior analysis.
20#[derive(Debug)]
21pub struct AdvancedAnalytics {
22    /// Analytics configuration
23    config: AnalyticsConfig,
24    /// Historical hidden state data
25    hidden_states_history: VecDeque<HiddenStateData>,
26    /// Performance correlation data
27    performance_correlations: HashMap<String, CorrelationData>,
28    /// Temporal analysis cache
29    temporal_analysis_cache: TemporalAnalysisCache,
30    /// Clustering analysis results
31    clustering_results_cache: HashMap<String, ClusteringResults>,
32}
33
34/// Configuration for advanced analytics.
35#[derive(Debug, Clone)]
36pub struct AnalyticsConfig {
37    /// Maximum number of historical samples to retain
38    pub max_history_samples: usize,
39    /// Minimum samples required for clustering analysis
40    pub min_clustering_samples: usize,
41    /// Number of clusters for k-means analysis
42    pub default_num_clusters: usize,
43    /// Window size for temporal analysis
44    pub temporal_analysis_window: usize,
45    /// Drift detection sensitivity
46    pub drift_detection_sensitivity: f64,
47    /// Correlation analysis threshold
48    pub correlation_threshold: f64,
49    /// Enable advanced visualizations
50    pub enable_visualizations: bool,
51}
52
53/// Hidden state data for analysis.
54#[derive(Debug, Clone)]
55pub struct HiddenStateData {
56    /// Layer name
57    pub layer_name: String,
58    /// Hidden state vectors
59    pub hidden_states: Vec<Vec<f64>>,
60    /// Corresponding labels or metadata
61    pub labels: Option<Vec<String>>,
62    /// Timestamp of collection
63    pub timestamp: chrono::DateTime<chrono::Utc>,
64    /// Training step when collected
65    pub training_step: usize,
66}
67
68/// Correlation analysis data.
69#[derive(Debug, Clone)]
70pub struct CorrelationData {
71    /// Metric name
72    pub metric_name: String,
73    /// Historical values
74    pub values: VecDeque<f64>,
75    /// Correlations with other metrics
76    pub correlations: HashMap<String, f64>,
77    /// Last update timestamp
78    pub last_updated: chrono::DateTime<chrono::Utc>,
79}
80
81/// Temporal analysis cache for performance optimization.
82#[derive(Debug, Clone)]
83pub struct TemporalAnalysisCache {
84    /// Cached drift detection results
85    pub drift_results: HashMap<String, DriftInfo>,
86    /// Cached temporal consistency scores
87    pub consistency_scores: HashMap<String, f64>,
88    /// Cached stability windows
89    pub stability_windows: HashMap<String, Vec<(usize, usize)>>,
90    /// Last analysis timestamp
91    pub last_analysis: chrono::DateTime<chrono::Utc>,
92}
93
94/// Advanced clustering analysis parameters.
95#[derive(Debug, Clone)]
96pub struct ClusteringParameters {
97    /// Number of clusters
98    pub num_clusters: usize,
99    /// Maximum iterations for k-means
100    pub max_iterations: usize,
101    /// Convergence tolerance
102    pub tolerance: f64,
103    /// Random seed for reproducibility
104    pub random_seed: Option<u64>,
105    /// Distance metric to use
106    pub distance_metric: DistanceMetric,
107}
108
109/// Distance metrics for clustering.
110#[derive(Debug, Clone)]
111pub enum DistanceMetric {
112    /// Euclidean distance
113    Euclidean,
114    /// Manhattan distance
115    Manhattan,
116    /// Cosine similarity
117    Cosine,
118    /// Minkowski distance with parameter p
119    Minkowski { p: f64 },
120}
121
122/// Dimensionality reduction parameters.
123#[derive(Debug, Clone)]
124pub struct DimensionalityReductionParams {
125    /// Target dimensions
126    pub target_dimensions: usize,
127    /// Reduction method
128    pub method: ReductionMethod,
129    /// Preserve variance ratio
130    pub preserve_variance_ratio: f64,
131}
132
133/// Dimensionality reduction methods.
134#[derive(Debug, Clone)]
135pub enum ReductionMethod {
136    /// Principal Component Analysis
137    PCA,
138    /// t-SNE
139    TSNE { perplexity: f64 },
140    /// UMAP
141    UMAP { n_neighbors: usize, min_dist: f64 },
142}
143
144/// Visualization generation parameters.
145#[derive(Debug, Clone)]
146pub struct VisualizationParams {
147    /// Output dimensions (width, height)
148    pub dimensions: (usize, usize),
149    /// Color scheme
150    pub color_scheme: ColorScheme,
151    /// Include annotations
152    pub include_annotations: bool,
153    /// Export format
154    pub export_format: ExportFormat,
155}
156
157/// Color schemes for visualizations.
158#[derive(Debug, Clone)]
159pub enum ColorScheme {
160    /// Viridis color scale
161    Viridis,
162    /// Plasma color scale
163    Plasma,
164    /// Inferno color scale
165    Inferno,
166    /// Custom color map
167    Custom(Vec<(f64, f64, f64)>),
168}
169
170/// Export formats for visualizations.
171#[derive(Debug, Clone)]
172pub enum ExportFormat {
173    /// PNG image
174    PNG,
175    /// SVG vector
176    SVG,
177    /// JSON data
178    JSON,
179    /// CSV data
180    CSV,
181}
182
183/// Statistical analysis results.
184#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
185pub struct StatisticalAnalysis {
186    /// Mean values
187    pub means: Vec<f64>,
188    /// Standard deviations
189    pub std_devs: Vec<f64>,
190    /// Correlation matrix
191    pub correlation_matrix: Vec<Vec<f64>>,
192    /// Principal components
193    pub principal_components: Vec<Vec<f64>>,
194    /// Explained variance ratios
195    pub explained_variance_ratios: Vec<f64>,
196    /// Statistical significance tests
197    pub significance_tests: Vec<SignificanceTest>,
198}
199
200/// Statistical significance test result.
201#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
202pub struct SignificanceTest {
203    /// Test name
204    pub test_name: String,
205    /// Test statistic
206    pub statistic: f64,
207    /// P-value
208    pub p_value: f64,
209    /// Degrees of freedom
210    pub degrees_of_freedom: Option<usize>,
211    /// Confidence interval
212    pub confidence_interval: Option<(f64, f64)>,
213}
214
215/// Anomaly detection results.
216#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
217pub struct AnomalyDetectionResults {
218    /// Detected anomalies
219    pub anomalies: Vec<Anomaly>,
220    /// Anomaly scores for all data points
221    pub anomaly_scores: Vec<f64>,
222    /// Detection threshold used
223    pub threshold: f64,
224    /// Detection method
225    pub method: AnomalyDetectionMethod,
226}
227
228/// Individual anomaly information.
229#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
230pub struct Anomaly {
231    /// Index in dataset
232    pub index: usize,
233    /// Anomaly score
234    pub score: f64,
235    /// Timestamp
236    pub timestamp: chrono::DateTime<chrono::Utc>,
237    /// Additional context
238    pub context: HashMap<String, String>,
239}
240
241/// Anomaly detection methods.
242#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
243pub enum AnomalyDetectionMethod {
244    /// Isolation Forest
245    IsolationForest { n_trees: usize },
246    /// Local Outlier Factor
247    LocalOutlierFactor { n_neighbors: usize },
248    /// One-Class SVM
249    OneClassSVM { nu: f64 },
250    /// Statistical threshold
251    StatisticalThreshold { n_std: f64 },
252}
253
254impl Default for AnalyticsConfig {
255    fn default() -> Self {
256        Self {
257            max_history_samples: 10000,
258            min_clustering_samples: 50,
259            default_num_clusters: 8,
260            temporal_analysis_window: 100,
261            drift_detection_sensitivity: 0.05,
262            correlation_threshold: 0.7,
263            enable_visualizations: true,
264        }
265    }
266}
267
268impl Default for ClusteringParameters {
269    fn default() -> Self {
270        Self {
271            num_clusters: 8,
272            max_iterations: 100,
273            tolerance: 1e-4,
274            random_seed: Some(42),
275            distance_metric: DistanceMetric::Euclidean,
276        }
277    }
278}
279
280impl AdvancedAnalytics {
281    /// Create a new advanced analytics engine.
282    pub fn new() -> Self {
283        Self {
284            config: AnalyticsConfig::default(),
285            hidden_states_history: VecDeque::new(),
286            performance_correlations: HashMap::new(),
287            temporal_analysis_cache: TemporalAnalysisCache::new(),
288            clustering_results_cache: HashMap::new(),
289        }
290    }
291
292    /// Create analytics engine with custom configuration.
293    pub fn with_config(config: AnalyticsConfig) -> Self {
294        Self {
295            config,
296            hidden_states_history: VecDeque::new(),
297            performance_correlations: HashMap::new(),
298            temporal_analysis_cache: TemporalAnalysisCache::new(),
299            clustering_results_cache: HashMap::new(),
300        }
301    }
302
303    /// Record hidden state data for analysis.
304    pub fn record_hidden_states(&mut self, hidden_states: HiddenStateData) {
305        self.hidden_states_history.push_back(hidden_states);
306
307        while self.hidden_states_history.len() > self.config.max_history_samples {
308            self.hidden_states_history.pop_front();
309        }
310    }
311
312    /// Record performance metrics for correlation analysis.
313    pub fn record_performance_metrics(&mut self, metrics: &ModelPerformanceMetrics) {
314        self.update_correlation_data("loss", metrics.loss);
315        self.update_correlation_data("throughput", metrics.throughput_samples_per_sec);
316        self.update_correlation_data("memory_usage", metrics.memory_usage_mb);
317
318        if let Some(accuracy) = metrics.accuracy {
319            self.update_correlation_data("accuracy", accuracy);
320        }
321
322        if let Some(gpu_util) = metrics.gpu_utilization {
323            self.update_correlation_data("gpu_utilization", gpu_util);
324        }
325    }
326
327    /// Perform comprehensive hidden state analysis.
328    pub fn analyze_hidden_states(&self, layer_name: &str) -> Result<HiddenStateAnalysis> {
329        let layer_data: Vec<_> = self
330            .hidden_states_history
331            .iter()
332            .filter(|data| data.layer_name == layer_name)
333            .collect();
334
335        if layer_data.is_empty() {
336            return Err(anyhow::anyhow!(
337                "No hidden state data available for layer: {}",
338                layer_name
339            ));
340        }
341
342        // Extract all hidden states for this layer
343        let all_states: Vec<Vec<f64>> =
344            layer_data.iter().flat_map(|data| data.hidden_states.iter()).cloned().collect();
345
346        if all_states.is_empty() {
347            return Err(anyhow::anyhow!(
348                "No hidden states found for layer: {}",
349                layer_name
350            ));
351        }
352
353        let dimensionality = all_states[0].len();
354
355        // Perform clustering analysis
356        let clustering_results = self.perform_clustering_analysis(&all_states)?;
357
358        // Analyze temporal dynamics
359        let temporal_dynamics = self.analyze_temporal_dynamics(&layer_data)?;
360
361        // Assess representation stability
362        let representation_stability = self.assess_representation_stability(&all_states)?;
363
364        // Calculate information content
365        let information_content = self.calculate_information_content(&all_states)?;
366
367        Ok(HiddenStateAnalysis {
368            dimensionality,
369            information_content,
370            clustering_results,
371            temporal_dynamics,
372            representation_stability,
373        })
374    }
375
376    /// Perform clustering analysis on hidden states.
377    pub fn perform_clustering_analysis(&self, data: &[Vec<f64>]) -> Result<ClusteringResults> {
378        if data.len() < self.config.min_clustering_samples {
379            return Err(anyhow::anyhow!("Insufficient data for clustering analysis"));
380        }
381
382        let params = ClusteringParameters::default();
383        let num_clusters = params.num_clusters.min(data.len() / 2);
384
385        // Simple k-means clustering implementation
386        let mut cluster_centers = self.initialize_cluster_centers(data, num_clusters)?;
387        let mut cluster_assignments = vec![0; data.len()];
388
389        for _iteration in 0..params.max_iterations {
390            // Assign points to nearest cluster
391            let mut new_assignments = vec![0; data.len()];
392            for (i, point) in data.iter().enumerate() {
393                let mut best_distance = f64::INFINITY;
394                let mut best_cluster = 0;
395
396                for (j, center) in cluster_centers.iter().enumerate() {
397                    let distance =
398                        self.calculate_distance(point, center, &params.distance_metric)?;
399                    if distance < best_distance {
400                        best_distance = distance;
401                        best_cluster = j;
402                    }
403                }
404                new_assignments[i] = best_cluster;
405            }
406
407            // Check for convergence
408            if new_assignments == cluster_assignments {
409                break;
410            }
411            cluster_assignments = new_assignments;
412
413            // Update cluster centers
414            cluster_centers =
415                self.update_cluster_centers(data, &cluster_assignments, num_clusters)?;
416        }
417
418        // Calculate silhouette score
419        let silhouette_score =
420            self.calculate_silhouette_score(data, &cluster_assignments, &cluster_centers)?;
421
422        // Calculate inertia
423        let inertia = self.calculate_inertia(data, &cluster_assignments, &cluster_centers)?;
424
425        Ok(ClusteringResults {
426            num_clusters,
427            cluster_centers,
428            cluster_assignments,
429            silhouette_score,
430            inertia,
431        })
432    }
433
434    /// Analyze temporal dynamics of hidden states.
435    pub fn analyze_temporal_dynamics(
436        &self,
437        layer_data: &[&HiddenStateData],
438    ) -> Result<TemporalDynamics> {
439        if layer_data.len() < 2 {
440            return Err(anyhow::anyhow!("Insufficient temporal data"));
441        }
442
443        // Calculate temporal consistency
444        let temporal_consistency = self.calculate_temporal_consistency(layer_data)?;
445
446        // Calculate change rate
447        let change_rate = self.calculate_change_rate(layer_data)?;
448
449        // Identify stability windows
450        let stability_windows = self.identify_stability_windows(layer_data)?;
451
452        // Detect distribution drift
453        let drift_detection = self.detect_distribution_drift(layer_data)?;
454
455        Ok(TemporalDynamics {
456            temporal_consistency,
457            change_rate,
458            stability_windows,
459            drift_detection,
460        })
461    }
462
463    /// Assess representation stability.
464    pub fn assess_representation_stability(
465        &self,
466        hidden_states: &[Vec<f64>],
467    ) -> Result<RepresentationStability> {
468        if hidden_states.is_empty() {
469            return Err(anyhow::anyhow!("No hidden states provided"));
470        }
471
472        // Calculate overall stability score
473        let stability_score = self.calculate_stability_score(hidden_states)?;
474
475        let variance_across_batches = self.calculate_batch_variance(hidden_states)?;
476
477        // Calculate consistency measure
478        let consistency_measure = self.calculate_consistency_measure(hidden_states)?;
479
480        let robustness_to_noise = self.assess_noise_robustness(hidden_states)?;
481
482        Ok(RepresentationStability {
483            stability_score,
484            variance_across_batches,
485            consistency_measure,
486            robustness_to_noise,
487        })
488    }
489
490    /// Generate activation heatmap for visualization.
491    pub fn generate_activation_heatmap(
492        &self,
493        layer_stats: &[LayerActivationStats],
494    ) -> Result<ActivationHeatmap> {
495        if layer_stats.is_empty() {
496            return Err(anyhow::anyhow!("No layer statistics provided"));
497        }
498
499        // Create heatmap data based on layer statistics
500        let mut data = Vec::new();
501        let mut min_val = f64::INFINITY;
502        let mut max_val = f64::NEG_INFINITY;
503
504        for stats in layer_stats {
505            let row = vec![
506                stats.mean_activation,
507                stats.std_activation,
508                stats.min_activation,
509                stats.max_activation,
510                stats.dead_neurons_ratio,
511                stats.saturated_neurons_ratio,
512                stats.sparsity,
513            ];
514
515            for &val in &row {
516                min_val = min_val.min(val);
517                max_val = max_val.max(val);
518            }
519
520            data.push(row);
521        }
522
523        let dimensions = (data.len(), data.first().map_or(0, |row| row.len()));
524
525        Ok(ActivationHeatmap {
526            data,
527            dimensions,
528            value_range: (min_val, max_val),
529            interpretation: "Activation statistics heatmap showing layer behavior patterns"
530                .to_string(),
531        })
532    }
533
534    /// Perform anomaly detection on performance metrics.
535    pub fn detect_performance_anomalies(&self) -> Result<AnomalyDetectionResults> {
536        // Extract performance data
537        let mut all_values = Vec::new();
538        for correlation_data in self.performance_correlations.values() {
539            all_values.extend(correlation_data.values.iter().cloned());
540        }
541
542        if all_values.is_empty() {
543            return Err(anyhow::anyhow!("No performance data available"));
544        }
545
546        // Use statistical threshold method for anomaly detection
547        let method = AnomalyDetectionMethod::StatisticalThreshold { n_std: 2.0 };
548
549        let mean = all_values.iter().sum::<f64>() / all_values.len() as f64;
550        let variance =
551            all_values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / all_values.len() as f64;
552        let std_dev = variance.sqrt();
553
554        let threshold = mean + 2.0 * std_dev;
555
556        let mut anomalies = Vec::new();
557        let mut anomaly_scores = Vec::new();
558
559        for (i, &value) in all_values.iter().enumerate() {
560            let score = (value - mean).abs() / std_dev;
561            anomaly_scores.push(score);
562
563            if value > threshold {
564                anomalies.push(Anomaly {
565                    index: i,
566                    score,
567                    timestamp: chrono::Utc::now(),
568                    context: HashMap::new(),
569                });
570            }
571        }
572
573        Ok(AnomalyDetectionResults {
574            anomalies,
575            anomaly_scores,
576            threshold,
577            method,
578        })
579    }
580
581    /// Calculate correlation matrix for all metrics.
582    pub fn calculate_correlation_matrix(&self) -> Result<Vec<Vec<f64>>> {
583        let metric_names: Vec<_> = self.performance_correlations.keys().cloned().collect();
584        let n_metrics = metric_names.len();
585
586        if n_metrics == 0 {
587            return Err(anyhow::anyhow!(
588                "No metrics available for correlation analysis"
589            ));
590        }
591
592        let mut correlation_matrix = vec![vec![0.0; n_metrics]; n_metrics];
593
594        for (i, metric1) in metric_names.iter().enumerate() {
595            for (j, metric2) in metric_names.iter().enumerate() {
596                if i == j {
597                    correlation_matrix[i][j] = 1.0;
598                } else {
599                    let correlation = self.calculate_correlation(metric1, metric2)?;
600                    correlation_matrix[i][j] = correlation;
601                }
602            }
603        }
604
605        Ok(correlation_matrix)
606    }
607
608    /// Perform statistical analysis on collected data.
609    pub fn perform_statistical_analysis(&self) -> Result<StatisticalAnalysis> {
610        if self.performance_correlations.is_empty() {
611            return Err(anyhow::anyhow!(
612                "No data available for statistical analysis"
613            ));
614        }
615
616        // Calculate means and standard deviations
617        let mut means = Vec::new();
618        let mut std_devs = Vec::new();
619
620        for correlation_data in self.performance_correlations.values() {
621            let values: Vec<f64> = correlation_data.values.iter().cloned().collect();
622            if !values.is_empty() {
623                let mean = values.iter().sum::<f64>() / values.len() as f64;
624                let variance =
625                    values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
626                let std_dev = variance.sqrt();
627
628                means.push(mean);
629                std_devs.push(std_dev);
630            }
631        }
632
633        // Calculate correlation matrix
634        let correlation_matrix = self.calculate_correlation_matrix()?;
635
636        // REAL principal components: the eigen-decomposition of the
637        // correlation matrix (PCA on standardised variables), largest
638        // eigenvalue first. `principal_components[k]` is the k-th loading
639        // vector and `explained_variance_ratios[k]` its share of the total
640        // variance.
641        //
642        // These used to be an all-ones matrix and a flat `1/n` vector, i.e.
643        // "every variable loads equally on every component and each component
644        // explains the same share", published as a PCA result.
645        let (principal_components, explained_variance_ratios) =
646            Self::principal_components_of(&correlation_matrix);
647
648        // Hypothesis tests need a stated null and paired samples to test it
649        // against; `perform_statistical_analysis` receives neither. This used
650        // to carry one "Sample t-test" with statistic 1.0, p-value 0.05 and a
651        // (0.0, 1.0) confidence interval -- constants regardless of the data.
652        let significance_tests = Vec::new();
653
654        Ok(StatisticalAnalysis {
655            means,
656            std_devs,
657            correlation_matrix,
658            principal_components,
659            explained_variance_ratios,
660            significance_tests,
661        })
662    }
663
664    /// Eigen-decompose a symmetric correlation matrix into principal
665    /// components and explained-variance ratios, largest first.
666    ///
667    /// Returns empty vectors for an empty or non-square input.
668    fn principal_components_of(correlation_matrix: &[Vec<f64>]) -> (Vec<Vec<f64>>, Vec<f64>) {
669        let n = correlation_matrix.len();
670        if n == 0 || correlation_matrix.iter().any(|row| row.len() != n) {
671            return (Vec::new(), Vec::new());
672        }
673        if correlation_matrix.iter().flatten().any(|v| !v.is_finite()) {
674            return (Vec::new(), Vec::new());
675        }
676
677        let matrix = nalgebra::DMatrix::<f64>::from_fn(n, n, |i, j| correlation_matrix[i][j]);
678        // The correlation matrix is symmetric by construction, so the
679        // symmetric (Jacobi) eigensolver applies and returns real eigenvalues.
680        let eigen = nalgebra::linalg::SymmetricEigen::new(matrix);
681
682        let mut order: Vec<usize> = (0..n).collect();
683        order.sort_by(|&a, &b| {
684            eigen.eigenvalues[b]
685                .partial_cmp(&eigen.eigenvalues[a])
686                .unwrap_or(std::cmp::Ordering::Equal)
687        });
688
689        // Negative eigenvalues can only appear as round-off on a
690        // positive-semidefinite correlation matrix; clamp them at zero.
691        let total: f64 = eigen.eigenvalues.iter().map(|v| v.max(0.0)).sum();
692        let components: Vec<Vec<f64>> = order
693            .iter()
694            .map(|&k| eigen.eigenvectors.column(k).iter().copied().collect())
695            .collect();
696        let ratios: Vec<f64> = order
697            .iter()
698            .map(|&k| if total > 0.0 { eigen.eigenvalues[k].max(0.0) / total } else { 0.0 })
699            .collect();
700
701        (components, ratios)
702    }
703
704    /// Generate comprehensive analytics report.
705    pub fn generate_analytics_report(&self) -> Result<AnalyticsReport> {
706        let correlation_matrix = self.calculate_correlation_matrix().unwrap_or_default();
707        let statistical_analysis = self.perform_statistical_analysis().unwrap_or_default();
708        let anomaly_detection = self.detect_performance_anomalies().unwrap_or_default();
709
710        // Analyze each layer if data is available
711        let mut layer_analyses = HashMap::new();
712        let unique_layers: std::collections::HashSet<String> =
713            self.hidden_states_history.iter().map(|data| data.layer_name.clone()).collect();
714
715        for layer_name in unique_layers {
716            if let Ok(analysis) = self.analyze_hidden_states(&layer_name) {
717                layer_analyses.insert(layer_name, analysis);
718            }
719        }
720
721        Ok(AnalyticsReport {
722            correlation_matrix,
723            statistical_analysis,
724            layer_analyses,
725            anomaly_detection,
726            temporal_summary: self.generate_temporal_summary(),
727            recommendations: self.generate_analytics_recommendations(),
728        })
729    }
730
731    // Helper methods
732
733    /// Update correlation data for a metric.
734    fn update_correlation_data(&mut self, metric_name: &str, value: f64) {
735        let correlation_data = self
736            .performance_correlations
737            .entry(metric_name.to_string())
738            .or_insert_with(|| CorrelationData {
739                metric_name: metric_name.to_string(),
740                values: VecDeque::new(),
741                correlations: HashMap::new(),
742                last_updated: chrono::Utc::now(),
743            });
744
745        correlation_data.values.push_back(value);
746        correlation_data.last_updated = chrono::Utc::now();
747
748        // Limit history size
749        while correlation_data.values.len() > self.config.max_history_samples {
750            correlation_data.values.pop_front();
751        }
752    }
753
754    /// Initialize cluster centers using k-means++ algorithm.
755    fn initialize_cluster_centers(
756        &self,
757        data: &[Vec<f64>],
758        num_clusters: usize,
759    ) -> Result<Vec<Vec<f64>>> {
760        if data.is_empty() || num_clusters == 0 {
761            return Err(anyhow::anyhow!("Invalid input for cluster initialization"));
762        }
763
764        let mut centers = Vec::new();
765        let _dimensions = data[0].len();
766
767        // Deterministic seeding: take the first point rather than a random
768        // one, so repeated runs over the same data cluster identically. (This
769        // is the only departure from textbook k-means++, which samples the
770        // first centre uniformly at random.)
771        centers.push(data[0].clone());
772
773        // Remaining centres by the k-means++ D^2 rule.
774        for _ in 1..num_clusters {
775            if centers.len() >= data.len() {
776                break;
777            }
778
779            let mut best_distance = 0.0;
780            let mut best_point = data[0].clone();
781
782            for point in data {
783                let mut min_distance = f64::INFINITY;
784                for center in &centers {
785                    let distance =
786                        self.calculate_distance(point, center, &DistanceMetric::Euclidean)?;
787                    min_distance = min_distance.min(distance);
788                }
789
790                if min_distance > best_distance {
791                    best_distance = min_distance;
792                    best_point = point.clone();
793                }
794            }
795
796            centers.push(best_point);
797        }
798
799        Ok(centers)
800    }
801
802    /// Calculate distance between two points.
803    fn calculate_distance(
804        &self,
805        point1: &[f64],
806        point2: &[f64],
807        metric: &DistanceMetric,
808    ) -> Result<f64> {
809        if point1.len() != point2.len() {
810            return Err(anyhow::anyhow!("Points must have same dimensionality"));
811        }
812
813        match metric {
814            DistanceMetric::Euclidean => {
815                let sum_squared =
816                    point1.iter().zip(point2.iter()).map(|(a, b)| (a - b).powi(2)).sum::<f64>();
817                Ok(sum_squared.sqrt())
818            },
819            DistanceMetric::Manhattan => {
820                let sum_abs =
821                    point1.iter().zip(point2.iter()).map(|(a, b)| (a - b).abs()).sum::<f64>();
822                Ok(sum_abs)
823            },
824            DistanceMetric::Cosine => {
825                let dot_product = point1.iter().zip(point2.iter()).map(|(a, b)| a * b).sum::<f64>();
826                let norm1 = point1.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
827                let norm2 = point2.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
828
829                if norm1 == 0.0 || norm2 == 0.0 {
830                    Ok(1.0)
831                } else {
832                    Ok(1.0 - (dot_product / (norm1 * norm2)))
833                }
834            },
835            DistanceMetric::Minkowski { p } => {
836                let sum_powered = point1
837                    .iter()
838                    .zip(point2.iter())
839                    .map(|(a, b)| (a - b).abs().powf(*p))
840                    .sum::<f64>();
841                Ok(sum_powered.powf(1.0 / p))
842            },
843        }
844    }
845
846    /// Update cluster centers based on current assignments.
847    fn update_cluster_centers(
848        &self,
849        data: &[Vec<f64>],
850        assignments: &[usize],
851        num_clusters: usize,
852    ) -> Result<Vec<Vec<f64>>> {
853        let dimensions = data[0].len();
854        let mut new_centers = vec![vec![0.0; dimensions]; num_clusters];
855        let mut cluster_counts = vec![0; num_clusters];
856
857        // Sum points in each cluster
858        for (point, &cluster_id) in data.iter().zip(assignments.iter()) {
859            if cluster_id < num_clusters {
860                for (i, &value) in point.iter().enumerate() {
861                    new_centers[cluster_id][i] += value;
862                }
863                cluster_counts[cluster_id] += 1;
864            }
865        }
866
867        // Average to get new centers
868        for (cluster_id, count) in cluster_counts.iter().enumerate() {
869            if *count > 0 {
870                for value in &mut new_centers[cluster_id] {
871                    *value /= *count as f64;
872                }
873            }
874        }
875
876        Ok(new_centers)
877    }
878
879    /// Calculate silhouette score for clustering quality.
880    fn calculate_silhouette_score(
881        &self,
882        data: &[Vec<f64>],
883        assignments: &[usize],
884        centers: &[Vec<f64>],
885    ) -> Result<f64> {
886        if data.is_empty() {
887            return Ok(0.0);
888        }
889
890        let mut total_score = 0.0;
891        let mut valid_points = 0;
892
893        for (i, point) in data.iter().enumerate() {
894            let cluster_id = assignments[i];
895
896            // Calculate average distance to points in same cluster (a)
897            let mut same_cluster_distances = Vec::new();
898            for (j, other_point) in data.iter().enumerate() {
899                if i != j && assignments[j] == cluster_id {
900                    let distance =
901                        self.calculate_distance(point, other_point, &DistanceMetric::Euclidean)?;
902                    same_cluster_distances.push(distance);
903                }
904            }
905
906            let a = if same_cluster_distances.is_empty() {
907                0.0
908            } else {
909                same_cluster_distances.iter().sum::<f64>() / same_cluster_distances.len() as f64
910            };
911
912            // Calculate minimum average distance to points in other clusters (b)
913            let mut min_other_cluster_distance = f64::INFINITY;
914            for (other_cluster_id, _) in centers.iter().enumerate() {
915                if other_cluster_id != cluster_id {
916                    let mut other_cluster_distances = Vec::new();
917                    for (j, other_point) in data.iter().enumerate() {
918                        if assignments[j] == other_cluster_id {
919                            let distance = self.calculate_distance(
920                                point,
921                                other_point,
922                                &DistanceMetric::Euclidean,
923                            )?;
924                            other_cluster_distances.push(distance);
925                        }
926                    }
927
928                    if !other_cluster_distances.is_empty() {
929                        let avg_distance = other_cluster_distances.iter().sum::<f64>()
930                            / other_cluster_distances.len() as f64;
931                        min_other_cluster_distance = min_other_cluster_distance.min(avg_distance);
932                    }
933                }
934            }
935
936            let b = min_other_cluster_distance;
937
938            if a < b {
939                total_score += (b - a) / b;
940            } else if a > b {
941                total_score += (b - a) / a;
942            }
943            // If a == b, silhouette score is 0 (no contribution)
944
945            valid_points += 1;
946        }
947
948        Ok(if valid_points > 0 { total_score / valid_points as f64 } else { 0.0 })
949    }
950
951    /// Calculate inertia (sum of squared distances to centroids).
952    fn calculate_inertia(
953        &self,
954        data: &[Vec<f64>],
955        assignments: &[usize],
956        centers: &[Vec<f64>],
957    ) -> Result<f64> {
958        let mut inertia = 0.0;
959
960        for (point, &cluster_id) in data.iter().zip(assignments.iter()) {
961            if cluster_id < centers.len() {
962                let distance = self.calculate_distance(
963                    point,
964                    &centers[cluster_id],
965                    &DistanceMetric::Euclidean,
966                )?;
967                inertia += distance.powi(2);
968            }
969        }
970
971        Ok(inertia)
972    }
973
974    /// Calculate information content of hidden states.
975    fn calculate_information_content(&self, hidden_states: &[Vec<f64>]) -> Result<f64> {
976        if hidden_states.is_empty() {
977            return Ok(0.0);
978        }
979
980        let dimensions = hidden_states[0].len();
981        let mut total_variance = 0.0;
982
983        for dim in 0..dimensions {
984            let values: Vec<f64> = hidden_states.iter().map(|state| state[dim]).collect();
985            if values.len() > 1 {
986                let mean = values.iter().sum::<f64>() / values.len() as f64;
987                let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
988                    / (values.len() - 1) as f64;
989                total_variance += variance;
990            }
991        }
992
993        // Information content as normalized variance
994        Ok(total_variance / dimensions as f64)
995    }
996
997    /// Calculate temporal consistency of hidden states.
998    fn calculate_temporal_consistency(&self, layer_data: &[&HiddenStateData]) -> Result<f64> {
999        if layer_data.len() < 2 {
1000            return Ok(1.0);
1001        }
1002
1003        let mut consistency_scores = Vec::new();
1004
1005        for i in 1..layer_data.len() {
1006            let prev_states = &layer_data[i - 1].hidden_states;
1007            let curr_states = &layer_data[i].hidden_states;
1008
1009            if !prev_states.is_empty() && !curr_states.is_empty() {
1010                // Simple consistency measure based on mean state similarity
1011                let prev_mean = self.calculate_mean_state(prev_states);
1012                let curr_mean = self.calculate_mean_state(curr_states);
1013
1014                if prev_mean.len() == curr_mean.len() {
1015                    let distance = self.calculate_distance(
1016                        &prev_mean,
1017                        &curr_mean,
1018                        &DistanceMetric::Euclidean,
1019                    )?;
1020                    consistency_scores.push(1.0 / (1.0 + distance));
1021                }
1022            }
1023        }
1024
1025        Ok(if consistency_scores.is_empty() {
1026            1.0
1027        } else {
1028            consistency_scores.iter().sum::<f64>() / consistency_scores.len() as f64
1029        })
1030    }
1031
1032    /// Calculate mean state from a collection of states.
1033    fn calculate_mean_state(&self, states: &[Vec<f64>]) -> Vec<f64> {
1034        if states.is_empty() {
1035            return Vec::new();
1036        }
1037
1038        let dimensions = states[0].len();
1039        let mut mean_state = vec![0.0; dimensions];
1040
1041        for state in states {
1042            for (i, &value) in state.iter().enumerate() {
1043                if i < dimensions {
1044                    mean_state[i] += value;
1045                }
1046            }
1047        }
1048
1049        for value in &mut mean_state {
1050            *value /= states.len() as f64;
1051        }
1052
1053        mean_state
1054    }
1055
1056    /// Calculate change rate between temporal samples.
1057    fn calculate_change_rate(&self, layer_data: &[&HiddenStateData]) -> Result<f64> {
1058        if layer_data.len() < 2 {
1059            return Ok(0.0);
1060        }
1061
1062        let mut total_change = 0.0;
1063        let mut valid_comparisons = 0;
1064
1065        for i in 1..layer_data.len() {
1066            let prev_mean = self.calculate_mean_state(&layer_data[i - 1].hidden_states);
1067            let curr_mean = self.calculate_mean_state(&layer_data[i].hidden_states);
1068
1069            if !prev_mean.is_empty() && !curr_mean.is_empty() && prev_mean.len() == curr_mean.len()
1070            {
1071                let change =
1072                    self.calculate_distance(&prev_mean, &curr_mean, &DistanceMetric::Euclidean)?;
1073                total_change += change;
1074                valid_comparisons += 1;
1075            }
1076        }
1077
1078        Ok(if valid_comparisons > 0 {
1079            total_change / valid_comparisons as f64
1080        } else {
1081            0.0
1082        })
1083    }
1084
1085    /// Identify stability windows in temporal data.
1086    fn identify_stability_windows(
1087        &self,
1088        layer_data: &[&HiddenStateData],
1089    ) -> Result<Vec<(usize, usize)>> {
1090        if layer_data.len() < 3 {
1091            return Ok(Vec::new());
1092        }
1093
1094        let mut stability_windows = Vec::new();
1095        let mut window_start = 0;
1096        let stability_threshold = 0.1; // Configurable threshold
1097
1098        for i in 1..layer_data.len() {
1099            let prev_mean = self.calculate_mean_state(&layer_data[i - 1].hidden_states);
1100            let curr_mean = self.calculate_mean_state(&layer_data[i].hidden_states);
1101
1102            if !prev_mean.is_empty() && !curr_mean.is_empty() && prev_mean.len() == curr_mean.len()
1103            {
1104                let change = self
1105                    .calculate_distance(&prev_mean, &curr_mean, &DistanceMetric::Euclidean)
1106                    .unwrap_or(f64::INFINITY);
1107
1108                if change > stability_threshold {
1109                    // End of stability window
1110                    if i - window_start > 2 {
1111                        stability_windows.push((window_start, i - 1));
1112                    }
1113                    window_start = i;
1114                }
1115            }
1116        }
1117
1118        // Handle final window
1119        if layer_data.len() - window_start > 2 {
1120            stability_windows.push((window_start, layer_data.len() - 1));
1121        }
1122
1123        Ok(stability_windows)
1124    }
1125
1126    /// Detect distribution drift in temporal data.
1127    fn detect_distribution_drift(&self, layer_data: &[&HiddenStateData]) -> Result<DriftInfo> {
1128        if layer_data.len() < self.config.temporal_analysis_window {
1129            return Ok(DriftInfo {
1130                drift_detected: false,
1131                drift_magnitude: 0.0,
1132                drift_direction: "unknown".to_string(),
1133                onset_step: None,
1134            });
1135        }
1136
1137        let window_size = self.config.temporal_analysis_window;
1138        let mid_point = layer_data.len() / 2;
1139
1140        // Compare early and late windows
1141        let early_data = &layer_data[0..window_size.min(mid_point)];
1142        let late_data = &layer_data[mid_point.max(layer_data.len() - window_size)..];
1143
1144        let early_mean = self.calculate_aggregated_mean(early_data);
1145        let late_mean = self.calculate_aggregated_mean(late_data);
1146
1147        if early_mean.len() == late_mean.len() && !early_mean.is_empty() {
1148            let drift_magnitude =
1149                self.calculate_distance(&early_mean, &late_mean, &DistanceMetric::Euclidean)?;
1150            let drift_detected = drift_magnitude > self.config.drift_detection_sensitivity;
1151
1152            Ok(DriftInfo {
1153                drift_detected,
1154                drift_magnitude,
1155                drift_direction: if drift_detected {
1156                    "forward".to_string()
1157                } else {
1158                    "stable".to_string()
1159                },
1160                onset_step: if drift_detected { Some(mid_point) } else { None },
1161            })
1162        } else {
1163            Ok(DriftInfo {
1164                drift_detected: false,
1165                drift_magnitude: 0.0,
1166                drift_direction: "unknown".to_string(),
1167                onset_step: None,
1168            })
1169        }
1170    }
1171
1172    /// Calculate aggregated mean across multiple data samples.
1173    fn calculate_aggregated_mean(&self, layer_data: &[&HiddenStateData]) -> Vec<f64> {
1174        let all_states: Vec<Vec<f64>> =
1175            layer_data.iter().flat_map(|data| data.hidden_states.iter()).cloned().collect();
1176
1177        self.calculate_mean_state(&all_states)
1178    }
1179
1180    /// Calculate stability score for representation.
1181    fn calculate_stability_score(&self, hidden_states: &[Vec<f64>]) -> Result<f64> {
1182        if hidden_states.len() < 2 {
1183            return Ok(1.0);
1184        }
1185
1186        let mut stability_scores = Vec::new();
1187        let window_size = (hidden_states.len() / 10).max(2);
1188
1189        for i in window_size..hidden_states.len() {
1190            let current_window = &hidden_states[i - window_size..i];
1191            let mean_current = self.calculate_mean_state(current_window);
1192
1193            if i >= 2 * window_size {
1194                let prev_window = &hidden_states[i - 2 * window_size..i - window_size];
1195                let mean_prev = self.calculate_mean_state(prev_window);
1196
1197                if mean_current.len() == mean_prev.len() && !mean_current.is_empty() {
1198                    let distance = self.calculate_distance(
1199                        &mean_current,
1200                        &mean_prev,
1201                        &DistanceMetric::Euclidean,
1202                    )?;
1203                    stability_scores.push(1.0 / (1.0 + distance));
1204                }
1205            }
1206        }
1207
1208        Ok(if stability_scores.is_empty() {
1209            1.0
1210        } else {
1211            stability_scores.iter().sum::<f64>() / stability_scores.len() as f64
1212        })
1213    }
1214
1215    /// Mean per-dimension sample variance across the supplied hidden states.
1216    fn calculate_batch_variance(&self, hidden_states: &[Vec<f64>]) -> Result<f64> {
1217        if hidden_states.is_empty() {
1218            return Ok(0.0);
1219        }
1220
1221        let dimensions = hidden_states[0].len();
1222        let mut total_variance = 0.0;
1223
1224        for dim in 0..dimensions {
1225            let values: Vec<f64> = hidden_states.iter().map(|state| state[dim]).collect();
1226            if values.len() > 1 {
1227                let mean = values.iter().sum::<f64>() / values.len() as f64;
1228                let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
1229                    / (values.len() - 1) as f64;
1230                total_variance += variance;
1231            }
1232        }
1233
1234        Ok(total_variance / dimensions as f64)
1235    }
1236
1237    /// Calculate consistency measure for representation.
1238    fn calculate_consistency_measure(&self, hidden_states: &[Vec<f64>]) -> Result<f64> {
1239        if hidden_states.len() < 2 {
1240            return Ok(1.0);
1241        }
1242
1243        // Calculate pairwise similarities and return average
1244        let mut similarities = Vec::new();
1245        let sample_size = hidden_states.len().min(100); // Limit for performance
1246
1247        for i in 0..sample_size {
1248            for j in (i + 1)..sample_size {
1249                let distance = self.calculate_distance(
1250                    &hidden_states[i],
1251                    &hidden_states[j],
1252                    &DistanceMetric::Cosine,
1253                )?;
1254                similarities.push(1.0 - distance); // Convert distance to similarity
1255            }
1256        }
1257
1258        Ok(if similarities.is_empty() {
1259            1.0
1260        } else {
1261            similarities.iter().sum::<f64>() / similarities.len() as f64
1262        })
1263    }
1264
1265    /// Variance-derived noise-robustness PROXY, `1 / (1 + variance)`, in
1266    /// `(0, 1]`.
1267    ///
1268    /// It is a monotone transform of the real
1269    /// [`Self::calculate_batch_variance`], not a measurement of robustness:
1270    /// nothing is perturbed and the model is never re-evaluated. Tightly
1271    /// clustered representations score near 1 and widely spread ones near 0,
1272    /// which is a heuristic for -- not evidence of -- noise robustness.
1273    fn assess_noise_robustness(&self, hidden_states: &[Vec<f64>]) -> Result<f64> {
1274        self.calculate_batch_variance(hidden_states).map(|variance| {
1275            // High variance might indicate low robustness to noise
1276            1.0 / (1.0 + variance)
1277        })
1278    }
1279
1280    /// Calculate correlation between two metrics.
1281    fn calculate_correlation(&self, metric1: &str, metric2: &str) -> Result<f64> {
1282        let data1 = self
1283            .performance_correlations
1284            .get(metric1)
1285            .ok_or_else(|| anyhow::anyhow!("Metric {} not found", metric1))?;
1286
1287        let data2 = self
1288            .performance_correlations
1289            .get(metric2)
1290            .ok_or_else(|| anyhow::anyhow!("Metric {} not found", metric2))?;
1291
1292        let values1: Vec<f64> = data1.values.iter().cloned().collect();
1293        let values2: Vec<f64> = data2.values.iter().cloned().collect();
1294
1295        if values1.len() != values2.len() || values1.is_empty() {
1296            return Ok(0.0);
1297        }
1298
1299        let mean1 = values1.iter().sum::<f64>() / values1.len() as f64;
1300        let mean2 = values2.iter().sum::<f64>() / values2.len() as f64;
1301
1302        let numerator: f64 = values1
1303            .iter()
1304            .zip(values2.iter())
1305            .map(|(x1, x2)| (x1 - mean1) * (x2 - mean2))
1306            .sum();
1307
1308        let var1: f64 = values1.iter().map(|x| (x - mean1).powi(2)).sum();
1309        let var2: f64 = values2.iter().map(|x| (x - mean2).powi(2)).sum();
1310
1311        let denominator = (var1 * var2).sqrt();
1312
1313        Ok(if denominator == 0.0 { 0.0 } else { numerator / denominator })
1314    }
1315
1316    /// Generate temporal summary.
1317    fn generate_temporal_summary(&self) -> String {
1318        format!(
1319            "Temporal analysis: {} hidden state samples collected across {} layers. \
1320            Average stability observed with {} correlation metrics tracked.",
1321            self.hidden_states_history.len(),
1322            self.hidden_states_history
1323                .iter()
1324                .map(|data| &data.layer_name)
1325                .collect::<std::collections::HashSet<_>>()
1326                .len(),
1327            self.performance_correlations.len()
1328        )
1329    }
1330
1331    /// Generate analytics recommendations.
1332    fn generate_analytics_recommendations(&self) -> Vec<String> {
1333        let mut recommendations = Vec::new();
1334
1335        if self.performance_correlations.len() < 3 {
1336            recommendations.push(
1337                "Collect more performance metrics for comprehensive correlation analysis"
1338                    .to_string(),
1339            );
1340        }
1341
1342        if self.hidden_states_history.len() < 50 {
1343            recommendations
1344                .push("Increase hidden state sampling for better temporal analysis".to_string());
1345        }
1346
1347        recommendations
1348            .push("Consider implementing automated anomaly detection alerts".to_string());
1349        recommendations.push("Enable advanced visualization for better insights".to_string());
1350
1351        recommendations
1352    }
1353}
1354
1355/// Comprehensive analytics report.
1356#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1357pub struct AnalyticsReport {
1358    /// Correlation matrix between metrics
1359    pub correlation_matrix: Vec<Vec<f64>>,
1360    /// Statistical analysis results
1361    pub statistical_analysis: StatisticalAnalysis,
1362    /// Layer-specific analyses
1363    pub layer_analyses: HashMap<String, HiddenStateAnalysis>,
1364    /// Anomaly detection results
1365    pub anomaly_detection: AnomalyDetectionResults,
1366    /// Temporal analysis summary
1367    pub temporal_summary: String,
1368    /// Analytics recommendations
1369    pub recommendations: Vec<String>,
1370}
1371
1372impl TemporalAnalysisCache {
1373    /// Create a new temporal analysis cache.
1374    fn new() -> Self {
1375        Self {
1376            drift_results: HashMap::new(),
1377            consistency_scores: HashMap::new(),
1378            stability_windows: HashMap::new(),
1379            last_analysis: chrono::Utc::now(),
1380        }
1381    }
1382}
1383
1384impl Default for AnomalyDetectionResults {
1385    fn default() -> Self {
1386        Self {
1387            anomalies: Vec::new(),
1388            anomaly_scores: Vec::new(),
1389            threshold: 0.0,
1390            method: AnomalyDetectionMethod::StatisticalThreshold { n_std: 2.0 },
1391        }
1392    }
1393}
1394
1395impl Default for AdvancedAnalytics {
1396    fn default() -> Self {
1397        Self::new()
1398    }
1399}
1400
1401#[cfg(test)]
1402mod tests {
1403    use super::*;
1404
1405    // ---- Wave 6c debug-sweep2: real PCA ----------------------------------
1406
1407    #[test]
1408    fn principal_components_of_the_identity_are_unit_and_equally_weighted() {
1409        let identity = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
1410        let (components, ratios) = AdvancedAnalytics::principal_components_of(&identity);
1411        assert_eq!(components.len(), 2);
1412        // Uncorrelated unit-variance variables split the variance evenly.
1413        for ratio in &ratios {
1414            assert!((ratio - 0.5).abs() < 1e-9, "expected 0.5, got {ratio}");
1415        }
1416    }
1417
1418    #[test]
1419    fn principal_components_of_perfectly_correlated_variables_collapse_to_one() {
1420        // Two perfectly correlated variables: PC1 explains everything.
1421        let correlated = vec![vec![1.0, 1.0], vec![1.0, 1.0]];
1422        let (components, ratios) = AdvancedAnalytics::principal_components_of(&correlated);
1423        assert_eq!(components.len(), 2);
1424        assert!(
1425            (ratios[0] - 1.0).abs() < 1e-9,
1426            "PC1 must explain all the variance, got {}",
1427            ratios[0]
1428        );
1429        assert!(
1430            ratios[1].abs() < 1e-9,
1431            "PC2 must explain none, got {}",
1432            ratios[1]
1433        );
1434        // The old placeholder returned a flat 1/n for every component, so this
1435        // pair would both have been 0.5.
1436        assert!(
1437            (ratios[0] - ratios[1]).abs() > 0.5,
1438            "the ratios must actually differ"
1439        );
1440    }
1441
1442    #[test]
1443    fn principal_components_are_ordered_by_explained_variance() {
1444        let matrix = vec![vec![1.0, 0.8], vec![0.8, 1.0]];
1445        let (_, ratios) = AdvancedAnalytics::principal_components_of(&matrix);
1446        assert!(
1447            ratios[0] >= ratios[1],
1448            "components must be sorted descending: {ratios:?}"
1449        );
1450        assert!(
1451            (ratios.iter().sum::<f64>() - 1.0).abs() < 1e-9,
1452            "ratios must sum to 1"
1453        );
1454        // Eigenvalues of [[1,0.8],[0.8,1]] are 1.8 and 0.2 => 0.9 / 0.1.
1455        assert!((ratios[0] - 0.9).abs() < 1e-9, "{ratios:?}");
1456    }
1457
1458    #[test]
1459    fn principal_components_reject_a_malformed_matrix() {
1460        assert_eq!(
1461            AdvancedAnalytics::principal_components_of(&[]),
1462            (Vec::new(), Vec::new())
1463        );
1464        let ragged = vec![vec![1.0, 0.0], vec![0.0]];
1465        assert_eq!(
1466            AdvancedAnalytics::principal_components_of(&ragged),
1467            (Vec::new(), Vec::new())
1468        );
1469    }
1470
1471    #[test]
1472    fn test_advanced_analytics_creation() {
1473        let analytics = AdvancedAnalytics::new();
1474        assert_eq!(analytics.hidden_states_history.len(), 0);
1475        assert_eq!(analytics.performance_correlations.len(), 0);
1476    }
1477
1478    #[test]
1479    fn test_distance_calculation() {
1480        let analytics = AdvancedAnalytics::new();
1481        let point1 = vec![1.0, 2.0, 3.0];
1482        let point2 = vec![4.0, 5.0, 6.0];
1483
1484        let distance = analytics
1485            .calculate_distance(&point1, &point2, &DistanceMetric::Euclidean)
1486            .expect("operation failed in test");
1487        assert!(distance > 0.0);
1488    }
1489
1490    #[test]
1491    fn test_clustering_parameters() {
1492        let params = ClusteringParameters::default();
1493        assert_eq!(params.num_clusters, 8);
1494        assert_eq!(params.max_iterations, 100);
1495    }
1496
1497    #[test]
1498    fn test_correlation_calculation() {
1499        let mut analytics = AdvancedAnalytics::new();
1500
1501        // Add some test data
1502        analytics.update_correlation_data("metric1", 1.0);
1503        analytics.update_correlation_data("metric1", 2.0);
1504        analytics.update_correlation_data("metric2", 3.0);
1505        analytics.update_correlation_data("metric2", 4.0);
1506
1507        assert_eq!(analytics.performance_correlations.len(), 2);
1508    }
1509}