Skip to main content

trustformers_debug/gradient_debugger/
visualization.rs

1//! Gradient Flow Visualization Data Generation
2//!
3//! This module provides comprehensive visualization data generation for gradient flow
4//! analysis, including network topology, temporal flows, and critical path identification.
5
6use super::types::*;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Complete gradient flow visualization data
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct GradientFlowVisualization {
13    pub layer_flows: HashMap<String, GradientLayerFlow>,
14    pub temporal_flows: Vec<TemporalGradientFlow>,
15    pub flow_network: GradientFlowNetwork,
16    pub critical_paths: Vec<CriticalGradientPath>,
17    pub vanishing_regions: Vec<VanishingRegion>,
18    pub exploding_regions: Vec<ExplodingRegion>,
19    pub dead_zones: Vec<GradientDeadZone>,
20    pub visualization_config: GradientVisualizationConfig,
21}
22
23/// Gradient flow data for a specific layer
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct GradientLayerFlow {
26    pub layer_name: String,
27    pub gradient_magnitudes: Vec<f64>,
28    pub gradient_directions: Vec<GradientDirection>,
29    pub flow_consistency: f64,
30    pub bottleneck_score: f64,
31    pub information_flow_rate: f64,
32}
33
34/// Direction and characteristics of gradient flow
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct GradientDirection {
37    pub step: usize,
38    /// Signed change in gradient norm vs. the previous recorded step
39    /// (`norm(step) - norm(step - 1)`; `0.0` on the first step, where
40    /// there is no previous value to compare against). Positive means the
41    /// norm grew, negative means it shrank. This is a real, computed
42    /// trend indicator -- not a per-parameter gradient direction vector:
43    /// [`GradientHistory`] retains only reduced per-step statistics
44    /// (norm/mean/std), so a true high-dimensional direction is not
45    /// available here. Previously misnamed `direction_vector` and set to
46    /// `vec![norm]`, which duplicated `magnitude` rather than carrying
47    /// any directional information.
48    pub norm_delta: f64,
49    pub magnitude: f64,
50    pub consistency_score: f64,
51}
52
53/// Temporal gradient flow at a specific timestep
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct TemporalGradientFlow {
56    pub step: usize,
57    pub layer_name: String,
58    pub gradient_magnitude: f64,
59    pub flow_direction: FlowDirection,
60    pub stability_score: f64,
61}
62
63/// Flow direction classification
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub enum FlowDirection {
66    Forward,
67    Backward,
68    Oscillating,
69    Stagnant,
70}
71
72/// Network representation of gradient flows
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct GradientFlowNetwork {
75    pub nodes: Vec<FlowNode>,
76    pub edges: Vec<FlowEdge>,
77    pub network_metrics: NetworkMetrics,
78}
79
80/// Node in the gradient flow network
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct FlowNode {
83    pub layer_name: String,
84    pub node_type: NodeType,
85    pub gradient_strength: f64,
86    pub connectivity: usize,
87    pub influence_score: f64,
88}
89
90/// Type of node in the flow network
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum NodeType {
93    Source,
94    Sink,
95    Bottleneck,
96    Amplifier,
97    Normal,
98}
99
100/// Edge in the gradient flow network
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct FlowEdge {
103    pub from_layer: String,
104    pub to_layer: String,
105    pub flow_strength: f64,
106    pub flow_consistency: f64,
107    pub edge_type: EdgeType,
108}
109
110/// Type of edge in the flow network
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub enum EdgeType {
113    Strong,
114    Weak,
115    Intermittent,
116    Blocked,
117}
118
119/// Network-level gradient flow metrics
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct NetworkMetrics {
122    pub overall_flow_efficiency: f64,
123    pub network_connectivity: f64,
124    pub bottleneck_density: f64,
125    pub flow_stability: f64,
126    pub information_propagation_speed: f64,
127}
128
129/// Critical path in gradient flow
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct CriticalGradientPath {
132    pub path_id: String,
133    pub layers: Vec<String>,
134    pub path_length: usize,
135    pub total_flow_strength: f64,
136    pub bottleneck_layers: Vec<String>,
137    /// Share of the path's layers classified as bottlenecks, in `[0, 1]`.
138    /// Previously the constant `0.8`.
139    pub criticality_score: f64,
140    /// Mean shortfall of the path's edge flow-consistency from `1.0`, i.e. how
141    /// much consistency is left to gain; `None` for a path with no edges.
142    /// Previously the constant `0.6`.
143    pub optimization_potential: Option<f64>,
144}
145
146/// Region where gradients are vanishing
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct VanishingRegion {
149    pub region_id: String,
150    pub affected_layers: Vec<String>,
151    pub severity_level: VanishingSeverity,
152    pub extent: RegionExtent,
153    pub mitigation_suggestions: Vec<String>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub enum VanishingSeverity {
158    Mild,
159    Moderate,
160    Severe,
161    Critical,
162}
163
164/// Region where gradients are exploding
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct ExplodingRegion {
167    pub region_id: String,
168    pub affected_layers: Vec<String>,
169    pub severity_level: ExplodingSeverity,
170    pub extent: RegionExtent,
171    pub mitigation_suggestions: Vec<String>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub enum ExplodingSeverity {
176    Mild,
177    Moderate,
178    Severe,
179    Critical,
180}
181
182/// Spatial extent of a gradient region
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct RegionExtent {
185    pub start_layer: String,
186    pub end_layer: String,
187    /// Real element count of the affected layer's gradient tensor, when a
188    /// caller has reported one via
189    /// [`super::debugger::GradientDebugger::set_layer_parameter_count`].
190    /// `GradientHistory` only ever holds reduced scalar statistics
191    /// (norm/mean/std) -- never the tensor itself -- so this is an honest
192    /// `None`, not a placeholder, until a caller with access to the real
193    /// shape opts in.
194    pub affected_parameters: Option<usize>,
195    pub duration_steps: usize,
196}
197
198/// Zone where gradient flow is effectively dead
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct GradientDeadZone {
201    pub zone_id: String,
202    pub affected_layers: Vec<String>,
203    pub dead_duration: usize,
204    pub recovery_potential: RecoveryPotential,
205    pub intervention_required: bool,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub enum RecoveryPotential {
210    High,
211    Medium,
212    Low,
213    None,
214}
215
216/// Configuration for gradient visualization
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct GradientVisualizationConfig {
219    pub show_temporal_flows: bool,
220    pub show_critical_paths: bool,
221    pub show_problem_regions: bool,
222    pub color_scheme: ColorScheme,
223    pub temporal_window: usize,
224    pub flow_threshold: f64,
225}
226
227impl Default for GradientVisualizationConfig {
228    fn default() -> Self {
229        Self {
230            show_temporal_flows: true,
231            show_critical_paths: true,
232            show_problem_regions: true,
233            color_scheme: ColorScheme::Default,
234            temporal_window: 50,
235            flow_threshold: 0.01,
236        }
237    }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub enum ColorScheme {
242    Default,
243    HighContrast,
244    ColorBlind,
245    Monochrome,
246}
247
248/// Gradient flow visualization generator
249#[derive(Debug, Default)]
250pub struct GradientFlowVisualizer {
251    config: GradientVisualizationConfig,
252}
253
254impl GradientFlowVisualizer {
255    pub fn new(config: GradientVisualizationConfig) -> Self {
256        Self { config }
257    }
258
259    pub fn generate_visualization(
260        &self,
261        gradient_histories: &HashMap<String, GradientHistory>,
262        current_step: usize,
263    ) -> GradientFlowVisualization {
264        let layer_flows = self.generate_layer_flows(gradient_histories);
265        let temporal_flows = self.generate_temporal_flows(gradient_histories, current_step);
266        let flow_network = self.build_gradient_flow_network(&layer_flows);
267        let critical_paths = self.identify_critical_gradient_paths(&flow_network);
268        let vanishing_regions = self.identify_vanishing_regions(gradient_histories);
269        let exploding_regions = self.identify_exploding_regions(gradient_histories);
270        let dead_zones = self.identify_gradient_dead_zones(gradient_histories);
271
272        GradientFlowVisualization {
273            layer_flows,
274            temporal_flows,
275            flow_network,
276            critical_paths,
277            vanishing_regions,
278            exploding_regions,
279            dead_zones,
280            visualization_config: self.config.clone(),
281        }
282    }
283
284    fn generate_layer_flows(
285        &self,
286        gradient_histories: &HashMap<String, GradientHistory>,
287    ) -> HashMap<String, GradientLayerFlow> {
288        let mut layer_flows = HashMap::new();
289
290        for (layer_name, history) in gradient_histories {
291            let gradient_magnitudes: Vec<f64> = history.gradient_norms.iter().cloned().collect();
292            let gradient_directions = self.compute_gradient_directions(history);
293            let flow_consistency = self.compute_flow_consistency(history);
294            let bottleneck_score = self.compute_bottleneck_score(history);
295            let information_flow_rate = self.compute_information_flow_rate(history);
296
297            let flow_data = GradientLayerFlow {
298                layer_name: layer_name.clone(),
299                gradient_magnitudes,
300                gradient_directions,
301                flow_consistency,
302                bottleneck_score,
303                information_flow_rate,
304            };
305
306            layer_flows.insert(layer_name.clone(), flow_data);
307        }
308
309        layer_flows
310    }
311
312    fn compute_gradient_directions(&self, history: &GradientHistory) -> Vec<GradientDirection> {
313        let mut directions = Vec::new();
314
315        for (i, (&norm, &step)) in
316            history.gradient_norms.iter().zip(history.step_numbers.iter()).enumerate()
317        {
318            let magnitude = norm;
319            let prev_norm = (i > 0).then(|| history.gradient_norms[i - 1]);
320            // Real signed change vs. the previous step -- see the field's
321            // doc comment for why this replaces the old `vec![norm]`
322            // "direction vector".
323            let norm_delta = prev_norm.map(|prev| norm - prev).unwrap_or(0.0);
324            let consistency_score = match prev_norm {
325                Some(prev) => 1.0 - ((norm - prev).abs() / (norm + prev + 1e-8)),
326                None => 1.0,
327            };
328
329            directions.push(GradientDirection {
330                step,
331                norm_delta,
332                magnitude,
333                consistency_score,
334            });
335        }
336
337        directions
338    }
339
340    fn compute_flow_consistency(&self, history: &GradientHistory) -> f64 {
341        if history.gradient_norms.len() < 2 {
342            return 1.0;
343        }
344
345        let variations: Vec<f64> = history
346            .gradient_norms
347            .iter()
348            .collect::<Vec<&f64>>()
349            .windows(2)
350            .map(|pair| (*pair[1] - *pair[0]).abs() / (*pair[0] + 1e-8))
351            .collect();
352
353        let avg_variation = variations.iter().sum::<f64>() / variations.len() as f64;
354        (1.0_f64 / (1.0 + avg_variation)).min(1.0)
355    }
356
357    fn compute_bottleneck_score(&self, history: &GradientHistory) -> f64 {
358        if history.gradient_norms.is_empty() {
359            return 0.0;
360        }
361
362        let mean = history.gradient_norms.iter().sum::<f64>() / history.gradient_norms.len() as f64;
363        let min_val = history.gradient_norms.iter().cloned().fold(f64::INFINITY, f64::min);
364
365        if mean == 0.0 {
366            return 1.0;
367        }
368
369        1.0 - (min_val / mean).min(1.0)
370    }
371
372    fn compute_information_flow_rate(&self, history: &GradientHistory) -> f64 {
373        if history.gradient_norms.len() < 2 {
374            return 0.0;
375        }
376
377        let total_change: f64 = history
378            .gradient_norms
379            .iter()
380            .collect::<Vec<&f64>>()
381            .windows(2)
382            .map(|pair| (*pair[1] - *pair[0]).abs())
383            .sum();
384
385        let time_span = history.gradient_norms.len() as f64;
386        total_change / time_span
387    }
388
389    fn generate_temporal_flows(
390        &self,
391        gradient_histories: &HashMap<String, GradientHistory>,
392        current_step: usize,
393    ) -> Vec<TemporalGradientFlow> {
394        let mut temporal_flows = Vec::new();
395
396        for (layer_name, history) in gradient_histories {
397            if let Some(latest_norm) = history.gradient_norms.back() {
398                let flow_direction = self.get_latest_flow_direction(history);
399                let stability_score = self.compute_stability_score(history);
400
401                temporal_flows.push(TemporalGradientFlow {
402                    step: current_step,
403                    layer_name: layer_name.clone(),
404                    gradient_magnitude: *latest_norm,
405                    flow_direction,
406                    stability_score,
407                });
408            }
409        }
410
411        temporal_flows
412    }
413
414    fn get_latest_flow_direction(&self, history: &GradientHistory) -> FlowDirection {
415        if history.gradient_norms.len() < 3 {
416            return FlowDirection::Forward;
417        }
418
419        let recent: Vec<f64> = history.gradient_norms.iter().rev().take(3).cloned().collect();
420        let trend = recent[0] - recent[2]; // Latest - oldest in recent window
421
422        if trend.abs() < 1e-6 {
423            FlowDirection::Stagnant
424        } else if trend > 0.0 {
425            FlowDirection::Forward
426        } else {
427            // Check for oscillation
428            let changes: Vec<f64> = recent.windows(2).map(|pair| pair[0] - pair[1]).collect();
429            let sign_changes = changes.windows(2).filter(|pair| pair[0] * pair[1] < 0.0).count();
430
431            if sign_changes > 0 {
432                FlowDirection::Oscillating
433            } else {
434                FlowDirection::Backward
435            }
436        }
437    }
438
439    fn compute_stability_score(&self, history: &GradientHistory) -> f64 {
440        if history.gradient_norms.len() < 3 {
441            return 1.0;
442        }
443
444        let recent: Vec<f64> = history.gradient_norms.iter().rev().take(5).cloned().collect();
445        let mean = recent.iter().sum::<f64>() / recent.len() as f64;
446        let variance =
447            recent.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / recent.len() as f64;
448
449        1.0 / (1.0 + variance)
450    }
451
452    fn build_gradient_flow_network(
453        &self,
454        layer_flows: &HashMap<String, GradientLayerFlow>,
455    ) -> GradientFlowNetwork {
456        let mut nodes = Vec::new();
457        let mut edges = Vec::new();
458
459        // Create nodes
460        for (layer_name, flow) in layer_flows {
461            let node_type = self.classify_node_type(flow);
462            let gradient_strength = flow.gradient_magnitudes.iter().sum::<f64>()
463                / flow.gradient_magnitudes.len() as f64;
464            // Every layer is treated as connected to every other, because no
465            // real layer topology is available here (see the edge construction
466            // below): `connectivity` is therefore the same for every node and
467            // carries no per-layer information.
468            let connectivity = layer_flows.len();
469            let influence_score = gradient_strength * flow.flow_consistency;
470
471            nodes.push(FlowNode {
472                layer_name: layer_name.clone(),
473                node_type,
474                gradient_strength,
475                connectivity,
476                influence_score,
477            });
478        }
479
480        // Edges chain the layers in NAME ORDER, which is an assumption, not
481        // measured topology: `GradientDebugger` records per-layer statistics
482        // keyed by name and never learns which layer feeds which. Name order is
483        // right for the usual `layer_0`, `layer_1`, ... naming and wrong for
484        // any other. It is at least deterministic -- this used to iterate a
485        // `HashMap`, so the "network" was re-wired differently on every run of
486        // the same data.
487        let mut layer_names: Vec<String> = layer_flows.keys().cloned().collect();
488        layer_names.sort();
489        for i in 0..layer_names.len().saturating_sub(1) {
490            let from_layer = &layer_names[i];
491            let to_layer = &layer_names[i + 1];
492
493            if let (Some(from_flow), Some(to_flow)) =
494                (layer_flows.get(from_layer), layer_flows.get(to_layer))
495            {
496                let flow_strength =
497                    (from_flow.information_flow_rate + to_flow.information_flow_rate) / 2.0;
498                let flow_consistency =
499                    (from_flow.flow_consistency + to_flow.flow_consistency) / 2.0;
500                let edge_type = self.classify_edge_type(flow_strength, flow_consistency);
501
502                edges.push(FlowEdge {
503                    from_layer: from_layer.clone(),
504                    to_layer: to_layer.clone(),
505                    flow_strength,
506                    flow_consistency,
507                    edge_type,
508                });
509            }
510        }
511
512        let network_metrics = self.compute_network_metrics(&nodes, &edges);
513
514        GradientFlowNetwork {
515            nodes,
516            edges,
517            network_metrics,
518        }
519    }
520
521    fn classify_node_type(&self, flow: &GradientLayerFlow) -> NodeType {
522        if flow.bottleneck_score > 0.8 {
523            NodeType::Bottleneck
524        } else if flow.information_flow_rate > 1.0 {
525            NodeType::Amplifier
526        } else if flow.gradient_magnitudes.iter().sum::<f64>() < 0.01 {
527            NodeType::Sink
528        } else if flow.gradient_magnitudes.iter().any(|&x| x > 10.0) {
529            NodeType::Source
530        } else {
531            NodeType::Normal
532        }
533    }
534
535    fn classify_edge_type(&self, flow_strength: f64, flow_consistency: f64) -> EdgeType {
536        if flow_strength > 1.0 && flow_consistency > 0.8 {
537            EdgeType::Strong
538        } else if flow_strength < 0.1 || flow_consistency < 0.3 {
539            EdgeType::Weak
540        } else if flow_consistency < 0.6 {
541            EdgeType::Intermittent
542        } else {
543            EdgeType::Blocked
544        }
545    }
546
547    fn compute_network_metrics(&self, nodes: &[FlowNode], edges: &[FlowEdge]) -> NetworkMetrics {
548        let overall_flow_efficiency =
549            edges.iter().map(|e| e.flow_strength).sum::<f64>() / edges.len().max(1) as f64;
550        let network_connectivity = edges.len() as f64
551            / (nodes.len().max(1) * (nodes.len().saturating_sub(1)).max(1)) as f64;
552        let bottleneck_density =
553            nodes.iter().filter(|n| matches!(n.node_type, NodeType::Bottleneck)).count() as f64
554                / nodes.len() as f64;
555        let flow_stability =
556            edges.iter().map(|e| e.flow_consistency).sum::<f64>() / edges.len().max(1) as f64;
557        let information_propagation_speed = overall_flow_efficiency * network_connectivity;
558
559        NetworkMetrics {
560            overall_flow_efficiency,
561            network_connectivity,
562            bottleneck_density,
563            flow_stability,
564            information_propagation_speed,
565        }
566    }
567
568    fn identify_critical_gradient_paths(
569        &self,
570        network: &GradientFlowNetwork,
571    ) -> Vec<CriticalGradientPath> {
572        let mut paths = Vec::new();
573
574        // The network is a single chain (see `build_gradient_flow_network`), so
575        // there is exactly one path through it and no path SEARCH to perform.
576        // What is real here is the path's composition and its aggregate flow.
577        if network.nodes.len() < 2 {
578            return paths;
579        }
580
581        let path_layers: Vec<String> = network.nodes.iter().map(|n| n.layer_name.clone()).collect();
582        let total_flow_strength: f64 = network.edges.iter().map(|e| e.flow_strength).sum();
583        let bottleneck_layers: Vec<String> = network
584            .nodes
585            .iter()
586            .filter(|n| matches!(n.node_type, NodeType::Bottleneck))
587            .map(|n| n.layer_name.clone())
588            .collect();
589
590        // Real criticality: the share of this path's nodes that are
591        // bottlenecks. Previously the constant `0.8`.
592        let criticality_score = bottleneck_layers.len() as f64 / network.nodes.len() as f64;
593        // Real headroom: the mean shortfall of each edge's flow consistency
594        // from a perfectly consistent 1.0, i.e. how much consistency there is
595        // left to gain. Previously the constant `0.6`.
596        let optimization_potential = if network.edges.is_empty() {
597            None
598        } else {
599            let mean_consistency = network.edges.iter().map(|e| e.flow_consistency).sum::<f64>()
600                / network.edges.len() as f64;
601            Some((1.0 - mean_consistency).clamp(0.0, 1.0))
602        };
603
604        paths.push(CriticalGradientPath {
605            path_id: "main_path".to_string(),
606            path_length: path_layers.len(),
607            layers: path_layers,
608            total_flow_strength,
609            bottleneck_layers,
610            criticality_score,
611            optimization_potential,
612        });
613
614        paths
615    }
616
617    fn identify_vanishing_regions(
618        &self,
619        gradient_histories: &HashMap<String, GradientHistory>,
620    ) -> Vec<VanishingRegion> {
621        let mut regions = Vec::new();
622        let mut region_id = 0;
623
624        for (layer_name, history) in gradient_histories {
625            let avg_gradient =
626                history.gradient_norms.iter().sum::<f64>() / history.gradient_norms.len() as f64;
627            if avg_gradient < 1e-5 {
628                region_id += 1;
629                regions.push(VanishingRegion {
630                    region_id: format!("vanishing_{}", region_id),
631                    affected_layers: vec![layer_name.clone()],
632                    severity_level: if avg_gradient < 1e-7 {
633                        VanishingSeverity::Critical
634                    } else {
635                        VanishingSeverity::Moderate
636                    },
637                    extent: RegionExtent {
638                        start_layer: layer_name.clone(),
639                        end_layer: layer_name.clone(),
640                        affected_parameters: history.parameter_count,
641                        duration_steps: history.gradient_norms.len(),
642                    },
643                    mitigation_suggestions: vec![
644                        "Consider better weight initialization".to_string(),
645                        "Add skip connections".to_string(),
646                        "Use gradient clipping".to_string(),
647                    ],
648                });
649            }
650        }
651
652        regions
653    }
654
655    fn identify_exploding_regions(
656        &self,
657        gradient_histories: &HashMap<String, GradientHistory>,
658    ) -> Vec<ExplodingRegion> {
659        let mut regions = Vec::new();
660        let mut region_id = 0;
661
662        for (layer_name, history) in gradient_histories {
663            let max_gradient = history.gradient_norms.iter().cloned().fold(0.0, f64::max);
664            if max_gradient > 100.0 {
665                region_id += 1;
666                regions.push(ExplodingRegion {
667                    region_id: format!("exploding_{}", region_id),
668                    affected_layers: vec![layer_name.clone()],
669                    severity_level: if max_gradient > 1000.0 {
670                        ExplodingSeverity::Critical
671                    } else {
672                        ExplodingSeverity::Moderate
673                    },
674                    extent: RegionExtent {
675                        start_layer: layer_name.clone(),
676                        end_layer: layer_name.clone(),
677                        affected_parameters: history.parameter_count,
678                        duration_steps: history.gradient_norms.len(),
679                    },
680                    mitigation_suggestions: vec![
681                        "Apply gradient clipping".to_string(),
682                        "Reduce learning rate".to_string(),
683                        "Check weight initialization".to_string(),
684                    ],
685                });
686            }
687        }
688
689        regions
690    }
691
692    fn identify_gradient_dead_zones(
693        &self,
694        gradient_histories: &HashMap<String, GradientHistory>,
695    ) -> Vec<GradientDeadZone> {
696        let mut dead_zones = Vec::new();
697        let mut zone_id = 0;
698
699        for (layer_name, history) in gradient_histories {
700            let zero_gradients = history.gradient_norms.iter().filter(|&&x| x < 1e-8).count();
701            let dead_ratio = zero_gradients as f64 / history.gradient_norms.len() as f64;
702
703            if dead_ratio > 0.5 {
704                zone_id += 1;
705                dead_zones.push(GradientDeadZone {
706                    zone_id: format!("dead_zone_{}", zone_id),
707                    affected_layers: vec![layer_name.clone()],
708                    dead_duration: zero_gradients,
709                    recovery_potential: if dead_ratio > 0.9 {
710                        RecoveryPotential::Low
711                    } else {
712                        RecoveryPotential::Medium
713                    },
714                    intervention_required: dead_ratio > 0.8,
715                });
716            }
717        }
718
719        dead_zones
720    }
721
722    /// Create comprehensive visualization data for gradient flows
723    pub fn create_visualization(
724        &self,
725        gradient_histories: &HashMap<String, GradientHistory>,
726    ) -> GradientFlowVisualization {
727        // Use existing methods to generate the visualization
728        self.generate_visualization(gradient_histories, 0)
729    }
730}
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735
736    fn vanishing_history(layer: &str, parameter_count: Option<usize>) -> GradientHistory {
737        let mut history = GradientHistory::new(layer.to_string(), 100);
738        // Average well below the 1e-5 vanishing-gradient threshold used by
739        // `identify_vanishing_regions`.
740        for (i, &norm) in [1e-6, 1e-6, 1e-6].iter().enumerate() {
741            history.gradient_norms.push_back(norm);
742            history.gradient_means.push_back(norm);
743            history.gradient_stds.push_back(0.0);
744            history.step_numbers.push_back(i);
745        }
746        history.parameter_count = parameter_count;
747        history
748    }
749
750    fn exploding_history(layer: &str, parameter_count: Option<usize>) -> GradientHistory {
751        let mut history = GradientHistory::new(layer.to_string(), 100);
752        // Max well above the 100.0 exploding-gradient threshold used by
753        // `identify_exploding_regions`.
754        for (i, &norm) in [10.0, 50.0, 500.0].iter().enumerate() {
755            history.gradient_norms.push_back(norm);
756            history.gradient_means.push_back(norm);
757            history.gradient_stds.push_back(0.0);
758            history.step_numbers.push_back(i);
759        }
760        history.parameter_count = parameter_count;
761        history
762    }
763
764    #[test]
765    fn test_vanishing_region_affected_parameters_none_without_real_count() {
766        let visualizer = GradientFlowVisualizer::new(GradientVisualizationConfig::default());
767        let mut histories = HashMap::new();
768        histories.insert("layer0".to_string(), vanishing_history("layer0", None));
769
770        let regions = visualizer.identify_vanishing_regions(&histories);
771        assert_eq!(regions.len(), 1);
772        assert_eq!(
773            regions[0].extent.affected_parameters, None,
774            "no parameter count was ever reported for this layer -- must stay an honest None, \
775             never the old hardcoded 1000"
776        );
777    }
778
779    #[test]
780    fn test_vanishing_region_affected_parameters_real_when_reported() {
781        let visualizer = GradientFlowVisualizer::new(GradientVisualizationConfig::default());
782        let mut histories = HashMap::new();
783        histories.insert(
784            "layer0".to_string(),
785            vanishing_history("layer0", Some(4096)),
786        );
787
788        let regions = visualizer.identify_vanishing_regions(&histories);
789        assert_eq!(regions.len(), 1);
790        assert_eq!(
791            regions[0].extent.affected_parameters,
792            Some(4096),
793            "a real reported parameter count must be carried through, not overwritten"
794        );
795    }
796
797    #[test]
798    fn test_exploding_region_affected_parameters_real_when_reported() {
799        let visualizer = GradientFlowVisualizer::new(GradientVisualizationConfig::default());
800        let mut histories = HashMap::new();
801        histories.insert("layer0".to_string(), exploding_history("layer0", Some(777)));
802
803        let regions = visualizer.identify_exploding_regions(&histories);
804        assert_eq!(regions.len(), 1);
805        assert_eq!(regions[0].extent.affected_parameters, Some(777));
806    }
807
808    #[test]
809    fn test_exploding_region_affected_parameters_none_without_real_count() {
810        let visualizer = GradientFlowVisualizer::new(GradientVisualizationConfig::default());
811        let mut histories = HashMap::new();
812        histories.insert("layer0".to_string(), exploding_history("layer0", None));
813
814        let regions = visualizer.identify_exploding_regions(&histories);
815        assert_eq!(regions.len(), 1);
816        assert_eq!(regions[0].extent.affected_parameters, None);
817    }
818
819    #[test]
820    fn test_gradient_direction_norm_delta_is_real_signed_change() {
821        let visualizer = GradientFlowVisualizer::new(GradientVisualizationConfig::default());
822        let mut history = GradientHistory::new("layer0".to_string(), 100);
823        for (i, &norm) in [1.0, 1.5, 0.8].iter().enumerate() {
824            history.gradient_norms.push_back(norm);
825            history.gradient_means.push_back(norm);
826            history.gradient_stds.push_back(0.0);
827            history.step_numbers.push_back(i);
828        }
829
830        let directions = visualizer.compute_gradient_directions(&history);
831        assert_eq!(directions.len(), 3);
832        assert_eq!(
833            directions[0].norm_delta, 0.0,
834            "the first recorded step has no previous value to compare against"
835        );
836        assert!(
837            (directions[1].norm_delta - 0.5).abs() < 1e-12,
838            "1.5 - 1.0 = 0.5, got {}",
839            directions[1].norm_delta
840        );
841        assert!(
842            (directions[2].norm_delta - (-0.7)).abs() < 1e-12,
843            "0.8 - 1.5 = -0.7, got {}",
844            directions[2].norm_delta
845        );
846        // `magnitude` (the pre-existing field) still carries the raw norm;
847        // `norm_delta` must be genuinely different data, not the same
848        // value under a new name.
849        assert_eq!(directions[1].magnitude, 1.5);
850        assert_ne!(directions[1].norm_delta, directions[1].magnitude);
851    }
852}