1use anyhow::Result;
4use scirs2_core::ndarray::*; use scirs2_core::random::random; use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, VecDeque};
8use std::fmt;
9use std::time::Instant;
10use uuid::Uuid;
11
12use crate::DebugConfig;
13
14#[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#[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#[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#[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 pub correlation: Option<f64>,
73 pub shape_match: bool,
74 pub dtype_match: bool,
75}
76
77#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct SpectralAnalysis {
125 pub singular_values: Vec<f64>,
133 pub condition_number: f64,
139 pub rank: usize,
142 pub spectral_norm: f64,
144 pub effective_rank: f64,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct InformationContent {
153 pub entropy: f64,
156 pub mutual_information: Option<f64>,
164 pub value_distribution_perplexity: f64,
173 pub distinct_value_fraction: f64,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct StabilityMetrics {
185 pub numerical_stability: f64,
186 pub gradient_stability: Option<f64>,
192 pub perturbation_sensitivity: f64,
193 pub robustness_score: f64,
194}
195
196#[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#[derive(Debug)]
206pub struct TensorInspector {
207 config: DebugConfig,
208 tracked_tensors: HashMap<Uuid, TensorInfo>,
209 comparisons: Vec<TensorComparison>,
210 alerts: Vec<TensorAlert>,
211 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 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 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 pub async fn start(&mut self) -> Result<()> {
238 tracing::info!("Starting tensor inspector");
239 Ok(())
240 }
241
242 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 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 self.check_tensor_alerts(&tensor_info)?;
284
285 if self.tracked_tensors.len() < self.config.max_tracked_tensors {
287 self.tracked_tensors.insert(id, tensor_info.clone());
288 }
289
290 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 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 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 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 pub fn get_tensor_info(&self, id: Uuid) -> Option<&TensorInfo> {
368 self.tracked_tensors.get(&id)
369 }
370
371 pub fn get_all_tensors(&self) -> Vec<&TensorInfo> {
373 self.tracked_tensors.values().collect()
374 }
375
376 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 pub fn get_alerts(&self) -> &[TensorAlert] {
398 &self.alerts
399 }
400
401 pub fn clear(&mut self) {
403 self.tracked_tensors.clear();
404 self.comparisons.clear();
405 self.alerts.clear();
406 self.time_series.clear();
408 self.dependencies.clear();
409 self.lifecycles.clear();
410 self.last_analysis_time = None;
411 }
412
413 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 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 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 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 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, });
492
493 time_series.timestamps.push_back(chrono::Utc::now());
494 time_series.values.push_back(stats);
495
496 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 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 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 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 pub fn get_dependencies(&self) -> &[TensorDependency] {
572 &self.dependencies
573 }
574
575 pub fn get_lifecycle(&self, tensor_id: Uuid) -> Option<&TensorLifecycle> {
577 self.lifecycles.get(&tensor_id)
578 }
579
580 pub fn get_time_series(&self, tensor_id: Uuid) -> Option<&TensorTimeSeries> {
582 self.time_series.get(&tensor_id)
583 }
584
585 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 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 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 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 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 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 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) .collect();
719
720 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 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 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 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 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 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 (stats1.mean - stats2.mean).powi(2)
858 }
859
860 fn compute_mae(&self, stats1: &TensorStats, stats2: &TensorStats) -> f64 {
861 (stats1.mean - stats2.mean).abs()
863 }
864
865 fn compute_cosine_similarity(&self, stats1: &TensorStats, stats2: &TensorStats) -> f64 {
866 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 fn compute_correlation(&self, _stats1: &TensorStats, _stats2: &TensorStats) -> Option<f64> {
884 None
885 }
886
887 fn compute_spectral_analysis(
898 &self,
899 values: &[f64],
900 shape: &[usize],
901 ) -> Result<Option<SpectralAnalysis>> {
902 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 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 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 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 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 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 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 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 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 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 let robustness_score = numerical_stability * (1.0 - perturbation_sensitivity.min(1.0));
1049
1050 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 fn compute_relationship_analysis(&self, _values: &[f64]) -> Result<RelationshipAnalysis> {
1089 Ok(RelationshipAnalysis {
1090 cross_correlations: HashMap::new(),
1091 dependency_strength: HashMap::new(),
1092 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 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
1153pub enum AlertSeverity {
1154 Info,
1155 Warning,
1156 Critical,
1157}
1158
1159#[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#[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 #[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 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 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 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 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 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 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 #[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 #[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 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}