Skip to main content

rust_igraph/algorithms/constructors/
generalized_petersen.rs

1//! Generalized Petersen graph constructor (ALGO-CN-010).
2//!
3//! Counterpart of `igraph_generalized_petersen()` in
4//! `references/igraph/src/constructors/generalized_petersen.c:58-95`.
5//!
6//! The generalized Petersen graph `G(n, k)` has `2n` vertices in two
7//! layers:
8//!
9//! * **outer cycle** `v_0, v_1, …, v_{n-1}` — a cycle `C_n` over ids
10//!   `0..n`.
11//! * **inner circulant** `u_0, u_1, …, u_{n-1}` — circulant graph over
12//!   ids `n..2n` where `u_i` is connected to `u_{(i + k) mod n}`.
13//! * **rungs** — every `v_i` is connected to its inner counterpart
14//!   `u_i`.
15//!
16//! It has exactly `3n` edges: `n` outer-cycle edges, `n` rung edges,
17//! and `n` inner-circulant edges.
18//!
19//! `G(5, 2)` is the classic Petersen graph. Other famous specializations
20//! include `G(8, 3)` (Möbius–Kantor), `G(10, 3)` (Desargues), and
21//! `G(12, 5)` (Nauru).
22//!
23//! Constraints (mirroring upstream):
24//!
25//! * `n >= 3`.
26//! * `0 < k < n / 2` (note: strict — `2k < n`).
27//!
28//! Returns an **undirected** graph in all cases — the upstream C
29//! constructor is fixed-direction (`IGRAPH_UNDIRECTED`).
30//!
31//! Reference: M. E. Watkins, *A Theorem on Tait Colorings with an
32//! Application to the Generalized Petersen Graphs*, Journal of
33//! Combinatorial Theory 6, 152–164 (1969).
34//!
35//! Time complexity: `O(|V|) = O(n)`.
36
37use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
38
39/// Build the generalized Petersen graph `G(n, k)`.
40///
41/// The result is **undirected** with `2n` vertices and `3n` edges:
42/// outer cycle, inner circulant of shift `k`, and one rung per index.
43///
44/// # Errors
45///
46/// * [`IgraphError::InvalidArgument`] — `n < 3`.
47/// * [`IgraphError::InvalidArgument`] — `k == 0`, or `k >= n / 2`
48///   (equivalently `2 * k >= n`).
49/// * [`IgraphError::InvalidArgument`] — `2 * n` overflows `u32`
50///   (theoretical: would require `n > u32::MAX / 2`).
51///
52/// # Examples
53///
54/// ```
55/// use rust_igraph::generalized_petersen;
56///
57/// // The classic Petersen graph G(5, 2).
58/// let g = generalized_petersen(5, 2).unwrap();
59/// assert_eq!(g.vcount(), 10);
60/// assert_eq!(g.ecount(), 15);
61/// assert!(!g.is_directed());
62/// ```
63pub fn generalized_petersen(n: u32, k: u32) -> IgraphResult<Graph> {
64    if n < 3 {
65        return Err(IgraphError::InvalidArgument(format!(
66            "generalized_petersen: n = {n} must be at least 3"
67        )));
68    }
69
70    // 0 < k < n/2 — the strict upper bound 2*k < n avoids parallel
71    // inner edges (e.g. k = n/2 with n even gives a perfect matching
72    // that doubles edges). Upstream rejects 2*k == n as well.
73    if k == 0 || k.checked_mul(2).is_none_or(|two_k| two_k >= n) {
74        return Err(IgraphError::InvalidArgument(format!(
75            "generalized_petersen: k = {k} must satisfy 0 < k < n/2 with n = {n}"
76        )));
77    }
78
79    let vcount: u32 = n.checked_mul(2).ok_or_else(|| {
80        IgraphError::InvalidArgument(format!(
81            "generalized_petersen: 2*n overflows u32 for n = {n}"
82        ))
83    })?;
84    let ecount: usize = usize::try_from(n)
85        .ok()
86        .and_then(|nu| nu.checked_mul(3))
87        .ok_or_else(|| {
88            IgraphError::InvalidArgument(format!(
89                "generalized_petersen: 3*n overflows usize for n = {n}"
90            ))
91        })?;
92
93    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
94    for i in 0..n {
95        // Outer cycle edge v_i — v_{(i+1) mod n}.
96        edges.push((i, (i + 1) % n));
97        // Rung v_i — u_i.
98        edges.push((i, i + n));
99        // Inner circulant edge u_i — u_{(i+k) mod n}.
100        edges.push((i + n, ((i + k) % n) + n));
101    }
102
103    let mut g = Graph::new(vcount, false)?;
104    g.add_edges(edges)?;
105    Ok(g)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    fn dump_edges(g: &Graph) -> Vec<(u32, u32)> {
113        let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
114        (0..m)
115            .map(|e| g.edge(e).expect("edge id in bounds"))
116            .collect()
117    }
118
119    fn canon(u: u32, v: u32) -> (u32, u32) {
120        if u <= v { (u, v) } else { (v, u) }
121    }
122
123    #[test]
124    fn petersen_graph_g_5_2() {
125        // The eponymous Petersen graph: 10 vertices, 15 edges, 3-regular.
126        let g = generalized_petersen(5, 2).expect("Petersen graph");
127        assert_eq!(g.vcount(), 10);
128        assert_eq!(g.ecount(), 15);
129        assert!(!g.is_directed());
130        for v in 0..g.vcount() {
131            assert_eq!(
132                g.degree(v).expect("in range"),
133                3,
134                "vertex {v} should be 3-regular"
135            );
136        }
137    }
138
139    #[test]
140    fn mobius_kantor_g_8_3() {
141        // Möbius–Kantor graph: 16 vertices, 24 edges, 3-regular, bipartite, girth 6.
142        let g = generalized_petersen(8, 3).expect("Möbius–Kantor");
143        assert_eq!(g.vcount(), 16);
144        assert_eq!(g.ecount(), 24);
145        for v in 0..g.vcount() {
146            assert_eq!(g.degree(v).expect("in range"), 3);
147        }
148    }
149
150    #[test]
151    fn desargues_g_10_3() {
152        let g = generalized_petersen(10, 3).expect("Desargues");
153        assert_eq!(g.vcount(), 20);
154        assert_eq!(g.ecount(), 30);
155        for v in 0..g.vcount() {
156            assert_eq!(g.degree(v).expect("in range"), 3);
157        }
158    }
159
160    #[test]
161    fn nauru_g_12_5() {
162        let g = generalized_petersen(12, 5).expect("Nauru");
163        assert_eq!(g.vcount(), 24);
164        assert_eq!(g.ecount(), 36);
165        for v in 0..g.vcount() {
166            assert_eq!(g.degree(v).expect("in range"), 3);
167        }
168    }
169
170    #[test]
171    fn three_regular_for_all_valid_n_k() {
172        // Every G(n, k) is 3-regular by construction; check a sweep.
173        for n in 3..=20u32 {
174            // valid k: 1 <= k < n/2 i.e. 2*k < n
175            let max_k = (n - 1) / 2;
176            for k in 1..=max_k {
177                let g = generalized_petersen(n, k).expect("valid (n,k)");
178                assert_eq!(g.vcount(), 2 * n);
179                assert_eq!(g.ecount(), 3 * (n as usize));
180                for v in 0..g.vcount() {
181                    assert_eq!(
182                        g.degree(v).expect("in range"),
183                        3,
184                        "G({n},{k}): vertex {v} should have degree 3"
185                    );
186                }
187            }
188        }
189    }
190
191    #[test]
192    fn edge_emission_order_matches_upstream_g_5_2() {
193        // Emission order: for each i in 0..n, push outer, rung, inner.
194        // For G(5, 2):
195        //   i=0: (0,1)(0,5)(5,7)
196        //   i=1: (1,2)(1,6)(6,8)
197        //   i=2: (2,3)(2,7)(7,9)
198        //   i=3: (3,4)(3,8)(8,5)
199        //   i=4: (4,0)(4,9)(9,6)
200        // Undirected Graph::add_edges canonicalises (lo, hi).
201        let g = generalized_petersen(5, 2).expect("G(5,2)");
202        let expected = vec![
203            (0, 1),
204            (0, 5),
205            (5, 7),
206            (1, 2),
207            (1, 6),
208            (6, 8),
209            (2, 3),
210            (2, 7),
211            (7, 9),
212            (3, 4),
213            (3, 8),
214            (5, 8),
215            (0, 4),
216            (4, 9),
217            (6, 9),
218        ];
219        assert_eq!(dump_edges(&g), expected);
220    }
221
222    #[test]
223    fn outer_cycle_present() {
224        // Every (i, (i+1) % n) edge with i, i+1 < n must exist.
225        let g = generalized_petersen(7, 2).expect("G(7,2)");
226        let edges: std::collections::HashSet<(u32, u32)> = dump_edges(&g).into_iter().collect();
227        for i in 0..7u32 {
228            assert!(edges.contains(&canon(i, (i + 1) % 7)));
229        }
230    }
231
232    #[test]
233    fn rung_edges_present() {
234        let g = generalized_petersen(7, 2).expect("G(7,2)");
235        let edges: std::collections::HashSet<(u32, u32)> = dump_edges(&g).into_iter().collect();
236        for i in 0..7u32 {
237            assert!(
238                edges.contains(&canon(i, i + 7)),
239                "missing rung {i}-{}",
240                i + 7
241            );
242        }
243    }
244
245    #[test]
246    fn inner_circulant_edges_present() {
247        let g = generalized_petersen(7, 2).expect("G(7,2)");
248        let edges: std::collections::HashSet<(u32, u32)> = dump_edges(&g).into_iter().collect();
249        for i in 0..7u32 {
250            let lhs = i + 7;
251            let rhs = ((i + 2) % 7) + 7;
252            assert!(
253                edges.contains(&canon(lhs, rhs)),
254                "missing inner edge {lhs}-{rhs}"
255            );
256        }
257    }
258
259    #[test]
260    fn rejects_n_less_than_three() {
261        for n in 0..3u32 {
262            let err = generalized_petersen(n, 1).unwrap_err();
263            assert!(matches!(err, IgraphError::InvalidArgument(_)));
264        }
265    }
266
267    #[test]
268    fn rejects_k_zero() {
269        let err = generalized_petersen(5, 0).unwrap_err();
270        assert!(matches!(err, IgraphError::InvalidArgument(_)));
271    }
272
273    #[test]
274    fn rejects_k_at_half_n() {
275        // k = n/2 (with n even) is rejected — 2*k == n.
276        let err = generalized_petersen(6, 3).unwrap_err();
277        assert!(matches!(err, IgraphError::InvalidArgument(_)));
278        // k just below n/2 (n=6, k=2) is fine.
279        assert!(generalized_petersen(6, 2).is_ok());
280    }
281
282    #[test]
283    fn rejects_k_too_large() {
284        let err = generalized_petersen(7, 4).unwrap_err();
285        assert!(matches!(err, IgraphError::InvalidArgument(_)));
286        // 7/2 = 3 (integer), 2*3 = 6 < 7 → k=3 is valid.
287        assert!(generalized_petersen(7, 3).is_ok());
288    }
289
290    #[test]
291    fn no_self_loops_no_parallel_edges() {
292        // 3-regular, simple — verify by canonical-set dedup.
293        for n in 3..=15u32 {
294            let max_k = (n - 1) / 2;
295            for k in 1..=max_k {
296                let g = generalized_petersen(n, k).expect("valid");
297                let edges = dump_edges(&g);
298                let mut set: std::collections::HashSet<(u32, u32)> =
299                    std::collections::HashSet::new();
300                for (u, v) in &edges {
301                    assert_ne!(u, v, "G({n},{k}) has self-loop {u}-{v}");
302                    let key = canon(*u, *v);
303                    assert!(set.insert(key), "G({n},{k}) has duplicate edge {u}-{v}");
304                }
305                assert_eq!(set.len(), 3 * (n as usize));
306            }
307        }
308    }
309}
310
311#[cfg(all(test, feature = "proptest-harness"))]
312mod proptest_invariants {
313    use super::*;
314    use proptest::prelude::*;
315
316    fn dump_edges(g: &Graph) -> Vec<(u32, u32)> {
317        let m = u32::try_from(g.ecount()).expect("ecount fits u32");
318        (0..m)
319            .map(|e| g.edge(e).expect("edge id in bounds"))
320            .collect()
321    }
322
323    fn canon(u: u32, v: u32) -> (u32, u32) {
324        if u <= v { (u, v) } else { (v, u) }
325    }
326
327    proptest! {
328        #![proptest_config(ProptestConfig {
329            cases: 64,
330            max_shrink_iters: 1000,
331            .. ProptestConfig::default()
332        })]
333
334        /// Vertex and edge counts always match the closed-form `2n` and
335        /// `3n`.
336        #[test]
337        fn vcount_ecount_match_formula(n in 3u32..=80, k_seed in 1u32..=200) {
338            let max_k = (n - 1) / 2;
339            prop_assume!(max_k >= 1);
340            let k = ((k_seed - 1) % max_k) + 1;
341            let g = generalized_petersen(n, k).expect("valid (n,k)");
342            prop_assert_eq!(g.vcount(), 2 * n);
343            prop_assert_eq!(g.ecount(), 3 * (n as usize));
344        }
345
346        /// Every emitted graph is 3-regular and simple.
347        #[test]
348        fn three_regular_and_simple(n in 3u32..=60, k_seed in 1u32..=200) {
349            let max_k = (n - 1) / 2;
350            prop_assume!(max_k >= 1);
351            let k = ((k_seed - 1) % max_k) + 1;
352            let g = generalized_petersen(n, k).expect("valid (n,k)");
353            for v in 0..g.vcount() {
354                prop_assert_eq!(g.degree(v).expect("in range"), 3);
355            }
356            // simplicity (no loops, no parallels)
357            let mut set: std::collections::HashSet<(u32, u32)> =
358                std::collections::HashSet::new();
359            for (u, w) in dump_edges(&g) {
360                prop_assert_ne!(u, w);
361                prop_assert!(set.insert(canon(u, w)));
362            }
363        }
364
365        /// The outer-cycle subgraph (vertices 0..n) is exactly a cycle
366        /// C_n: connecting (i, (i+1) % n) for every i.
367        #[test]
368        fn outer_layer_is_a_cycle(n in 3u32..=50, k_seed in 1u32..=200) {
369            let max_k = (n - 1) / 2;
370            prop_assume!(max_k >= 1);
371            let k = ((k_seed - 1) % max_k) + 1;
372            let g = generalized_petersen(n, k).expect("valid (n,k)");
373            let edges: std::collections::HashSet<(u32, u32)> =
374                dump_edges(&g).into_iter().collect();
375            for i in 0..n {
376                prop_assert!(edges.contains(&canon(i, (i + 1) % n)));
377            }
378        }
379
380        /// Rungs: every i in 0..n is connected to i + n.
381        #[test]
382        fn rungs_exist(n in 3u32..=50, k_seed in 1u32..=200) {
383            let max_k = (n - 1) / 2;
384            prop_assume!(max_k >= 1);
385            let k = ((k_seed - 1) % max_k) + 1;
386            let g = generalized_petersen(n, k).expect("valid (n,k)");
387            let edges: std::collections::HashSet<(u32, u32)> =
388                dump_edges(&g).into_iter().collect();
389            for i in 0..n {
390                prop_assert!(edges.contains(&canon(i, i + n)));
391            }
392        }
393
394        /// Inner layer (vertices n..2n) is exactly the circulant with
395        /// shift k.
396        #[test]
397        fn inner_layer_is_circulant_k(n in 3u32..=50, k_seed in 1u32..=200) {
398            let max_k = (n - 1) / 2;
399            prop_assume!(max_k >= 1);
400            let k = ((k_seed - 1) % max_k) + 1;
401            let g = generalized_petersen(n, k).expect("valid (n,k)");
402            let edges: std::collections::HashSet<(u32, u32)> =
403                dump_edges(&g).into_iter().collect();
404            for i in 0..n {
405                let lhs = i + n;
406                let rhs = ((i + k) % n) + n;
407                prop_assert!(edges.contains(&canon(lhs, rhs)));
408            }
409        }
410    }
411}