Skip to main content

rust_igraph/algorithms/constructors/
mycielskian.rs

1//! Mycielski construction (ALGO-CN-019).
2//!
3//! Counterpart of `igraph_mycielskian()` and `igraph_mycielski_graph()` in
4//! `references/igraph/src/constructors/mycielskian.c:97-244`.
5//!
6//! The Mycielski construction `M(G)` takes a graph `G` on `n` vertices and
7//! `m` edges and produces a larger graph with `2n + 1` vertices and `3m + n`
8//! edges that increases the chromatic number by one while preserving the
9//! triangle-free property.
10//!
11//! Per iteration, label the original vertices `v_0..v_{n-1}` and add:
12//! * `n` shadow vertices `u_0..u_{n-1}`, where `u_i` is connected to every
13//!   original neighbour of `v_i` (so for each `(v_i, v_j)` in `G` we emit
14//!   `(v_i, u_j)` and `(v_j, u_i)`);
15//! * a single apex vertex `w` connected to every `u_i`.
16//!
17//! Two corner cases are folded inline so iterative chains stay connected:
18//! the null graph (`vcount = 0`) consumes one `k` step by becoming the
19//! singleton, and the singleton (`vcount = 1`) consumes one `k` step by
20//! becoming the two-path `P_2`. After those reductions the loop runs the
21//! literal recurrence `vcount' = 2*vcount + 1`, `ecount' = 3*ecount + vcount`.
22//!
23//! The canonical Mycielski graphs `M_k` are produced by [`mycielski_graph`]:
24//! `M_0` = null, `M_1` = singleton, `M_2` = `P_2`, `M_3` = `C_5`, `M_4` =
25//! Grötzsch graph (11 vertices, 20 edges, triangle-free, chromatic number 4).
26//!
27//! Time complexity: `O(|V| · 2^k + |E| · 3^k)`.
28
29use crate::algorithms::constructors::ring::ring_graph;
30use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
31
32/// Apply `k` Mycielski iterations to `graph`.
33///
34/// Returns a new graph that preserves the input's directedness. Self-loops
35/// and parallel edges in the input are propagated through the construction
36/// rules literally — Mycielski is defined on simple graphs but the upstream
37/// extension to multigraphs is preserved here.
38///
39/// For `k == 0` the result is a structural copy of `graph` (same vertices,
40/// same edges, same directedness).
41///
42/// # Errors
43///
44/// * [`IgraphError::InvalidArgument`] — the computed `2^k` vcount or
45///   `3^k`-scaled ecount overflows `u32` / `usize`.
46///
47/// # Examples
48///
49/// ```
50/// use rust_igraph::{Graph, mycielskian};
51///
52/// // Mycielski of the null graph + 1 iteration = singleton.
53/// let null = Graph::new(0, false).unwrap();
54/// let m1 = mycielskian(&null, 1).unwrap();
55/// assert_eq!(m1.vcount(), 1);
56/// assert_eq!(m1.ecount(), 0);
57///
58/// // Mycielski of P_2 + 1 iteration = C_5 (Mycielski M_3).
59/// let mut p2 = Graph::new(2, false).unwrap();
60/// p2.add_edges([(0u32, 1u32)]).unwrap();
61/// let c5 = mycielskian(&p2, 1).unwrap();
62/// assert_eq!(c5.vcount(), 5);
63/// assert_eq!(c5.ecount(), 5);
64/// ```
65pub fn mycielskian(graph: &Graph, k: u32) -> IgraphResult<Graph> {
66    let directed = graph.is_directed();
67    let (mut vcount, mut ecount, mut edges) = stage_input_edges(graph)?;
68    let mut k_remaining = k;
69
70    // Special case: null graph. Promote to singleton and consume one step.
71    if vcount == 0 && k_remaining > 0 {
72        vcount = 1;
73        k_remaining -= 1;
74    }
75
76    // Special case: singleton. Promote to P_2 and consume one step.
77    if vcount == 1 && k_remaining > 0 {
78        vcount = 2;
79        ecount = ecount.checked_add(1).ok_or_else(|| {
80            IgraphError::InvalidArgument(
81                "mycielskian: singleton promotion ecount + 1 overflows u32".to_string(),
82            )
83        })?;
84        edges.push(0);
85        edges.push(1);
86        k_remaining -= 1;
87    }
88
89    let (final_v, final_e) = project_counts(vcount, ecount, k_remaining)?;
90
91    // Resize the flat edge buffer to its final capacity (2 entries per edge).
92    let final_buffer_len = usize::try_from(final_e)
93        .ok()
94        .and_then(|m| m.checked_mul(2))
95        .ok_or_else(|| {
96            IgraphError::InvalidArgument(format!(
97                "mycielskian: final edge buffer size 2 * {final_e} overflows usize"
98            ))
99        })?;
100    edges.resize(final_buffer_len, 0);
101
102    run_iterations(&mut edges, vcount, ecount, k_remaining)?;
103
104    let mut result = Graph::new(final_v, directed)?;
105    let pairs: Vec<(VertexId, VertexId)> = edges
106        .chunks_exact(2)
107        .map(|pair| (pair[0], pair[1]))
108        .collect();
109    result.add_edges(pairs)?;
110    Ok(result)
111}
112
113/// Returns `(vcount, ecount, flat-edges)` for `graph` as a row-major
114/// `[src0, dst0, src1, dst1, …]` buffer. Errors when `ecount` exceeds
115/// `u32::MAX` or when the buffer length overflows `usize`.
116fn stage_input_edges(graph: &Graph) -> IgraphResult<(u32, u32, Vec<u32>)> {
117    let ecount = u32::try_from(graph.ecount()).map_err(|_| {
118        IgraphError::InvalidArgument(format!(
119            "mycielskian: input ecount = {} exceeds u32::MAX",
120            graph.ecount()
121        ))
122    })?;
123    let cap = usize::try_from(ecount)
124        .ok()
125        .and_then(|m| m.checked_mul(2))
126        .ok_or_else(|| {
127            IgraphError::InvalidArgument(format!(
128                "mycielskian: input edge buffer size 2 * {ecount} overflows usize"
129            ))
130        })?;
131    let mut edges: Vec<u32> = Vec::with_capacity(cap);
132    for eid in 0..graph.ecount() {
133        let id = u32::try_from(eid).map_err(|_| {
134            IgraphError::InvalidArgument(format!("mycielskian: edge id {eid} exceeds u32::MAX"))
135        })?;
136        let (u, v) = graph.edge(id).map_err(|_| {
137            IgraphError::InvalidArgument(format!("mycielskian: edge id {eid} not in range"))
138        })?;
139        edges.push(u);
140        edges.push(v);
141    }
142    Ok((graph.vcount(), ecount, edges))
143}
144
145/// Apply the closed-form recurrence
146/// `(vcount', ecount') = (2*vcount + 1, 3*ecount + vcount)` for `k` steps,
147/// with overflow checks on every intermediate value.
148fn project_counts(vcount: u32, ecount: u32, k: u32) -> IgraphResult<(u32, u32)> {
149    let mut v = vcount;
150    let mut e = ecount;
151    for _ in 0..k {
152        e = e
153            .checked_mul(3)
154            .and_then(|x| x.checked_add(v))
155            .ok_or_else(|| {
156                IgraphError::InvalidArgument(format!(
157                    "mycielskian: ecount overflow at iter with vcount = {v}, ecount = {e}"
158                ))
159            })?;
160        v = v
161            .checked_mul(2)
162            .and_then(|x| x.checked_add(1))
163            .ok_or_else(|| {
164                IgraphError::InvalidArgument(format!("mycielskian: vcount overflow (2 * {v} + 1)"))
165            })?;
166    }
167    Ok((v, e))
168}
169
170/// Run `k` Mycielski iterations in place on the pre-sized `edges` buffer
171/// starting from `(vcount, ecount)` after any null/singleton promotions.
172fn run_iterations(edges: &mut [u32], vcount: u32, ecount: u32, k: u32) -> IgraphResult<()> {
173    let mut edge_index: usize = 2usize
174        .checked_mul(usize::try_from(ecount).map_err(|_| {
175            IgraphError::InvalidArgument(format!("mycielskian: ecount {ecount} too large"))
176        })?)
177        .ok_or_else(|| {
178            IgraphError::InvalidArgument("mycielskian: edge_index overflow".to_string())
179        })?;
180    let mut offset: u32 = vcount;
181
182    for _ in 0..k {
183        let prev_vcount: u32 = offset;
184        let w: u32 = offset.checked_mul(2).ok_or_else(|| {
185            IgraphError::InvalidArgument(format!("mycielskian: w = 2 * {offset} overflows u32"))
186        })?;
187        let last_edge_index: usize = edge_index;
188
189        // For each existing edge (v1, v2), emit (v1, offset + v2) and (v2, offset + v1).
190        let mut j = 0usize;
191        while j < last_edge_index {
192            let v1 = edges[j];
193            let v2 = edges[j + 1];
194            let v1_shadow = offset.checked_add(v1).ok_or_else(|| {
195                IgraphError::InvalidArgument(format!(
196                    "mycielskian: shadow id {offset} + {v1} overflows u32"
197                ))
198            })?;
199            let v2_shadow = offset.checked_add(v2).ok_or_else(|| {
200                IgraphError::InvalidArgument(format!(
201                    "mycielskian: shadow id {offset} + {v2} overflows u32"
202                ))
203            })?;
204            edges[edge_index] = v1;
205            edges[edge_index + 1] = v2_shadow;
206            edges[edge_index + 2] = v2;
207            edges[edge_index + 3] = v1_shadow;
208            edge_index += 4;
209            j += 2;
210        }
211
212        // Star edges: (j, w) for j in [prev_vcount, w).
213        let mut star = prev_vcount;
214        while star < w {
215            edges[edge_index] = star;
216            edges[edge_index + 1] = w;
217            edge_index += 2;
218            star += 1;
219        }
220
221        // offset' = offset * 2 + 1.
222        offset = offset
223            .checked_mul(2)
224            .and_then(|o| o.checked_add(1))
225            .ok_or_else(|| {
226                IgraphError::InvalidArgument(format!(
227                    "mycielskian: offset update (2 * {offset} + 1) overflows u32"
228                ))
229            })?;
230    }
231    Ok(())
232}
233
234/// Construct the canonical Mycielski graph `M_k`.
235///
236/// `M_0` is the null graph, `M_1` is a single vertex, `M_2` is the two-path
237/// `P_2`, `M_3` is the five-cycle `C_5`, `M_4` is the Grötzsch graph
238/// (11 vertices, 20 edges, triangle-free, chromatic number 4), and `M_k`
239/// for `k > 4` is the `(k - 2)`-fold Mycielskian of `P_2`.
240///
241/// # Errors
242///
243/// * [`IgraphError::InvalidArgument`] — the computed vcount / ecount for
244///   the given `k` overflows `u32` / `usize`.
245///
246/// # Examples
247///
248/// ```
249/// use rust_igraph::mycielski_graph;
250///
251/// // M_3 is the 5-cycle.
252/// let c5 = mycielski_graph(3).unwrap();
253/// assert_eq!(c5.vcount(), 5);
254/// assert_eq!(c5.ecount(), 5);
255///
256/// // M_4 is the Grötzsch graph: 11 vertices, 20 edges, triangle-free.
257/// let g = mycielski_graph(4).unwrap();
258/// assert_eq!(g.vcount(), 11);
259/// assert_eq!(g.ecount(), 20);
260/// ```
261pub fn mycielski_graph(k: u32) -> IgraphResult<Graph> {
262    if k <= 1 {
263        // M_0 = null, M_1 = singleton.
264        return Graph::new(k, false);
265    }
266    // M_k for k >= 2 is the (k - 2)-fold Mycielskian of P_2.
267    let p2 = ring_graph(2, false, false, false)?;
268    mycielskian(&p2, k - 2)
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use std::collections::BTreeSet;
275
276    fn canonical_edges(g: &Graph) -> BTreeSet<(VertexId, VertexId)> {
277        let mut s = BTreeSet::new();
278        for eid in 0..u32::try_from(g.ecount()).unwrap() {
279            let (a, b) = g.edge(eid).unwrap();
280            s.insert(if a <= b { (a, b) } else { (b, a) });
281        }
282        s
283    }
284
285    fn degree(g: &Graph, v: VertexId) -> usize {
286        g.neighbors(v).unwrap().len()
287    }
288
289    fn assert_simple_undirected(g: &Graph) {
290        assert!(!g.is_directed());
291        let mut seen = BTreeSet::new();
292        for eid in 0..u32::try_from(g.ecount()).unwrap() {
293            let (a, b) = g.edge(eid).unwrap();
294            assert_ne!(a, b, "self-loop on edge {eid}: ({a},{b})");
295            let key = if a <= b { (a, b) } else { (b, a) };
296            assert!(seen.insert(key), "parallel edge {key:?}");
297        }
298    }
299
300    #[test]
301    fn k_zero_is_identity_undirected() {
302        let mut g = Graph::new(4, false).unwrap();
303        g.add_edges([(0u32, 1u32), (1, 2), (2, 3), (3, 0)]).unwrap();
304        let r = mycielskian(&g, 0).unwrap();
305        assert_eq!(r.vcount(), 4);
306        assert_eq!(r.ecount(), 4);
307        assert!(!r.is_directed());
308        assert_eq!(canonical_edges(&r), canonical_edges(&g));
309    }
310
311    #[test]
312    fn k_zero_is_identity_directed() {
313        let mut g = Graph::new(3, true).unwrap();
314        g.add_edges([(0u32, 1u32), (1, 2), (2, 0)]).unwrap();
315        let r = mycielskian(&g, 0).unwrap();
316        assert_eq!(r.vcount(), 3);
317        assert_eq!(r.ecount(), 3);
318        assert!(r.is_directed());
319    }
320
321    #[test]
322    fn null_graph_promoted_to_singleton() {
323        let null = Graph::new(0, false).unwrap();
324        let r = mycielskian(&null, 1).unwrap();
325        assert_eq!(r.vcount(), 1);
326        assert_eq!(r.ecount(), 0);
327    }
328
329    #[test]
330    fn null_graph_two_iterations_is_p2() {
331        // M_0 + 1 → singleton; singleton + 1 → P_2.
332        let null = Graph::new(0, false).unwrap();
333        let r = mycielskian(&null, 2).unwrap();
334        assert_eq!(r.vcount(), 2);
335        assert_eq!(r.ecount(), 1);
336        assert_eq!(canonical_edges(&r), [(0u32, 1u32)].into_iter().collect());
337    }
338
339    #[test]
340    fn singleton_promoted_to_p2() {
341        let single = Graph::new(1, false).unwrap();
342        let r = mycielskian(&single, 1).unwrap();
343        assert_eq!(r.vcount(), 2);
344        assert_eq!(r.ecount(), 1);
345        assert_eq!(canonical_edges(&r), [(0u32, 1u32)].into_iter().collect());
346    }
347
348    #[test]
349    fn p2_one_iteration_is_c5() {
350        // M(P_2) = C_5 = M_3.
351        let mut p2 = Graph::new(2, false).unwrap();
352        p2.add_edges([(0u32, 1u32)]).unwrap();
353        let r = mycielskian(&p2, 1).unwrap();
354        assert_eq!(r.vcount(), 5);
355        assert_eq!(r.ecount(), 5);
356        assert_simple_undirected(&r);
357        // Every vertex in C_5 has degree 2.
358        for v in 0..r.vcount() {
359            assert_eq!(degree(&r, v), 2, "C_5 vertex {v} degree");
360        }
361    }
362
363    #[test]
364    fn mycielski_graph_m0_null() {
365        let m = mycielski_graph(0).unwrap();
366        assert_eq!(m.vcount(), 0);
367        assert_eq!(m.ecount(), 0);
368        assert!(!m.is_directed());
369    }
370
371    #[test]
372    fn mycielski_graph_m1_singleton() {
373        let m = mycielski_graph(1).unwrap();
374        assert_eq!(m.vcount(), 1);
375        assert_eq!(m.ecount(), 0);
376    }
377
378    #[test]
379    fn mycielski_graph_m2_p2() {
380        let m = mycielski_graph(2).unwrap();
381        assert_eq!(m.vcount(), 2);
382        assert_eq!(m.ecount(), 1);
383        assert_eq!(canonical_edges(&m), [(0u32, 1u32)].into_iter().collect());
384    }
385
386    #[test]
387    fn mycielski_graph_m3_c5() {
388        let m = mycielski_graph(3).unwrap();
389        assert_eq!(m.vcount(), 5);
390        assert_eq!(m.ecount(), 5);
391        assert_simple_undirected(&m);
392        for v in 0..m.vcount() {
393            assert_eq!(degree(&m, v), 2);
394        }
395    }
396
397    #[test]
398    fn mycielski_graph_m4_grotzsch() {
399        // Grötzsch graph: 11 vertices, 20 edges, triangle-free, chromatic 4.
400        let m = mycielski_graph(4).unwrap();
401        assert_eq!(m.vcount(), 11);
402        assert_eq!(m.ecount(), 20);
403        assert_simple_undirected(&m);
404        // Triangle-free check: for every undirected edge (u, v), no common neighbour.
405        for eid in 0..u32::try_from(m.ecount()).unwrap() {
406            let (u, v) = m.edge(eid).unwrap();
407            let nu: BTreeSet<VertexId> = m.neighbors(u).unwrap().into_iter().collect();
408            let nv: BTreeSet<VertexId> = m.neighbors(v).unwrap().into_iter().collect();
409            let inter: BTreeSet<&VertexId> = nu.intersection(&nv).collect();
410            assert!(inter.is_empty(), "triangle through edge ({u},{v})");
411        }
412    }
413
414    #[test]
415    fn mycielski_graph_m5_counts() {
416        // Per upstream docs: n_k = 3 * 2^(k-2) - 1 = 23, m_k = (7 * 3^(k-2) + 1)/2 - 3*2^(k-2)
417        //              = (7*9+1)/2 - 12 = 32 - 12 = 20? Recompute:
418        // M_4 = mycielskian(P_2, 2): start vcount=2 ecount=1 → after 1 iter v=5 e=5 →
419        //       after 2 iters v=11 e=20 ✓.
420        // M_5 = mycielskian(P_2, 3): after 3 iters v=23 e=70.
421        // Verify with the recurrence: e' = 3e + v; v' = 2v + 1.
422        // Step 1: v=5, e=5. Step 2: v=11, e=3*5+5=20. Step 3: v=23, e=3*20+11=71. So 71.
423        // Note: my hand-calc above was off; trust the recurrence.
424        let m = mycielski_graph(5).unwrap();
425        assert_eq!(m.vcount(), 23);
426        assert_eq!(m.ecount(), 71);
427        assert_simple_undirected(&m);
428    }
429
430    #[test]
431    fn directed_iteration_preserves_directedness() {
432        let mut g = Graph::new(2, true).unwrap();
433        g.add_edges([(0u32, 1u32)]).unwrap();
434        let r = mycielskian(&g, 1).unwrap();
435        assert!(r.is_directed());
436        // Directed iteration: 2 vcount→5, 1 ecount + new = 3*1 + 2 = 5 arcs.
437        assert_eq!(r.vcount(), 5);
438        assert_eq!(r.ecount(), 5);
439    }
440
441    #[test]
442    fn recurrence_after_two_iterations_on_k4() {
443        // K_4: 4 vertices, 6 edges. After 1 iter v=9 e=22; after 2 iters v=19 e=75.
444        let mut k4 = Graph::new(4, false).unwrap();
445        k4.add_edges([(0u32, 1u32), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)])
446            .unwrap();
447        let one = mycielskian(&k4, 1).unwrap();
448        assert_eq!(one.vcount(), 9);
449        assert_eq!(one.ecount(), 22);
450        let two = mycielskian(&k4, 2).unwrap();
451        assert_eq!(two.vcount(), 19);
452        assert_eq!(two.ecount(), 75);
453    }
454
455    #[test]
456    fn apex_vertex_degree_equals_prev_vcount() {
457        // After applying mycielskian to G with vcount = n, the new apex is
458        // the last vertex (id = 2n) and connects to the n shadow vertices.
459        let mut g = Graph::new(4, false).unwrap();
460        g.add_edges([(0u32, 1u32), (1, 2), (2, 3)]).unwrap(); // P_4
461        let r = mycielskian(&g, 1).unwrap();
462        // vcount 4 → 9; apex = 8; shadow ids 4..8.
463        let apex = r.vcount() - 1;
464        let nbrs: BTreeSet<VertexId> = r.neighbors(apex).unwrap().into_iter().collect();
465        assert_eq!(nbrs, (4u32..8).collect());
466    }
467
468    #[test]
469    fn shadow_vertex_mirrors_original_neighbours_plus_apex() {
470        // After M on K_3 (triangle): n=3, m=3. New v=7, edges:
471        //  - 3 original triangle edges
472        //  - per (i,j) emit (i, 3+j) and (j, 3+i): 6 new "shadow" edges
473        //  - star: (3, 6), (4, 6), (5, 6) = 3 apex edges
474        // Total: 3 + 6 + 3 = 12 = 3m + n = 9 + 3 ✓.
475        let mut k3 = Graph::new(3, false).unwrap();
476        k3.add_edges([(0u32, 1u32), (1, 2), (0, 2)]).unwrap();
477        let r = mycielskian(&k3, 1).unwrap();
478        assert_eq!(r.vcount(), 7);
479        assert_eq!(r.ecount(), 12);
480        // Shadow vertex u_0 (id=3) should connect to v_1 and v_2 (original
481        // neighbours of v_0) plus apex w (id=6).
482        let nbrs_u0: BTreeSet<VertexId> = r.neighbors(3).unwrap().into_iter().collect();
483        assert_eq!(nbrs_u0, [1u32, 2, 6].into_iter().collect());
484    }
485
486    #[test]
487    fn negative_k_rejected_at_type_level() {
488        // u32 cannot represent negative; the C `< 0` rejection is enforced
489        // by the type system. Smoke-test that k = u32::MAX without input
490        // overflows cleanly via the InvalidArgument error path.
491        let null = Graph::new(0, false).unwrap();
492        let r = mycielskian(&null, 64);
493        assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
494    }
495
496    #[test]
497    fn empty_input_k_zero_is_null_graph() {
498        let null = Graph::new(0, false).unwrap();
499        let r = mycielskian(&null, 0).unwrap();
500        assert_eq!(r.vcount(), 0);
501        assert_eq!(r.ecount(), 0);
502    }
503}
504
505#[cfg(all(test, feature = "proptest-harness"))]
506mod proptest_tests {
507    use super::*;
508    use proptest::prelude::*;
509    use std::collections::BTreeSet;
510
511    /// Small random simple graph with `n ∈ [0, 8]` and a Bernoulli edge.
512    fn arb_small_graph() -> impl Strategy<Value = Graph> {
513        (0u32..=8).prop_flat_map(|n| {
514            // For each unordered pair (i, j) with i < j, decide independently.
515            let pairs = if n <= 1 {
516                0
517            } else {
518                (n * (n - 1) / 2) as usize
519            };
520            prop::collection::vec(any::<bool>(), pairs).prop_map(move |flags| {
521                let mut g = Graph::new(n, false).unwrap();
522                if n >= 2 {
523                    let mut pair_idx = 0usize;
524                    let mut edges = Vec::new();
525                    for i in 0..n {
526                        for j in (i + 1)..n {
527                            if flags[pair_idx] {
528                                edges.push((i, j));
529                            }
530                            pair_idx += 1;
531                        }
532                    }
533                    g.add_edges(edges).unwrap();
534                }
535                g
536            })
537        })
538    }
539
540    proptest! {
541        #[test]
542        fn k_zero_preserves_counts(g in arb_small_graph()) {
543            let r = mycielskian(&g, 0).unwrap();
544            prop_assert_eq!(r.vcount(), g.vcount());
545            prop_assert_eq!(r.ecount(), g.ecount());
546            prop_assert_eq!(r.is_directed(), g.is_directed());
547        }
548
549        #[test]
550        fn vcount_recurrence_holds(g in arb_small_graph(), k in 0u32..=4) {
551            let r = mycielskian(&g, k).unwrap();
552            // After the inline null→singleton (free vcount=1) and singleton→P_2
553            // (free v=2,e=1) reductions, apply v' = 2v + 1 per remaining step.
554            let mut vc = g.vcount();
555            let mut ec = u32::try_from(g.ecount()).unwrap();
556            let mut k_left = k;
557            if vc == 0 && k_left > 0 { vc = 1; k_left -= 1; }
558            if vc == 1 && k_left > 0 { vc = 2; ec += 1; k_left -= 1; }
559            for _ in 0..k_left {
560                ec = ec.checked_mul(3).unwrap().checked_add(vc).unwrap();
561                vc = vc.checked_mul(2).unwrap().checked_add(1).unwrap();
562            }
563            prop_assert_eq!(r.vcount(), vc);
564            prop_assert_eq!(u32::try_from(r.ecount()).unwrap(), ec);
565        }
566
567        #[test]
568        fn no_edge_references_out_of_range(g in arb_small_graph(), k in 0u32..=3) {
569            let r = mycielskian(&g, k).unwrap();
570            for eid in 0..u32::try_from(r.ecount()).unwrap() {
571                let (u, v) = r.edge(eid).unwrap();
572                prop_assert!(u < r.vcount());
573                prop_assert!(v < r.vcount());
574            }
575        }
576
577        #[test]
578        fn mycielski_graph_recurrence(k in 0u32..=6) {
579            let m = mycielski_graph(k).unwrap();
580            // Same recurrence rooted at P_2 for k >= 2; null/singleton otherwise.
581            let (vc, ec) = match k {
582                0 => (0u32, 0u32),
583                1 => (1, 0),
584                _ => {
585                    let mut vc = 2u32;
586                    let mut ec = 1u32;
587                    for _ in 0..(k - 2) {
588                        ec = ec.checked_mul(3).unwrap().checked_add(vc).unwrap();
589                        vc = vc.checked_mul(2).unwrap().checked_add(1).unwrap();
590                    }
591                    (vc, ec)
592                }
593            };
594            prop_assert_eq!(m.vcount(), vc);
595            prop_assert_eq!(u32::try_from(m.ecount()).unwrap(), ec);
596            prop_assert!(!m.is_directed());
597        }
598
599        #[test]
600        fn shadow_of_simple_graph_is_simple(g in arb_small_graph(), k in 0u32..=3) {
601            // For simple undirected input with no self-loops, every iteration
602            // also produces simple undirected output (no self-loops, no
603            // parallels). This is a structural property of the construction.
604            let r = mycielskian(&g, k).unwrap();
605            let mut seen = BTreeSet::new();
606            for eid in 0..u32::try_from(r.ecount()).unwrap() {
607                let (u, v) = r.edge(eid).unwrap();
608                prop_assert_ne!(u, v);
609                let key = if u <= v { (u, v) } else { (v, u) };
610                prop_assert!(seen.insert(key));
611            }
612        }
613
614        #[test]
615        fn apex_vertex_is_universal_to_shadow_block(g in arb_small_graph(), k in 1u32..=3) {
616            let r = mycielskian(&g, k).unwrap();
617            // Final iteration's apex is the last vertex, and its degree
618            // equals the per-iteration shadow-block size (i.e. half of
619            // (vcount - 1) at that step). Quick smoke: apex degree > 0
620            // whenever there's at least one iteration AND the post-reduction
621            // vcount was > 0.
622            let apex = r.vcount() - 1;
623            prop_assert!(r.neighbors(apex).unwrap().len() > 0 || g.vcount() == 0);
624        }
625    }
626}