use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub fn hamming(n: u32, q: u32, directed: bool) -> IgraphResult<Graph> {
if n == 0 {
return Graph::new(1, directed);
}
if q == 0 {
return Graph::new(0, directed);
}
if q == 1 {
return Graph::new(1, directed);
}
let vcount: u32 = q.checked_pow(n).ok_or_else(|| {
IgraphError::InvalidArgument(format!(
"hamming: parameters n={n}, q={q} overflow u32 vertex count (q^n)"
))
})?;
let qm1: u64 = u64::from(q) - 1;
let n_u64: u64 = u64::from(n);
let vc_u64: u64 = u64::from(vcount);
let prod = qm1
.checked_mul(n_u64)
.ok_or_else(|| {
IgraphError::InvalidArgument(format!("hamming: (q-1)*n overflows u64 for n={n}, q={q}"))
})?
.checked_mul(vc_u64)
.ok_or_else(|| {
IgraphError::InvalidArgument(format!(
"hamming: ecount product overflows u64 for n={n}, q={q}"
))
})?;
debug_assert!(prod % 2 == 0, "hamming ecount product must be even");
let ecount: usize = usize::try_from(prod / 2).map_err(|_| {
IgraphError::InvalidArgument(format!(
"hamming: edge count {} overflows usize for n={n}, q={q}",
prod / 2
))
})?;
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
for v in 0..vcount {
let mut pos: u32 = 1;
for _i in 0..n {
let dig = (v / pos) % q;
let mut j: u32 = 1;
while j < q - dig {
let u = v + j * pos;
edges.push((v, u));
j += 1;
}
if let Some(next_pos) = pos.checked_mul(q) {
pos = next_pos;
} else {
break;
}
}
}
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() {
for q in [0u32, 1, 2, 5, 100] {
let g = hamming(0, q, false).expect("n=0");
assert_eq!(g.vcount(), 1, "n=0, q={q}");
assert_eq!(g.ecount(), 0, "n=0, q={q}");
}
}
#[test]
fn q0_with_positive_n_is_null_graph() {
let g = hamming(3, 0, false).expect("q=0");
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
}
#[test]
fn q1_is_singleton() {
for n in [1u32, 3, 7] {
let g = hamming(n, 1, false).expect("q=1");
assert_eq!(g.vcount(), 1, "n={n}, q=1");
assert_eq!(g.ecount(), 0, "n={n}, q=1");
}
}
#[test]
fn h_n1_q3_is_complete_k3() {
let g = hamming(1, 3, false).expect("H(1,3)");
assert_eq!(g.vcount(), 3);
assert_eq!(g.ecount(), 3);
assert_eq!(dump_edges(&g), vec![(0, 1), (0, 2), (1, 2)]);
}
#[test]
fn h_n2_q3_matches_upstream_edges() {
let g = hamming(2, 3, false).expect("H(2,3)");
assert_eq!(g.vcount(), 9);
assert_eq!(g.ecount(), 18);
assert_eq!(
dump_edges(&g),
vec![
(0, 1),
(0, 2),
(0, 3),
(0, 6), (1, 2),
(1, 4),
(1, 7), (2, 5),
(2, 8), (3, 4),
(3, 5),
(3, 6), (4, 5),
(4, 7), (5, 8), (6, 7),
(6, 8), (7, 8), ]
);
}
#[test]
fn h_n2_q3_is_four_regular() {
let g = hamming(2, 3, false).expect("H(2,3)");
for v in 0..g.vcount() {
assert_eq!(
degree_of(&g, v),
4,
"vertex {v} of H(2,3) should have degree 4"
);
}
}
#[test]
fn q2_equals_hypercube_q3() {
use crate::hypercube;
for n in 0u32..=4 {
let h = hamming(n, 2, false).expect("H(n,2)");
let q = hypercube(n, false).expect("Q_n");
assert_eq!(h.vcount(), q.vcount(), "n={n} vcount");
assert_eq!(h.ecount(), q.ecount(), "n={n} ecount");
assert_eq!(dump_edges(&h), dump_edges(&q), "n={n} edge sequence");
}
}
#[test]
fn directed_emits_low_to_high_arcs() {
let g = hamming(2, 3, true).expect("H(2,3) directed");
assert!(g.is_directed());
assert_eq!(g.ecount(), 18);
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 h_n3_q2_matches_hypercube_q3() {
let g = hamming(3, 2, false).expect("H(3,2)");
assert_eq!(g.vcount(), 8);
assert_eq!(g.ecount(), 12);
}
#[test]
#[allow(clippy::many_single_char_names)]
fn edges_differ_in_one_digit() {
let n: u32 = 2;
let q: u32 = 4;
let g = hamming(n, q, false).expect("H(2,4)");
let m = u32::try_from(g.ecount()).unwrap();
for e in 0..m {
let (u, v) = g.edge(e).unwrap();
let mut diffs = 0u32;
let mut a = u;
let mut b = v;
for _ in 0..n {
if a % q != b % q {
diffs += 1;
}
a /= q;
b /= q;
}
assert_eq!(
diffs, 1,
"edge ({u}, {v}) in H({n},{q}) must differ in one digit"
);
}
}
#[test]
fn vertex_count_overflow_rejected() {
let err = hamming(32, 2, false).expect_err("2^32 overflow");
match err {
IgraphError::InvalidArgument(msg) => assert!(msg.contains("overflow")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn vertex_count_overflow_high_alphabet_rejected() {
let err = hamming(5, 100, false).expect_err("100^5 overflow");
match err {
IgraphError::InvalidArgument(msg) => assert!(msg.contains("overflow")),
other => panic!("unexpected error: {other:?}"),
}
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn n_q_minus_one_regular(n in 0u32..=4, q in 2u32..=5) {
let g = hamming(n, q, false).unwrap();
let expected_degree = (n as usize) * (q as usize - 1);
for v in 0..g.vcount() {
prop_assert_eq!(g.degree(v).unwrap(), expected_degree);
}
}
#[test]
fn vertex_count_is_q_to_the_n(n in 0u32..=5, q in 2u32..=4) {
let g = hamming(n, q, false).unwrap();
let expected = (q as u64).pow(n);
prop_assert_eq!(g.vcount() as u64, expected);
}
#[test]
fn edge_count_formula(n in 0u32..=4, q in 2u32..=5) {
let g = hamming(n, q, false).unwrap();
let vc = (q as u64).pow(n);
let expected = (vc * (q as u64 - 1) * (n as u64) / 2) as usize;
prop_assert_eq!(g.ecount(), expected);
}
#[test]
fn q2_equals_hypercube(n in 0u32..=6) {
use crate::hypercube;
let h = hamming(n, 2, false).unwrap();
let q = hypercube(n, false).unwrap();
prop_assert_eq!(h.vcount(), q.vcount());
prop_assert_eq!(h.ecount(), q.ecount());
}
}
}