open_hypergraphs_dot/
lib.rs

1use dot_structures::{Attribute, Edge, EdgeTy, Graph, Id, Node, NodeId, Port, Stmt, Vertex};
2use open_hypergraphs::lax::OpenHypergraph;
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::hash::Hash;
6use std::fmt;
7
8/// Graph orientation for visualization
9#[derive(Debug, Clone, Copy)]
10pub enum Orientation {
11    /// Left to right layout
12    LR,
13    /// Top to bottom layout
14    TB,
15}
16
17impl fmt::Display for Orientation {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            Orientation::LR => write!(f, "LR"),
21            Orientation::TB => write!(f, "TB"),
22        }
23    }
24}
25
26/// Theme for graph visualization
27pub struct Theme {
28    pub bgcolor: String,
29    pub fontcolor: String,
30    pub color: String,
31    pub orientation: Orientation,
32}
33
34impl Default for Theme {
35    fn default() -> Self {
36        Theme {
37            bgcolor: String::from("white"),
38            fontcolor: String::from("black"),
39            color: String::from("black"),
40            orientation: Orientation::LR,
41        }
42    }
43}
44
45/// A dark theme preset
46pub fn dark_theme() -> Theme {
47    Theme {
48        bgcolor: String::from("#4a4a4a"),
49        fontcolor: String::from("white"),
50        color: String::from("white"),
51        orientation: Orientation::LR,
52    }
53}
54
55/// Generates a GraphViz DOT representation of a lax open hypergraph
56pub fn generate_dot<O, A>(graph: &OpenHypergraph<O, A>, theme: &Theme) -> Graph
57where
58    O: Clone + Debug + PartialEq + Hash,
59    A: Clone + Debug + PartialEq,
60{
61    // Create a directed graph
62    let mut dot_graph = Graph::DiGraph {
63        id: Id::Plain(String::from("G")),
64        strict: false,
65        stmts: Vec::new(),
66    };
67
68    // Set graph attributes
69    dot_graph.add_stmt(Stmt::Attribute(Attribute(
70        Id::Plain(String::from("rankdir")),
71        Id::Plain(theme.orientation.to_string()),
72    )));
73
74    // Set background color
75    dot_graph.add_stmt(Stmt::Attribute(Attribute(
76        Id::Plain(String::from("bgcolor")),
77        Id::Plain(format!("\"{}\"", theme.bgcolor.clone())),
78    )));
79
80    // Add default node attributes statement
81    dot_graph.add_stmt(Stmt::Node(Node {
82        id: NodeId(Id::Plain(String::from("node")), None),
83        attributes: vec![
84            Attribute(
85                Id::Plain(String::from("shape")),
86                Id::Plain(String::from("record")),
87            ),
88            Attribute(
89                Id::Plain(String::from("style")),
90                Id::Plain(String::from("rounded")),
91            ),
92            Attribute(
93                Id::Plain(String::from("fontcolor")),
94                Id::Plain(format!("\"{}\"", theme.fontcolor.clone())),
95            ),
96            Attribute(
97                Id::Plain(String::from("color")),
98                Id::Plain(format!("\"{}\"", theme.color.clone())),
99            ),
100        ],
101    }));
102
103    // Add default edge attributes statement
104    dot_graph.add_stmt(Stmt::Node(Node {
105        id: NodeId(Id::Plain(String::from("edge")), None),
106        attributes: vec![
107            Attribute(
108                Id::Plain(String::from("fontcolor")),
109                Id::Plain(format!("\"{}\"", theme.fontcolor.clone())),
110            ),
111            Attribute(
112                Id::Plain(String::from("color")),
113                Id::Plain(format!("\"{}\"", theme.color.clone())),
114            ),
115            Attribute(
116                Id::Plain(String::from("arrowhead")),
117                Id::Plain(String::from("none")),
118            ),
119        ],
120    }));
121
122    // Add nodes for each node in the hypergraph
123    let node_stmts = generate_node_stmts(graph);
124    for stmt in node_stmts {
125        dot_graph.add_stmt(stmt);
126    }
127
128    // Add record nodes for each hyperedge
129    let edge_stmts = generate_edge_stmts(graph);
130    for stmt in edge_stmts {
131        dot_graph.add_stmt(stmt);
132    }
133
134    // Add source and target interface nodes
135    let interface_stmts = generate_interface_stmts(graph);
136    for stmt in interface_stmts {
137        dot_graph.add_stmt(stmt);
138    }
139
140    // Connect nodes to edges
141    let connection_stmts = generate_connection_stmts(graph);
142    for stmt in connection_stmts {
143        dot_graph.add_stmt(stmt);
144    }
145
146    // Add quotient connections (dotted lines between unified nodes)
147    let quotient_stmts = generate_quotient_stmts(graph);
148    for stmt in quotient_stmts {
149        dot_graph.add_stmt(stmt);
150    }
151
152    dot_graph
153}
154
155/// Generate node statements for each node in the hypergraph
156fn generate_node_stmts<O, A>(graph: &OpenHypergraph<O, A>) -> Vec<Stmt>
157where
158    O: Clone + Debug + PartialEq,
159    A: Clone + Debug + PartialEq,
160{
161    let mut stmts = Vec::new();
162
163    for i in 0..graph.hypergraph.nodes.len() {
164        let label = format!("{:?}", graph.hypergraph.nodes[i]);
165
166        stmts.push(Stmt::Node(Node {
167            id: NodeId(Id::Plain(format!("n_{}", i)), None),
168            attributes: vec![
169                Attribute(
170                    Id::Plain(String::from("shape")),
171                    Id::Plain(String::from("point")),
172                ),
173                Attribute(
174                    Id::Plain(String::from("xlabel")),
175                    Id::Plain(format!("\"{}\"", label)),
176                ),
177            ],
178        }));
179    }
180
181    stmts
182}
183
184/// Generate record node statements for each hyperedge
185fn generate_edge_stmts<O, A>(graph: &OpenHypergraph<O, A>) -> Vec<Stmt>
186where
187    O: Clone + Debug + PartialEq,
188    A: Clone + Debug + PartialEq,
189{
190    let mut stmts = Vec::new();
191
192    for i in 0..graph.hypergraph.edges.len() {
193        let hyperedge = &graph.hypergraph.adjacency[i];
194        let label = format!("{:?}", graph.hypergraph.edges[i]);
195
196        // Create port sections for sources
197        let mut source_ports = String::new();
198        for j in 0..hyperedge.sources.len() {
199            source_ports.push_str(&format!("<s_{j}> | "));
200        }
201        if !source_ports.is_empty() {
202            source_ports.truncate(source_ports.len() - 3); // Remove last " | "
203        }
204
205        // Create port sections for targets
206        let mut target_ports = String::new();
207        for j in 0..hyperedge.targets.len() {
208            target_ports.push_str(&format!("<t_{j}> | "));
209        }
210        if !target_ports.is_empty() {
211            target_ports.truncate(target_ports.len() - 3); // Remove last " | "
212        }
213
214        // Create full record label with proper quoting for GraphViz DOT format
215        let record_label = if source_ports.is_empty() && target_ports.is_empty() {
216            format!("\"{}\"", label)
217        } else if source_ports.is_empty() {
218            format!("\"{{ {} | {{ {} }} }}\"", label, target_ports)
219        } else if target_ports.is_empty() {
220            format!("\"{{ {{ {} }} | {} }}\"", source_ports, label)
221        } else {
222            format!(
223                "\"{{ {{ {} }} | {} | {{ {} }} }}\"",
224                source_ports, label, target_ports
225            )
226        };
227
228        stmts.push(Stmt::Node(Node {
229            id: NodeId(Id::Plain(format!("e_{}", i)), None),
230            attributes: vec![
231                Attribute(Id::Plain(String::from("label")), Id::Plain(record_label)),
232                Attribute(
233                    Id::Plain(String::from("shape")),
234                    Id::Plain(String::from("record")),
235                ),
236            ],
237        }));
238    }
239
240    stmts
241}
242
243/// Generate statements connecting nodes to edges
244fn generate_connection_stmts<O, A>(graph: &OpenHypergraph<O, A>) -> Vec<Stmt>
245where
246    O: Clone + Debug + PartialEq,
247    A: Clone + Debug + PartialEq,
248{
249    let mut stmts = Vec::new();
250
251    // Connect source nodes to edge ports
252    for (i, hyperedge) in graph.hypergraph.adjacency.iter().enumerate() {
253        for (_j, &node_id) in hyperedge.sources.iter().enumerate() {
254            let node_idx = node_id.0; // Convert NodeId to usize
255
256            let edge = Edge {
257                ty: EdgeTy::Pair(
258                    Vertex::N(NodeId(Id::Plain(format!("n_{}", node_idx)), None)),
259                    Vertex::N(NodeId(Id::Plain(format!("e_{}", i)), None)),
260                ),
261                attributes: vec![],
262            };
263            stmts.push(Stmt::Edge(edge));
264        }
265
266        // Connect edge target ports to target nodes
267        for (j, &node_id) in hyperedge.targets.iter().enumerate() {
268            let node_idx = node_id.0; // Convert NodeId to usize
269
270            // Create a port with the correct format
271            let port = Some(Port(None, Some(format!("t_{}", j))));
272
273            let edge = Edge {
274                ty: EdgeTy::Pair(
275                    Vertex::N(NodeId(Id::Plain(format!("e_{}", i)), port)),
276                    Vertex::N(NodeId(Id::Plain(format!("n_{}", node_idx)), None)),
277                ),
278                attributes: vec![],
279            };
280            stmts.push(Stmt::Edge(edge));
281        }
282    }
283
284    stmts
285}
286
287/// Generate interface nodes for sources and targets of the hypergraph
288fn generate_interface_stmts<O, A>(graph: &OpenHypergraph<O, A>) -> Vec<Stmt>
289where
290    O: Clone + Debug + PartialEq,
291    A: Clone + Debug + PartialEq,
292{
293    let mut stmts = Vec::new();
294
295    // Create source interface record node
296    if !graph.sources.is_empty() {
297        // Create port sections for sources
298        let mut source_ports = String::new();
299        for i in 0..graph.sources.len() {
300            source_ports.push_str(&format!("<p_{i}> | "));
301        }
302        // Remove last " | "
303        if !source_ports.is_empty() {
304            source_ports.truncate(source_ports.len() - 3);
305        }
306
307        // Create the source interface node
308        stmts.push(Stmt::Node(Node {
309            id: NodeId(Id::Plain(String::from("sources")), None),
310            attributes: vec![
311                Attribute(
312                    Id::Plain(String::from("label")),
313                    Id::Plain(format!("\"{{ {{}} | {{ {} }} }}\"", source_ports)),
314                ),
315                Attribute(
316                    Id::Plain(String::from("shape")),
317                    Id::Plain(String::from("record")),
318                ),
319                Attribute(
320                    Id::Plain(String::from("style")),
321                    Id::Plain(String::from("invisible")),
322                ),
323                Attribute(
324                    Id::Plain(String::from("rank")),
325                    Id::Plain(String::from("source")),
326                ),
327            ],
328        }));
329
330        // Connect source interface ports to the source nodes
331        for (i, &source_node_id) in graph.sources.iter().enumerate() {
332            let edge = Edge {
333                ty: EdgeTy::Pair(
334                    Vertex::N(NodeId(
335                        Id::Plain(String::from("sources")),
336                        Some(Port(None, Some(format!("p_{}", i)))),
337                    )),
338                    Vertex::N(NodeId(Id::Plain(format!("n_{}", source_node_id.0)), None)),
339                ),
340                attributes: vec![Attribute(
341                    Id::Plain(String::from("style")),
342                    Id::Plain(String::from("dashed")),
343                )],
344            };
345            stmts.push(Stmt::Edge(edge));
346        }
347    }
348
349    // Create target interface record node
350    if !graph.targets.is_empty() {
351        // Create port sections for targets
352        let mut target_ports = String::new();
353        for i in 0..graph.targets.len() {
354            target_ports.push_str(&format!("<p_{i}> | "));
355        }
356        // Remove last " | "
357        if !target_ports.is_empty() {
358            target_ports.truncate(target_ports.len() - 3);
359        }
360
361        // Create the target interface node
362        stmts.push(Stmt::Node(Node {
363            id: NodeId(Id::Plain(String::from("targets")), None),
364            attributes: vec![
365                Attribute(
366                    Id::Plain(String::from("label")),
367                    Id::Plain(format!("\"{{ {{ {} }} | {{}} }}\"", target_ports)),
368                ),
369                Attribute(
370                    Id::Plain(String::from("shape")),
371                    Id::Plain(String::from("record")),
372                ),
373                Attribute(
374                    Id::Plain(String::from("style")),
375                    Id::Plain(String::from("invisible")),
376                ),
377                Attribute(
378                    Id::Plain(String::from("rank")),
379                    Id::Plain(String::from("sink")),
380                ),
381            ],
382        }));
383
384        // Connect target nodes to target interface ports
385        for (i, &target_node_id) in graph.targets.iter().enumerate() {
386            let edge = Edge {
387                ty: EdgeTy::Pair(
388                    Vertex::N(NodeId(Id::Plain(format!("n_{}", target_node_id.0)), None)),
389                    Vertex::N(NodeId(
390                        Id::Plain(String::from("targets")),
391                        Some(Port(None, Some(format!("p_{}", i)))),
392                    )),
393                ),
394                attributes: vec![Attribute(
395                    Id::Plain(String::from("style")),
396                    Id::Plain(String::from("dashed")),
397                )],
398            };
399            stmts.push(Stmt::Edge(edge));
400        }
401    }
402
403    stmts
404}
405
406/// Generate statements for quotient connections (dotted lines between unified nodes)
407fn generate_quotient_stmts<O, A>(graph: &OpenHypergraph<O, A>) -> Vec<Stmt>
408where
409    O: Clone + Debug + PartialEq,
410    A: Clone + Debug + PartialEq,
411{
412    let mut stmts = Vec::new();
413
414    // Extract unified node pairs from the quotient
415    let (lefts, rights) = &graph.hypergraph.quotient;
416
417    // Create a map to track which nodes are unified
418    let mut unified_nodes = HashMap::new();
419
420    for (left, right) in lefts.iter().zip(rights.iter()) {
421        let left_idx = left.0; // Access the internal usize
422        let right_idx = right.0;
423
424        // Check if we've already seen this pair (in any order)
425        let pair_key = if left_idx < right_idx {
426            (left_idx, right_idx)
427        } else {
428            (right_idx, left_idx)
429        };
430
431        if unified_nodes.insert(pair_key, true).is_none() {
432            // Create a dashed edge between unified nodes
433            let edge = Edge {
434                ty: EdgeTy::Pair(
435                    Vertex::N(NodeId(Id::Plain(format!("n_{}", left_idx)), None)),
436                    Vertex::N(NodeId(Id::Plain(format!("n_{}", right_idx)), None)),
437                ),
438                attributes: vec![
439                    Attribute(
440                        Id::Plain(String::from("style")),
441                        Id::Plain(String::from("dotted")),
442                    ),
443                    Attribute(
444                        Id::Plain(String::from("dir")),
445                        Id::Plain(String::from("none")),
446                    ),
447                ],
448            };
449            stmts.push(Stmt::Edge(edge));
450        }
451    }
452
453    stmts
454}