Skip to main content

rust_igraph/algorithms/constructors/
hypercube.rs

1//! Hypercube constructor (ALGO-CN-007).
2//!
3//! Counterpart of `igraph_hypercube()` in
4//! `references/igraph/src/constructors/regular.c:983-1003`.
5//!
6//! The n-dimensional **hypercube graph** `Q_n` has `2^n` vertices and
7//! `2^(n-1) * n` edges. Two vertices are connected when the binary
8//! representations of their zero-based vertex IDs differ in **exactly
9//! one bit**.
10//!
11//! Small cases:
12//!
13//! * n = 0 → singleton (one vertex, zero edges)
14//! * n = 1 → K2 (two vertices, one edge: `(0, 1)`)
15//! * n = 2 → 4-cycle (`(0,1), (0,2), (1,3), (2,3)`)
16//! * n = 3 → 8-vertex cube (12 edges)
17//!
18//! The graph is `n`-regular (every vertex has degree `n`) and bipartite
19//! (vertices split into two classes by the parity of their popcount).
20//!
21//! Algorithm: enumerate all `2^n` vertices in id order; for each vertex
22//! `v` and bit `i ∈ [0, n)` toggle that bit to get `u = v ^ (1 << i)`,
23//! and emit the canonical edge `(v, u)` only when `v < u` so each edge
24//! is produced exactly once. Mirrors the upstream C loop.
25//!
26//! Time complexity: O(2^n · n) = O(|V| · log |V|) = O(|E|).
27//!
28//! # Upper bound
29//!
30//! `n` is capped at 30 so that:
31//!
32//! * vertex count `2^n` fits comfortably in `u32`,
33//! * edge count `2^(n-1) · n` fits in `usize` on every supported
34//!   platform (≤ ~1.6 × 10^10 on `n = 30`, well within 64-bit
35//!   `usize::MAX`; this lets the `edges` `Vec` allocation succeed even
36//!   on 32-bit targets where `usize::MAX ≈ 4 × 10^9` — at `n = 30` it
37//!   would exhaust `usize`, but real workloads stay far below it).
38
39use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
40
41/// Maximum dimension accepted by [`hypercube`].
42///
43/// At `n = 30` the graph already has `2^30 ≈ 1.07 × 10^9` vertices and
44/// `2^29 · 30 ≈ 1.6 × 10^10` edges, which is well beyond practical
45/// workloads. The upper bound also guarantees `1u32 << n` is always
46/// well-defined.
47pub const MAX_HYPERCUBE_DIMENSION: u32 = 30;
48
49/// Build the n-dimensional hypercube graph `Q_n`.
50///
51/// Returns a graph on `2^n` vertices where two vertices share an edge
52/// iff their zero-based IDs differ in exactly one bit. For the directed
53/// variant every edge points from the lower-indexed endpoint to the
54/// higher-indexed one (the canonical orientation also used by upstream
55/// igraph). For `n = 0` the graph is the singleton; for `n = 1` it is
56/// `K_2`.
57///
58/// # Errors
59///
60/// * [`IgraphError::InvalidArgument`] — `n > 30` (see
61///   [`MAX_HYPERCUBE_DIMENSION`]).
62///
63/// # Examples
64///
65/// ```
66/// use rust_igraph::hypercube;
67///
68/// // Q_3 — 8-vertex cube, 12 edges, every vertex has degree 3.
69/// let g = hypercube(3, false).unwrap();
70/// assert_eq!(g.vcount(), 8);
71/// assert_eq!(g.ecount(), 12);
72/// ```
73pub fn hypercube(n: u32, directed: bool) -> IgraphResult<Graph> {
74    if n > MAX_HYPERCUBE_DIMENSION {
75        return Err(IgraphError::InvalidArgument(format!(
76            "hypercube: dimension {n} exceeds the supported maximum of {MAX_HYPERCUBE_DIMENSION}"
77        )));
78    }
79
80    let vcount: u32 = 1u32 << n;
81
82    // ecount = n == 0 ? 0 : 2^(n-1) * n. Always fits usize on 64-bit
83    // and 32-bit (per the MAX_HYPERCUBE_DIMENSION analysis above), but
84    // compute through checked_* so future bound changes can't silently
85    // wrap around.
86    let ecount: usize = if n == 0 {
87        0
88    } else {
89        let half = 1usize.checked_shl(n - 1).ok_or_else(|| {
90            IgraphError::InvalidArgument(format!(
91                "hypercube: edge count overflows usize for dimension {n}"
92            ))
93        })?;
94        half.checked_mul(n as usize).ok_or_else(|| {
95            IgraphError::InvalidArgument(format!(
96                "hypercube: edge count overflows usize for dimension {n}"
97            ))
98        })?
99    };
100
101    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
102    for v in 0..vcount {
103        for i in 0..n {
104            let u = v ^ (1u32 << i);
105            if v < u {
106                edges.push((v, u));
107            }
108        }
109    }
110
111    let mut g = Graph::new(vcount, directed)?;
112    g.add_edges(edges)?;
113    Ok(g)
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    fn dump_edges(g: &Graph) -> Vec<(u32, u32)> {
121        let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
122        (0..m)
123            .map(|e| g.edge(e).expect("edge id in bounds"))
124            .collect()
125    }
126
127    fn degree_of(g: &Graph, v: VertexId) -> usize {
128        g.degree(v).expect("vertex in range")
129    }
130
131    #[test]
132    fn n0_is_singleton() {
133        let g = hypercube(0, false).expect("n=0");
134        assert_eq!(g.vcount(), 1);
135        assert_eq!(g.ecount(), 0);
136        assert!(!g.is_directed());
137    }
138
139    #[test]
140    fn n0_directed_is_singleton() {
141        let g = hypercube(0, true).expect("n=0 directed");
142        assert_eq!(g.vcount(), 1);
143        assert_eq!(g.ecount(), 0);
144        assert!(g.is_directed());
145    }
146
147    #[test]
148    fn n1_is_k2() {
149        let g = hypercube(1, false).expect("n=1");
150        assert_eq!(g.vcount(), 2);
151        assert_eq!(g.ecount(), 1);
152        assert_eq!(dump_edges(&g), vec![(0, 1)]);
153    }
154
155    #[test]
156    fn n2_is_four_cycle() {
157        // Q_2 is the 4-cycle.
158        // Edges from upstream loop: (0,1), (0,2), (1,3), (2,3).
159        let g = hypercube(2, false).expect("n=2");
160        assert_eq!(g.vcount(), 4);
161        assert_eq!(g.ecount(), 4);
162        assert_eq!(dump_edges(&g), vec![(0, 1), (0, 2), (1, 3), (2, 3)]);
163    }
164
165    #[test]
166    fn n3_is_three_cube() {
167        let g = hypercube(3, false).expect("n=3");
168        assert_eq!(g.vcount(), 8);
169        assert_eq!(g.ecount(), 12);
170        // Bit-flip edges in upstream order.
171        assert_eq!(
172            dump_edges(&g),
173            vec![
174                (0, 1),
175                (0, 2),
176                (0, 4),
177                (1, 3),
178                (1, 5),
179                (2, 3),
180                (2, 6),
181                (3, 7),
182                (4, 5),
183                (4, 6),
184                (5, 7),
185                (6, 7),
186            ]
187        );
188    }
189
190    #[test]
191    fn n3_directed_same_edges_but_directed() {
192        // Directed variant emits the same (v, u) pairs (each canonical
193        // with v < u) and stores them as arcs.
194        let g = hypercube(3, true).expect("n=3 directed");
195        assert!(g.is_directed());
196        assert_eq!(g.ecount(), 12);
197        let edges = dump_edges(&g);
198        // First edge oriented low → high.
199        assert_eq!(edges[0], (0, 1));
200        assert_eq!(edges[1], (0, 2));
201    }
202
203    #[test]
204    fn n3_is_three_regular() {
205        let g = hypercube(3, false).expect("n=3");
206        for v in 0..g.vcount() {
207            assert_eq!(degree_of(&g, v), 3, "vertex {v} should have degree 3");
208        }
209    }
210
211    #[test]
212    fn n4_vertex_and_edge_counts() {
213        let g = hypercube(4, false).expect("n=4");
214        // vcount = 2^4 = 16; ecount = 2^3 * 4 = 32.
215        assert_eq!(g.vcount(), 16);
216        assert_eq!(g.ecount(), 32);
217        // 4-regular.
218        for v in 0..g.vcount() {
219            assert_eq!(degree_of(&g, v), 4, "vertex {v} should have degree 4");
220        }
221    }
222
223    #[test]
224    fn n4_is_bipartite_by_popcount() {
225        // Hypercube is bipartite — every edge crosses popcount parity.
226        let g = hypercube(4, false).expect("n=4");
227        let m = u32::try_from(g.ecount()).unwrap();
228        for e in 0..m {
229            let (u, v) = g.edge(e).unwrap();
230            assert_ne!(
231                u.count_ones() % 2,
232                v.count_ones() % 2,
233                "edge ({u}, {v}) should cross popcount parity"
234            );
235        }
236    }
237
238    #[test]
239    fn edge_endpoints_differ_in_one_bit() {
240        for n in 0..=5u32 {
241            let g = hypercube(n, false).expect("hypercube ok");
242            let m = u32::try_from(g.ecount()).unwrap();
243            for e in 0..m {
244                let (u, v) = g.edge(e).unwrap();
245                assert_eq!(
246                    (u ^ v).count_ones(),
247                    1,
248                    "edge ({u}, {v}) in Q_{n} should differ in exactly one bit"
249                );
250            }
251        }
252    }
253
254    #[test]
255    fn edges_are_canonical_v_less_than_u() {
256        // Every emitted edge has v < u (lower index first).
257        let g = hypercube(5, true).expect("Q_5 directed");
258        let m = u32::try_from(g.ecount()).unwrap();
259        for e in 0..m {
260            let (u, v) = g.edge(e).unwrap();
261            assert!(u < v, "edge ({u}, {v}) must be canonically ordered");
262        }
263    }
264
265    #[test]
266    fn dimension_too_large_rejected() {
267        let err = hypercube(31, false).expect_err("n=31 should reject");
268        match err {
269            IgraphError::InvalidArgument(msg) => assert!(msg.contains("exceeds")),
270            other => panic!("unexpected error: {other:?}"),
271        }
272    }
273
274    #[test]
275    fn dimension_far_too_large_rejected() {
276        let err = hypercube(64, false).expect_err("n=64 should reject");
277        match err {
278            IgraphError::InvalidArgument(msg) => assert!(msg.contains("exceeds")),
279            other => panic!("unexpected error: {other:?}"),
280        }
281    }
282}
283
284#[cfg(all(test, feature = "proptest-harness"))]
285mod proptests {
286    use super::*;
287    use proptest::prelude::*;
288
289    proptest! {
290        // For every n in [0, 8], the hypercube is n-regular.
291        #[test]
292        fn n_regular(n in 0u32..=8) {
293            let g = hypercube(n, false).unwrap();
294            for v in 0..g.vcount() {
295                prop_assert_eq!(g.degree(v).unwrap(), n as usize);
296            }
297        }
298
299        // Vertex count is 2^n.
300        #[test]
301        fn vertex_count_is_power_of_two(n in 0u32..=10) {
302            let g = hypercube(n, false).unwrap();
303            prop_assert_eq!(g.vcount(), 1u32 << n);
304        }
305
306        // Edge count is 2^(n-1) * n (and 0 when n = 0).
307        #[test]
308        fn edge_count_is_half_n_times_two_to_n(n in 0u32..=8) {
309            let g = hypercube(n, false).unwrap();
310            let expected: usize = if n == 0 {
311                0
312            } else {
313                (1usize << (n - 1)) * (n as usize)
314            };
315            prop_assert_eq!(g.ecount(), expected);
316        }
317
318        // Every edge differs in exactly one bit.
319        #[test]
320        fn edges_have_hamming_distance_one(n in 1u32..=8) {
321            let g = hypercube(n, false).unwrap();
322            let m = u32::try_from(g.ecount()).unwrap();
323            for e in 0..m {
324                let (u, v) = g.edge(e).unwrap();
325                prop_assert_eq!((u ^ v).count_ones(), 1);
326            }
327        }
328    }
329}