Skip to main content

rust_igraph/algorithms/operators/
to_undirected.rs

1//! Directed → undirected conversion (ALGO-OP-020).
2//!
3//! Converts a directed graph to an undirected graph using various edge
4//! preservation modes. Counterpart of `igraph_to_undirected`.
5
6use crate::core::{Graph, IgraphResult};
7
8/// Mode for converting a directed graph to undirected.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ToUndirectedMode {
11    /// Each directed edge becomes an undirected edge.
12    /// Parallel directed edges (same direction) become parallel undirected edges.
13    Each,
14    /// Collapse mutual edges: if both (u,v) and (v,u) exist, produce one
15    /// undirected edge. Non-mutual directed edges are also kept as one
16    /// undirected edge.
17    Collapse,
18    /// Keep only mutual edges: an undirected edge (u,v) is produced only if
19    /// both (u,v) and (v,u) exist in the directed graph.
20    Mutual,
21}
22
23/// Converts a directed graph to an undirected graph.
24///
25/// If the graph is already undirected, returns a copy.
26///
27/// # Modes
28///
29/// - `Each`: every directed edge becomes an undirected edge. A pair of
30///   mutual edges (u→v and v→u) becomes two parallel undirected edges.
31/// - `Collapse`: each pair of vertices gets at most one undirected edge
32///   regardless of how many directed edges connect them.
33/// - `Mutual`: only vertex pairs connected by mutual edges (both u→v and
34///   v→u) get an undirected edge.
35///
36/// # Examples
37///
38/// ```
39/// use rust_igraph::{Graph, to_undirected, ToUndirectedMode};
40///
41/// // Mutual pair collapses to one edge
42/// let mut g = Graph::new(3, true).unwrap();
43/// g.add_edge(0, 1).unwrap();
44/// g.add_edge(1, 0).unwrap();
45/// g.add_edge(1, 2).unwrap();
46///
47/// let u = to_undirected(&g, ToUndirectedMode::Collapse).unwrap();
48/// assert!(!u.is_directed());
49/// assert_eq!(u.ecount(), 2); // (0,1) collapsed + (1,2)
50///
51/// let u = to_undirected(&g, ToUndirectedMode::Mutual).unwrap();
52/// assert_eq!(u.ecount(), 1); // only (0,1) mutual pair survives
53///
54/// let u = to_undirected(&g, ToUndirectedMode::Each).unwrap();
55/// assert_eq!(u.ecount(), 3); // every directed edge → undirected
56/// ```
57pub fn to_undirected(graph: &Graph, mode: ToUndirectedMode) -> IgraphResult<Graph> {
58    let n = graph.vcount();
59
60    if !graph.is_directed() {
61        // Already undirected — clone the structure
62        let mut result = Graph::with_vertices(n);
63        let ecount = graph.ecount();
64        for eid in 0..ecount {
65            #[allow(clippy::cast_possible_truncation)]
66            let (src, tgt) = graph.edge(eid as u32)?;
67            result.add_edge(src, tgt)?;
68        }
69        return Ok(result);
70    }
71
72    let mut result = Graph::with_vertices(n);
73    let ecount = graph.ecount();
74
75    match mode {
76        ToUndirectedMode::Each => {
77            for eid in 0..ecount {
78                #[allow(clippy::cast_possible_truncation)]
79                let (src, tgt) = graph.edge(eid as u32)?;
80                result.add_edge(src, tgt)?;
81            }
82        }
83        ToUndirectedMode::Collapse => {
84            let mut seen = std::collections::HashSet::with_capacity(ecount);
85            for eid in 0..ecount {
86                #[allow(clippy::cast_possible_truncation)]
87                let (src, tgt) = graph.edge(eid as u32)?;
88                let key = if src <= tgt { (src, tgt) } else { (tgt, src) };
89                if seen.insert(key) {
90                    result.add_edge(key.0, key.1)?;
91                }
92            }
93        }
94        ToUndirectedMode::Mutual => {
95            // Build set of all directed edges, then add undirected for mutual pairs
96            let mut forward = std::collections::HashSet::with_capacity(ecount);
97            for eid in 0..ecount {
98                #[allow(clippy::cast_possible_truncation)]
99                let (src, tgt) = graph.edge(eid as u32)?;
100                forward.insert((src, tgt));
101            }
102            // Track which canonical pairs we've already added
103            let mut added = std::collections::HashSet::new();
104            for &(src, tgt) in &forward {
105                if src != tgt && forward.contains(&(tgt, src)) {
106                    let key = if src <= tgt { (src, tgt) } else { (tgt, src) };
107                    if added.insert(key) {
108                        result.add_edge(key.0, key.1)?;
109                    }
110                }
111            }
112        }
113    }
114
115    Ok(result)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_already_undirected() {
124        let mut g = Graph::with_vertices(3);
125        g.add_edge(0, 1).unwrap();
126        g.add_edge(1, 2).unwrap();
127        let u = to_undirected(&g, ToUndirectedMode::Each).unwrap();
128        assert!(!u.is_directed());
129        assert_eq!(u.vcount(), 3);
130        assert_eq!(u.ecount(), 2);
131    }
132
133    #[test]
134    fn test_each_mode_mutual_pair() {
135        let mut g = Graph::new(2, true).unwrap();
136        g.add_edge(0, 1).unwrap();
137        g.add_edge(1, 0).unwrap();
138        let u = to_undirected(&g, ToUndirectedMode::Each).unwrap();
139        assert!(!u.is_directed());
140        assert_eq!(u.ecount(), 2); // two parallel undirected edges
141    }
142
143    #[test]
144    fn test_each_mode_single_edge() {
145        let mut g = Graph::new(3, true).unwrap();
146        g.add_edge(0, 1).unwrap();
147        g.add_edge(1, 2).unwrap();
148        let u = to_undirected(&g, ToUndirectedMode::Each).unwrap();
149        assert_eq!(u.ecount(), 2);
150    }
151
152    #[test]
153    fn test_collapse_mode_mutual() {
154        let mut g = Graph::new(3, true).unwrap();
155        g.add_edge(0, 1).unwrap();
156        g.add_edge(1, 0).unwrap();
157        g.add_edge(1, 2).unwrap();
158        let u = to_undirected(&g, ToUndirectedMode::Collapse).unwrap();
159        assert_eq!(u.ecount(), 2); // (0,1) collapsed + (1,2)
160    }
161
162    #[test]
163    fn test_collapse_mode_parallel() {
164        let mut g = Graph::new(2, true).unwrap();
165        g.add_edge(0, 1).unwrap();
166        g.add_edge(0, 1).unwrap();
167        g.add_edge(0, 1).unwrap();
168        let u = to_undirected(&g, ToUndirectedMode::Collapse).unwrap();
169        assert_eq!(u.ecount(), 1); // all collapse to one
170    }
171
172    #[test]
173    fn test_mutual_mode_basic() {
174        let mut g = Graph::new(3, true).unwrap();
175        g.add_edge(0, 1).unwrap();
176        g.add_edge(1, 0).unwrap();
177        g.add_edge(1, 2).unwrap();
178        let u = to_undirected(&g, ToUndirectedMode::Mutual).unwrap();
179        assert_eq!(u.ecount(), 1); // only (0,1) is mutual
180    }
181
182    #[test]
183    fn test_mutual_mode_no_mutual() {
184        let mut g = Graph::new(3, true).unwrap();
185        g.add_edge(0, 1).unwrap();
186        g.add_edge(1, 2).unwrap();
187        g.add_edge(2, 0).unwrap();
188        let u = to_undirected(&g, ToUndirectedMode::Mutual).unwrap();
189        assert_eq!(u.ecount(), 0); // no mutual pairs
190    }
191
192    #[test]
193    fn test_mutual_mode_all_mutual() {
194        let mut g = Graph::new(3, true).unwrap();
195        for i in 0..3u32 {
196            for j in 0..3u32 {
197                if i != j {
198                    g.add_edge(i, j).unwrap();
199                }
200            }
201        }
202        let u = to_undirected(&g, ToUndirectedMode::Mutual).unwrap();
203        assert_eq!(u.ecount(), 3); // 3 mutual pairs → 3 undirected
204    }
205
206    #[test]
207    fn test_empty_graph() {
208        let g = Graph::new(0, true).unwrap();
209        let u = to_undirected(&g, ToUndirectedMode::Each).unwrap();
210        assert!(!u.is_directed());
211        assert_eq!(u.vcount(), 0);
212        assert_eq!(u.ecount(), 0);
213    }
214
215    #[test]
216    fn test_no_edges() {
217        let g = Graph::new(5, true).unwrap();
218        let u = to_undirected(&g, ToUndirectedMode::Collapse).unwrap();
219        assert_eq!(u.vcount(), 5);
220        assert_eq!(u.ecount(), 0);
221    }
222
223    #[test]
224    fn test_self_loops_each() {
225        let mut g = Graph::new(2, true).unwrap();
226        g.add_edge(0, 0).unwrap();
227        g.add_edge(0, 1).unwrap();
228        let u = to_undirected(&g, ToUndirectedMode::Each).unwrap();
229        assert_eq!(u.ecount(), 2);
230    }
231
232    #[test]
233    fn test_self_loops_mutual() {
234        // Self-loop: (0,0) is its own reverse, so it IS mutual
235        let mut g = Graph::new(2, true).unwrap();
236        g.add_edge(0, 0).unwrap();
237        g.add_edge(0, 1).unwrap();
238        let u = to_undirected(&g, ToUndirectedMode::Mutual).unwrap();
239        // Self-loop: forward.contains((0,0)) and forward.contains((0,0)) → yes
240        // But src != tgt check filters self-loops in mutual mode
241        assert_eq!(u.ecount(), 0);
242    }
243
244    #[test]
245    fn test_vcount_preserved() {
246        let mut g = Graph::new(10, true).unwrap();
247        g.add_edge(0, 5).unwrap();
248        g.add_edge(5, 0).unwrap();
249        let u = to_undirected(&g, ToUndirectedMode::Collapse).unwrap();
250        assert_eq!(u.vcount(), 10);
251    }
252}