Skip to main content

rust_igraph/algorithms/operators/
simplify.rs

1//! Simplify (ALGO-OP-001).
2//!
3//! Counterpart of `igraph_simplify()` from
4//! `references/igraph/src/operators/simplify.c`. Returns a new graph with
5//! self-loops and/or parallel edges removed, according to the two boolean
6//! flags. The input is borrowed and left untouched (upstream igraph mutates
7//! the graph in place; we prefer immutability — call sites do
8//! `g = simplify(&g, …)?` if they want C-style replacement).
9//!
10//! Phase-1 minimal slice: no edge-attribute combination (the upstream
11//! `edge_comb` argument is ignored — attributes are an out-of-scope ALGO-AT
12//! milestone). Both directed and undirected graphs are supported.
13//!
14//! Time complexity: O(|V| + |E| log |E|) when `remove_multiple = true`
15//! (sort dominated); O(|V| + |E|) when only loops are removed.
16
17use crate::core::graph::EdgeId;
18use crate::core::{Graph, IgraphResult, VertexId};
19
20/// Return a copy of `graph` with self-loops and/or parallel edges removed.
21///
22/// - `remove_multiple` — collapse parallel edges to a single edge between
23///   each ordered pair (directed) / unordered pair (undirected).
24/// - `remove_loops`    — drop every self-loop edge `(v, v)`.
25///
26/// If both flags are `false` this returns a structural clone of `graph`.
27///
28/// Edge attributes are dropped (Phase-1 minimal slice — see module docs).
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, simplify};
34///
35/// // Triangle plus a self-loop and a parallel edge.
36/// let mut g = Graph::with_vertices(3);
37/// g.add_edge(0, 0).unwrap();
38/// g.add_edge(0, 1).unwrap();
39/// g.add_edge(0, 1).unwrap();
40/// g.add_edge(1, 2).unwrap();
41/// g.add_edge(2, 0).unwrap();
42///
43/// let s = simplify(&g, true, true).unwrap();
44/// assert_eq!(s.vcount(), 3);
45/// assert_eq!(s.ecount(), 3);
46/// ```
47pub fn simplify(graph: &Graph, remove_multiple: bool, remove_loops: bool) -> IgraphResult<Graph> {
48    let n = graph.vcount();
49    let directed = graph.is_directed();
50
51    if !remove_multiple && !remove_loops {
52        return Ok(graph.clone());
53    }
54
55    let m = graph.ecount();
56    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(m);
57    for eid in 0..m {
58        let eid_u32 = u32::try_from(eid)
59            .map_err(|_| crate::IgraphError::Internal("edge id exceeds u32::MAX"))?;
60        let (f, t) = graph.edge(eid_u32 as EdgeId)?;
61        if remove_loops && f == t {
62            continue;
63        }
64        edges.push((f, t));
65    }
66
67    if remove_multiple {
68        // For undirected graphs, `Graph::add_edges` canonicalises endpoints
69        // so that stored from <= to (see `core::graph` module docs). The
70        // edges we just collected already obey that invariant for
71        // undirected inputs, and for directed inputs we treat (a,b) and
72        // (b,a) as distinct — sort+dedup on the tuple does the right
73        // thing in both cases.
74        edges.sort_unstable();
75        edges.dedup();
76    }
77
78    let mut g = Graph::new(n, directed)?;
79    g.add_edges(edges)?;
80    Ok(g)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    fn sorted_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
88        let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
89        let mut v: Vec<_> = (0..m).map(|e| g.edge(e).unwrap()).collect();
90        v.sort_unstable();
91        v
92    }
93
94    #[test]
95    fn no_op_when_both_flags_false() {
96        let mut g = Graph::with_vertices(3);
97        g.add_edge(0, 0).unwrap();
98        g.add_edge(0, 1).unwrap();
99        g.add_edge(0, 1).unwrap();
100        let s = simplify(&g, false, false).unwrap();
101        assert_eq!(s.vcount(), g.vcount());
102        assert_eq!(s.ecount(), g.ecount());
103        assert_eq!(sorted_edges(&s), sorted_edges(&g));
104    }
105
106    #[test]
107    fn removes_self_loops_only() {
108        let mut g = Graph::with_vertices(3);
109        g.add_edge(0, 0).unwrap();
110        g.add_edge(1, 1).unwrap();
111        g.add_edge(0, 1).unwrap();
112        g.add_edge(0, 1).unwrap();
113
114        let s = simplify(&g, false, true).unwrap();
115        assert_eq!(s.ecount(), 2);
116        assert_eq!(sorted_edges(&s), vec![(0, 1), (0, 1)]);
117    }
118
119    #[test]
120    fn removes_multi_edges_only_undirected() {
121        let mut g = Graph::with_vertices(3);
122        g.add_edge(0, 1).unwrap();
123        g.add_edge(0, 1).unwrap();
124        g.add_edge(1, 0).unwrap();
125        g.add_edge(1, 2).unwrap();
126
127        let s = simplify(&g, true, false).unwrap();
128        assert_eq!(s.ecount(), 2);
129        assert_eq!(sorted_edges(&s), vec![(0, 1), (1, 2)]);
130    }
131
132    #[test]
133    fn removes_multi_edges_directed() {
134        // Directed: (a,b) and (b,a) are distinct.
135        let mut g = Graph::new(3, true).unwrap();
136        g.add_edge(0, 1).unwrap();
137        g.add_edge(0, 1).unwrap();
138        g.add_edge(1, 0).unwrap();
139        g.add_edge(1, 2).unwrap();
140
141        let s = simplify(&g, true, false).unwrap();
142        assert!(s.is_directed());
143        assert_eq!(s.ecount(), 3);
144        assert_eq!(sorted_edges(&s), vec![(0, 1), (1, 0), (1, 2)]);
145    }
146
147    #[test]
148    fn removes_both_loops_and_multi() {
149        let mut g = Graph::with_vertices(3);
150        g.add_edge(0, 0).unwrap();
151        g.add_edge(0, 1).unwrap();
152        g.add_edge(0, 1).unwrap();
153        g.add_edge(1, 2).unwrap();
154        g.add_edge(2, 0).unwrap();
155
156        let s = simplify(&g, true, true).unwrap();
157        assert_eq!(s.ecount(), 3);
158        assert_eq!(sorted_edges(&s), vec![(0, 1), (0, 2), (1, 2)]);
159    }
160
161    #[test]
162    fn idempotent_on_simple_graph() {
163        let mut g = Graph::with_vertices(4);
164        g.add_edge(0, 1).unwrap();
165        g.add_edge(1, 2).unwrap();
166        g.add_edge(2, 3).unwrap();
167        let s = simplify(&g, true, true).unwrap();
168        assert_eq!(s.ecount(), g.ecount());
169        assert_eq!(sorted_edges(&s), sorted_edges(&g));
170    }
171
172    #[test]
173    fn empty_graph() {
174        let g = Graph::with_vertices(0);
175        let s = simplify(&g, true, true).unwrap();
176        assert_eq!(s.vcount(), 0);
177        assert_eq!(s.ecount(), 0);
178    }
179
180    #[test]
181    fn vertices_preserved_when_no_edges_remain() {
182        let mut g = Graph::with_vertices(3);
183        g.add_edge(0, 0).unwrap();
184        g.add_edge(1, 1).unwrap();
185        let s = simplify(&g, true, true).unwrap();
186        assert_eq!(s.vcount(), 3);
187        assert_eq!(s.ecount(), 0);
188    }
189
190    #[test]
191    fn igraph_simplify_example_directed_5_parallel() {
192        // From references/igraph/examples/simple/igraph_simplify.c case 1:
193        // directed 0->1 ×5, simplify(true,true) → single edge 0→1.
194        let mut g = Graph::new(2, true).unwrap();
195        for _ in 0..5 {
196            g.add_edge(0, 1).unwrap();
197        }
198        let s = simplify(&g, true, true).unwrap();
199        assert_eq!(s.ecount(), 1);
200        assert_eq!(s.edge(0).unwrap(), (0, 1));
201    }
202
203    #[test]
204    fn igraph_simplify_example_directed_loops_and_multi() {
205        // From igraph_simplify.c case 3:
206        // directed 0-0, 1-1, 2-2, 1-2; simplify(true,true) → only 1→2.
207        let mut g = Graph::new(3, true).unwrap();
208        g.add_edge(0, 0).unwrap();
209        g.add_edge(1, 1).unwrap();
210        g.add_edge(2, 2).unwrap();
211        g.add_edge(1, 2).unwrap();
212        let s = simplify(&g, true, true).unwrap();
213        assert_eq!(s.ecount(), 1);
214        assert_eq!(s.edge(0).unwrap(), (1, 2));
215    }
216}