1use super::types::*;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct GradientDirection {
37 pub step: usize,
38 pub norm_delta: f64,
49 pub magnitude: f64,
50 pub consistency_score: f64,
51}
52
53#[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#[derive(Debug, Clone, Serialize, Deserialize)]
65pub enum FlowDirection {
66 Forward,
67 Backward,
68 Oscillating,
69 Stagnant,
70}
71
72#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum NodeType {
93 Source,
94 Sink,
95 Bottleneck,
96 Amplifier,
97 Normal,
98}
99
100#[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#[derive(Debug, Clone, Serialize, Deserialize)]
112pub enum EdgeType {
113 Strong,
114 Weak,
115 Intermittent,
116 Blocked,
117}
118
119#[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#[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 pub criticality_score: f64,
140 pub optimization_potential: Option<f64>,
144}
145
146#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct RegionExtent {
185 pub start_layer: String,
186 pub end_layer: String,
187 pub affected_parameters: Option<usize>,
195 pub duration_steps: usize,
196}
197
198#[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#[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#[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 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]; if trend.abs() < 1e-6 {
423 FlowDirection::Stagnant
424 } else if trend > 0.0 {
425 FlowDirection::Forward
426 } else {
427 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 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 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 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 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 let criticality_score = bottleneck_layers.len() as f64 / network.nodes.len() as f64;
593 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 pub fn create_visualization(
724 &self,
725 gradient_histories: &HashMap<String, GradientHistory>,
726 ) -> GradientFlowVisualization {
727 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 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 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 assert_eq!(directions[1].magnitude, 1.5);
850 assert_ne!(directions[1].norm_delta, directions[1].magnitude);
851 }
852}