Skip to main content

torsh_nn/
visualization.rs

1//! Network architecture visualization tools
2//!
3//! This module provides tools for visualizing neural network architectures,
4//! including graph representations, layer diagrams, and model flow charts.
5
6use crate::Module;
7use std::collections::{HashMap, HashSet};
8use std::fmt::{self, Display};
9use torsh_core::error::{Result, TorshError};
10
11/// Represents a node in the network graph
12#[derive(Debug, Clone)]
13pub struct GraphNode {
14    /// Unique identifier for the node
15    pub id: String,
16    /// Display name for the node
17    pub name: String,
18    /// Type of the layer/module
19    pub layer_type: String,
20    /// Input shape
21    pub input_shape: Vec<usize>,
22    /// Output shape
23    pub output_shape: Vec<usize>,
24    /// Number of parameters
25    pub parameter_count: usize,
26    /// Position in the graph (x, y)
27    pub position: Option<(f32, f32)>,
28    /// Additional metadata
29    pub metadata: HashMap<String, String>,
30}
31
32impl GraphNode {
33    /// Create a new graph node
34    pub fn new(
35        id: String,
36        name: String,
37        layer_type: String,
38        input_shape: Vec<usize>,
39        output_shape: Vec<usize>,
40        parameter_count: usize,
41    ) -> Self {
42        Self {
43            id,
44            name,
45            layer_type,
46            input_shape,
47            output_shape,
48            parameter_count,
49            position: None,
50            metadata: HashMap::new(),
51        }
52    }
53
54    /// Set the position of the node
55    pub fn with_position(mut self, x: f32, y: f32) -> Self {
56        self.position = Some((x, y));
57        self
58    }
59
60    /// Add metadata to the node
61    pub fn with_metadata(mut self, key: String, value: String) -> Self {
62        self.metadata.insert(key, value);
63        self
64    }
65
66    /// Get a short description of the node
67    pub fn short_description(&self) -> String {
68        format!("{} ({})", self.name, self.layer_type)
69    }
70
71    /// Get a detailed description of the node
72    pub fn detailed_description(&self) -> String {
73        format!(
74            "{}\nType: {}\nInput: {:?}\nOutput: {:?}\nParams: {}",
75            self.name,
76            self.layer_type,
77            self.input_shape,
78            self.output_shape,
79            format_number(self.parameter_count)
80        )
81    }
82}
83
84/// Represents an edge connecting two nodes
85#[derive(Debug, Clone)]
86pub struct GraphEdge {
87    /// Source node ID
88    pub from: String,
89    /// Target node ID
90    pub to: String,
91    /// Data shape flowing through this edge
92    pub shape: Vec<usize>,
93    /// Edge weight or importance
94    pub weight: f32,
95    /// Edge style
96    pub style: EdgeStyle,
97}
98
99/// Edge styling options
100#[derive(Debug, Clone, PartialEq)]
101pub enum EdgeStyle {
102    /// Regular connection
103    Normal,
104    /// Skip connection (residual)
105    Skip,
106    /// Attention connection
107    Attention,
108    /// Recurrent connection
109    Recurrent,
110}
111
112impl GraphEdge {
113    /// Create a new graph edge
114    pub fn new(from: String, to: String, shape: Vec<usize>) -> Self {
115        Self {
116            from,
117            to,
118            shape,
119            weight: 1.0,
120            style: EdgeStyle::Normal,
121        }
122    }
123
124    /// Set the edge style
125    pub fn with_style(mut self, style: EdgeStyle) -> Self {
126        self.style = style;
127        self
128    }
129
130    /// Set the edge weight
131    pub fn with_weight(mut self, weight: f32) -> Self {
132        self.weight = weight;
133        self
134    }
135}
136
137/// Complete network graph representation
138#[derive(Debug, Clone)]
139pub struct NetworkGraph {
140    /// All nodes in the graph
141    pub nodes: HashMap<String, GraphNode>,
142    /// All edges in the graph
143    pub edges: Vec<GraphEdge>,
144    /// Input node IDs
145    pub inputs: Vec<String>,
146    /// Output node IDs
147    pub outputs: Vec<String>,
148    /// Graph metadata
149    pub metadata: HashMap<String, String>,
150}
151
152impl NetworkGraph {
153    /// Create a new empty network graph
154    pub fn new() -> Self {
155        Self {
156            nodes: HashMap::new(),
157            edges: Vec::new(),
158            inputs: Vec::new(),
159            outputs: Vec::new(),
160            metadata: HashMap::new(),
161        }
162    }
163
164    /// Add a node to the graph
165    pub fn add_node(&mut self, node: GraphNode) {
166        self.nodes.insert(node.id.clone(), node);
167    }
168
169    /// Add an edge to the graph
170    pub fn add_edge(&mut self, edge: GraphEdge) {
171        self.edges.push(edge);
172    }
173
174    /// Set input nodes
175    pub fn set_inputs(&mut self, inputs: Vec<String>) {
176        self.inputs = inputs;
177    }
178
179    /// Set output nodes
180    pub fn set_outputs(&mut self, outputs: Vec<String>) {
181        self.outputs = outputs;
182    }
183
184    /// Get topological ordering of nodes
185    pub fn topological_sort(&self) -> Result<Vec<String>> {
186        let mut in_degree: HashMap<String, usize> = HashMap::new();
187        let mut adj_list: HashMap<String, Vec<String>> = HashMap::new();
188
189        // Initialize
190        for node_id in self.nodes.keys() {
191            in_degree.insert(node_id.clone(), 0);
192            adj_list.insert(node_id.clone(), Vec::new());
193        }
194
195        // Build adjacency list and calculate in-degrees
196        for edge in &self.edges {
197            adj_list
198                .get_mut(&edge.from)
199                .expect("edge.from should exist in adj_list")
200                .push(edge.to.clone());
201            *in_degree
202                .get_mut(&edge.to)
203                .expect("edge.to should exist in in_degree") += 1;
204        }
205
206        // Kahn's algorithm
207        let mut queue = Vec::new();
208        let mut result = Vec::new();
209
210        // Start with nodes that have no incoming edges
211        for (node_id, &degree) in &in_degree {
212            if degree == 0 {
213                queue.push(node_id.clone());
214            }
215        }
216
217        while let Some(node_id) = queue.pop() {
218            result.push(node_id.clone());
219
220            if let Some(neighbors) = adj_list.get(&node_id) {
221                for neighbor in neighbors {
222                    let degree = in_degree
223                        .get_mut(neighbor)
224                        .expect("neighbor should exist in in_degree");
225                    *degree -= 1;
226                    if *degree == 0 {
227                        queue.push(neighbor.clone());
228                    }
229                }
230            }
231        }
232
233        if result.len() != self.nodes.len() {
234            return Err(TorshError::InvalidArgument(
235                "Graph contains cycles".to_string(),
236            ));
237        }
238
239        Ok(result)
240    }
241
242    /// Calculate graph statistics
243    pub fn calculate_statistics(&self) -> GraphStatistics {
244        let total_params: usize = self.nodes.values().map(|n| n.parameter_count).sum();
245        let total_nodes = self.nodes.len();
246        let total_edges = self.edges.len();
247
248        let layer_types: HashSet<String> =
249            self.nodes.values().map(|n| n.layer_type.clone()).collect();
250
251        let max_depth = self.calculate_depth();
252
253        GraphStatistics {
254            total_nodes,
255            total_edges,
256            total_parameters: total_params,
257            unique_layer_types: layer_types.len(),
258            max_depth,
259            layer_type_counts: self.count_layer_types(),
260        }
261    }
262
263    /// Calculate the maximum depth of the graph
264    fn calculate_depth(&self) -> usize {
265        // Simplified depth calculation
266        if self.inputs.is_empty() {
267            return 0;
268        }
269
270        let mut depths: HashMap<String, usize> = HashMap::new();
271
272        // Set input depths to 0
273        for input_id in &self.inputs {
274            depths.insert(input_id.clone(), 0);
275        }
276
277        // Calculate depths using BFS
278        if let Ok(sorted_nodes) = self.topological_sort() {
279            for node_id in sorted_nodes {
280                let mut max_input_depth = 0;
281
282                // Find maximum depth of incoming edges
283                for edge in &self.edges {
284                    if edge.to == node_id {
285                        if let Some(&depth) = depths.get(&edge.from) {
286                            max_input_depth = max_input_depth.max(depth);
287                        }
288                    }
289                }
290
291                depths.insert(node_id, max_input_depth + 1);
292            }
293        }
294
295        depths.values().max().copied().unwrap_or(0)
296    }
297
298    /// Count occurrences of each layer type
299    fn count_layer_types(&self) -> HashMap<String, usize> {
300        let mut counts = HashMap::new();
301        for node in self.nodes.values() {
302            *counts.entry(node.layer_type.clone()).or_insert(0) += 1;
303        }
304        counts
305    }
306}
307
308impl Default for NetworkGraph {
309    fn default() -> Self {
310        Self::new()
311    }
312}
313
314/// Statistics about the network graph
315#[derive(Debug, Clone)]
316pub struct GraphStatistics {
317    /// Total number of nodes
318    pub total_nodes: usize,
319    /// Total number of edges
320    pub total_edges: usize,
321    /// Total number of parameters
322    pub total_parameters: usize,
323    /// Number of unique layer types
324    pub unique_layer_types: usize,
325    /// Maximum depth of the graph
326    pub max_depth: usize,
327    /// Count of each layer type
328    pub layer_type_counts: HashMap<String, usize>,
329}
330
331impl Display for GraphStatistics {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        writeln!(f, "Graph Statistics:")?;
334        writeln!(f, "  Total Nodes: {}", self.total_nodes)?;
335        writeln!(f, "  Total Edges: {}", self.total_edges)?;
336        writeln!(
337            f,
338            "  Total Parameters: {}",
339            format_number(self.total_parameters)
340        )?;
341        writeln!(f, "  Unique Layer Types: {}", self.unique_layer_types)?;
342        writeln!(f, "  Maximum Depth: {}", self.max_depth)?;
343        writeln!(f, "  Layer Type Distribution:")?;
344
345        for (layer_type, count) in &self.layer_type_counts {
346            writeln!(f, "    {}: {}", layer_type, count)?;
347        }
348
349        Ok(())
350    }
351}
352
353/// Configuration for graph visualization
354#[derive(Debug, Clone)]
355pub struct VisualizationConfig {
356    /// Width of the output
357    pub width: usize,
358    /// Height of the output
359    pub height: usize,
360    /// Whether to show parameter counts
361    pub show_parameters: bool,
362    /// Whether to show shapes
363    pub show_shapes: bool,
364    /// Whether to show layer types
365    pub show_layer_types: bool,
366    /// Layout algorithm to use
367    pub layout: LayoutAlgorithm,
368    /// Color scheme
369    pub color_scheme: ColorScheme,
370}
371
372impl Default for VisualizationConfig {
373    fn default() -> Self {
374        Self {
375            width: 800,
376            height: 600,
377            show_parameters: true,
378            show_shapes: true,
379            show_layer_types: true,
380            layout: LayoutAlgorithm::Hierarchical,
381            color_scheme: ColorScheme::Default,
382        }
383    }
384}
385
386/// Layout algorithms for graph visualization
387#[derive(Debug, Clone, PartialEq)]
388pub enum LayoutAlgorithm {
389    /// Hierarchical top-down layout
390    Hierarchical,
391    /// Force-directed layout
392    ForceDirected,
393    /// Circular layout
394    Circular,
395    /// Grid layout
396    Grid,
397}
398
399/// Color schemes for visualization
400#[derive(Debug, Clone, PartialEq)]
401pub enum ColorScheme {
402    /// Default colors
403    Default,
404    /// Grayscale
405    Grayscale,
406    /// Colorful
407    Colorful,
408    /// High contrast
409    HighContrast,
410}
411
412/// Generate a network graph from a model
413pub fn create_graph_from_model<M: Module>(
414    model: &M,
415    input_shape: &[usize],
416) -> Result<NetworkGraph> {
417    let mut graph = NetworkGraph::new();
418
419    // Create input node
420    let input_node = GraphNode::new(
421        "input".to_string(),
422        "Input".to_string(),
423        "Input".to_string(),
424        vec![],
425        input_shape.to_vec(),
426        0,
427    );
428    graph.add_node(input_node);
429    graph.set_inputs(vec!["input".to_string()]);
430
431    // Create model node (simplified)
432    let params = model.parameters();
433    let param_count: usize = params
434        .values()
435        .map(|p| p.tensor().read().shape().dims().iter().product::<usize>())
436        .sum();
437
438    let model_node = GraphNode::new(
439        "model".to_string(),
440        "Model".to_string(),
441        "Model".to_string(),
442        input_shape.to_vec(),
443        input_shape.to_vec(), // Simplified - would compute actual output shape
444        param_count,
445    );
446    graph.add_node(model_node);
447
448    // Create edge from input to model
449    let edge = GraphEdge::new(
450        "input".to_string(),
451        "model".to_string(),
452        input_shape.to_vec(),
453    );
454    graph.add_edge(edge);
455
456    graph.set_outputs(vec!["model".to_string()]);
457
458    Ok(graph)
459}
460
461/// Text-based graph renderer
462pub struct TextRenderer {
463    config: VisualizationConfig,
464}
465
466impl TextRenderer {
467    /// Create a new text renderer
468    pub fn new(config: VisualizationConfig) -> Self {
469        Self { config }
470    }
471
472    /// Render a graph as text
473    pub fn render(&self, graph: &NetworkGraph) -> String {
474        let mut output = String::new();
475
476        output.push_str("Network Architecture Visualization\n");
477        output.push_str("==================================\n\n");
478
479        // Show statistics
480        let stats = graph.calculate_statistics();
481        output.push_str(&format!("{}\n", stats));
482
483        // Show nodes
484        output.push_str("Nodes:\n");
485        output.push_str("------\n");
486
487        for (id, node) in &graph.nodes {
488            output.push_str(&format!("{}:\n", id));
489            output.push_str(&format!("  Name: {}\n", node.name));
490            output.push_str(&format!("  Type: {}\n", node.layer_type));
491
492            if self.config.show_shapes {
493                output.push_str(&format!("  Input Shape: {:?}\n", node.input_shape));
494                output.push_str(&format!("  Output Shape: {:?}\n", node.output_shape));
495            }
496
497            if self.config.show_parameters && node.parameter_count > 0 {
498                output.push_str(&format!(
499                    "  Parameters: {}\n",
500                    format_number(node.parameter_count)
501                ));
502            }
503
504            output.push('\n');
505        }
506
507        // Show edges
508        output.push_str("Connections:\n");
509        output.push_str("------------\n");
510
511        for edge in &graph.edges {
512            let style_indicator = match edge.style {
513                EdgeStyle::Normal => "->",
514                EdgeStyle::Skip => "~~>",
515                EdgeStyle::Attention => "==>",
516                EdgeStyle::Recurrent => "<->",
517            };
518
519            output.push_str(&format!(
520                "{} {} {} (shape: {:?})\n",
521                edge.from, style_indicator, edge.to, edge.shape
522            ));
523        }
524
525        output
526    }
527}
528
529/// ASCII art graph renderer
530pub struct AsciiRenderer {
531    config: VisualizationConfig,
532}
533
534impl AsciiRenderer {
535    /// Create a new ASCII renderer
536    pub fn new(config: VisualizationConfig) -> Self {
537        Self { config }
538    }
539
540    /// Render a graph as ASCII art
541    pub fn render(&self, graph: &NetworkGraph) -> String {
542        let mut output = String::new();
543
544        // Simple ASCII representation
545        if let Ok(sorted_nodes) = graph.topological_sort() {
546            for (i, node_id) in sorted_nodes.iter().enumerate() {
547                if let Some(node) = graph.nodes.get(node_id) {
548                    // Add indentation based on depth
549                    let indent = "  ".repeat(i.min(10));
550
551                    // Node representation
552                    let node_repr = if self.config.show_parameters && node.parameter_count > 0 {
553                        format!(
554                            "[{}] {} ({})",
555                            node.layer_type,
556                            node.name,
557                            format_number(node.parameter_count)
558                        )
559                    } else {
560                        format!("[{}] {}", node.layer_type, node.name)
561                    };
562
563                    output.push_str(&format!("{}{}\n", indent, node_repr));
564
565                    if self.config.show_shapes && !node.output_shape.is_empty() {
566                        output
567                            .push_str(&format!("{}  └─ Output: {:?}\n", indent, node.output_shape));
568                    }
569
570                    // Add connection lines
571                    if i < sorted_nodes.len() - 1 {
572                        output.push_str(&format!("{}  |\n", indent));
573                    }
574                }
575            }
576        }
577
578        output
579    }
580}
581
582/// DOT format renderer for Graphviz
583pub struct DotRenderer {
584    config: VisualizationConfig,
585}
586
587impl DotRenderer {
588    /// Create a new DOT renderer
589    pub fn new(config: VisualizationConfig) -> Self {
590        Self { config }
591    }
592
593    /// Render a graph in DOT format
594    pub fn render(&self, graph: &NetworkGraph) -> String {
595        let mut output = String::new();
596
597        output.push_str("digraph NetworkGraph {\n");
598        output.push_str("  rankdir=TB;\n");
599        output.push_str("  node [shape=box, style=filled];\n\n");
600
601        // Add nodes
602        for (id, node) in &graph.nodes {
603            let label = if self.config.show_parameters && node.parameter_count > 0 {
604                format!(
605                    "{}\\n{}\\n{} params",
606                    node.name,
607                    node.layer_type,
608                    format_number(node.parameter_count)
609                )
610            } else {
611                format!("{}\\n{}", node.name, node.layer_type)
612            };
613
614            let color = self.get_node_color(&node.layer_type);
615
616            output.push_str(&format!(
617                "  \"{}\" [label=\"{}\", fillcolor=\"{}\"];\n",
618                id, label, color
619            ));
620        }
621
622        output.push('\n');
623
624        // Add edges
625        for edge in &graph.edges {
626            let style = match edge.style {
627                EdgeStyle::Normal => "solid",
628                EdgeStyle::Skip => "dashed",
629                EdgeStyle::Attention => "bold",
630                EdgeStyle::Recurrent => "dotted",
631            };
632
633            output.push_str(&format!(
634                "  \"{}\" -> \"{}\" [style={}];\n",
635                edge.from, edge.to, style
636            ));
637        }
638
639        output.push_str("}\n");
640        output
641    }
642
643    /// Get color for a layer type
644    fn get_node_color(&self, layer_type: &str) -> &'static str {
645        match self.config.color_scheme {
646            ColorScheme::Default => match layer_type {
647                "Input" => "lightblue",
648                "Linear" => "lightgreen",
649                "Conv2d" => "orange",
650                "ReLU" => "yellow",
651                "BatchNorm2d" => "pink",
652                _ => "lightgray",
653            },
654            ColorScheme::Grayscale => "lightgray",
655            ColorScheme::Colorful => match layer_type {
656                "Input" => "cyan",
657                "Linear" => "green",
658                "Conv2d" => "red",
659                "ReLU" => "yellow",
660                "BatchNorm2d" => "magenta",
661                _ => "white",
662            },
663            ColorScheme::HighContrast => match layer_type {
664                "Input" => "black",
665                "Linear" => "white",
666                "Conv2d" => "black",
667                "ReLU" => "white",
668                "BatchNorm2d" => "black",
669                _ => "gray",
670            },
671        }
672    }
673}
674
675/// Helper function to format numbers with appropriate units
676fn format_number(num: usize) -> String {
677    if num >= 1_000_000_000 {
678        format!("{:.1}B", num as f64 / 1_000_000_000.0)
679    } else if num >= 1_000_000 {
680        format!("{:.1}M", num as f64 / 1_000_000.0)
681    } else if num >= 1_000 {
682        format!("{:.1}K", num as f64 / 1_000.0)
683    } else {
684        num.to_string()
685    }
686}
687
688/// Utility functions for common visualization tasks
689pub mod utils {
690    use super::*;
691
692    /// Quick text visualization of a model
693    pub fn quick_text_viz<M: Module>(model: &M, input_shape: &[usize]) -> Result<String> {
694        let graph = create_graph_from_model(model, input_shape)?;
695        let renderer = TextRenderer::new(VisualizationConfig::default());
696        Ok(renderer.render(&graph))
697    }
698
699    /// Quick ASCII art visualization of a model
700    pub fn quick_ascii_viz<M: Module>(model: &M, input_shape: &[usize]) -> Result<String> {
701        let graph = create_graph_from_model(model, input_shape)?;
702        let renderer = AsciiRenderer::new(VisualizationConfig::default());
703        Ok(renderer.render(&graph))
704    }
705
706    /// Generate DOT format for Graphviz
707    pub fn generate_dot<M: Module>(model: &M, input_shape: &[usize]) -> Result<String> {
708        let graph = create_graph_from_model(model, input_shape)?;
709        let renderer = DotRenderer::new(VisualizationConfig::default());
710        Ok(renderer.render(&graph))
711    }
712
713    /// Print a quick visualization to stdout
714    pub fn print_model_viz<M: Module>(model: &M, input_shape: &[usize]) -> Result<()> {
715        let viz = quick_text_viz(model, input_shape)?;
716        println!("{}", viz);
717        Ok(())
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724    use crate::layers::Linear;
725
726    #[test]
727    fn test_graph_node_creation() {
728        let node = GraphNode::new(
729            "linear1".to_string(),
730            "Linear Layer 1".to_string(),
731            "Linear".to_string(),
732            vec![10, 20],
733            vec![10, 30],
734            630,
735        );
736
737        assert_eq!(node.id, "linear1");
738        assert_eq!(node.layer_type, "Linear");
739        assert_eq!(node.parameter_count, 630);
740    }
741
742    #[test]
743    fn test_graph_edge_creation() {
744        let edge = GraphEdge::new("input".to_string(), "linear1".to_string(), vec![10, 20])
745            .with_style(EdgeStyle::Skip);
746
747        assert_eq!(edge.from, "input");
748        assert_eq!(edge.to, "linear1");
749        assert_eq!(edge.style, EdgeStyle::Skip);
750    }
751
752    #[test]
753    fn test_network_graph() {
754        let mut graph = NetworkGraph::new();
755
756        let node1 = GraphNode::new(
757            "input".to_string(),
758            "Input".to_string(),
759            "Input".to_string(),
760            vec![],
761            vec![10, 20],
762            0,
763        );
764
765        let node2 = GraphNode::new(
766            "linear".to_string(),
767            "Linear".to_string(),
768            "Linear".to_string(),
769            vec![10, 20],
770            vec![10, 30],
771            630,
772        );
773
774        graph.add_node(node1);
775        graph.add_node(node2);
776
777        let edge = GraphEdge::new("input".to_string(), "linear".to_string(), vec![10, 20]);
778        graph.add_edge(edge);
779
780        assert_eq!(graph.nodes.len(), 2);
781        assert_eq!(graph.edges.len(), 1);
782    }
783
784    #[test]
785    fn test_topological_sort() -> Result<()> {
786        let mut graph = NetworkGraph::new();
787
788        // Create linear chain: A -> B -> C
789        for (id, name) in [("A", "Node A"), ("B", "Node B"), ("C", "Node C")] {
790            let node = GraphNode::new(
791                id.to_string(),
792                name.to_string(),
793                "Test".to_string(),
794                vec![10],
795                vec![10],
796                0,
797            );
798            graph.add_node(node);
799        }
800
801        graph.add_edge(GraphEdge::new("A".to_string(), "B".to_string(), vec![10]));
802        graph.add_edge(GraphEdge::new("B".to_string(), "C".to_string(), vec![10]));
803
804        let sorted = graph.topological_sort()?;
805        assert_eq!(
806            sorted,
807            vec!["A".to_string(), "B".to_string(), "C".to_string()]
808        );
809
810        Ok(())
811    }
812
813    #[test]
814    fn test_graph_statistics() -> Result<()> {
815        let mut graph = NetworkGraph::new();
816
817        let node1 = GraphNode::new(
818            "linear1".to_string(),
819            "Linear 1".to_string(),
820            "Linear".to_string(),
821            vec![10],
822            vec![20],
823            210,
824        );
825
826        let node2 = GraphNode::new(
827            "linear2".to_string(),
828            "Linear 2".to_string(),
829            "Linear".to_string(),
830            vec![20],
831            vec![30],
832            630,
833        );
834
835        graph.add_node(node1);
836        graph.add_node(node2);
837
838        let stats = graph.calculate_statistics();
839        assert_eq!(stats.total_nodes, 2);
840        assert_eq!(stats.total_parameters, 840);
841        assert_eq!(stats.unique_layer_types, 1);
842
843        Ok(())
844    }
845
846    #[test]
847    fn test_text_renderer() -> Result<()> {
848        let model = Linear::new(64, 32, true);
849        let graph = create_graph_from_model(&model, &[10, 64])?;
850
851        let config = VisualizationConfig::default();
852        let renderer = TextRenderer::new(config);
853        let output = renderer.render(&graph);
854
855        assert!(output.contains("Network Architecture Visualization"));
856        assert!(output.contains("Nodes:"));
857        assert!(output.contains("Connections:"));
858
859        Ok(())
860    }
861
862    #[test]
863    fn test_ascii_renderer() -> Result<()> {
864        let model = Linear::new(32, 16, true);
865        let graph = create_graph_from_model(&model, &[5, 32])?;
866
867        let config = VisualizationConfig::default();
868        let renderer = AsciiRenderer::new(config);
869        let output = renderer.render(&graph);
870
871        assert!(!output.is_empty());
872
873        Ok(())
874    }
875
876    #[test]
877    fn test_dot_renderer() -> Result<()> {
878        let model = Linear::new(16, 8, true);
879        let graph = create_graph_from_model(&model, &[3, 16])?;
880
881        let config = VisualizationConfig::default();
882        let renderer = DotRenderer::new(config);
883        let output = renderer.render(&graph);
884
885        assert!(output.contains("digraph NetworkGraph"));
886        assert!(output.contains("->"));
887
888        Ok(())
889    }
890
891    #[test]
892    fn test_utils_functions() -> Result<()> {
893        let model = Linear::new(128, 64, true);
894
895        let text_viz = utils::quick_text_viz(&model, &[8, 128])?;
896        assert!(!text_viz.is_empty());
897
898        let ascii_viz = utils::quick_ascii_viz(&model, &[8, 128])?;
899        assert!(!ascii_viz.is_empty());
900
901        let dot_viz = utils::generate_dot(&model, &[8, 128])?;
902        assert!(dot_viz.contains("digraph"));
903
904        Ok(())
905    }
906}