Skip to main content

rust_igraph/algorithms/constructors/
ring.rs

1//! Ring / path / cycle constructors (ALGO-CN-001).
2//!
3//! Counterpart of `igraph_ring()`, `igraph_path_graph()` and
4//! `igraph_cycle_graph()` in
5//! `references/igraph/src/constructors/regular.c:495-604`.
6//!
7//! The model lays `n` vertices in a sequence `0, 1, …, n-1` and joins
8//! consecutive vertices in order. Three boolean flags shape the result:
9//!
10//! * `directed` — emit a digraph. Edges always run in the forward
11//!   direction `(i, i+1)`. Ignored only by the parity of `mutual`.
12//! * `mutual` — *only* applied when `directed` is `true`; emits the
13//!   back-arc `(i+1, i)` alongside every forward arc. Undirected
14//!   construction ignores `mutual` entirely (matching upstream).
15//! * `circular` — close the sequence with the wrap-around edge
16//!   `(n-1, 0)`. With `circular = false` the result is the path
17//!   `P_n`; with `circular = true` it is the cycle `C_n`.
18//!
19//! Edge count is `n - 1` for a path and `n` for a cycle, doubled when
20//! `directed && mutual`. The two convenience wrappers
21//! [`path_graph`] (alias for `circular = false`) and
22//! [`cycle_graph`] (alias for `circular = true`) mirror the upstream
23//! one-line wrappers exactly.
24//!
25//! Degenerate cases mirror upstream behaviour verbatim:
26//!
27//! * `n == 0` — the empty graph.
28//! * `n == 1`, `circular == false` — a single isolated vertex.
29//! * `n == 1`, `circular == true` — a self-loop `(0, 0)`. **Not simple.**
30//! * `n == 2`, `circular == true`, `directed == false` — two parallel
31//!   edges between `0` and `1`. **Not simple.**
32//!
33//! Time complexity: O(|V|).
34
35use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
36
37/// Lay `n` vertices in a sequence and join them; close the loop iff
38/// `circular`.
39///
40/// See the module-level docs for the precise role of each flag.
41///
42/// # Errors
43///
44/// Returns [`IgraphError::Internal`] if the implied edge count would
45/// overflow `usize` — only reachable on 32-bit targets with absurdly
46/// large `n`.
47///
48/// # Examples
49///
50/// ```
51/// use rust_igraph::ring_graph;
52///
53/// // Undirected cycle on 5 vertices.
54/// let c5 = ring_graph(5, false, false, true).unwrap();
55/// assert_eq!(c5.vcount(), 5);
56/// assert_eq!(c5.ecount(), 5);
57///
58/// // Open path on 4 vertices, no wrap-around.
59/// let p4 = ring_graph(4, false, false, false).unwrap();
60/// assert_eq!(p4.ecount(), 3);
61/// ```
62pub fn ring_graph(n: u32, directed: bool, mutual: bool, circular: bool) -> IgraphResult<Graph> {
63    if n == 0 {
64        return Graph::new(0, directed);
65    }
66
67    let n_usize = n as usize;
68    let forward_edges = if circular { n_usize } else { n_usize - 1 };
69    let edges_per_link = if directed && mutual { 2 } else { 1 };
70    let total_edges = forward_edges
71        .checked_mul(edges_per_link)
72        .ok_or(IgraphError::Internal("ring_graph edge count overflow"))?;
73
74    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
75
76    for i in 0..n.saturating_sub(1) {
77        let next = i + 1;
78        edges.push((i, next));
79        if directed && mutual {
80            edges.push((next, i));
81        }
82    }
83    if circular {
84        let last = n - 1;
85        edges.push((last, 0));
86        if directed && mutual {
87            edges.push((0, last));
88        }
89    }
90
91    let mut g = Graph::new(n, directed)?;
92    g.add_edges(edges)?;
93    Ok(g)
94}
95
96/// Path graph `P_n`: convenience wrapper for `ring_graph(n, directed,
97/// mutual, false)`.
98///
99/// # Examples
100///
101/// ```
102/// use rust_igraph::path_graph;
103///
104/// let p3 = path_graph(3, false, false).unwrap();
105/// assert_eq!(p3.ecount(), 2);
106/// ```
107pub fn path_graph(n: u32, directed: bool, mutual: bool) -> IgraphResult<Graph> {
108    ring_graph(n, directed, mutual, false)
109}
110
111/// Cycle graph `C_n`: convenience wrapper for `ring_graph(n, directed,
112/// mutual, true)`.
113///
114/// # Examples
115///
116/// ```
117/// use rust_igraph::cycle_graph;
118///
119/// let c4 = cycle_graph(4, false, false).unwrap();
120/// assert_eq!(c4.ecount(), 4);
121/// ```
122pub fn cycle_graph(n: u32, directed: bool, mutual: bool) -> IgraphResult<Graph> {
123    ring_graph(n, directed, mutual, true)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
131        let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
132        (0..m)
133            .map(|eid| g.edge(eid).expect("edge id in bounds"))
134            .collect()
135    }
136
137    #[test]
138    fn empty_graph() {
139        for &directed in &[false, true] {
140            for &mutual in &[false, true] {
141                for &circular in &[false, true] {
142                    let g = ring_graph(0, directed, mutual, circular).expect("ring n=0");
143                    assert_eq!(g.vcount(), 0);
144                    assert_eq!(g.ecount(), 0);
145                    assert_eq!(g.is_directed(), directed);
146                }
147            }
148        }
149    }
150
151    #[test]
152    fn singleton_path_is_isolated_vertex() {
153        let g = ring_graph(1, false, false, false).expect("ring n=1 path");
154        assert_eq!(g.vcount(), 1);
155        assert_eq!(g.ecount(), 0);
156    }
157
158    #[test]
159    fn singleton_cycle_is_self_loop() {
160        let g = ring_graph(1, false, false, true).expect("ring n=1 cycle");
161        assert_eq!(g.vcount(), 1);
162        assert_eq!(g.ecount(), 1);
163        assert_eq!(collect_edges(&g), vec![(0, 0)]);
164    }
165
166    #[test]
167    fn two_vertex_undirected_cycle_has_parallel_edges() {
168        let g = ring_graph(2, false, false, true).expect("ring n=2 undirected cycle");
169        // Undirected canonicalisation stores both as (0, 1) — two parallel edges.
170        assert_eq!(g.ecount(), 2);
171        assert_eq!(collect_edges(&g), vec![(0, 1), (0, 1)]);
172    }
173
174    #[test]
175    fn undirected_path_p5_canonical_edges() {
176        let g = ring_graph(5, false, false, false).expect("ring P5");
177        assert_eq!(g.vcount(), 5);
178        assert_eq!(g.ecount(), 4);
179        assert!(!g.is_directed());
180        assert_eq!(collect_edges(&g), vec![(0, 1), (1, 2), (2, 3), (3, 4)]);
181    }
182
183    #[test]
184    fn undirected_cycle_c5_closes_with_wraparound() {
185        let g = ring_graph(5, false, false, true).expect("ring C5");
186        // Undirected canonicalisation stores the wrap-around as (0, 4), not (4, 0).
187        assert_eq!(g.ecount(), 5);
188        assert_eq!(
189            collect_edges(&g),
190            vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)]
191        );
192    }
193
194    #[test]
195    fn directed_path_p4_forward_only() {
196        let g = ring_graph(4, true, false, false).expect("ring directed P4");
197        assert!(g.is_directed());
198        assert_eq!(collect_edges(&g), vec![(0, 1), (1, 2), (2, 3)]);
199    }
200
201    #[test]
202    fn directed_cycle_c4_forward_only() {
203        let g = ring_graph(4, true, false, true).expect("ring directed C4");
204        assert_eq!(collect_edges(&g), vec![(0, 1), (1, 2), (2, 3), (3, 0)]);
205    }
206
207    #[test]
208    fn directed_mutual_path_emits_back_arcs_in_order() {
209        let g = ring_graph(4, true, true, false).expect("ring directed mutual P4");
210        assert_eq!(g.ecount(), 6);
211        assert_eq!(
212            collect_edges(&g),
213            vec![(0, 1), (1, 0), (1, 2), (2, 1), (2, 3), (3, 2)]
214        );
215    }
216
217    #[test]
218    fn directed_mutual_cycle_emits_wraparound_back_arc_last() {
219        let g = ring_graph(4, true, true, true).expect("ring directed mutual C4");
220        assert_eq!(g.ecount(), 8);
221        assert_eq!(
222            collect_edges(&g),
223            vec![
224                (0, 1),
225                (1, 0),
226                (1, 2),
227                (2, 1),
228                (2, 3),
229                (3, 2),
230                (3, 0),
231                (0, 3),
232            ]
233        );
234    }
235
236    #[test]
237    fn undirected_ignores_mutual_flag() {
238        // mutual must be IGNORED when directed == false. Both shapes
239        // must agree edge-for-edge.
240        for &circular in &[false, true] {
241            let g_no_mutual = ring_graph(6, false, false, circular).expect("undirected no mutual");
242            let g_mutual = ring_graph(6, false, true, circular).expect("undirected w/ mutual");
243            assert_eq!(collect_edges(&g_no_mutual), collect_edges(&g_mutual));
244        }
245    }
246
247    #[test]
248    fn path_wrapper_matches_ring_circular_false() {
249        for &directed in &[false, true] {
250            for &mutual in &[false, true] {
251                let a = path_graph(7, directed, mutual).expect("path_graph");
252                let b = ring_graph(7, directed, mutual, false).expect("ring circular=false");
253                assert_eq!(collect_edges(&a), collect_edges(&b));
254                assert_eq!(a.is_directed(), b.is_directed());
255            }
256        }
257    }
258
259    #[test]
260    fn cycle_wrapper_matches_ring_circular_true() {
261        for &directed in &[false, true] {
262            for &mutual in &[false, true] {
263                let a = cycle_graph(8, directed, mutual).expect("cycle_graph");
264                let b = ring_graph(8, directed, mutual, true).expect("ring circular=true");
265                assert_eq!(collect_edges(&a), collect_edges(&b));
266                assert_eq!(a.is_directed(), b.is_directed());
267            }
268        }
269    }
270
271    #[test]
272    fn vertex_degrees_path_undirected() {
273        let g = ring_graph(10, false, false, false).expect("undirected P10");
274        // Interior vertices have degree 2; endpoints have degree 1.
275        for v in 0..10u32 {
276            let deg = g.neighbors(v).expect("neighbors").len();
277            let expected = if v == 0 || v == 9 { 1 } else { 2 };
278            assert_eq!(deg, expected, "vertex {v}");
279        }
280    }
281
282    #[test]
283    fn vertex_degrees_cycle_undirected() {
284        let g = ring_graph(10, false, false, true).expect("undirected C10");
285        for v in 0..10u32 {
286            let deg = g.neighbors(v).expect("neighbors").len();
287            assert_eq!(deg, 2, "vertex {v} (cycle)");
288        }
289    }
290}
291
292#[cfg(all(test, feature = "proptest-harness"))]
293mod proptest_tests {
294    use super::*;
295    use proptest::prelude::*;
296
297    proptest! {
298        #[test]
299        fn ring_edge_count_matches_formula(
300            n in 0u32..40,
301            directed in any::<bool>(),
302            mutual in any::<bool>(),
303            circular in any::<bool>(),
304        ) {
305            let g = ring_graph(n, directed, mutual, circular).unwrap();
306            prop_assert_eq!(g.vcount(), n);
307            prop_assert_eq!(g.is_directed(), directed);
308
309            let expected = if n == 0 {
310                0
311            } else {
312                let base = if circular { n } else { n - 1 };
313                let factor = if directed && mutual { 2 } else { 1 };
314                base * factor
315            };
316            prop_assert_eq!(u32::try_from(g.ecount()).unwrap(), expected);
317        }
318
319        #[test]
320        fn undirected_ignores_mutual_property(
321            n in 0u32..40,
322            circular in any::<bool>(),
323        ) {
324            let a = ring_graph(n, false, false, circular).unwrap();
325            let b = ring_graph(n, false, true, circular).unwrap();
326            let m_a = u32::try_from(a.ecount()).unwrap();
327            let m_b = u32::try_from(b.ecount()).unwrap();
328            let edges_a: Vec<_> = (0..m_a).map(|e| a.edge(e).unwrap()).collect();
329            let edges_b: Vec<_> = (0..m_b).map(|e| b.edge(e).unwrap()).collect();
330            prop_assert_eq!(edges_a, edges_b);
331        }
332
333        #[test]
334        fn path_cycle_wrappers_agree_with_ring(
335            n in 0u32..40,
336            directed in any::<bool>(),
337            mutual in any::<bool>(),
338        ) {
339            let p_wrap = path_graph(n, directed, mutual).unwrap();
340            let p_ring = ring_graph(n, directed, mutual, false).unwrap();
341            let c_wrap = cycle_graph(n, directed, mutual).unwrap();
342            let c_ring = ring_graph(n, directed, mutual, true).unwrap();
343
344            let collect = |g: &Graph| -> Vec<(VertexId, VertexId)> {
345                let m = u32::try_from(g.ecount()).unwrap();
346                (0..m).map(|e| g.edge(e).unwrap()).collect()
347            };
348
349            prop_assert_eq!(collect(&p_wrap), collect(&p_ring));
350            prop_assert_eq!(collect(&c_wrap), collect(&c_ring));
351        }
352    }
353}