rust_igraph/algorithms/constructors/
hypercube.rs1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
40
41pub const MAX_HYPERCUBE_DIMENSION: u32 = 30;
48
49pub 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 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 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 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 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 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 assert_eq!(g.vcount(), 16);
216 assert_eq!(g.ecount(), 32);
217 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 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 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 #[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 #[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 #[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 #[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}