1use crate::Module;
7use std::collections::{HashMap, HashSet};
8use std::fmt::{self, Display};
9use torsh_core::error::{Result, TorshError};
10
11#[derive(Debug, Clone)]
13pub struct GraphNode {
14 pub id: String,
16 pub name: String,
18 pub layer_type: String,
20 pub input_shape: Vec<usize>,
22 pub output_shape: Vec<usize>,
24 pub parameter_count: usize,
26 pub position: Option<(f32, f32)>,
28 pub metadata: HashMap<String, String>,
30}
31
32impl GraphNode {
33 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 pub fn with_position(mut self, x: f32, y: f32) -> Self {
56 self.position = Some((x, y));
57 self
58 }
59
60 pub fn with_metadata(mut self, key: String, value: String) -> Self {
62 self.metadata.insert(key, value);
63 self
64 }
65
66 pub fn short_description(&self) -> String {
68 format!("{} ({})", self.name, self.layer_type)
69 }
70
71 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#[derive(Debug, Clone)]
86pub struct GraphEdge {
87 pub from: String,
89 pub to: String,
91 pub shape: Vec<usize>,
93 pub weight: f32,
95 pub style: EdgeStyle,
97}
98
99#[derive(Debug, Clone, PartialEq)]
101pub enum EdgeStyle {
102 Normal,
104 Skip,
106 Attention,
108 Recurrent,
110}
111
112impl GraphEdge {
113 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 pub fn with_style(mut self, style: EdgeStyle) -> Self {
126 self.style = style;
127 self
128 }
129
130 pub fn with_weight(mut self, weight: f32) -> Self {
132 self.weight = weight;
133 self
134 }
135}
136
137#[derive(Debug, Clone)]
139pub struct NetworkGraph {
140 pub nodes: HashMap<String, GraphNode>,
142 pub edges: Vec<GraphEdge>,
144 pub inputs: Vec<String>,
146 pub outputs: Vec<String>,
148 pub metadata: HashMap<String, String>,
150}
151
152impl NetworkGraph {
153 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 pub fn add_node(&mut self, node: GraphNode) {
166 self.nodes.insert(node.id.clone(), node);
167 }
168
169 pub fn add_edge(&mut self, edge: GraphEdge) {
171 self.edges.push(edge);
172 }
173
174 pub fn set_inputs(&mut self, inputs: Vec<String>) {
176 self.inputs = inputs;
177 }
178
179 pub fn set_outputs(&mut self, outputs: Vec<String>) {
181 self.outputs = outputs;
182 }
183
184 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 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 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 let mut queue = Vec::new();
208 let mut result = Vec::new();
209
210 for (node_id, °ree) 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 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 fn calculate_depth(&self) -> usize {
265 if self.inputs.is_empty() {
267 return 0;
268 }
269
270 let mut depths: HashMap<String, usize> = HashMap::new();
271
272 for input_id in &self.inputs {
274 depths.insert(input_id.clone(), 0);
275 }
276
277 if let Ok(sorted_nodes) = self.topological_sort() {
279 for node_id in sorted_nodes {
280 let mut max_input_depth = 0;
281
282 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 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#[derive(Debug, Clone)]
316pub struct GraphStatistics {
317 pub total_nodes: usize,
319 pub total_edges: usize,
321 pub total_parameters: usize,
323 pub unique_layer_types: usize,
325 pub max_depth: usize,
327 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#[derive(Debug, Clone)]
355pub struct VisualizationConfig {
356 pub width: usize,
358 pub height: usize,
360 pub show_parameters: bool,
362 pub show_shapes: bool,
364 pub show_layer_types: bool,
366 pub layout: LayoutAlgorithm,
368 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#[derive(Debug, Clone, PartialEq)]
388pub enum LayoutAlgorithm {
389 Hierarchical,
391 ForceDirected,
393 Circular,
395 Grid,
397}
398
399#[derive(Debug, Clone, PartialEq)]
401pub enum ColorScheme {
402 Default,
404 Grayscale,
406 Colorful,
408 HighContrast,
410}
411
412pub 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 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 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(), param_count,
445 );
446 graph.add_node(model_node);
447
448 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
461pub struct TextRenderer {
463 config: VisualizationConfig,
464}
465
466impl TextRenderer {
467 pub fn new(config: VisualizationConfig) -> Self {
469 Self { config }
470 }
471
472 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 let stats = graph.calculate_statistics();
481 output.push_str(&format!("{}\n", stats));
482
483 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 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
529pub struct AsciiRenderer {
531 config: VisualizationConfig,
532}
533
534impl AsciiRenderer {
535 pub fn new(config: VisualizationConfig) -> Self {
537 Self { config }
538 }
539
540 pub fn render(&self, graph: &NetworkGraph) -> String {
542 let mut output = String::new();
543
544 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 let indent = " ".repeat(i.min(10));
550
551 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 if i < sorted_nodes.len() - 1 {
572 output.push_str(&format!("{} |\n", indent));
573 }
574 }
575 }
576 }
577
578 output
579 }
580}
581
582pub struct DotRenderer {
584 config: VisualizationConfig,
585}
586
587impl DotRenderer {
588 pub fn new(config: VisualizationConfig) -> Self {
590 Self { config }
591 }
592
593 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 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 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 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
675fn 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
688pub mod utils {
690 use super::*;
691
692 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 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 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 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 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}