1#![allow(dead_code)]
10
11use anyhow::{Context, Result};
12use std::collections::HashMap;
13
14use super::analytics::{AdvancedAnalytics, HiddenStateData};
15use super::types::{
16 ActivationHeatmap, AttentionVisualization, HiddenStateAnalysis, LayerActivationStats,
17 LayerAnalysis, WeightDistribution,
18};
19
20#[derive(Debug)]
22pub struct LayerAnalyzer {
23 layer_activations: HashMap<String, Vec<LayerActivationStats>>,
25 config: LayerAnalysisConfig,
27 layer_states: HashMap<String, LayerState>,
29 layer_weights: HashMap<String, Vec<f64>>,
34 activation_grids: HashMap<String, Vec<Vec<f64>>>,
38 hidden_state_history: HashMap<String, Vec<HiddenStateData>>,
44 attention_samples: HashMap<String, AttentionSample>,
48 analytics: AdvancedAnalytics,
52}
53
54#[derive(Debug, Clone)]
57struct AttentionSample {
58 weights: Vec<Vec<f64>>,
59 input_tokens: Vec<String>,
60 output_tokens: Vec<String>,
61}
62
63#[derive(Debug, Clone)]
65pub struct LayerAnalysisConfig {
66 pub dead_neuron_threshold: f64,
68 pub saturated_neuron_threshold: f64,
70 pub max_activation_variance: f64,
72 pub min_health_score: f64,
74 pub history_length: usize,
76}
77
78impl Default for LayerAnalysisConfig {
79 fn default() -> Self {
80 Self {
81 dead_neuron_threshold: 0.1,
82 saturated_neuron_threshold: 0.1,
83 max_activation_variance: 2.0,
84 min_health_score: 0.7,
85 history_length: 100,
86 }
87 }
88}
89
90#[derive(Debug, Clone, Default)]
92struct LayerState {
93 health_scores: Vec<f64>,
95 detected_issues: Vec<String>,
97 last_analysis_step: usize,
99}
100
101impl LayerAnalyzer {
102 pub fn new() -> Self {
104 Self {
105 layer_activations: HashMap::new(),
106 config: LayerAnalysisConfig::default(),
107 layer_states: HashMap::new(),
108 layer_weights: HashMap::new(),
109 activation_grids: HashMap::new(),
110 hidden_state_history: HashMap::new(),
111 attention_samples: HashMap::new(),
112 analytics: AdvancedAnalytics::new(),
113 }
114 }
115
116 pub fn with_config(config: LayerAnalysisConfig) -> Self {
118 Self {
119 layer_activations: HashMap::new(),
120 config,
121 layer_states: HashMap::new(),
122 layer_weights: HashMap::new(),
123 activation_grids: HashMap::new(),
124 hidden_state_history: HashMap::new(),
125 attention_samples: HashMap::new(),
126 analytics: AdvancedAnalytics::new(),
127 }
128 }
129
130 pub fn record_layer_weights(&mut self, layer_name: &str, weights: Vec<f64>) {
133 self.layer_weights.insert(layer_name.to_string(), weights);
134 }
135
136 pub fn record_activation_grid(&mut self, layer_name: &str, grid: Vec<Vec<f64>>) {
140 self.activation_grids.insert(layer_name.to_string(), grid);
141 }
142
143 pub fn record_hidden_state_sample(&mut self, layer_name: &str, hidden_states: Vec<Vec<f64>>) {
148 let history = self.hidden_state_history.entry(layer_name.to_string()).or_default();
149 let training_step = history.len();
150 history.push(HiddenStateData {
151 layer_name: layer_name.to_string(),
152 hidden_states,
153 labels: None,
154 timestamp: chrono::Utc::now(),
155 training_step,
156 });
157 }
158
159 pub fn record_attention_weights(
165 &mut self,
166 layer_name: &str,
167 weights: Vec<Vec<f64>>,
168 input_tokens: Option<Vec<String>>,
169 output_tokens: Option<Vec<String>>,
170 ) {
171 let seq_len = weights.len();
172 let positional = || (0..seq_len).map(|i| format!("pos_{i}")).collect();
173 let input_tokens = input_tokens.unwrap_or_else(positional);
174 let output_tokens = output_tokens.unwrap_or_else(positional);
175 self.attention_samples.insert(
176 layer_name.to_string(),
177 AttentionSample {
178 weights,
179 input_tokens,
180 output_tokens,
181 },
182 );
183 }
184
185 pub fn record_layer_activations(&mut self, layer_name: &str, stats: LayerActivationStats) {
187 let health_score = self.calculate_layer_health_score(&stats);
189
190 let layer_stats = self.layer_activations.entry(layer_name.to_string()).or_default();
191 layer_stats.push(stats);
192
193 if layer_stats.len() > self.config.history_length {
195 layer_stats.remove(0);
196 }
197
198 let layer_state = self.layer_states.entry(layer_name.to_string()).or_default();
200 layer_state.health_scores.push(health_score);
201
202 if layer_state.health_scores.len() > 50 {
203 layer_state.health_scores.remove(0);
204 }
205
206 layer_state.last_analysis_step += 1;
207 }
208
209 pub fn record_layer_stats(&mut self, stats: LayerActivationStats) {
211 let layer_name = stats.layer_name.clone();
212 self.record_layer_activations(&layer_name, stats);
213 }
214
215 pub fn get_layer_activations(&self, layer_name: &str) -> Option<&[LayerActivationStats]> {
217 self.layer_activations.get(layer_name).map(|v| v.as_slice())
218 }
219
220 pub fn perform_layer_by_layer_analysis(&self) -> Vec<LayerAnalysis> {
222 let mut analyses = Vec::new();
223
224 for (layer_name, stats_history) in &self.layer_activations {
225 if let Some(latest_stats) = stats_history.last() {
226 let analysis = self.analyze_single_layer(layer_name, latest_stats, stats_history);
227 analyses.push(analysis);
228 }
229 }
230
231 analyses.sort_by(|a, b| {
232 a.health_score.partial_cmp(&b.health_score).unwrap_or(std::cmp::Ordering::Equal)
233 });
234 analyses
235 }
236
237 pub fn analyze_single_layer(
239 &self,
240 layer_name: &str,
241 current_stats: &LayerActivationStats,
242 stats_history: &[LayerActivationStats],
243 ) -> LayerAnalysis {
244 let layer_type = self.infer_layer_type(layer_name);
245 let health_score = self.calculate_layer_health_score(current_stats);
246 let issues = self.identify_layer_issues(current_stats, stats_history);
247 let recommendations = self.generate_layer_recommendations(&issues, &layer_type);
248 let activation_summary = self.generate_activation_summary(current_stats);
249
250 LayerAnalysis {
251 layer_name: layer_name.to_string(),
252 layer_type,
253 health_score,
254 issues,
255 recommendations,
256 activation_summary,
257 }
258 }
259
260 pub fn calculate_layer_health_score(&self, stats: &LayerActivationStats) -> f64 {
262 let mut score = 1.0;
263
264 if stats.dead_neurons_ratio > self.config.dead_neuron_threshold {
266 score -= stats.dead_neurons_ratio * 0.5;
267 }
268
269 if stats.saturated_neurons_ratio > self.config.saturated_neuron_threshold {
271 score -= stats.saturated_neurons_ratio * 0.3;
272 }
273
274 let activation_range = stats.max_activation - stats.min_activation;
276 if activation_range > 10.0 {
277 score -= 0.2;
278 }
279
280 if stats.std_activation > self.config.max_activation_variance {
282 score -= 0.2;
283 }
284
285 if stats.sparsity > 0.1 && stats.sparsity < 0.8 {
287 score += 0.1;
288 }
289
290 score.max(0.0).min(1.0)
291 }
292
293 pub fn identify_layer_issues(
295 &self,
296 current_stats: &LayerActivationStats,
297 stats_history: &[LayerActivationStats],
298 ) -> Vec<String> {
299 let mut issues = Vec::new();
300
301 if current_stats.dead_neurons_ratio > self.config.dead_neuron_threshold {
303 issues.push(format!(
304 "High dead neuron ratio: {:.1}%",
305 current_stats.dead_neurons_ratio * 100.0
306 ));
307 }
308
309 if current_stats.saturated_neurons_ratio > self.config.saturated_neuron_threshold {
311 issues.push(format!(
312 "High saturated neuron ratio: {:.1}%",
313 current_stats.saturated_neurons_ratio * 100.0
314 ));
315 }
316
317 if current_stats.max_activation - current_stats.min_activation > 100.0 {
319 issues.push("Extremely wide activation range detected".to_string());
320 }
321
322 if current_stats.std_activation > self.config.max_activation_variance {
324 issues.push("High activation variance detected".to_string());
325 }
326
327 if stats_history.len() > 5 {
329 let variance_trend = self.analyze_variance_trend(stats_history);
330 if variance_trend > 0.1 {
331 issues.push("Increasing activation variance over time".to_string());
332 }
333 }
334
335 if current_stats.mean_activation.abs() < 1e-6 {
337 issues.push("Near-zero mean activation detected".to_string());
338 }
339
340 issues
341 }
342
343 pub fn generate_layer_recommendations(
345 &self,
346 issues: &[String],
347 layer_type: &str,
348 ) -> Vec<String> {
349 let mut recommendations = Vec::new();
350
351 for issue in issues {
352 if issue.contains("dead neuron") {
353 match layer_type {
354 "Linear" => recommendations
355 .push("Consider using LeakyReLU or ELU activation".to_string()),
356 "Convolutional" => recommendations.push(
357 "Consider batch normalization or different initialization".to_string(),
358 ),
359 _ => recommendations.push(
360 "Consider different activation function or initialization".to_string(),
361 ),
362 }
363 }
364
365 if issue.contains("saturated neuron") {
366 recommendations
367 .push("Consider gradient clipping or learning rate reduction".to_string());
368 recommendations.push("Consider batch normalization".to_string());
369 }
370
371 if issue.contains("activation range") {
372 recommendations.push("Consider activation clipping or normalization".to_string());
373 }
374
375 if issue.contains("variance") {
376 recommendations.push("Consider weight initialization adjustment".to_string());
377 recommendations.push("Consider adding regularization".to_string());
378 }
379
380 if issue.contains("zero activation") {
381 recommendations
382 .push("Check weight initialization and input preprocessing".to_string());
383 }
384 }
385
386 recommendations.dedup();
387 recommendations
388 }
389
390 pub fn analyze_weight_distributions(&self) -> HashMap<String, WeightDistribution> {
395 let mut distributions = HashMap::new();
396
397 for layer_name in self.layer_weights.keys() {
398 if let Ok(distribution) = self.analyze_layer_weight_distribution(layer_name) {
399 distributions.insert(layer_name.clone(), distribution);
400 }
401 }
402
403 distributions
404 }
405
406 pub fn generate_activation_heatmaps(&self) -> HashMap<String, ActivationHeatmap> {
409 let mut heatmaps = HashMap::new();
410
411 for layer_name in self.activation_grids.keys() {
412 if let Ok(heatmap) = self.create_activation_heatmap(layer_name) {
413 heatmaps.insert(layer_name.clone(), heatmap);
414 }
415 }
416
417 heatmaps
418 }
419
420 pub fn generate_attention_visualizations(&self) -> HashMap<String, AttentionVisualization> {
423 let mut visualizations = HashMap::new();
424
425 for layer_name in self.attention_samples.keys() {
426 if let Ok(visualization) = self.create_attention_visualization(layer_name) {
427 visualizations.insert(layer_name.clone(), visualization);
428 }
429 }
430
431 visualizations
432 }
433
434 pub fn analyze_hidden_states(&self) -> HashMap<String, HiddenStateAnalysis> {
440 let mut analyses = HashMap::new();
441
442 for layer_name in self.hidden_state_history.keys() {
443 if let Ok(analysis) = self.analyze_layer_hidden_states(layer_name) {
444 analyses.insert(layer_name.clone(), analysis);
445 }
446 }
447
448 analyses
449 }
450
451 fn infer_layer_type(&self, layer_name: &str) -> String {
454 let name_lower = layer_name.to_lowercase();
455
456 if name_lower.contains("attention") || name_lower.contains("attn") {
457 "Attention".to_string()
458 } else if name_lower.contains("linear")
459 || name_lower.contains("dense")
460 || name_lower.contains("fc")
461 {
462 "Linear".to_string()
463 } else if name_lower.contains("conv") {
464 "Convolutional".to_string()
465 } else if name_lower.contains("norm")
466 || name_lower.contains("bn")
467 || name_lower.contains("ln")
468 {
469 "Normalization".to_string()
470 } else if name_lower.contains("dropout") {
471 "Dropout".to_string()
472 } else if name_lower.contains("embed") {
473 "Embedding".to_string()
474 } else {
475 "Unknown".to_string()
476 }
477 }
478
479 fn generate_activation_summary(&self, stats: &LayerActivationStats) -> String {
480 format!(
481 "Mean: {:.3}, Std: {:.3}, Range: [{:.3}, {:.3}], Dead: {:.1}%, Saturated: {:.1}%, Sparsity: {:.1}%",
482 stats.mean_activation,
483 stats.std_activation,
484 stats.min_activation,
485 stats.max_activation,
486 stats.dead_neurons_ratio * 100.0,
487 stats.saturated_neurons_ratio * 100.0,
488 stats.sparsity * 100.0
489 )
490 }
491
492 fn analyze_variance_trend(&self, stats_history: &[LayerActivationStats]) -> f64 {
493 if stats_history.len() < 2 {
494 return 0.0;
495 }
496
497 let variances: Vec<f64> = stats_history.iter().map(|s| s.std_activation.powi(2)).collect();
498 self.calculate_trend(&variances)
499 }
500
501 fn calculate_trend(&self, values: &[f64]) -> f64 {
502 if values.len() < 2 {
503 return 0.0;
504 }
505
506 let n = values.len() as f64;
507 let x_mean = (n - 1.0) / 2.0;
508 let y_mean = values.iter().sum::<f64>() / n;
509
510 let mut numerator = 0.0;
511 let mut denominator = 0.0;
512
513 for (i, &y) in values.iter().enumerate() {
514 let x = i as f64;
515 numerator += (x - x_mean) * (y - y_mean);
516 denominator += (x - x_mean).powi(2);
517 }
518
519 if denominator == 0.0 {
520 0.0
521 } else {
522 numerator / denominator
523 }
524 }
525
526 fn analyze_layer_weight_distribution(&self, layer_name: &str) -> Result<WeightDistribution> {
530 let weights = self.layer_weights.get(layer_name).with_context(|| {
531 format!(
532 "no weight tensor recorded for layer '{layer_name}'; call \
533 LayerAnalyzer::record_layer_weights first"
534 )
535 })?;
536 if weights.is_empty() {
537 anyhow::bail!("recorded weight tensor for layer '{layer_name}' is empty");
538 }
539
540 let n = weights.len() as f64;
541 let mean = weights.iter().sum::<f64>() / n;
542 let variance = weights.iter().map(|w| (w - mean).powi(2)).sum::<f64>() / n;
543 let std_dev = variance.sqrt();
544 let min = weights.iter().copied().fold(f64::INFINITY, f64::min);
545 let max = weights.iter().copied().fold(f64::NEG_INFINITY, f64::max);
546 let near_zero_count = weights.iter().filter(|w| w.abs() < 1e-6).count();
547 let sparsity = near_zero_count as f64 / n;
548
549 let distribution_shape = if std_dev > 0.0 {
552 let skewness = weights.iter().map(|w| ((w - mean) / std_dev).powi(3)).sum::<f64>() / n;
553 if skewness > 0.5 {
554 "Right-skewed"
555 } else if skewness < -0.5 {
556 "Left-skewed"
557 } else {
558 "Approximately symmetric"
559 }
560 } else {
561 "Degenerate (zero variance)"
562 }
563 .to_string();
564
565 Ok(WeightDistribution {
566 mean,
567 std_dev,
568 min,
569 max,
570 sparsity,
571 distribution_shape,
572 })
573 }
574
575 fn create_activation_heatmap(&self, layer_name: &str) -> Result<ActivationHeatmap> {
579 let grid = self.activation_grids.get(layer_name).with_context(|| {
580 format!(
581 "no activation sample recorded for layer '{layer_name}'; call \
582 LayerAnalyzer::record_activation_grid first"
583 )
584 })?;
585 if grid.is_empty() || grid[0].is_empty() {
586 anyhow::bail!("recorded activation grid for layer '{layer_name}' is empty");
587 }
588
589 let height = grid.len();
590 let width = grid[0].len();
591 let mut min_v = f64::INFINITY;
592 let mut max_v = f64::NEG_INFINITY;
593 for row in grid {
594 for &v in row {
595 min_v = min_v.min(v);
596 max_v = max_v.max(v);
597 }
598 }
599
600 Ok(ActivationHeatmap {
601 data: grid.clone(),
602 dimensions: (height, width),
603 value_range: (min_v, max_v),
604 interpretation: format!(
605 "Real captured activations for {} layer ({height}x{width})",
606 self.infer_layer_type(layer_name)
607 ),
608 })
609 }
610
611 fn create_attention_visualization(&self, layer_name: &str) -> Result<AttentionVisualization> {
617 let sample = self.attention_samples.get(layer_name).with_context(|| {
618 format!(
619 "no attention weights recorded for layer '{layer_name}'; call \
620 LayerAnalyzer::record_attention_weights first"
621 )
622 })?;
623 if sample.weights.is_empty() {
624 anyhow::bail!("recorded attention weights for layer '{layer_name}' are empty");
625 }
626
627 let seq_len = sample.weights.len();
628 let diagonal_mass: f64 =
629 (0..seq_len).map(|i| sample.weights[i].get(i).copied().unwrap_or(0.0)).sum();
630 let total_mass: f64 = sample.weights.iter().flatten().sum();
631
632 let mut patterns = Vec::new();
633 if total_mass > 0.0 {
634 let diagonal_ratio = diagonal_mass / total_mass;
635 let description = if diagonal_ratio > 0.5 {
636 "Strongly self-attending (diagonal-dominant)"
637 } else if diagonal_ratio > 0.2 {
638 "Partially local/self-attending"
639 } else {
640 "Diffuse/global attention (low diagonal mass)"
641 };
642 patterns.push(format!(
643 "{description}: {:.1}% of total attention mass on the diagonal",
644 diagonal_ratio * 100.0
645 ));
646 } else {
647 patterns.push("All-zero attention weights recorded".to_string());
648 }
649
650 Ok(AttentionVisualization {
651 attention_weights: sample.weights.clone(),
652 input_tokens: sample.input_tokens.clone(),
653 output_tokens: sample.output_tokens.clone(),
654 patterns,
655 })
656 }
657
658 fn analyze_layer_hidden_states(&self, layer_name: &str) -> Result<HiddenStateAnalysis> {
665 let history = self.hidden_state_history.get(layer_name).with_context(|| {
666 format!(
667 "no hidden-state samples recorded for layer '{layer_name}'; call \
668 LayerAnalyzer::record_hidden_state_sample first"
669 )
670 })?;
671 if history.is_empty() {
672 anyhow::bail!("recorded hidden-state history for layer '{layer_name}' is empty");
673 }
674
675 let all_states: Vec<Vec<f64>> =
676 history.iter().flat_map(|sample| sample.hidden_states.iter().cloned()).collect();
677 if all_states.is_empty() || all_states[0].is_empty() {
678 anyhow::bail!(
679 "recorded hidden-state samples for layer '{layer_name}' contain no vectors"
680 );
681 }
682
683 let dimensionality = all_states[0].len();
684 let information_content = information_content(&all_states);
685
686 let clustering_results = self
689 .analytics
690 .perform_clustering_analysis(&all_states)
691 .with_context(|| format!("clustering analysis for layer '{layer_name}'"))?;
692
693 let layer_refs: Vec<&HiddenStateData> = history.iter().collect();
694 let temporal_dynamics = self
695 .analytics
696 .analyze_temporal_dynamics(&layer_refs)
697 .with_context(|| format!("temporal-dynamics analysis for layer '{layer_name}'"))?;
698
699 let representation_stability =
700 self.analytics.assess_representation_stability(&all_states).with_context(|| {
701 format!("representation-stability analysis for layer '{layer_name}'")
702 })?;
703
704 Ok(HiddenStateAnalysis {
705 dimensionality,
706 information_content,
707 clustering_results,
708 temporal_dynamics,
709 representation_stability,
710 })
711 }
712
713 pub fn clear(&mut self) {
715 self.layer_activations.clear();
716 self.layer_states.clear();
717 self.layer_weights.clear();
718 self.activation_grids.clear();
719 self.hidden_state_history.clear();
720 self.attention_samples.clear();
721 }
722}
723
724fn information_content(hidden_states: &[Vec<f64>]) -> f64 {
729 if hidden_states.len() < 2 {
730 return 0.0;
731 }
732 let dimensions = hidden_states[0].len();
733 if dimensions == 0 {
734 return 0.0;
735 }
736
737 let mut total_variance = 0.0;
738 for dim in 0..dimensions {
739 let values: Vec<f64> =
740 hidden_states.iter().filter_map(|state| state.get(dim).copied()).collect();
741 if values.len() > 1 {
742 let mean = values.iter().sum::<f64>() / values.len() as f64;
743 let variance =
744 values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
745 total_variance += variance;
746 }
747 }
748 total_variance / dimensions as f64
749}
750
751impl Default for LayerAnalyzer {
752 fn default() -> Self {
753 Self::new()
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760
761 fn create_test_layer_stats(layer_name: &str) -> LayerActivationStats {
762 LayerActivationStats {
763 layer_name: layer_name.to_string(),
764 mean_activation: 0.5,
765 std_activation: 0.2,
766 min_activation: 0.0,
767 max_activation: 1.0,
768 dead_neurons_ratio: 0.05,
769 saturated_neurons_ratio: 0.03,
770 sparsity: 0.3,
771 output_shape: vec![128, 256],
772 }
773 }
774
775 #[test]
776 fn test_layer_analyzer_creation() {
777 let analyzer = LayerAnalyzer::new();
778 assert_eq!(analyzer.layer_activations.len(), 0);
779 }
780
781 #[test]
782 fn test_record_layer_activations() {
783 let mut analyzer = LayerAnalyzer::new();
784 let stats = create_test_layer_stats("test_layer");
785
786 analyzer.record_layer_activations("test_layer", stats);
787 assert_eq!(analyzer.layer_activations.len(), 1);
788 assert!(analyzer.layer_activations.contains_key("test_layer"));
789 }
790
791 #[test]
792 fn test_layer_health_score_calculation() {
793 let analyzer = LayerAnalyzer::new();
794 let stats = create_test_layer_stats("test_layer");
795
796 let health_score = analyzer.calculate_layer_health_score(&stats);
797 assert!(health_score > 0.0 && health_score <= 1.0);
798 }
799
800 #[test]
801 fn test_layer_type_inference() {
802 let analyzer = LayerAnalyzer::new();
803
804 assert_eq!(analyzer.infer_layer_type("attention_layer"), "Attention");
805 assert_eq!(analyzer.infer_layer_type("linear_projection"), "Linear");
806 assert_eq!(analyzer.infer_layer_type("conv2d_layer"), "Convolutional");
807 assert_eq!(analyzer.infer_layer_type("batch_norm"), "Normalization");
808 }
809
810 #[test]
811 fn test_issue_identification() {
812 let analyzer = LayerAnalyzer::new();
813 let mut stats = create_test_layer_stats("test_layer");
814 stats.dead_neurons_ratio = 0.2; let issues = analyzer.identify_layer_issues(&stats, &[]);
817 assert!(!issues.is_empty());
818 assert!(issues[0].contains("dead neuron"));
819 }
820
821 #[test]
822 fn test_layer_analysis() {
823 let analyzer = LayerAnalyzer::new();
824 let stats = create_test_layer_stats("attention_layer");
825 let history = vec![stats.clone()];
826
827 let analysis = analyzer.analyze_single_layer("attention_layer", &stats, &history);
828 assert_eq!(analysis.layer_name, "attention_layer");
829 assert_eq!(analysis.layer_type, "Attention");
830 assert!(analysis.health_score > 0.0);
831 }
832
833 #[test]
834 fn test_weight_distribution_errors_without_recorded_weights() {
835 let analyzer = LayerAnalyzer::new();
836 let result = analyzer.analyze_layer_weight_distribution("unknown_layer");
837 assert!(
838 result.is_err(),
839 "must error rather than fabricate a distribution"
840 );
841 }
842
843 #[test]
844 fn test_weight_distribution_is_computed_from_real_tensor() {
845 let mut analyzer = LayerAnalyzer::new();
846 let weights = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
848 analyzer.record_layer_weights("fc1", weights.clone());
849
850 let d1 = analyzer.analyze_layer_weight_distribution("fc1").expect("weights recorded");
851 let d2 = analyzer.analyze_layer_weight_distribution("fc1").expect("weights recorded");
852
853 assert_eq!(d1.mean, d2.mean);
856 assert_eq!(d1.std_dev, d2.std_dev);
857
858 let n = weights.len() as f64;
859 let expected_mean = weights.iter().sum::<f64>() / n;
860 let expected_variance =
861 weights.iter().map(|w| (w - expected_mean).powi(2)).sum::<f64>() / n;
862 assert!((d1.mean - expected_mean).abs() < 1e-12);
863 assert!((d1.std_dev - expected_variance.sqrt()).abs() < 1e-12);
864 assert_eq!(d1.min, -2.0);
865 assert_eq!(d1.max, 2.0);
866 assert!((d1.sparsity - 0.2).abs() < 1e-12); }
868
869 #[test]
870 fn test_activation_heatmap_errors_without_recorded_grid() {
871 let analyzer = LayerAnalyzer::new();
872 let result = analyzer.create_activation_heatmap("unknown_layer");
873 assert!(
874 result.is_err(),
875 "must error rather than fabricate a heatmap"
876 );
877 }
878
879 #[test]
880 fn test_activation_heatmap_reflects_real_grid() {
881 let mut analyzer = LayerAnalyzer::new();
882 let grid = vec![vec![0.0, 1.0, 2.0], vec![3.0, 4.0, 5.0]];
883 analyzer.record_activation_grid("conv1", grid.clone());
884
885 let heatmap = analyzer.create_activation_heatmap("conv1").expect("grid recorded");
886 assert_eq!(heatmap.data, grid);
887 assert_eq!(heatmap.dimensions, (2, 3));
888 assert_eq!(heatmap.value_range, (0.0, 5.0));
889 }
890
891 #[test]
892 fn test_attention_visualization_errors_without_recorded_weights() {
893 let analyzer = LayerAnalyzer::new();
894 let result = analyzer.create_attention_visualization("unknown_layer");
895 assert!(
896 result.is_err(),
897 "must error rather than fabricate attention weights"
898 );
899 }
900
901 #[test]
902 fn test_attention_visualization_uses_real_weights_and_tokens() {
903 let mut analyzer = LayerAnalyzer::new();
904 let weights = vec![
906 vec![0.9, 0.05, 0.05],
907 vec![0.05, 0.9, 0.05],
908 vec![0.05, 0.05, 0.9],
909 ];
910 analyzer.record_attention_weights(
911 "self_attn",
912 weights.clone(),
913 Some(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
914 None,
915 );
916
917 let viz = analyzer.create_attention_visualization("self_attn").expect("weights recorded");
918 assert_eq!(viz.attention_weights, weights);
919 assert_eq!(viz.input_tokens, vec!["a", "b", "c"]);
920 assert_eq!(viz.output_tokens, vec!["pos_0", "pos_1", "pos_2"]);
924 assert!(viz.patterns.iter().any(|p| p.contains("self-attending")));
925 }
926
927 #[test]
928 fn test_hidden_state_analysis_errors_without_recorded_samples() {
929 let analyzer = LayerAnalyzer::new();
930 let result = analyzer.analyze_layer_hidden_states("unknown_layer");
931 assert!(
932 result.is_err(),
933 "must error rather than fabricate a hidden-state analysis"
934 );
935 }
936
937 #[test]
938 fn test_hidden_state_analysis_computes_real_statistics() {
939 let mut analyzer = LayerAnalyzer::new();
940
941 for sample_idx in 0..2 {
946 let batch: Vec<Vec<f64>> = (0..30)
947 .map(|i| {
948 let x = (sample_idx * 30 + i) as f64;
949 vec![x, x * 2.0, x.sin()]
950 })
951 .collect();
952 analyzer.record_hidden_state_sample("hidden1", batch);
953 }
954
955 let analysis = analyzer.analyze_layer_hidden_states("hidden1").expect("samples recorded");
956 assert_eq!(analysis.dimensionality, 3);
957 assert!(analysis.information_content > 0.0);
958 assert!(analysis.clustering_results.num_clusters > 0);
959 assert_eq!(analysis.clustering_results.cluster_assignments.len(), 60);
960 }
961}