Skip to main content

trustformers_debug/model_diagnostics/
layers.rs

1//! Layer-level analysis and activation monitoring.
2//!
3//! This module provides comprehensive layer-level diagnostics including
4//! activation analysis, weight distribution monitoring, attention visualization,
5//! and layer health assessment for deep learning models.
6// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
7// are retained for the data model, serialization completeness, and future consumers that
8// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
9#![allow(dead_code)]
10
11use anyhow::{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/// Layer analyzer for monitoring and analyzing individual layer behavior.
21#[derive(Debug)]
22pub struct LayerAnalyzer {
23    /// Layer activation statistics history
24    layer_activations: HashMap<String, Vec<LayerActivationStats>>,
25    /// Layer health monitoring configuration
26    config: LayerAnalysisConfig,
27    /// Current layer states
28    layer_states: HashMap<String, LayerState>,
29    /// Real weight tensors (flattened), recorded by the caller via
30    /// [`LayerAnalyzer::record_layer_weights`]. Drives
31    /// [`LayerAnalyzer::analyze_layer_weight_distribution`] -- no tensor
32    /// recorded means no distribution can be reported, honestly.
33    layer_weights: HashMap<String, Vec<f64>>,
34    /// Real activation grids (e.g. `[batch][feature]` or a spatial slice),
35    /// recorded by the caller via [`LayerAnalyzer::record_activation_grid`].
36    /// Drives [`LayerAnalyzer::create_activation_heatmap`].
37    activation_grids: HashMap<String, Vec<Vec<f64>>>,
38    /// Real hidden-state vector history per layer, recorded by the caller via
39    /// [`LayerAnalyzer::record_hidden_state_sample`]. Drives
40    /// [`LayerAnalyzer::analyze_layer_hidden_states`] (dimensionality,
41    /// information content, clustering, temporal dynamics, representation
42    /// stability), delegated to [`AdvancedAnalytics`]'s real implementations.
43    hidden_state_history: HashMap<String, Vec<HiddenStateData>>,
44    /// Real attention-weight matrices recorded per layer, with the tokens
45    /// they were computed over. Drives
46    /// [`LayerAnalyzer::create_attention_visualization`].
47    attention_samples: HashMap<String, AttentionSample>,
48    /// Real numeric routines (k-means clustering, temporal-dynamics and
49    /// representation-stability statistics) shared with [`AdvancedAnalytics`]
50    /// rather than reimplemented here.
51    analytics: AdvancedAnalytics,
52}
53
54/// A real, caller-supplied attention weight matrix plus the tokens it was
55/// computed over.
56#[derive(Debug, Clone)]
57struct AttentionSample {
58    weights: Vec<Vec<f64>>,
59    input_tokens: Vec<String>,
60    output_tokens: Vec<String>,
61}
62
63/// Configuration for layer analysis.
64#[derive(Debug, Clone)]
65pub struct LayerAnalysisConfig {
66    /// Threshold for dead neuron detection
67    pub dead_neuron_threshold: f64,
68    /// Threshold for saturated neuron detection
69    pub saturated_neuron_threshold: f64,
70    /// Maximum acceptable activation variance
71    pub max_activation_variance: f64,
72    /// Minimum acceptable layer health score
73    pub min_health_score: f64,
74    /// History length for temporal analysis
75    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/// Current state information for a layer.
91#[derive(Debug, Clone, Default)]
92struct LayerState {
93    /// Health score history
94    health_scores: Vec<f64>,
95    /// Issues detected in the layer
96    detected_issues: Vec<String>,
97    /// Last analysis timestamp
98    last_analysis_step: usize,
99}
100
101impl LayerAnalyzer {
102    /// Create a new layer analyzer.
103    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    /// Create a new layer analyzer with custom configuration.
117    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    /// Record a real weight tensor (flattened) for a layer. Required before
131    /// `Self::analyze_layer_weight_distribution` can report anything.
132    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    /// Record a real captured activation grid (e.g. `[batch][feature]`, or a
137    /// spatial `[height][width]` slice) for a layer. Required before
138    /// `Self::create_activation_heatmap` can report anything.
139    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    /// Record one real hidden-state sample (a batch of hidden-state vectors
144    /// captured at one point in time/training) for a layer. Accumulates into
145    /// that layer's history; required before
146    /// `Self::analyze_layer_hidden_states` can report anything.
147    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    /// Record a real attention-weight matrix for a layer. `input_tokens` /
160    /// `output_tokens` default to positional labels (`pos_0`, `pos_1`, ...)
161    /// when not supplied -- a real (if generic) label derived from the
162    /// matrix's own shape, never fabricated content. Required before
163    /// `Self::create_attention_visualization` can report anything.
164    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    /// Record layer activation statistics.
186    pub fn record_layer_activations(&mut self, layer_name: &str, stats: LayerActivationStats) {
187        // Calculate health score before mutable borrow
188        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        // Maintain reasonable history length
194        if layer_stats.len() > self.config.history_length {
195            layer_stats.remove(0);
196        }
197
198        // Update layer state
199        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    /// Record layer statistics (extracts layer name and calls record_layer_activations).
210    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    /// Get layer activation statistics for a specific layer.
216    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    /// Perform comprehensive layer-by-layer analysis.
221    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    /// Analyze a single layer comprehensively.
238    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    /// Calculate layer health score.
261    pub fn calculate_layer_health_score(&self, stats: &LayerActivationStats) -> f64 {
262        let mut score = 1.0;
263
264        // Penalize dead neurons
265        if stats.dead_neurons_ratio > self.config.dead_neuron_threshold {
266            score -= stats.dead_neurons_ratio * 0.5;
267        }
268
269        // Penalize saturated neurons
270        if stats.saturated_neurons_ratio > self.config.saturated_neuron_threshold {
271            score -= stats.saturated_neurons_ratio * 0.3;
272        }
273
274        // Penalize extreme activation ranges
275        let activation_range = stats.max_activation - stats.min_activation;
276        if activation_range > 10.0 {
277            score -= 0.2;
278        }
279
280        // Penalize high variance
281        if stats.std_activation > self.config.max_activation_variance {
282            score -= 0.2;
283        }
284
285        // Bonus for good sparsity
286        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    /// Identify issues in a layer.
294    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        // Dead neuron issues
302        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        // Saturated neuron issues
310        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        // Activation range issues
318        if current_stats.max_activation - current_stats.min_activation > 100.0 {
319            issues.push("Extremely wide activation range detected".to_string());
320        }
321
322        // Variance issues
323        if current_stats.std_activation > self.config.max_activation_variance {
324            issues.push("High activation variance detected".to_string());
325        }
326
327        // Temporal issues (if history is available)
328        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        // Zero activation issues
336        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    /// Generate recommendations for layer improvement.
344    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    /// Analyze weight distributions for every layer with a real weight
391    /// tensor on record (see [`Self::record_layer_weights`]). Layers without
392    /// one are simply absent from the result -- never filled with a
393    /// fabricated distribution.
394    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    /// Generate activation heatmaps for every layer with a real recorded
407    /// activation grid (see [`Self::record_activation_grid`]).
408    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    /// Generate attention visualizations for every layer with real recorded
421    /// attention weights (see [`Self::record_attention_weights`]).
422    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    /// Analyze hidden states for every layer with real recorded hidden-state
435    /// samples (see [`Self::record_hidden_state_sample`]). A layer is
436    /// omitted (never fabricated) when its sample count is too small for a
437    /// statistically meaningful analysis -- see
438    /// `Self::analyze_layer_hidden_states`.
439    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    // Helper methods
452
453    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    /// Compute a real weight distribution from the tensor recorded via
527    /// [`Self::record_layer_weights`]. Errors (rather than fabricating) when
528    /// no tensor has been recorded for this layer.
529    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        // Real (Fisher-Pearson) skewness of the actual data, rather than
550        // always reporting "Normal".
551        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    /// Build a real activation heatmap from the grid recorded via
576    /// [`Self::record_activation_grid`]. Errors (rather than fabricating)
577    /// when no grid has been recorded for this layer.
578    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    /// Build a real attention visualization from the weights recorded via
612    /// [`Self::record_attention_weights`]. Errors (rather than fabricating)
613    /// when no weights have been recorded for this layer. `patterns` is a
614    /// real, deterministic description derived from the matrix's own
615    /// diagonal mass, not a fixed literal list.
616    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    /// Compute a real hidden-state analysis from the samples recorded via
659    /// [`Self::record_hidden_state_sample`]. Errors (rather than
660    /// fabricating) when no samples have been recorded, or when there are
661    /// too few for a statistically meaningful analysis (clustering and
662    /// temporal-dynamics both require a minimum sample count, enforced by
663    /// [`AdvancedAnalytics`] and propagated here rather than worked around).
664    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        // Real k-means clustering / temporal-dynamics / stability statistics,
687        // shared with `AdvancedAnalytics` rather than reimplemented here.
688        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    /// Clear all layer analysis data, including recorded real tensors.
714    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
724/// Real information-content proxy: total per-dimension variance across the
725/// given hidden-state vectors, normalized by dimensionality. Zero for a
726/// single (or no) sample, since variance is undefined with fewer than two
727/// observations.
728fn 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; // High dead neuron ratio
815
816        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        // Deterministic tensor: mean 0, known std_dev, known sparsity.
847        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        // The old implementation drew fresh random numbers on every call; a
854        // real computation over the same recorded tensor is deterministic.
855        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); // exactly one of five is ~0
867    }
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        // Diagonal-dominant matrix: each position mostly attends to itself.
905        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        // No input tokens were supplied for the output side, so it must fall
921        // back to real positional labels derived from the matrix shape --
922        // never fabricated token text.
923        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        // Two samples (>= 2 for temporal dynamics), 30 vectors each (60 total
942        // >= the 50-sample floor `AdvancedAnalytics` requires for clustering
943        // to be statistically meaningful), all derived deterministically --
944        // never randomly.
945        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}