rust_igraph/algorithms/constructors/
circulant.rs1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
40
41pub fn circulant(n: u32, shifts: &[i64], directed: bool) -> IgraphResult<Graph> {
72 if n == 0 {
73 return Graph::new(0, directed);
74 }
75
76 let n_i64 = i64::from(n);
81 let mut seen: Vec<u32> = Vec::with_capacity(shifts.len());
82 seen.push(0); let mut canonical_shifts: Vec<u32> = Vec::with_capacity(shifts.len());
85 for &raw in shifts {
86 let mut s = raw % n_i64;
88 if s < 0 {
89 s += n_i64;
90 }
91 let mut s = u32::try_from(s).map_err(|_| {
92 IgraphError::InvalidArgument(format!(
93 "circulant: shift {raw} (normalised {s}) cannot fit u32"
94 ))
95 })?;
96
97 if !directed && s >= n.div_ceil(2) {
98 s = n - s;
99 }
100
101 if !seen.contains(&s) {
102 seen.push(s);
103 canonical_shifts.push(s);
104 }
105 }
106
107 let cap = usize::try_from(n)
111 .ok()
112 .and_then(|nu| nu.checked_mul(canonical_shifts.len()))
113 .ok_or_else(|| {
114 IgraphError::InvalidArgument(format!(
115 "circulant: n * |shifts| overflows usize (n = {n}, |shifts| = {})",
116 canonical_shifts.len()
117 ))
118 })?;
119
120 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(cap);
121 for &shift in &canonical_shifts {
122 let limit = if !directed && n % 2 == 0 && shift == n / 2 {
124 n / 2
125 } else {
126 n
127 };
128 for j in 0..limit {
129 edges.push((j, (j + shift) % n));
130 }
131 }
132
133 let mut g = Graph::new(n, directed)?;
134 g.add_edges(edges)?;
135 Ok(g)
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn dump_edges(g: &Graph) -> Vec<(u32, u32)> {
143 let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
144 (0..m)
145 .map(|e| g.edge(e).expect("edge id in bounds"))
146 .collect()
147 }
148
149 fn canon(u: u32, v: u32) -> (u32, u32) {
150 if u <= v { (u, v) } else { (v, u) }
151 }
152
153 fn canon_set(edges: &[(u32, u32)]) -> std::collections::BTreeSet<(u32, u32)> {
154 edges.iter().map(|&(u, v)| canon(u, v)).collect()
155 }
156
157 #[test]
158 fn empty_graph_for_n_zero() {
159 let g = circulant(0, &[1], false).expect("n=0 ok");
160 assert_eq!(g.vcount(), 0);
161 assert_eq!(g.ecount(), 0);
162 }
163
164 #[test]
165 fn n_one_with_any_shift_is_singleton_no_edges() {
166 let g = circulant(1, &[0, 1, 2, 5], false).expect("n=1 ok");
168 assert_eq!(g.vcount(), 1);
169 assert_eq!(g.ecount(), 0);
170 }
171
172 #[test]
173 fn shift_one_equals_ring_graph() {
174 for n in 3..=12u32 {
176 let g = circulant(n, &[1], false).expect("valid");
177 assert_eq!(g.vcount(), n);
178 assert_eq!(g.ecount(), n as usize);
179 for v in 0..n {
180 assert_eq!(
181 g.degree(v).expect("in range"),
182 2,
183 "C_{n} should be 2-regular"
184 );
185 }
186 }
187 }
188
189 #[test]
190 fn directed_shift_one_is_directed_cycle() {
191 let g = circulant(5, &[1], true).expect("valid");
192 assert!(g.is_directed());
193 assert_eq!(g.ecount(), 5);
194 let edges = dump_edges(&g);
196 for j in 0..5 {
197 assert!(edges.iter().any(|&e| e == (j, (j + 1) % 5)));
198 }
199 }
200
201 #[test]
202 fn shift_zero_yields_no_edges() {
203 let g = circulant(5, &[0], false).expect("ok");
204 assert_eq!(g.ecount(), 0);
205 }
206
207 #[test]
208 fn duplicate_shifts_are_deduplicated() {
209 let g = circulant(7, &[2, 2, 9, -5], false).expect("ok");
210 assert_eq!(g.ecount(), 7);
213 }
214
215 #[test]
216 fn undirected_complementary_shift_collapses() {
217 let a = circulant(7, &[2], false).expect("ok");
219 let b = circulant(7, &[5], false).expect("ok");
220 assert_eq!(canon_set(&dump_edges(&a)), canon_set(&dump_edges(&b)));
221 }
222
223 #[test]
224 fn directed_keeps_complementary_distinct() {
225 let a = circulant(7, &[2], true).expect("ok");
226 let b = circulant(7, &[5], true).expect("ok");
227 let ea: std::collections::BTreeSet<(u32, u32)> = dump_edges(&a).into_iter().collect();
229 let eb: std::collections::BTreeSet<(u32, u32)> = dump_edges(&b).into_iter().collect();
230 assert_ne!(ea, eb);
231 }
232
233 #[test]
234 fn even_n_antipodal_shift_halves_emission() {
235 let g = circulant(6, &[3], false).expect("ok");
237 assert_eq!(g.ecount(), 3);
238 let edges = canon_set(&dump_edges(&g));
239 for j in 0..3u32 {
240 assert!(edges.contains(&(j, j + 3)));
241 }
242 }
243
244 #[test]
245 fn odd_n_no_antipodal_shortcut() {
246 let g = circulant(7, &[3], false).expect("ok");
248 assert_eq!(g.ecount(), 7);
249 }
250
251 #[test]
252 fn complete_graph_via_all_distinct_shifts() {
253 for n in 3..=10u32 {
256 let shifts: Vec<i64> = (1..=(n / 2)).map(i64::from).collect();
257 let g = circulant(n, &shifts, false).expect("valid");
258 let expected_edges = (n as usize) * ((n as usize) - 1) / 2;
259 assert_eq!(g.ecount(), expected_edges, "K_{n}");
260 for v in 0..n {
261 assert_eq!(
262 g.degree(v).expect("in range"),
263 (n - 1) as usize,
264 "K_{n} regularity"
265 );
266 }
267 }
268 }
269
270 #[test]
271 fn negative_shifts_normalised_into_range() {
272 let g = circulant(5, &[-1], false).expect("ok");
274 let r = circulant(5, &[1], false).expect("ok");
275 assert_eq!(canon_set(&dump_edges(&g)), canon_set(&dump_edges(&r)));
276 }
277
278 #[test]
279 fn no_self_loops_in_any_simple_case() {
280 for n in 1..=12u32 {
281 let max_shift = i64::from(n / 2);
282 if max_shift == 0 {
283 continue;
284 }
285 let shifts: Vec<i64> = (1..=max_shift).collect();
286 let g = circulant(n, &shifts, false).expect("valid");
287 for (u, v) in dump_edges(&g) {
288 assert_ne!(u, v, "self-loop in circulant({n}, [{shifts:?}])");
289 }
290 }
291 }
292
293 #[test]
294 fn no_parallel_edges() {
295 for n in 3..=12u32 {
297 let shifts: Vec<i64> = (1..=i64::from(n / 2)).collect();
298 let g = circulant(n, &shifts, false).expect("valid");
299 let edges = dump_edges(&g);
300 let canon: std::collections::HashSet<(u32, u32)> = edges
301 .iter()
302 .map(|&(u, v)| super::tests::canon(u, v))
303 .collect();
304 assert_eq!(
305 canon.len(),
306 edges.len(),
307 "parallel edges in circulant({n}, [{shifts:?}])"
308 );
309 }
310 }
311
312 #[test]
313 fn matches_inner_layer_of_generalized_petersen() {
314 use crate::generalized_petersen;
317 for n in 3..=12u32 {
318 let max_k = (n - 1) / 2;
319 for k in 1..=max_k {
320 let gpg = generalized_petersen(n, k).expect("valid");
321 let inner_edges: std::collections::BTreeSet<(u32, u32)> = (0..gpg.ecount())
322 .map(|e| {
323 gpg.edge(u32::try_from(e).expect("ecount fits u32"))
324 .expect("in range")
325 })
326 .filter(|&(u, v)| u >= n && v >= n)
327 .map(|(u, v)| canon(u - n, v - n))
328 .collect();
329 let c = circulant(n, &[i64::from(k)], false).expect("valid");
330 let c_edges = canon_set(&dump_edges(&c));
331 assert_eq!(inner_edges, c_edges, "inner layer of G({n},{k})");
332 }
333 }
334 }
335}
336
337#[cfg(all(test, feature = "proptest-harness"))]
338mod proptest_invariants {
339 use super::*;
340 use proptest::prelude::*;
341
342 fn dump_edges(g: &Graph) -> Vec<(u32, u32)> {
343 let m = u32::try_from(g.ecount()).expect("ecount fits u32");
344 (0..m)
345 .map(|e| g.edge(e).expect("edge id in bounds"))
346 .collect()
347 }
348
349 fn canon(u: u32, v: u32) -> (u32, u32) {
350 if u <= v { (u, v) } else { (v, u) }
351 }
352
353 proptest! {
354 #![proptest_config(ProptestConfig {
355 cases: 64,
356 max_shrink_iters: 1000,
357 .. ProptestConfig::default()
358 })]
359
360 #[test]
363 fn simple_undirected(n in 1u32..=40, mask in 0u64..(1u64 << 20)) {
364 let max = (n / 2).min(20);
365 if max == 0 { return Ok(()); }
366 let shifts: Vec<i64> = (1..=max)
367 .filter(|s| mask & (1u64 << (s - 1)) != 0)
368 .map(i64::from)
369 .collect();
370 if shifts.is_empty() { return Ok(()); }
371 let g = circulant(n, &shifts, false).expect("valid");
372 let mut set: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
373 for (u, v) in dump_edges(&g) {
374 prop_assert_ne!(u, v);
375 prop_assert!(set.insert(canon(u, v)));
376 }
377 }
378
379 #[test]
384 fn vertex_regularity(n in 2u32..=30, mask in 0u64..(1u64 << 15)) {
385 let max = (n / 2).min(15);
386 if max == 0 { return Ok(()); }
387 let shifts: Vec<i64> = (1..=max)
388 .filter(|s| mask & (1u64 << (s - 1)) != 0)
389 .map(i64::from)
390 .collect();
391 if shifts.is_empty() { return Ok(()); }
392 let g = circulant(n, &shifts, false).expect("valid");
393 let d0 = g.degree(0).expect("in range");
394 for v in 1..n {
395 prop_assert_eq!(g.degree(v).expect("in range"), d0, "vertex {} differs", v);
396 }
397 }
398
399 #[test]
402 fn negative_shifts_equiv(n in 3u32..=30, raw_shift in 1i64..=200) {
403 let g_pos = circulant(n, &[raw_shift], false).expect("valid");
404 let g_neg = circulant(n, &[-raw_shift], false).expect("valid");
405 let ep: std::collections::BTreeSet<(u32, u32)> =
406 dump_edges(&g_pos).into_iter().map(|(u, v)| canon(u, v)).collect();
407 let en: std::collections::BTreeSet<(u32, u32)> =
408 dump_edges(&g_neg).into_iter().map(|(u, v)| canon(u, v)).collect();
409 prop_assert_eq!(ep, en);
410 }
411
412 #[test]
416 fn complete_specialization(n in 2u32..=30) {
417 let shifts: Vec<i64> = (1..=(n / 2)).map(i64::from).collect();
418 let g = circulant(n, &shifts, false).expect("valid");
419 let exp = (n as usize) * ((n as usize) - 1) / 2;
420 prop_assert_eq!(g.ecount(), exp);
421 for v in 0..n {
422 prop_assert_eq!(g.degree(v).expect("in range"), (n - 1) as usize);
423 }
424 }
425 }
426}