Skip to main content

rust_igraph/algorithms/properties/
is_simple.rs

1//! `is_simple` (ALGO-PR-013 + ALGO-PR-013b).
2//!
3//! Counterpart of `igraph_is_simple()` from
4//! `references/igraph/src/properties/multiplicity.c`. A graph is *simple*
5//! if it has no self-loops and no parallel edges. Useful as a fast
6//! predicate before algorithms that assume simplicity (or to short-circuit
7//! [`crate::simplify()`] when nothing needs to change).
8//!
9//! [`is_simple`] (PR-013) treats directed graphs structurally — `(a,b)`
10//! and `(b,a)` are distinct edges so a mutual pair stays simple.
11//! [`is_simple_with_mode`] (PR-013b) adds [`SimpleMode::DirectedAsUndirected`]
12//! which collapses each directed edge to its endpoint pair, so a mutual
13//! `{a → b, b → a}` is reported as a multi-edge.
14
15use crate::core::graph::EdgeId;
16use crate::core::{Graph, IgraphError, IgraphResult};
17
18/// Direction-handling for [`is_simple_with_mode`]. Counterpart of
19/// upstream's `directed` boolean parameter.
20///
21/// On undirected graphs both modes produce the same answer.
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum SimpleMode {
24    /// `(a, b)` and `(b, a)` are distinct directed edges. Default;
25    /// matches what plain [`is_simple`] returns.
26    DirectedAsDirected,
27    /// `(a, b)` and `(b, a)` collapse to the same undirected edge,
28    /// so a mutual pair is reported as a multi-edge.
29    DirectedAsUndirected,
30}
31
32/// Returns `true` if `graph` has neither self-loops nor parallel edges.
33///
34/// Empty / no-edge graphs return `true` (vacuously simple).
35///
36/// # Examples
37///
38/// ```
39/// use rust_igraph::{Graph, is_simple};
40///
41/// let mut g = Graph::with_vertices(3);
42/// g.add_edge(0, 1).unwrap();
43/// g.add_edge(1, 2).unwrap();
44/// assert!(is_simple(&g).unwrap());
45///
46/// // Add a parallel edge → no longer simple.
47/// g.add_edge(0, 1).unwrap();
48/// assert!(!is_simple(&g).unwrap());
49/// ```
50pub fn is_simple(graph: &Graph) -> IgraphResult<bool> {
51    is_simple_with_mode(graph, SimpleMode::DirectedAsDirected)
52}
53
54/// `is_simple` with explicit [`SimpleMode`] (ALGO-PR-013b).
55///
56/// On undirected graphs both modes are equivalent. On directed graphs
57/// [`SimpleMode::DirectedAsUndirected`] returns `false` whenever a
58/// mutual pair `{a → b, b → a}` exists (treated as a multi-edge in
59/// the undirected view).
60///
61/// # Examples
62///
63/// ```
64/// use rust_igraph::{Graph, is_simple_with_mode, SimpleMode};
65///
66/// // Mutual directed pair: simple in directed mode, NOT simple as
67/// // undirected (would be a doubled undirected edge).
68/// let mut g = Graph::new(2, true).unwrap();
69/// g.add_edge(0, 1).unwrap();
70/// g.add_edge(1, 0).unwrap();
71/// assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
72/// assert!(!is_simple_with_mode(&g, SimpleMode::DirectedAsUndirected).unwrap());
73/// ```
74pub fn is_simple_with_mode(graph: &Graph, mode: SimpleMode) -> IgraphResult<bool> {
75    let n = graph.vcount();
76    let m = graph.ecount();
77    if n == 0 || m == 0 {
78        return Ok(true);
79    }
80
81    // Undirected graphs: both modes are equivalent because the
82    // existing per-vertex sorted-neighbour scan already collapses
83    // `(a,b)` / `(b,a)` (each undirected edge contributes once to
84    // both endpoints' neighbour lists in lexicographic order). The
85    // structural-directed scan below also works for undirected
86    // graphs since `Graph::neighbors` returns the merged adjacency
87    // view.
88    if !graph.is_directed() || mode == SimpleMode::DirectedAsDirected {
89        for v in 0..n {
90            let neis = graph.neighbors(v)?;
91            let mut prev: Option<u32> = None;
92            for &u in &neis {
93                if u == v {
94                    return Ok(false);
95                }
96                if Some(u) == prev {
97                    return Ok(false);
98                }
99                prev = Some(u);
100            }
101        }
102        return Ok(true);
103    }
104
105    // Directed-as-undirected: canonicalise each edge to `(min, max)`
106    // and look for duplicates after sorting. Self-loops fail
107    // immediately. O(|E| log |E|).
108    let m_u32 = u32::try_from(m).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
109    let mut pairs: Vec<(u32, u32)> = Vec::with_capacity(m);
110    for eid in 0..m_u32 {
111        let (src, tgt) = graph.edge(eid as EdgeId)?;
112        if src == tgt {
113            return Ok(false);
114        }
115        pairs.push(if src < tgt { (src, tgt) } else { (tgt, src) });
116    }
117    pairs.sort_unstable();
118    for w in pairs.windows(2) {
119        if w[0] == w[1] {
120            return Ok(false);
121        }
122    }
123    Ok(true)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn empty_graph_is_simple() {
132        let g = Graph::with_vertices(0);
133        assert!(is_simple(&g).unwrap());
134    }
135
136    #[test]
137    fn no_edge_graph_is_simple() {
138        let g = Graph::with_vertices(5);
139        assert!(is_simple(&g).unwrap());
140    }
141
142    #[test]
143    fn path_is_simple() {
144        let mut g = Graph::with_vertices(4);
145        g.add_edge(0, 1).unwrap();
146        g.add_edge(1, 2).unwrap();
147        g.add_edge(2, 3).unwrap();
148        assert!(is_simple(&g).unwrap());
149    }
150
151    #[test]
152    fn self_loop_breaks_simplicity() {
153        let mut g = Graph::with_vertices(2);
154        g.add_edge(0, 0).unwrap();
155        g.add_edge(0, 1).unwrap();
156        assert!(!is_simple(&g).unwrap());
157    }
158
159    #[test]
160    fn parallel_edge_breaks_simplicity_undirected() {
161        let mut g = Graph::with_vertices(2);
162        g.add_edge(0, 1).unwrap();
163        g.add_edge(0, 1).unwrap();
164        assert!(!is_simple(&g).unwrap());
165    }
166
167    #[test]
168    fn reversed_parallel_undirected_breaks_simplicity() {
169        // Undirected (1,0) and (0,1) are the same edge.
170        let mut g = Graph::with_vertices(2);
171        g.add_edge(0, 1).unwrap();
172        g.add_edge(1, 0).unwrap();
173        assert!(!is_simple(&g).unwrap());
174    }
175
176    #[test]
177    fn directed_mutual_pair_is_simple() {
178        // Directed (a,b) and (b,a) are distinct edges — phase-1 minimal
179        // slice treats this as simple (matches upstream's directed=true).
180        let mut g = Graph::new(2, true).unwrap();
181        g.add_edge(0, 1).unwrap();
182        g.add_edge(1, 0).unwrap();
183        assert!(is_simple(&g).unwrap());
184    }
185
186    #[test]
187    fn directed_parallel_breaks_simplicity() {
188        let mut g = Graph::new(2, true).unwrap();
189        g.add_edge(0, 1).unwrap();
190        g.add_edge(0, 1).unwrap();
191        assert!(!is_simple(&g).unwrap());
192    }
193
194    #[test]
195    fn directed_self_loop_breaks_simplicity() {
196        let mut g = Graph::new(2, true).unwrap();
197        g.add_edge(0, 0).unwrap();
198        assert!(!is_simple(&g).unwrap());
199    }
200
201    // ----- ALGO-PR-013b: SimpleMode::DirectedAsUndirected -----
202
203    #[test]
204    fn directed_mutual_pair_not_simple_in_undirected_mode() {
205        let mut g = Graph::new(2, true).unwrap();
206        g.add_edge(0, 1).unwrap();
207        g.add_edge(1, 0).unwrap();
208        assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
209        assert!(!is_simple_with_mode(&g, SimpleMode::DirectedAsUndirected).unwrap());
210    }
211
212    #[test]
213    fn directed_mode_default_matches_is_simple() {
214        let mut g = Graph::new(3, true).unwrap();
215        g.add_edge(0, 1).unwrap();
216        g.add_edge(1, 2).unwrap();
217        g.add_edge(2, 0).unwrap();
218        assert_eq!(
219            is_simple(&g).unwrap(),
220            is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap()
221        );
222    }
223
224    #[test]
225    fn directed_3_cycle_simple_in_undirected_mode() {
226        // Three directed edges forming a cycle 0→1→2→0. As undirected,
227        // they form three distinct edges (0-1, 1-2, 0-2) → simple.
228        let mut g = Graph::new(3, true).unwrap();
229        g.add_edge(0, 1).unwrap();
230        g.add_edge(1, 2).unwrap();
231        g.add_edge(2, 0).unwrap();
232        assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsUndirected).unwrap());
233    }
234
235    #[test]
236    fn directed_self_loop_not_simple_in_either_mode() {
237        let mut g = Graph::new(2, true).unwrap();
238        g.add_edge(0, 0).unwrap();
239        assert!(!is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
240        assert!(!is_simple_with_mode(&g, SimpleMode::DirectedAsUndirected).unwrap());
241    }
242
243    #[test]
244    fn undirected_graph_modes_are_equivalent() {
245        let mut g = Graph::with_vertices(3);
246        g.add_edge(0, 1).unwrap();
247        g.add_edge(1, 2).unwrap();
248        let a = is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap();
249        let b = is_simple_with_mode(&g, SimpleMode::DirectedAsUndirected).unwrap();
250        assert_eq!(a, b);
251        assert!(a);
252    }
253
254    #[test]
255    fn simplify_makes_graph_simple() {
256        // Round-trip: simplify(g) is always simple.
257        let mut g = Graph::with_vertices(3);
258        g.add_edge(0, 0).unwrap();
259        g.add_edge(0, 1).unwrap();
260        g.add_edge(0, 1).unwrap();
261        g.add_edge(1, 2).unwrap();
262        assert!(!is_simple(&g).unwrap());
263        let s = crate::simplify(&g, true, true).unwrap();
264        assert!(is_simple(&s).unwrap());
265    }
266}