Skip to main content

trustformers_debug/
tensor_inspector.rs

1//! Tensor inspection and analysis tools
2
3use anyhow::Result;
4use scirs2_core::ndarray::*; // SciRS2 Integration Policy - was: use ndarray::{Array, ArrayD, Axis, IxDyn};
5use scirs2_core::random::random; // SciRS2 Integration Policy
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, VecDeque};
8use std::fmt;
9use std::time::Instant;
10use uuid::Uuid;
11
12use crate::DebugConfig;
13
14/// Statistics about a tensor
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct TensorStats {
17    pub shape: Vec<usize>,
18    pub dtype: String,
19    pub total_elements: usize,
20    pub mean: f64,
21    pub std: f64,
22    pub min: f64,
23    pub max: f64,
24    pub median: f64,
25    pub l1_norm: f64,
26    pub l2_norm: f64,
27    pub infinity_norm: f64,
28    pub nan_count: usize,
29    pub inf_count: usize,
30    pub zero_count: usize,
31    pub memory_usage_bytes: usize,
32    pub sparsity: f64,
33}
34
35/// Distribution analysis of tensor values
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct TensorDistribution {
38    pub histogram: Vec<(f64, usize)>,
39    pub percentiles: HashMap<String, f64>,
40    pub outliers: Vec<f64>,
41    pub skewness: f64,
42    pub kurtosis: f64,
43}
44
45/// Tensor metadata and tracking information
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TensorInfo {
48    pub id: Uuid,
49    pub name: String,
50    pub layer_name: Option<String>,
51    pub operation: Option<String>,
52    pub timestamp: chrono::DateTime<chrono::Utc>,
53    pub stats: TensorStats,
54    pub distribution: Option<TensorDistribution>,
55    pub gradient_stats: Option<TensorStats>,
56}
57
58/// Comparison between two tensors
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct TensorComparison {
61    pub tensor1_id: Uuid,
62    pub tensor2_id: Uuid,
63    pub mse: f64,
64    pub mae: f64,
65    pub max_diff: f64,
66    pub cosine_similarity: f64,
67    /// Pearson correlation between the two tensors' real element-wise
68    /// values. `None` when it cannot be honestly computed -- see
69    /// `TensorInspector::compute_correlation` for why that is currently
70    /// always the case (this crate only retains [`TensorStats`] summaries,
71    /// not raw tensor data, once a tensor has been inspected).
72    pub correlation: Option<f64>,
73    pub shape_match: bool,
74    pub dtype_match: bool,
75}
76
77/// Real-time tensor monitoring data
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct TensorTimeSeries {
80    pub tensor_id: Uuid,
81    pub timestamps: VecDeque<chrono::DateTime<chrono::Utc>>,
82    pub values: VecDeque<TensorStats>,
83    pub max_history: usize,
84}
85
86/// Tensor dependency tracking
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct TensorDependency {
89    pub source_id: Uuid,
90    pub target_id: Uuid,
91    pub operation: String,
92    pub weight: f64,
93}
94
95/// Tensor lifecycle event
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub enum TensorLifecycleEvent {
98    Created { size_bytes: usize },
99    Modified { operation: String },
100    Accessed { access_type: String },
101    Destroyed,
102}
103
104/// Tensor lifecycle tracking
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct TensorLifecycle {
107    pub tensor_id: Uuid,
108    pub events: Vec<(chrono::DateTime<chrono::Utc>, TensorLifecycleEvent)>,
109    pub total_accesses: usize,
110    pub creation_time: chrono::DateTime<chrono::Utc>,
111}
112
113/// Advanced tensor analysis results
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct AdvancedTensorAnalysis {
116    pub spectral_analysis: Option<SpectralAnalysis>,
117    pub information_content: InformationContent,
118    pub stability_metrics: StabilityMetrics,
119    pub relationship_analysis: RelationshipAnalysis,
120}
121
122/// Spectral analysis of tensor values
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct SpectralAnalysis {
125    /// Every singular value of the matrix, sorted descending.
126    ///
127    /// This field used to be called `eigenvalues` and held a single number
128    /// produced by ten steps of power iteration -- which is not an eigenvalue
129    /// for a non-symmetric matrix and was hardcoded to `0.0` for every
130    /// non-square matrix. Singular values are what a rectangular matrix
131    /// actually has, and these are the real ones from a full SVD.
132    pub singular_values: Vec<f64>,
133    /// 2-norm condition number `sigma_max / sigma_min`.
134    ///
135    /// `f64::INFINITY` when the smallest singular value is numerically zero,
136    /// i.e. the matrix is singular. (The previous value, `lambda_max` divided
137    /// by the Frobenius norm, was not a condition number under any definition.)
138    pub condition_number: f64,
139    /// Numerical rank: the number of singular values above the standard
140    /// LAPACK/NumPy tolerance `max(rows, cols) * sigma_max * f64::EPSILON`.
141    pub rank: usize,
142    /// Spectral norm (largest singular value).
143    pub spectral_norm: f64,
144    /// Roy & Vetterli (2007) effective rank: `exp(H(p))` where `p_i` is the
145    /// singular-value distribution `sigma_i / sum(sigma)` and `H` is its
146    /// Shannon entropy in nats. Lies in `[1, rank]`.
147    pub effective_rank: f64,
148}
149
150/// Information content metrics
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct InformationContent {
153    /// Shannon entropy (bits) of the tensor's values after quantising them
154    /// into 100 equal-width buckets over `[min, max]`.
155    pub entropy: f64,
156    /// Mutual information with another tensor.
157    ///
158    /// Always `None` from [`TensorInspector::perform_advanced_analysis`]: mutual
159    /// information is a property of a *pair* of aligned variables and this call
160    /// only ever sees one tensor, so there is nothing to compute it against.
161    /// It used to be published as a hardcoded `0.0`, which reads as "measured
162    /// zero mutual information".
163    pub mutual_information: Option<f64>,
164    /// Perplexity `2^entropy` of the quantised value distribution, i.e. the
165    /// effective number of distinct value buckets the tensor occupies.
166    ///
167    /// This was called `effective_rank`, which in linear algebra means
168    /// `exp(H(singular value distribution))` -- an entirely different quantity
169    /// that this function cannot compute because it never sees the matrix
170    /// shape. The real effective rank now lives on
171    /// [`SpectralAnalysis::effective_rank`].
172    pub value_distribution_perplexity: f64,
173    /// Fraction of elements that are distinct at a `1e-10` tolerance
174    /// (`distinct / total`).
175    ///
176    /// This was called `compression_ratio`, which it is not: nothing is
177    /// compressed and no codec is consulted. It is a crude proxy for
178    /// redundancy, so it is now named for what it measures.
179    pub distinct_value_fraction: f64,
180}
181
182/// Stability metrics
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct StabilityMetrics {
185    pub numerical_stability: f64,
186    /// Coefficient-of-variation-based stability of the real gradient
187    /// values, computed the same way as `numerical_stability` but over the
188    /// gradient tensor. `None` (never a fabricated constant) when
189    /// [`TensorInspector::perform_advanced_analysis`] was called without a
190    /// gradient tensor.
191    pub gradient_stability: Option<f64>,
192    pub perturbation_sensitivity: f64,
193    pub robustness_score: f64,
194}
195
196/// Relationship analysis between tensors
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct RelationshipAnalysis {
199    pub cross_correlations: HashMap<Uuid, f64>,
200    pub dependency_strength: HashMap<Uuid, f64>,
201    pub causal_relationships: Vec<TensorDependency>,
202}
203
204/// Enhanced tensor inspector for detailed analysis
205#[derive(Debug)]
206pub struct TensorInspector {
207    config: DebugConfig,
208    tracked_tensors: HashMap<Uuid, TensorInfo>,
209    comparisons: Vec<TensorComparison>,
210    alerts: Vec<TensorAlert>,
211    // New advanced features
212    time_series: HashMap<Uuid, TensorTimeSeries>,
213    dependencies: Vec<TensorDependency>,
214    lifecycles: HashMap<Uuid, TensorLifecycle>,
215    monitoring_enabled: bool,
216    last_analysis_time: Option<Instant>,
217}
218
219impl TensorInspector {
220    /// Create a new tensor inspector
221    pub fn new(config: &DebugConfig) -> Self {
222        Self {
223            config: config.clone(),
224            tracked_tensors: HashMap::new(),
225            comparisons: Vec::new(),
226            alerts: Vec::new(),
227            // Initialize new advanced features
228            time_series: HashMap::new(),
229            dependencies: Vec::new(),
230            lifecycles: HashMap::new(),
231            monitoring_enabled: false,
232            last_analysis_time: None,
233        }
234    }
235
236    /// Start the tensor inspector
237    pub async fn start(&mut self) -> Result<()> {
238        tracing::info!("Starting tensor inspector");
239        Ok(())
240    }
241
242    /// Inspect a tensor and return detailed analysis
243    pub fn inspect_tensor<T>(
244        &mut self,
245        tensor: &ArrayD<T>,
246        name: &str,
247        layer_name: Option<&str>,
248        operation: Option<&str>,
249    ) -> Result<Uuid>
250    where
251        T: Clone + Into<f64> + fmt::Debug + 'static,
252    {
253        let id = Uuid::new_v4();
254
255        // Convert to f64 for analysis
256        let values: Vec<f64> = tensor.iter().map(|x| x.clone().into()).collect();
257        let shape = tensor.shape().to_vec();
258
259        let stats = self.compute_tensor_stats(
260            &values,
261            &shape,
262            std::mem::size_of::<T>(),
263            std::any::type_name::<T>(),
264        )?;
265        let distribution = if self.should_compute_distribution() {
266            Some(self.compute_distribution(&values)?)
267        } else {
268            None
269        };
270
271        let tensor_info = TensorInfo {
272            id,
273            name: name.to_string(),
274            layer_name: layer_name.map(|s| s.to_string()),
275            operation: operation.map(|s| s.to_string()),
276            timestamp: chrono::Utc::now(),
277            stats,
278            distribution,
279            gradient_stats: None,
280        };
281
282        // Check for alerts
283        self.check_tensor_alerts(&tensor_info)?;
284
285        // Store tensor info if we have space
286        if self.tracked_tensors.len() < self.config.max_tracked_tensors {
287            self.tracked_tensors.insert(id, tensor_info.clone());
288        }
289
290        // Advanced features integration
291        self.record_lifecycle_event(
292            id,
293            TensorLifecycleEvent::Created {
294                size_bytes: std::mem::size_of::<T>() * tensor.len(),
295            },
296        );
297
298        if self.monitoring_enabled {
299            if let Some(tensor_info) = self.tracked_tensors.get(&id) {
300                self.update_time_series(id, tensor_info.stats.clone());
301            }
302        }
303
304        Ok(id)
305    }
306
307    /// Inspect tensor gradients
308    pub fn inspect_gradients<T>(&mut self, tensor_id: Uuid, gradients: &ArrayD<T>) -> Result<()>
309    where
310        T: Clone + Into<f64> + fmt::Debug + 'static,
311    {
312        let values: Vec<f64> = gradients.iter().map(|x| x.clone().into()).collect();
313        let shape = gradients.shape().to_vec();
314
315        let gradient_stats = self.compute_tensor_stats(
316            &values,
317            &shape,
318            std::mem::size_of::<T>(),
319            std::any::type_name::<T>(),
320        )?;
321
322        if let Some(tensor_info) = self.tracked_tensors.get_mut(&tensor_id) {
323            tensor_info.gradient_stats = Some(gradient_stats);
324        }
325
326        // Check for gradient-specific alerts - get the data we need first
327        let tensor_info_for_alerts = self
328            .tracked_tensors
329            .get(&tensor_id)
330            .map(|info| (info.id, info.name.clone(), info.gradient_stats.clone()));
331
332        if let Some((id, name, grad_stats)) = tensor_info_for_alerts {
333            self.check_gradient_alerts_with_data(id, &name, grad_stats)?;
334        }
335
336        Ok(())
337    }
338
339    /// Compare two tensors
340    pub fn compare_tensors(&mut self, id1: Uuid, id2: Uuid) -> Result<TensorComparison> {
341        let tensor1 = self
342            .tracked_tensors
343            .get(&id1)
344            .ok_or_else(|| anyhow::anyhow!("Tensor {} not found", id1))?;
345        let tensor2 = self
346            .tracked_tensors
347            .get(&id2)
348            .ok_or_else(|| anyhow::anyhow!("Tensor {} not found", id2))?;
349
350        let comparison = TensorComparison {
351            tensor1_id: id1,
352            tensor2_id: id2,
353            mse: self.compute_mse(&tensor1.stats, &tensor2.stats),
354            mae: self.compute_mae(&tensor1.stats, &tensor2.stats),
355            max_diff: (tensor1.stats.max - tensor2.stats.max).abs(),
356            cosine_similarity: self.compute_cosine_similarity(&tensor1.stats, &tensor2.stats),
357            correlation: self.compute_correlation(&tensor1.stats, &tensor2.stats),
358            shape_match: tensor1.stats.shape == tensor2.stats.shape,
359            dtype_match: tensor1.stats.dtype == tensor2.stats.dtype,
360        };
361
362        self.comparisons.push(comparison.clone());
363        Ok(comparison)
364    }
365
366    /// Get tensor information by ID
367    pub fn get_tensor_info(&self, id: Uuid) -> Option<&TensorInfo> {
368        self.tracked_tensors.get(&id)
369    }
370
371    /// Get all tracked tensors
372    pub fn get_all_tensors(&self) -> Vec<&TensorInfo> {
373        self.tracked_tensors.values().collect()
374    }
375
376    /// Get tensors by layer name
377    /// Id of the most recently inspected tensor with this name, if any.
378    ///
379    /// Names are not unique (the same tensor can be inspected at many steps),
380    /// so this returns the newest by inspection timestamp.
381    pub fn tensor_id_for_name(&self, name: &str) -> Option<Uuid> {
382        self.tracked_tensors
383            .values()
384            .filter(|info| info.name == name)
385            .max_by_key(|info| info.timestamp)
386            .map(|info| info.id)
387    }
388
389    pub fn get_tensors_by_layer(&self, layer_name: &str) -> Vec<&TensorInfo> {
390        self.tracked_tensors
391            .values()
392            .filter(|info| info.layer_name.as_ref() == Some(&layer_name.to_string()))
393            .collect()
394    }
395
396    /// Get all alerts
397    pub fn get_alerts(&self) -> &[TensorAlert] {
398        &self.alerts
399    }
400
401    /// Clear tracking data
402    pub fn clear(&mut self) {
403        self.tracked_tensors.clear();
404        self.comparisons.clear();
405        self.alerts.clear();
406        // Clear new advanced features
407        self.time_series.clear();
408        self.dependencies.clear();
409        self.lifecycles.clear();
410        self.last_analysis_time = None;
411    }
412
413    /// Generate inspection report
414    pub async fn generate_report(&self) -> Result<TensorInspectionReport> {
415        let total_tensors = self.tracked_tensors.len();
416        let tensors_with_issues = self
417            .tracked_tensors
418            .values()
419            .filter(|info| info.stats.nan_count > 0 || info.stats.inf_count > 0)
420            .count();
421
422        let memory_usage =
423            self.tracked_tensors.values().map(|info| info.stats.memory_usage_bytes).sum();
424
425        Ok(TensorInspectionReport {
426            total_tensors,
427            tensors_with_issues,
428            total_memory_usage: memory_usage,
429            alerts: self.alerts.clone(),
430            comparisons: self.comparisons.clone(),
431            summary_stats: self.compute_summary_stats(),
432        })
433    }
434
435    // Advanced debugging features
436
437    /// Enable real-time tensor monitoring
438    pub fn enable_monitoring(&mut self, enable: bool) {
439        self.monitoring_enabled = enable;
440        if enable {
441            tracing::info!("Real-time tensor monitoring enabled");
442        } else {
443            tracing::info!("Real-time tensor monitoring disabled");
444        }
445    }
446
447    /// Track tensor dependency
448    pub fn track_dependency(
449        &mut self,
450        source_id: Uuid,
451        target_id: Uuid,
452        operation: &str,
453        weight: f64,
454    ) {
455        let dependency = TensorDependency {
456            source_id,
457            target_id,
458            operation: operation.to_string(),
459            weight,
460        };
461        self.dependencies.push(dependency);
462    }
463
464    /// Record tensor lifecycle event
465    pub fn record_lifecycle_event(&mut self, tensor_id: Uuid, event: TensorLifecycleEvent) {
466        let lifecycle = self.lifecycles.entry(tensor_id).or_insert_with(|| TensorLifecycle {
467            tensor_id,
468            events: Vec::new(),
469            total_accesses: 0,
470            creation_time: chrono::Utc::now(),
471        });
472
473        lifecycle.events.push((chrono::Utc::now(), event.clone()));
474
475        if matches!(event, TensorLifecycleEvent::Accessed { .. }) {
476            lifecycle.total_accesses += 1;
477        }
478    }
479
480    /// Update tensor time series data
481    pub fn update_time_series(&mut self, tensor_id: Uuid, stats: TensorStats) {
482        if !self.monitoring_enabled {
483            return;
484        }
485
486        let time_series = self.time_series.entry(tensor_id).or_insert_with(|| TensorTimeSeries {
487            tensor_id,
488            timestamps: VecDeque::new(),
489            values: VecDeque::new(),
490            max_history: 1000, // Default history size
491        });
492
493        time_series.timestamps.push_back(chrono::Utc::now());
494        time_series.values.push_back(stats);
495
496        // Maintain maximum history size
497        while time_series.timestamps.len() > time_series.max_history {
498            time_series.timestamps.pop_front();
499            time_series.values.pop_front();
500        }
501    }
502
503    /// Perform advanced tensor analysis.
504    ///
505    /// `gradients`, when supplied, must be the real gradient tensor for
506    /// `tensor` (same element count) and is used to compute a real
507    /// `stability_metrics.gradient_stability` (see
508    /// `Self::compute_stability_metrics`). Without it, `gradient_stability`
509    /// is honestly `None` rather than a fabricated constant.
510    pub fn perform_advanced_analysis<T>(
511        &self,
512        tensor: &ArrayD<T>,
513        gradients: Option<&ArrayD<T>>,
514    ) -> Result<AdvancedTensorAnalysis>
515    where
516        T: Clone + Into<f64> + fmt::Debug + 'static,
517    {
518        let values: Vec<f64> = tensor.iter().map(|x| x.clone().into()).collect();
519        let gradient_values: Option<Vec<f64>> =
520            gradients.map(|g| g.iter().map(|x| x.clone().into()).collect());
521
522        Ok(AdvancedTensorAnalysis {
523            spectral_analysis: self.compute_spectral_analysis(&values, tensor.shape())?,
524            information_content: self.compute_information_content(&values)?,
525            stability_metrics: self
526                .compute_stability_metrics(&values, gradient_values.as_deref())?,
527            relationship_analysis: self.compute_relationship_analysis(&values)?,
528        })
529    }
530
531    /// Detect tensor anomalies using advanced algorithms
532    pub fn detect_advanced_anomalies(&self, tensor_id: Uuid) -> Result<Vec<TensorAlert>> {
533        let mut alerts = Vec::new();
534
535        if let Some(time_series) = self.time_series.get(&tensor_id) {
536            // Detect drift in tensor statistics over time
537            if time_series.values.len() >= 10 {
538                let recent_mean =
539                    time_series.values.iter().rev().take(5).map(|stats| stats.mean).sum::<f64>()
540                        / 5.0;
541                let historical_mean =
542                    time_series.values.iter().take(5).map(|stats| stats.mean).sum::<f64>() / 5.0;
543
544                let drift_ratio =
545                    (recent_mean - historical_mean).abs() / historical_mean.abs().max(1e-8);
546
547                if drift_ratio > 0.5 {
548                    if let Some(tensor_info) = self.tracked_tensors.get(&tensor_id) {
549                        alerts.push(TensorAlert {
550                            id: Uuid::new_v4(),
551                            tensor_id,
552                            tensor_name: tensor_info.name.clone(),
553                            alert_type: TensorAlertType::ExtremeValues,
554                            severity: AlertSeverity::Warning,
555                            message: format!(
556                                "Detected statistical drift in tensor '{}': {:.2}% change",
557                                tensor_info.name,
558                                drift_ratio * 100.0
559                            ),
560                            timestamp: chrono::Utc::now(),
561                        });
562                    }
563                }
564            }
565        }
566
567        Ok(alerts)
568    }
569
570    /// Get tensor dependencies
571    pub fn get_dependencies(&self) -> &[TensorDependency] {
572        &self.dependencies
573    }
574
575    /// Get tensor lifecycle
576    pub fn get_lifecycle(&self, tensor_id: Uuid) -> Option<&TensorLifecycle> {
577        self.lifecycles.get(&tensor_id)
578    }
579
580    /// Get tensor time series
581    pub fn get_time_series(&self, tensor_id: Uuid) -> Option<&TensorTimeSeries> {
582        self.time_series.get(&tensor_id)
583    }
584
585    /// Analyze tensor relationships
586    pub fn analyze_tensor_relationships(&self) -> HashMap<Uuid, Vec<Uuid>> {
587        let mut relationships = HashMap::new();
588
589        for dependency in &self.dependencies {
590            relationships
591                .entry(dependency.source_id)
592                .or_insert_with(Vec::new)
593                .push(dependency.target_id);
594        }
595
596        relationships
597    }
598
599    /// Get frequently accessed tensors
600    pub fn get_frequent_tensors(&self, min_accesses: usize) -> Vec<Uuid> {
601        self.lifecycles
602            .iter()
603            .filter(|(_, lifecycle)| lifecycle.total_accesses >= min_accesses)
604            .map(|(id, _)| *id)
605            .collect()
606    }
607
608    // Private helper methods
609
610    /// Compute [`TensorStats`] for an already-flattened `f64` view of a tensor.
611    ///
612    /// `dtype` must be the caller's real element type name (the callers pass
613    /// `std::any::type_name::<T>()`); it used to be hardcoded to `"f64"`, which
614    /// reported the analysis working type rather than the tensor's own dtype and
615    /// was therefore wrong for every `f32`/`f16`/integer tensor ever inspected.
616    fn compute_tensor_stats(
617        &self,
618        values: &[f64],
619        shape: &[usize],
620        element_size: usize,
621        dtype: &str,
622    ) -> Result<TensorStats> {
623        let total_elements = values.len();
624        let mean = values.iter().sum::<f64>() / total_elements as f64;
625
626        let variance =
627            values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / total_elements as f64;
628        let std = variance.sqrt();
629
630        let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
631        let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
632
633        let mut sorted_values = values.to_vec();
634        // Filter out NaN values before sorting to avoid panic
635        sorted_values.retain(|x| !x.is_nan());
636        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
637        let median = if sorted_values.is_empty() {
638            f64::NAN
639        } else if sorted_values.len().is_multiple_of(2) {
640            (sorted_values[sorted_values.len() / 2 - 1] + sorted_values[sorted_values.len() / 2])
641                / 2.0
642        } else {
643            sorted_values[sorted_values.len() / 2]
644        };
645
646        let l1_norm = values.iter().map(|x| x.abs()).sum::<f64>();
647        let l2_norm = values.iter().map(|x| x * x).sum::<f64>().sqrt();
648        let infinity_norm = values.iter().map(|x| x.abs()).fold(0.0, f64::max);
649
650        let nan_count = values.iter().filter(|x| x.is_nan()).count();
651        let inf_count = values.iter().filter(|x| x.is_infinite()).count();
652        let zero_count = values.iter().filter(|x| **x == 0.0).count();
653
654        let memory_usage_bytes = total_elements * element_size;
655        let sparsity = zero_count as f64 / total_elements as f64;
656
657        Ok(TensorStats {
658            shape: shape.to_vec(),
659            dtype: dtype.to_string(),
660            total_elements,
661            mean,
662            std,
663            min,
664            max,
665            median,
666            l1_norm,
667            l2_norm,
668            infinity_norm,
669            nan_count,
670            inf_count,
671            zero_count,
672            memory_usage_bytes,
673            sparsity,
674        })
675    }
676
677    fn compute_distribution(&self, values: &[f64]) -> Result<TensorDistribution> {
678        // Simple histogram computation
679        let num_bins = 50;
680        let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
681        let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
682        let bin_width = (max - min) / num_bins as f64;
683
684        let mut histogram = vec![(0.0, 0); num_bins];
685        for &value in values {
686            if !value.is_finite() {
687                continue;
688            }
689            let bin_idx = ((value - min) / bin_width).floor() as usize;
690            let bin_idx = bin_idx.min(num_bins - 1);
691            histogram[bin_idx].0 = min + bin_idx as f64 * bin_width;
692            histogram[bin_idx].1 += 1;
693        }
694
695        // Compute percentiles
696        let mut sorted_values =
697            values.iter().cloned().filter(|x| x.is_finite()).collect::<Vec<_>>();
698        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
699
700        let mut percentiles = HashMap::new();
701        for &p in &[5.0, 25.0, 50.0, 75.0, 95.0, 99.0] {
702            let idx = ((p / 100.0) * (sorted_values.len() - 1) as f64) as usize;
703            percentiles.insert(format!("p{}", p as u8), sorted_values[idx]);
704        }
705
706        // Identify outliers (simple method using IQR)
707        let q1 = percentiles["p25"];
708        let q3 = percentiles["p75"];
709        let iqr = q3 - q1;
710        let lower_bound = q1 - 1.5 * iqr;
711        let upper_bound = q3 + 1.5 * iqr;
712
713        let outliers: Vec<f64> = sorted_values
714            .iter()
715            .cloned()
716            .filter(|&x| x < lower_bound || x > upper_bound)
717            .take(100) // Limit outliers to avoid memory issues
718            .collect();
719
720        // Population (method-of-moments) skewness `g1 = m3 / m2^(3/2)` and
721        // population EXCESS kurtosis `g2 = m4 / m2^2 - 3`, both computed over
722        // the NaN-filtered values with the biased (divide-by-n) central
723        // moments. These are the standard estimators -- not an approximation --
724        // and they match `scipy.stats.skew(bias=True)` /
725        // `scipy.stats.kurtosis(bias=True, fisher=True)` exactly.
726        let mean = sorted_values.iter().sum::<f64>() / sorted_values.len() as f64;
727        let variance = sorted_values.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
728            / sorted_values.len() as f64;
729        let std = variance.sqrt();
730
731        let skewness = if std > 0.0 {
732            sorted_values.iter().map(|x| ((x - mean) / std).powi(3)).sum::<f64>()
733                / sorted_values.len() as f64
734        } else {
735            0.0
736        };
737
738        let kurtosis = if std > 0.0 {
739            sorted_values.iter().map(|x| ((x - mean) / std).powi(4)).sum::<f64>()
740                / sorted_values.len() as f64
741                - 3.0
742        } else {
743            0.0
744        };
745
746        Ok(TensorDistribution {
747            histogram,
748            percentiles,
749            outliers,
750            skewness,
751            kurtosis,
752        })
753    }
754
755    fn should_compute_distribution(&self) -> bool {
756        self.config.sampling_rate >= 1.0
757            || (self.config.sampling_rate > 0.0 && random::<f32>() < self.config.sampling_rate)
758    }
759
760    fn check_tensor_alerts(&mut self, tensor_info: &TensorInfo) -> Result<()> {
761        // Check for NaN values
762        if tensor_info.stats.nan_count > 0 {
763            self.alerts.push(TensorAlert {
764                id: Uuid::new_v4(),
765                tensor_id: tensor_info.id,
766                tensor_name: tensor_info.name.clone(),
767                alert_type: TensorAlertType::NaNValues,
768                severity: AlertSeverity::Critical,
769                message: format!(
770                    "Found {} NaN values in tensor '{}'",
771                    tensor_info.stats.nan_count, tensor_info.name
772                ),
773                timestamp: chrono::Utc::now(),
774            });
775        }
776
777        // Check for infinite values
778        if tensor_info.stats.inf_count > 0 {
779            self.alerts.push(TensorAlert {
780                id: Uuid::new_v4(),
781                tensor_id: tensor_info.id,
782                tensor_name: tensor_info.name.clone(),
783                alert_type: TensorAlertType::InfiniteValues,
784                severity: AlertSeverity::Critical,
785                message: format!(
786                    "Found {} infinite values in tensor '{}'",
787                    tensor_info.stats.inf_count, tensor_info.name
788                ),
789                timestamp: chrono::Utc::now(),
790            });
791        }
792
793        // Check for extreme values
794        if tensor_info.stats.max.abs() > 1e10 || tensor_info.stats.min.abs() > 1e10 {
795            self.alerts.push(TensorAlert {
796                id: Uuid::new_v4(),
797                tensor_id: tensor_info.id,
798                tensor_name: tensor_info.name.clone(),
799                alert_type: TensorAlertType::ExtremeValues,
800                severity: AlertSeverity::Warning,
801                message: format!(
802                    "Extreme values detected in tensor '{}': min={:.2e}, max={:.2e}",
803                    tensor_info.name, tensor_info.stats.min, tensor_info.stats.max
804                ),
805                timestamp: chrono::Utc::now(),
806            });
807        }
808
809        Ok(())
810    }
811
812    fn check_gradient_alerts_with_data(
813        &mut self,
814        tensor_id: Uuid,
815        tensor_name: &str,
816        grad_stats: Option<TensorStats>,
817    ) -> Result<()> {
818        if let Some(ref stats) = grad_stats {
819            // Check for vanishing gradients
820            if stats.l2_norm < 1e-8 {
821                self.alerts.push(TensorAlert {
822                    id: Uuid::new_v4(),
823                    tensor_id,
824                    tensor_name: tensor_name.to_string(),
825                    alert_type: TensorAlertType::VanishingGradients,
826                    severity: AlertSeverity::Warning,
827                    message: format!(
828                        "Vanishing gradients detected in '{}': L2 norm = {:.2e}",
829                        tensor_name, stats.l2_norm
830                    ),
831                    timestamp: chrono::Utc::now(),
832                });
833            }
834
835            // Check for exploding gradients
836            if stats.l2_norm > 100.0 {
837                self.alerts.push(TensorAlert {
838                    id: Uuid::new_v4(),
839                    tensor_id,
840                    tensor_name: tensor_name.to_string(),
841                    alert_type: TensorAlertType::ExplodingGradients,
842                    severity: AlertSeverity::Critical,
843                    message: format!(
844                        "Exploding gradients detected in '{}': L2 norm = {:.2e}",
845                        tensor_name, stats.l2_norm
846                    ),
847                    timestamp: chrono::Utc::now(),
848                });
849            }
850        }
851
852        Ok(())
853    }
854
855    fn compute_mse(&self, stats1: &TensorStats, stats2: &TensorStats) -> f64 {
856        // Simplified MSE using means (would need actual tensor data for real MSE)
857        (stats1.mean - stats2.mean).powi(2)
858    }
859
860    fn compute_mae(&self, stats1: &TensorStats, stats2: &TensorStats) -> f64 {
861        // Simplified MAE using means
862        (stats1.mean - stats2.mean).abs()
863    }
864
865    fn compute_cosine_similarity(&self, stats1: &TensorStats, stats2: &TensorStats) -> f64 {
866        // Simplified using L2 norms (would need actual tensors for real cosine similarity)
867        if stats1.l2_norm == 0.0 || stats2.l2_norm == 0.0 {
868            0.0
869        } else {
870            (stats1.mean * stats2.mean) / (stats1.l2_norm * stats2.l2_norm)
871        }
872    }
873
874    /// Pearson correlation requires paired per-element values (`sum((x_i -
875    /// mean_x)(y_i - mean_y))`), but `TensorInspector` only retains
876    /// [`TensorStats`] summaries once a tensor has been inspected -- see
877    /// [`TensorInspector::tracked_tensors`] -- never the raw values, so it
878    /// can be tracked across many tensors without unbounded memory growth.
879    /// With only `(mean, std, ...)` for each side there is no honest way to
880    /// recover the cross term, so this returns `None` rather than a
881    /// fabricated constant. [`Self::compute_relationship_analysis`] returns an
882    /// empty relationship map for the same reason.
883    fn compute_correlation(&self, _stats1: &TensorStats, _stats2: &TensorStats) -> Option<f64> {
884        None
885    }
886
887    // Advanced analysis helper methods
888
889    /// Real spectral analysis of a 2-D tensor via a full singular value
890    /// decomposition (`nalgebra`, Pure Rust).
891    ///
892    /// Returns `Ok(None)` -- an honest absence, never fabricated numbers --
893    /// when the tensor is not a 2-D matrix, when the element count does not
894    /// match `rows * cols`, when any element is non-finite (an SVD of a matrix
895    /// containing NaN/inf is meaningless), or when the iterative SVD does not
896    /// converge.
897    fn compute_spectral_analysis(
898        &self,
899        values: &[f64],
900        shape: &[usize],
901    ) -> Result<Option<SpectralAnalysis>> {
902        // Only perform spectral analysis for 2D matrices
903        if shape.len() != 2 || values.len() < 4 {
904            return Ok(None);
905        }
906
907        let rows = shape[0];
908        let cols = shape[1];
909        if rows == 0 || cols == 0 || rows.saturating_mul(cols) != values.len() {
910            tracing::debug!(
911                rows,
912                cols,
913                len = values.len(),
914                "skipping spectral analysis: shape does not match the element count"
915            );
916            return Ok(None);
917        }
918        if values.iter().any(|v| !v.is_finite()) {
919            tracing::debug!("skipping spectral analysis: matrix contains non-finite entries");
920            return Ok(None);
921        }
922
923        let matrix = nalgebra::DMatrix::<f64>::from_row_slice(rows, cols, values);
924        // `try_new` returns `None` instead of panicking when the implicit-shift
925        // QR sweep fails to converge; `0` means "no iteration cap".
926        let Some(svd) = nalgebra::linalg::SVD::try_new(matrix, false, false, f64::EPSILON, 0)
927        else {
928            tracing::debug!(
929                rows,
930                cols,
931                "SVD did not converge; reporting no spectral analysis"
932            );
933            return Ok(None);
934        };
935
936        let mut singular_values: Vec<f64> = svd.singular_values.iter().copied().collect();
937        // nalgebra does not guarantee an ordering, and every metric below
938        // depends on sigma_max / sigma_min.
939        singular_values.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
940
941        let sigma_max = singular_values.first().copied().unwrap_or(0.0);
942        let sigma_min = singular_values.last().copied().unwrap_or(0.0);
943
944        // Standard LAPACK/NumPy numerical-rank tolerance.
945        let tol = rows.max(cols) as f64 * sigma_max * f64::EPSILON;
946        let rank = singular_values.iter().filter(|&&s| s > tol).count();
947
948        let condition_number = if sigma_min > tol { sigma_max / sigma_min } else { f64::INFINITY };
949
950        // Roy & Vetterli (2007), "The effective rank: a measure of effective
951        // dimensionality": erank(A) = exp(H(p)), p_i = sigma_i / sum(sigma).
952        let sigma_sum: f64 = singular_values.iter().sum();
953        let effective_rank = if sigma_sum > 0.0 {
954            let entropy: f64 = singular_values
955                .iter()
956                .map(|&s| s / sigma_sum)
957                .filter(|&p| p > 0.0)
958                .map(|p| -p * p.ln())
959                .sum();
960            entropy.exp()
961        } else {
962            0.0
963        };
964
965        Ok(Some(SpectralAnalysis {
966            singular_values,
967            condition_number,
968            rank,
969            spectral_norm: sigma_max,
970            effective_rank,
971        }))
972    }
973
974    fn compute_information_content(&self, values: &[f64]) -> Result<InformationContent> {
975        // Shannon entropy calculation
976        let mut histogram = std::collections::HashMap::new();
977        let quantization_levels = 100;
978        let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min);
979        let max_val = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
980        let range = max_val - min_val;
981
982        if range > 1e-12 {
983            for &value in values {
984                let bucket = ((value - min_val) / range * quantization_levels as f64) as usize;
985                let bucket = bucket.min(quantization_levels - 1);
986                *histogram.entry(bucket).or_insert(0) += 1;
987            }
988        }
989
990        let total_count = values.len() as f64;
991        let entropy = if total_count > 0.0 {
992            histogram
993                .values()
994                .map(|&count| {
995                    let p = count as f64 / total_count;
996                    if p > 0.0 {
997                        -p * p.log2()
998                    } else {
999                        0.0
1000                    }
1001                })
1002                .sum()
1003        } else {
1004            0.0
1005        };
1006
1007        // Perplexity of the quantised value distribution: 2^H.
1008        let value_distribution_perplexity = if entropy > 0.0 { 2.0_f64.powf(entropy) } else { 1.0 };
1009
1010        let mut sorted_values = values.to_vec();
1011        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1012        sorted_values.dedup_by(|a, b| (*a - *b).abs() < 1e-10);
1013        let unique_values = sorted_values.len();
1014        let distinct_value_fraction =
1015            if values.is_empty() { 0.0 } else { unique_values as f64 / values.len() as f64 };
1016
1017        Ok(InformationContent {
1018            entropy,
1019            // Single-tensor call: there is no second variable to share
1020            // information with, so this is an honest absence.
1021            mutual_information: None,
1022            value_distribution_perplexity,
1023            distinct_value_fraction,
1024        })
1025    }
1026
1027    fn compute_stability_metrics(
1028        &self,
1029        values: &[f64],
1030        gradients: Option<&[f64]>,
1031    ) -> Result<StabilityMetrics> {
1032        // Numerical stability based on condition of values
1033        let mean = values.iter().sum::<f64>() / values.len() as f64;
1034        let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
1035        let std_dev = variance.sqrt();
1036
1037        let numerical_stability = if std_dev > 1e-12 {
1038            1.0 / (1.0 + std_dev / mean.abs().max(1e-12))
1039        } else {
1040            1.0
1041        };
1042
1043        // Simple perturbation sensitivity
1044        let max_abs = values.iter().map(|x| x.abs()).fold(0.0, f64::max);
1045        let perturbation_sensitivity = if max_abs > 1e-12 { std_dev / max_abs } else { 0.0 };
1046
1047        // Overall robustness score
1048        let robustness_score = numerical_stability * (1.0 - perturbation_sensitivity.min(1.0));
1049
1050        // Real gradient stability: the same coefficient-of-variation-based
1051        // formula as `numerical_stability`, computed over the real gradient
1052        // values when they were provided. `None` (never the old hardcoded
1053        // `0.8`) when no gradient tensor is available to this call.
1054        let gradient_stability = gradients.filter(|g| !g.is_empty()).map(|g| {
1055            let g_mean = g.iter().sum::<f64>() / g.len() as f64;
1056            let g_variance = g.iter().map(|x| (x - g_mean).powi(2)).sum::<f64>() / g.len() as f64;
1057            let g_std = g_variance.sqrt();
1058            if g_std > 1e-12 {
1059                1.0 / (1.0 + g_std / g_mean.abs().max(1e-12))
1060            } else {
1061                1.0
1062            }
1063        });
1064
1065        Ok(StabilityMetrics {
1066            numerical_stability,
1067            gradient_stability,
1068            perturbation_sensitivity,
1069            robustness_score,
1070        })
1071    }
1072
1073    /// Cross-tensor relationships for the tensor being analysed.
1074    ///
1075    /// Always empty, and deliberately so. Pearson correlation needs paired
1076    /// per-element values from both tensors, but [`Self::tracked_tensors`]
1077    /// retains only [`TensorStats`] summaries (see [`Self::compute_correlation`]
1078    /// for the same reasoning) -- the raw values of previously inspected
1079    /// tensors are dropped so tracking stays O(1) per tensor.
1080    ///
1081    /// The previous implementation looked like it computed something: it called
1082    /// `compute_simple_correlation(values, &[other.stats.mean])`, correlating an
1083    /// N-element vector against a ONE-element vector. With `min_len == 1` the
1084    /// second sum-of-squares term is identically zero, so the denominator was
1085    /// always below the `1e-12` guard and the function returned `0.0` for every
1086    /// pair, every time. Publishing that as a measured cross-correlation of
1087    /// zero is worse than publishing nothing.
1088    fn compute_relationship_analysis(&self, _values: &[f64]) -> Result<RelationshipAnalysis> {
1089        Ok(RelationshipAnalysis {
1090            cross_correlations: HashMap::new(),
1091            dependency_strength: HashMap::new(),
1092            // Causal discovery needs temporal ordering across tensors, which
1093            // this call does not receive.
1094            causal_relationships: Vec::new(),
1095        })
1096    }
1097
1098    fn compute_summary_stats(&self) -> HashMap<String, f64> {
1099        let mut stats = HashMap::new();
1100
1101        if !self.tracked_tensors.is_empty() {
1102            let values: Vec<f64> = self.tracked_tensors.values().map(|t| t.stats.mean).collect();
1103            stats.insert(
1104                "mean_of_means".to_string(),
1105                values.iter().sum::<f64>() / values.len() as f64,
1106            );
1107
1108            let total_memory: usize =
1109                self.tracked_tensors.values().map(|t| t.stats.memory_usage_bytes).sum();
1110            stats.insert(
1111                "total_memory_mb".to_string(),
1112                total_memory as f64 / (1024.0 * 1024.0),
1113            );
1114
1115            let avg_sparsity: f64 =
1116                self.tracked_tensors.values().map(|t| t.stats.sparsity).sum::<f64>()
1117                    / self.tracked_tensors.len() as f64;
1118            stats.insert("avg_sparsity".to_string(), avg_sparsity);
1119
1120            // Add advanced statistics
1121            stats.insert(
1122                "total_dependencies".to_string(),
1123                self.dependencies.len() as f64,
1124            );
1125            stats.insert(
1126                "monitored_tensors".to_string(),
1127                self.time_series.len() as f64,
1128            );
1129            stats.insert(
1130                "active_lifecycles".to_string(),
1131                self.lifecycles.len() as f64,
1132            );
1133        }
1134
1135        stats
1136    }
1137}
1138
1139/// Alert types for tensor issues
1140#[derive(Debug, Clone, Serialize, Deserialize)]
1141pub enum TensorAlertType {
1142    NaNValues,
1143    InfiniteValues,
1144    ExtremeValues,
1145    VanishingGradients,
1146    ExplodingGradients,
1147    MemoryUsage,
1148    ShapeMismatch,
1149}
1150
1151/// Alert severity levels
1152#[derive(Debug, Clone, Serialize, Deserialize)]
1153pub enum AlertSeverity {
1154    Info,
1155    Warning,
1156    Critical,
1157}
1158
1159/// Tensor alert information
1160#[derive(Debug, Clone, Serialize, Deserialize)]
1161pub struct TensorAlert {
1162    pub id: Uuid,
1163    pub tensor_id: Uuid,
1164    pub tensor_name: String,
1165    pub alert_type: TensorAlertType,
1166    pub severity: AlertSeverity,
1167    pub message: String,
1168    pub timestamp: chrono::DateTime<chrono::Utc>,
1169}
1170
1171/// Tensor inspection report
1172#[derive(Debug, Clone, Serialize, Deserialize)]
1173pub struct TensorInspectionReport {
1174    pub total_tensors: usize,
1175    pub tensors_with_issues: usize,
1176    pub total_memory_usage: usize,
1177    pub alerts: Vec<TensorAlert>,
1178    pub comparisons: Vec<TensorComparison>,
1179    pub summary_stats: HashMap<String, f64>,
1180}
1181
1182impl TensorInspectionReport {
1183    pub fn has_nan_values(&self) -> bool {
1184        self.alerts.iter().any(|a| matches!(a.alert_type, TensorAlertType::NaNValues))
1185    }
1186
1187    pub fn has_inf_values(&self) -> bool {
1188        self.alerts
1189            .iter()
1190            .any(|a| matches!(a.alert_type, TensorAlertType::InfiniteValues))
1191    }
1192
1193    pub fn total_nan_count(&self) -> usize {
1194        self.alerts
1195            .iter()
1196            .filter(|a| matches!(a.alert_type, TensorAlertType::NaNValues))
1197            .count()
1198    }
1199
1200    pub fn total_inf_count(&self) -> usize {
1201        self.alerts
1202            .iter()
1203            .filter(|a| matches!(a.alert_type, TensorAlertType::InfiniteValues))
1204            .count()
1205    }
1206
1207    pub fn has_critical_alerts(&self) -> bool {
1208        self.alerts.iter().any(|a| matches!(a.severity, AlertSeverity::Critical))
1209    }
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214    use super::*;
1215    use scirs2_core::ndarray::{ArrayD, IxDyn};
1216
1217    fn make_config() -> DebugConfig {
1218        DebugConfig::default()
1219    }
1220
1221    fn make_tensor(values: &[f64], shape: &[usize]) -> ArrayD<f64> {
1222        ArrayD::from_shape_vec(IxDyn(shape), values.to_vec())
1223            .expect("tensor creation should succeed")
1224    }
1225
1226    // ---- honesty regression tests (Wave 6c debug-sweep2) -----------------
1227
1228    #[test]
1229    fn dtype_reports_the_real_element_type_not_a_hardcoded_f64() {
1230        let config = make_config();
1231        let mut inspector = TensorInspector::new(&config);
1232        let tensor: ArrayD<f32> =
1233            ArrayD::from_shape_vec(IxDyn(&[2, 2]), vec![1.0f32, 2.0, 3.0, 4.0]).expect("tensor");
1234        let id = inspector.inspect_tensor(&tensor, "f32_tensor", None, None).expect("inspect");
1235        let info = inspector.get_tensor_info(id).expect("tracked");
1236        assert_eq!(
1237            info.stats.dtype, "f32",
1238            "dtype was hardcoded to \"f64\" before"
1239        );
1240        assert_eq!(info.stats.memory_usage_bytes, 4 * 4);
1241    }
1242
1243    #[test]
1244    fn spectral_rank_is_a_real_rank_not_a_non_zero_element_count() {
1245        let config = make_config();
1246        let inspector = TensorInspector::new(&config);
1247        // Every entry non-zero, but every row identical: real rank is 1.
1248        // The old implementation counted non-zero ENTRIES (9) and clamped to
1249        // min(rows, cols) = 3.
1250        let ones = make_tensor(&[1.0; 9], &[3, 3]);
1251        let analysis = inspector.perform_advanced_analysis(&ones, None).expect("analysis");
1252        let spectral = analysis.spectral_analysis.expect("2-D matrix must get spectral analysis");
1253        assert_eq!(spectral.rank, 1, "rank-1 matrix must report rank 1");
1254        assert!(
1255            spectral.condition_number.is_infinite(),
1256            "a singular matrix is ill-conditioned"
1257        );
1258        assert!(
1259            (spectral.spectral_norm - 3.0).abs() < 1e-9,
1260            "sigma_max of the 3x3 all-ones matrix is exactly 3, got {}",
1261            spectral.spectral_norm
1262        );
1263        assert!(
1264            (spectral.effective_rank - 1.0).abs() < 1e-9,
1265            "one non-zero singular value => effective rank 1, got {}",
1266            spectral.effective_rank
1267        );
1268    }
1269
1270    #[test]
1271    fn spectral_condition_number_matches_the_closed_form() {
1272        let config = make_config();
1273        let inspector = TensorInspector::new(&config);
1274        // diag(4, 1): singular values 4 and 1 => cond = 4, rank 2.
1275        let m = make_tensor(&[4.0, 0.0, 0.0, 1.0], &[2, 2]);
1276        let spectral = inspector
1277            .perform_advanced_analysis(&m, None)
1278            .expect("analysis")
1279            .spectral_analysis
1280            .expect("2-D matrix");
1281        assert_eq!(spectral.rank, 2);
1282        assert!(
1283            (spectral.condition_number - 4.0).abs() < 1e-9,
1284            "{}",
1285            spectral.condition_number
1286        );
1287        assert_eq!(spectral.singular_values.len(), 2);
1288        assert!((spectral.singular_values[0] - 4.0).abs() < 1e-9);
1289        assert!((spectral.singular_values[1] - 1.0).abs() < 1e-9);
1290        assert!(
1291            spectral.singular_values[0] >= spectral.singular_values[1],
1292            "singular values must come back sorted descending"
1293        );
1294    }
1295
1296    #[test]
1297    fn spectral_analysis_handles_rectangular_matrices() {
1298        let config = make_config();
1299        let inspector = TensorInspector::new(&config);
1300        // 2x3 with rank 2. The old power_iteration returned 0.0 for every
1301        // non-square matrix and published it as the spectral norm.
1302        let m = make_tensor(&[1.0, 0.0, 0.0, 0.0, 2.0, 0.0], &[2, 3]);
1303        let spectral = inspector
1304            .perform_advanced_analysis(&m, None)
1305            .expect("analysis")
1306            .spectral_analysis
1307            .expect("2-D matrix");
1308        assert!(
1309            spectral.spectral_norm > 0.0,
1310            "must not be the old hardcoded 0.0"
1311        );
1312        assert!((spectral.spectral_norm - 2.0).abs() < 1e-9);
1313        assert_eq!(spectral.rank, 2);
1314    }
1315
1316    #[test]
1317    fn spectral_analysis_is_absent_for_non_finite_matrices() {
1318        let config = make_config();
1319        let inspector = TensorInspector::new(&config);
1320        let m = make_tensor(&[1.0, f64::NAN, 3.0, 4.0], &[2, 2]);
1321        let analysis = inspector.perform_advanced_analysis(&m, None).expect("analysis");
1322        assert!(
1323            analysis.spectral_analysis.is_none(),
1324            "an SVD of a NaN matrix is meaningless: report absence, not numbers"
1325        );
1326    }
1327
1328    #[test]
1329    fn mutual_information_is_absent_rather_than_a_measured_zero() {
1330        let config = make_config();
1331        let inspector = TensorInspector::new(&config);
1332        let m = make_tensor(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
1333        let info = inspector
1334            .perform_advanced_analysis(&m, None)
1335            .expect("analysis")
1336            .information_content;
1337        assert!(
1338            info.mutual_information.is_none(),
1339            "one tensor cannot yield a real MI"
1340        );
1341        assert!(
1342            info.entropy > 0.0,
1343            "entropy over distinct values must be positive"
1344        );
1345        assert!(info.value_distribution_perplexity >= 1.0);
1346        assert!(
1347            (info.distinct_value_fraction - 1.0).abs() < 1e-12,
1348            "4 distinct of 4"
1349        );
1350    }
1351
1352    #[test]
1353    fn relationship_analysis_reports_nothing_instead_of_a_fabricated_zero() {
1354        let config = make_config();
1355        let mut inspector = TensorInspector::new(&config);
1356        let a = make_tensor(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
1357        inspector.inspect_tensor(&a, "a", None, None).expect("inspect");
1358        let b = make_tensor(&[5.0, 6.0, 7.0, 8.0], &[2, 2]);
1359        let rel = inspector
1360            .perform_advanced_analysis(&b, None)
1361            .expect("analysis")
1362            .relationship_analysis;
1363        // The old code inserted a 0.0 "correlation" for every tracked tensor.
1364        assert!(
1365            rel.cross_correlations.is_empty(),
1366            "raw values of past tensors are not retained, so no correlation is computable"
1367        );
1368        assert!(rel.dependency_strength.is_empty());
1369    }
1370
1371    #[test]
1372    fn skewness_and_kurtosis_match_the_population_estimators() {
1373        let config = make_config();
1374        let inspector = TensorInspector::new(&config);
1375        // Symmetric data => skew 0; this exact sample has a known excess
1376        // kurtosis of -1.3 for the population (Fisher) estimator.
1377        let data = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
1378        let tensor = make_tensor(&data, &[5]);
1379        let dist = inspector.compute_distribution(&data).expect("distribution");
1380        assert!(
1381            dist.skewness.abs() < 1e-12,
1382            "symmetric data has zero skew: {}",
1383            dist.skewness
1384        );
1385        // m2 = 2, m4 = 6.8 => g2 = 6.8 / 4 - 3 = -1.3
1386        assert!(
1387            (dist.kurtosis + 1.3).abs() < 1e-12,
1388            "population excess kurtosis must be -1.3, got {}",
1389            dist.kurtosis
1390        );
1391        let _ = tensor;
1392    }
1393
1394    #[test]
1395    fn test_tensor_inspector_creation() {
1396        let config = make_config();
1397        let inspector = TensorInspector::new(&config);
1398        assert!(inspector.get_all_tensors().is_empty());
1399        assert!(inspector.get_alerts().is_empty());
1400    }
1401
1402    #[test]
1403    fn test_inspect_tensor_basic() {
1404        let config = make_config();
1405        let mut inspector = TensorInspector::new(&config);
1406        let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
1407        let tensor = make_tensor(&data, &[3, 4]);
1408        let result = inspector.inspect_tensor(&tensor, "test_tensor", None, None);
1409        assert!(result.is_ok());
1410        let id = result.expect("inspect should succeed");
1411        let info = inspector.get_tensor_info(id);
1412        assert!(info.is_some());
1413        let i = info.expect("info should exist");
1414        assert_eq!(i.name, "test_tensor");
1415        assert_eq!(i.stats.shape, vec![3, 4]);
1416        assert_eq!(i.stats.total_elements, 12);
1417    }
1418
1419    #[test]
1420    fn test_inspect_tensor_with_layer_name() {
1421        let config = make_config();
1422        let mut inspector = TensorInspector::new(&config);
1423        let tensor = make_tensor(&[1.0, 2.0, 3.0, 4.0], &[4]);
1424        let id = inspector
1425            .inspect_tensor(&tensor, "w", Some("layer_0"), Some("matmul"))
1426            .expect("inspect should succeed");
1427        let info = inspector.get_tensor_info(id).expect("info should exist");
1428        assert_eq!(info.layer_name, Some("layer_0".to_string()));
1429        assert_eq!(info.operation, Some("matmul".to_string()));
1430    }
1431
1432    #[test]
1433    fn test_tensor_stats_computation() {
1434        let config = make_config();
1435        let mut inspector = TensorInspector::new(&config);
1436        let tensor = make_tensor(&[1.0, 2.0, 3.0, 4.0, 5.0], &[5]);
1437        let id = inspector
1438            .inspect_tensor(&tensor, "stats_test", None, None)
1439            .expect("inspect should succeed");
1440        let info = inspector.get_tensor_info(id).expect("info should exist");
1441        assert!((info.stats.mean - 3.0).abs() < 0.01);
1442        assert!((info.stats.min - 1.0).abs() < f64::EPSILON);
1443        assert!((info.stats.max - 5.0).abs() < f64::EPSILON);
1444        assert_eq!(info.stats.nan_count, 0);
1445        assert_eq!(info.stats.inf_count, 0);
1446    }
1447
1448    #[test]
1449    fn test_tensor_stats_sparsity() {
1450        let config = make_config();
1451        let mut inspector = TensorInspector::new(&config);
1452        let tensor = make_tensor(&[0.0, 0.0, 1.0, 0.0, 2.0], &[5]);
1453        let id = inspector
1454            .inspect_tensor(&tensor, "sparse_test", None, None)
1455            .expect("inspect should succeed");
1456        let info = inspector.get_tensor_info(id).expect("info should exist");
1457        assert!((info.stats.sparsity - 0.6).abs() < 0.01);
1458        assert_eq!(info.stats.zero_count, 3);
1459    }
1460
1461    #[test]
1462    fn test_tensor_norms() {
1463        let config = make_config();
1464        let mut inspector = TensorInspector::new(&config);
1465        let tensor = make_tensor(&[3.0, 4.0], &[2]);
1466        let id = inspector
1467            .inspect_tensor(&tensor, "norm_test", None, None)
1468            .expect("inspect should succeed");
1469        let info = inspector.get_tensor_info(id).expect("info should exist");
1470        assert!((info.stats.l1_norm - 7.0).abs() < 0.01);
1471        assert!((info.stats.l2_norm - 5.0).abs() < 0.01);
1472        assert!((info.stats.infinity_norm - 4.0).abs() < 0.01);
1473    }
1474
1475    #[test]
1476    fn test_get_all_tensors() {
1477        let config = make_config();
1478        let mut inspector = TensorInspector::new(&config);
1479        let t1 = make_tensor(&[1.0, 2.0], &[2]);
1480        let t2 = make_tensor(&[3.0, 4.0], &[2]);
1481        let _ = inspector.inspect_tensor(&t1, "t1", None, None);
1482        let _ = inspector.inspect_tensor(&t2, "t2", None, None);
1483        assert_eq!(inspector.get_all_tensors().len(), 2);
1484    }
1485
1486    #[test]
1487    fn test_get_tensors_by_layer() {
1488        let config = make_config();
1489        let mut inspector = TensorInspector::new(&config);
1490        let t1 = make_tensor(&[1.0], &[1]);
1491        let t2 = make_tensor(&[2.0], &[1]);
1492        let t3 = make_tensor(&[3.0], &[1]);
1493        let _ = inspector.inspect_tensor(&t1, "w1", Some("attn"), None);
1494        let _ = inspector.inspect_tensor(&t2, "w2", Some("attn"), None);
1495        let _ = inspector.inspect_tensor(&t3, "w3", Some("ffn"), None);
1496        let attn_tensors = inspector.get_tensors_by_layer("attn");
1497        assert_eq!(attn_tensors.len(), 2);
1498        let ffn_tensors = inspector.get_tensors_by_layer("ffn");
1499        assert_eq!(ffn_tensors.len(), 1);
1500    }
1501
1502    #[test]
1503    fn test_clear() {
1504        let config = make_config();
1505        let mut inspector = TensorInspector::new(&config);
1506        let t = make_tensor(&[1.0, 2.0], &[2]);
1507        let _ = inspector.inspect_tensor(&t, "t", None, None);
1508        inspector.clear();
1509        assert!(inspector.get_all_tensors().is_empty());
1510        assert!(inspector.get_alerts().is_empty());
1511    }
1512
1513    #[test]
1514    fn test_enable_monitoring() {
1515        let config = make_config();
1516        let mut inspector = TensorInspector::new(&config);
1517        inspector.enable_monitoring(true);
1518        assert!(inspector.monitoring_enabled);
1519        inspector.enable_monitoring(false);
1520        assert!(!inspector.monitoring_enabled);
1521    }
1522
1523    #[test]
1524    fn test_track_dependency() {
1525        let config = make_config();
1526        let mut inspector = TensorInspector::new(&config);
1527        let src = Uuid::new_v4();
1528        let tgt = Uuid::new_v4();
1529        inspector.track_dependency(src, tgt, "matmul", 1.0);
1530        assert_eq!(inspector.get_dependencies().len(), 1);
1531    }
1532
1533    #[test]
1534    fn test_record_lifecycle_event() {
1535        let config = make_config();
1536        let mut inspector = TensorInspector::new(&config);
1537        let tid = Uuid::new_v4();
1538        inspector.record_lifecycle_event(tid, TensorLifecycleEvent::Created { size_bytes: 100 });
1539        inspector.record_lifecycle_event(
1540            tid,
1541            TensorLifecycleEvent::Accessed {
1542                access_type: "read".to_string(),
1543            },
1544        );
1545        let lifecycle = inspector.get_lifecycle(tid);
1546        assert!(lifecycle.is_some());
1547        let lc = lifecycle.expect("lifecycle should exist");
1548        assert_eq!(lc.events.len(), 2);
1549        assert_eq!(lc.total_accesses, 1);
1550    }
1551
1552    #[test]
1553    fn test_update_time_series_disabled() {
1554        let config = make_config();
1555        let mut inspector = TensorInspector::new(&config);
1556        let tid = Uuid::new_v4();
1557        let stats = TensorStats {
1558            shape: vec![2],
1559            dtype: "f64".to_string(),
1560            total_elements: 2,
1561            mean: 1.0,
1562            std: 0.5,
1563            min: 0.5,
1564            max: 1.5,
1565            median: 1.0,
1566            l1_norm: 2.0,
1567            l2_norm: 1.5,
1568            infinity_norm: 1.5,
1569            nan_count: 0,
1570            inf_count: 0,
1571            zero_count: 0,
1572            memory_usage_bytes: 16,
1573            sparsity: 0.0,
1574        };
1575        inspector.update_time_series(tid, stats);
1576        assert!(inspector.get_time_series(tid).is_none());
1577    }
1578
1579    #[test]
1580    fn test_update_time_series_enabled() {
1581        let config = make_config();
1582        let mut inspector = TensorInspector::new(&config);
1583        inspector.enable_monitoring(true);
1584        let tid = Uuid::new_v4();
1585        let stats = TensorStats {
1586            shape: vec![2],
1587            dtype: "f64".to_string(),
1588            total_elements: 2,
1589            mean: 1.0,
1590            std: 0.5,
1591            min: 0.5,
1592            max: 1.5,
1593            median: 1.0,
1594            l1_norm: 2.0,
1595            l2_norm: 1.5,
1596            infinity_norm: 1.5,
1597            nan_count: 0,
1598            inf_count: 0,
1599            zero_count: 0,
1600            memory_usage_bytes: 16,
1601            sparsity: 0.0,
1602        };
1603        inspector.update_time_series(tid, stats);
1604        let ts = inspector.get_time_series(tid);
1605        assert!(ts.is_some());
1606        assert_eq!(ts.expect("ts should exist").values.len(), 1);
1607    }
1608
1609    #[test]
1610    fn test_analyze_tensor_relationships_empty() {
1611        let config = make_config();
1612        let inspector = TensorInspector::new(&config);
1613        let rels = inspector.analyze_tensor_relationships();
1614        assert!(rels.is_empty());
1615    }
1616
1617    #[test]
1618    fn test_analyze_tensor_relationships_with_deps() {
1619        let config = make_config();
1620        let mut inspector = TensorInspector::new(&config);
1621        let a = Uuid::new_v4();
1622        let b = Uuid::new_v4();
1623        let c = Uuid::new_v4();
1624        inspector.track_dependency(a, b, "add", 1.0);
1625        inspector.track_dependency(a, c, "mul", 0.5);
1626        let rels = inspector.analyze_tensor_relationships();
1627        assert_eq!(rels.len(), 1);
1628        let targets = &rels[&a];
1629        assert_eq!(targets.len(), 2);
1630    }
1631
1632    #[test]
1633    fn test_get_frequent_tensors() {
1634        let config = make_config();
1635        let mut inspector = TensorInspector::new(&config);
1636        let tid = Uuid::new_v4();
1637        for _ in 0..5 {
1638            inspector.record_lifecycle_event(
1639                tid,
1640                TensorLifecycleEvent::Accessed {
1641                    access_type: "read".to_string(),
1642                },
1643            );
1644        }
1645        let frequent = inspector.get_frequent_tensors(3);
1646        assert_eq!(frequent.len(), 1);
1647        let infrequent = inspector.get_frequent_tensors(10);
1648        assert!(infrequent.is_empty());
1649    }
1650
1651    #[test]
1652    fn test_perform_advanced_analysis() {
1653        let config = make_config();
1654        let inspector = TensorInspector::new(&config);
1655        let tensor = make_tensor(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
1656        let result = inspector.perform_advanced_analysis(&tensor, None);
1657        assert!(result.is_ok());
1658        let analysis = result.expect("analysis should succeed");
1659        assert!(analysis.information_content.entropy >= 0.0);
1660    }
1661
1662    /// Regression test: without a gradient tensor, `gradient_stability` must
1663    /// be honestly `None`, never the old hardcoded `0.8`.
1664    #[test]
1665    fn test_gradient_stability_is_none_without_gradients() {
1666        let config = make_config();
1667        let inspector = TensorInspector::new(&config);
1668        let tensor = make_tensor(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
1669        let analysis = inspector
1670            .perform_advanced_analysis(&tensor, None)
1671            .expect("analysis should succeed");
1672        assert_eq!(
1673            analysis.stability_metrics.gradient_stability, None,
1674            "must be None (not the old hardcoded 0.8) when no gradients were provided"
1675        );
1676    }
1677
1678    /// Regression test: with a real gradient tensor, `gradient_stability`
1679    /// must be a real, data-dependent value computed from those gradients
1680    /// -- never the old hardcoded `0.8` -- and must actually change when the
1681    /// gradients change.
1682    #[test]
1683    fn test_gradient_stability_is_computed_from_real_gradients() {
1684        let config = make_config();
1685        let inspector = TensorInspector::new(&config);
1686        let tensor = make_tensor(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
1687
1688        let stable_gradients = make_tensor(&[0.01, 0.01, 0.01, 0.01, 0.01, 0.01], &[2, 3]);
1689        let volatile_gradients = make_tensor(&[10.0, -8.0, 6.0, -12.0, 9.0, -7.0], &[2, 3]);
1690
1691        let stable = inspector
1692            .perform_advanced_analysis(&tensor, Some(&stable_gradients))
1693            .expect("analysis should succeed")
1694            .stability_metrics
1695            .gradient_stability
1696            .expect("gradient_stability must be Some when gradients are provided");
1697        let volatile = inspector
1698            .perform_advanced_analysis(&tensor, Some(&volatile_gradients))
1699            .expect("analysis should succeed")
1700            .stability_metrics
1701            .gradient_stability
1702            .expect("gradient_stability must be Some when gradients are provided");
1703
1704        assert_ne!(stable, 0.8, "must not be the old fabricated constant");
1705        assert_ne!(volatile, 0.8, "must not be the old fabricated constant");
1706        assert!(
1707            stable > volatile,
1708            "low-variance gradients ({stable}) must score more stable than \
1709             high-variance gradients ({volatile})"
1710        );
1711    }
1712
1713    #[tokio::test]
1714    async fn test_generate_report() {
1715        let config = make_config();
1716        let mut inspector = TensorInspector::new(&config);
1717        let t = make_tensor(&[1.0, 2.0, 3.0], &[3]);
1718        let _ = inspector.inspect_tensor(&t, "report_test", None, None);
1719        let report = inspector.generate_report().await;
1720        assert!(report.is_ok());
1721        let r = report.expect("report should succeed");
1722        assert_eq!(r.total_tensors, 1);
1723        assert_eq!(r.tensors_with_issues, 0);
1724    }
1725
1726    #[tokio::test]
1727    async fn test_start() {
1728        let config = make_config();
1729        let mut inspector = TensorInspector::new(&config);
1730        let result = inspector.start().await;
1731        assert!(result.is_ok());
1732    }
1733
1734    #[test]
1735    fn test_inspection_report_methods() {
1736        let report = TensorInspectionReport {
1737            total_tensors: 5,
1738            tensors_with_issues: 0,
1739            total_memory_usage: 1024,
1740            alerts: vec![],
1741            comparisons: vec![],
1742            summary_stats: HashMap::new(),
1743        };
1744        assert!(!report.has_nan_values());
1745        assert!(!report.has_inf_values());
1746        assert_eq!(report.total_nan_count(), 0);
1747        assert_eq!(report.total_inf_count(), 0);
1748        assert!(!report.has_critical_alerts());
1749    }
1750
1751    #[test]
1752    fn test_compare_tensors() {
1753        let config = make_config();
1754        let mut inspector = TensorInspector::new(&config);
1755        let t1 = make_tensor(&[1.0, 2.0, 3.0], &[3]);
1756        let t2 = make_tensor(&[1.1, 2.1, 3.1], &[3]);
1757        let id1 = inspector.inspect_tensor(&t1, "t1", None, None).expect("inspect should succeed");
1758        let id2 = inspector.inspect_tensor(&t2, "t2", None, None).expect("inspect should succeed");
1759        let comparison = inspector.compare_tensors(id1, id2);
1760        assert!(comparison.is_ok());
1761        let c = comparison.expect("comparison should succeed");
1762        assert!(c.shape_match);
1763        assert!(c.dtype_match);
1764        // Regression: the old implementation returned a hardcoded `0.5`
1765        // (or `0.0`) `correlation` regardless of the tensors involved. Since
1766        // only summary stats are retained (see `compute_correlation`'s
1767        // docs), a real correlation cannot be computed here, and must be
1768        // honestly `None` rather than any fabricated float.
1769        assert_eq!(c.correlation, None);
1770    }
1771
1772    #[test]
1773    fn test_compare_tensors_missing() {
1774        let config = make_config();
1775        let mut inspector = TensorInspector::new(&config);
1776        let missing = Uuid::new_v4();
1777        let result = inspector.compare_tensors(missing, missing);
1778        assert!(result.is_err());
1779    }
1780}