use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub const MAX_HYPERCUBE_DIMENSION: u32 = 30;
pub fn hypercube(n: u32, directed: bool) -> IgraphResult<Graph> {
if n > MAX_HYPERCUBE_DIMENSION {
return Err(IgraphError::InvalidArgument(format!(
"hypercube: dimension {n} exceeds the supported maximum of {MAX_HYPERCUBE_DIMENSION}"
)));
}
let vcount: u32 = 1u32 << n;
let ecount: usize = if n == 0 {
0
} else {
let half = 1usize.checked_shl(n - 1).ok_or_else(|| {
IgraphError::InvalidArgument(format!(
"hypercube: edge count overflows usize for dimension {n}"
))
})?;
half.checked_mul(n as usize).ok_or_else(|| {
IgraphError::InvalidArgument(format!(
"hypercube: edge count overflows usize for dimension {n}"
))
})?
};
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
for v in 0..vcount {
for i in 0..n {
let u = v ^ (1u32 << i);
if v < u {
edges.push((v, u));
}
}
}
let mut g = Graph::new(vcount, directed)?;
g.add_edges(edges)?;
Ok(g)
}
#[cfg(test)]
mod tests {
use super::*;
fn dump_edges(g: &Graph) -> Vec<(u32, u32)> {
let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
(0..m)
.map(|e| g.edge(e).expect("edge id in bounds"))
.collect()
}
fn degree_of(g: &Graph, v: VertexId) -> usize {
g.degree(v).expect("vertex in range")
}
#[test]
fn n0_is_singleton() {
let g = hypercube(0, false).expect("n=0");
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
assert!(!g.is_directed());
}
#[test]
fn n0_directed_is_singleton() {
let g = hypercube(0, true).expect("n=0 directed");
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
assert!(g.is_directed());
}
#[test]
fn n1_is_k2() {
let g = hypercube(1, false).expect("n=1");
assert_eq!(g.vcount(), 2);
assert_eq!(g.ecount(), 1);
assert_eq!(dump_edges(&g), vec![(0, 1)]);
}
#[test]
fn n2_is_four_cycle() {
let g = hypercube(2, false).expect("n=2");
assert_eq!(g.vcount(), 4);
assert_eq!(g.ecount(), 4);
assert_eq!(dump_edges(&g), vec![(0, 1), (0, 2), (1, 3), (2, 3)]);
}
#[test]
fn n3_is_three_cube() {
let g = hypercube(3, false).expect("n=3");
assert_eq!(g.vcount(), 8);
assert_eq!(g.ecount(), 12);
assert_eq!(
dump_edges(&g),
vec![
(0, 1),
(0, 2),
(0, 4),
(1, 3),
(1, 5),
(2, 3),
(2, 6),
(3, 7),
(4, 5),
(4, 6),
(5, 7),
(6, 7),
]
);
}
#[test]
fn n3_directed_same_edges_but_directed() {
let g = hypercube(3, true).expect("n=3 directed");
assert!(g.is_directed());
assert_eq!(g.ecount(), 12);
let edges = dump_edges(&g);
assert_eq!(edges[0], (0, 1));
assert_eq!(edges[1], (0, 2));
}
#[test]
fn n3_is_three_regular() {
let g = hypercube(3, false).expect("n=3");
for v in 0..g.vcount() {
assert_eq!(degree_of(&g, v), 3, "vertex {v} should have degree 3");
}
}
#[test]
fn n4_vertex_and_edge_counts() {
let g = hypercube(4, false).expect("n=4");
assert_eq!(g.vcount(), 16);
assert_eq!(g.ecount(), 32);
for v in 0..g.vcount() {
assert_eq!(degree_of(&g, v), 4, "vertex {v} should have degree 4");
}
}
#[test]
fn n4_is_bipartite_by_popcount() {
let g = hypercube(4, false).expect("n=4");
let m = u32::try_from(g.ecount()).unwrap();
for e in 0..m {
let (u, v) = g.edge(e).unwrap();
assert_ne!(
u.count_ones() % 2,
v.count_ones() % 2,
"edge ({u}, {v}) should cross popcount parity"
);
}
}
#[test]
fn edge_endpoints_differ_in_one_bit() {
for n in 0..=5u32 {
let g = hypercube(n, false).expect("hypercube ok");
let m = u32::try_from(g.ecount()).unwrap();
for e in 0..m {
let (u, v) = g.edge(e).unwrap();
assert_eq!(
(u ^ v).count_ones(),
1,
"edge ({u}, {v}) in Q_{n} should differ in exactly one bit"
);
}
}
}
#[test]
fn edges_are_canonical_v_less_than_u() {
let g = hypercube(5, true).expect("Q_5 directed");
let m = u32::try_from(g.ecount()).unwrap();
for e in 0..m {
let (u, v) = g.edge(e).unwrap();
assert!(u < v, "edge ({u}, {v}) must be canonically ordered");
}
}
#[test]
fn dimension_too_large_rejected() {
let err = hypercube(31, false).expect_err("n=31 should reject");
match err {
IgraphError::InvalidArgument(msg) => assert!(msg.contains("exceeds")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn dimension_far_too_large_rejected() {
let err = hypercube(64, false).expect_err("n=64 should reject");
match err {
IgraphError::InvalidArgument(msg) => assert!(msg.contains("exceeds")),
other => panic!("unexpected error: {other:?}"),
}
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn n_regular(n in 0u32..=8) {
let g = hypercube(n, false).unwrap();
for v in 0..g.vcount() {
prop_assert_eq!(g.degree(v).unwrap(), n as usize);
}
}
#[test]
fn vertex_count_is_power_of_two(n in 0u32..=10) {
let g = hypercube(n, false).unwrap();
prop_assert_eq!(g.vcount(), 1u32 << n);
}
#[test]
fn edge_count_is_half_n_times_two_to_n(n in 0u32..=8) {
let g = hypercube(n, false).unwrap();
let expected: usize = if n == 0 {
0
} else {
(1usize << (n - 1)) * (n as usize)
};
prop_assert_eq!(g.ecount(), expected);
}
#[test]
fn edges_have_hamming_distance_one(n in 1u32..=8) {
let g = hypercube(n, false).unwrap();
let m = u32::try_from(g.ecount()).unwrap();
for e in 0..m {
let (u, v) = g.edge(e).unwrap();
prop_assert_eq!((u ^ v).count_ones(), 1);
}
}
}
}