rust_igraph/algorithms/constructors/
generalized_petersen.rs1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
38
39pub 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 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 edges.push((i, (i + 1) % n));
97 edges.push((i, i + n));
99 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 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 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 for n in 3..=20u32 {
174 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 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 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 let err = generalized_petersen(6, 3).unwrap_err();
277 assert!(matches!(err, IgraphError::InvalidArgument(_)));
278 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 assert!(generalized_petersen(7, 3).is_ok());
288 }
289
290 #[test]
291 fn no_self_loops_no_parallel_edges() {
292 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 #[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 #[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 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 #[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 #[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 #[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}