use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub fn de_bruijn(m: u32, n: u32) -> IgraphResult<Graph> {
if n == 0 {
return Graph::new(1, true);
}
if m == 0 {
return Graph::new(0, true);
}
let vcount = m.checked_pow(n).ok_or_else(|| {
IgraphError::InvalidArgument(format!("de_bruijn: m^n overflows u32 (m = {m}, n = {n})"))
})?;
let vcount_us = usize::try_from(vcount).map_err(|_| {
IgraphError::InvalidArgument(format!(
"de_bruijn: vcount {vcount} cannot fit usize on this target"
))
})?;
let m_us = usize::try_from(m).map_err(|_| {
IgraphError::InvalidArgument(format!("de_bruijn: m {m} cannot fit usize on this target"))
})?;
let ecount = vcount_us.checked_mul(m_us).ok_or_else(|| {
IgraphError::InvalidArgument(format!(
"de_bruijn: m^(n+1) overflows usize (m = {m}, n = {n})"
))
})?;
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
let vcount_u64 = u64::from(vcount);
let m_u64 = u64::from(m);
for i in 0..vcount {
let basis = (u64::from(i) * m_u64) % vcount_u64;
for b in 0..m {
#[allow(clippy::cast_possible_truncation)]
let target = (basis + u64::from(b)) as u32;
edges.push((i, target));
}
}
let mut g = Graph::new(vcount, true)?;
g.add_edges(edges)?;
Ok(g)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn dump_arcs(g: &Graph) -> Vec<(VertexId, VertexId)> {
let ec = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
let mut out = Vec::with_capacity(g.ecount());
for e in 0..ec {
let u = g.edge_source(e).expect("edge in range");
let v = g.edge_target(e).expect("edge in range");
out.push((u, v));
}
out
}
fn vcount_us(g: &Graph) -> usize {
usize::try_from(g.vcount()).expect("vcount fits usize")
}
#[test]
fn n_zero_yields_singleton_directed() {
let g = de_bruijn(5, 0).expect("n=0 valid");
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
assert!(g.is_directed());
}
#[test]
fn m_zero_yields_null_directed() {
let g = de_bruijn(0, 4).expect("m=0 valid");
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
assert!(g.is_directed());
}
#[test]
fn m_one_n_one_is_single_self_loop() {
let g = de_bruijn(1, 1).expect("B(1,1) valid");
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 1);
assert_eq!(dump_arcs(&g), vec![(0, 0)]);
}
#[test]
fn b_2_1_is_directed_k2_with_loops() {
let g = de_bruijn(2, 1).expect("B(2,1) valid");
assert_eq!(g.vcount(), 2);
assert_eq!(g.ecount(), 4);
let arcs: BTreeSet<_> = dump_arcs(&g).into_iter().collect();
let expected: BTreeSet<_> = [(0, 0), (0, 1), (1, 0), (1, 1)].into_iter().collect();
assert_eq!(arcs, expected);
}
#[test]
fn b_2_2_exact_arc_list() {
let g = de_bruijn(2, 2).expect("B(2,2) valid");
assert_eq!(g.vcount(), 4);
assert_eq!(g.ecount(), 8);
let expected = vec![
(0, 0),
(0, 1),
(1, 2),
(1, 3),
(2, 0),
(2, 1),
(3, 2),
(3, 3),
];
assert_eq!(dump_arcs(&g), expected);
}
#[test]
fn b_3_2_exact_arc_count_and_degrees() {
let g = de_bruijn(3, 2).expect("B(3,2) valid");
assert_eq!(g.vcount(), 9);
assert_eq!(g.ecount(), 27);
let arcs = dump_arcs(&g);
let mut out_deg = [0u32; 9];
let mut in_deg = [0u32; 9];
for (u, v) in arcs {
out_deg[u as usize] += 1;
in_deg[v as usize] += 1;
}
for (v, (&o, &i)) in out_deg.iter().zip(in_deg.iter()).enumerate() {
assert_eq!(o, 3, "out-degree at {v}");
assert_eq!(i, 3, "in-degree at {v}");
}
}
#[test]
fn b_2_3_total_counts() {
let g = de_bruijn(2, 3).expect("B(2,3) valid");
assert_eq!(g.vcount(), 8);
assert_eq!(g.ecount(), 16);
}
#[test]
fn b_4_3_total_counts() {
let g = de_bruijn(4, 3).expect("B(4,3) valid");
assert_eq!(g.vcount(), 64);
assert_eq!(g.ecount(), 256);
}
#[test]
fn rewrite_rule_holds() {
let m: u32 = 3;
let n: u32 = 2;
let g = de_bruijn(m, n).expect("valid");
let vcount = g.vcount();
for (u, v) in dump_arcs(&g) {
let basis = (u * m) % vcount;
assert!(
v >= basis && v < basis + m,
"arc ({u} → {v}) violates rewrite (basis = {basis}, m = {m})"
);
}
}
#[test]
fn vcount_overflow_rejected() {
let err = de_bruijn(2, 32).expect_err("vcount overflow");
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn out_degree_uniform_for_all_supported_inputs() {
for m in 1..=4u32 {
for n in 1..=3u32 {
let g = de_bruijn(m, n).expect("valid B(m, n)");
let vc = vcount_us(&g);
let expected_ecount = (m as usize).pow(n + 1);
assert_eq!(
g.ecount(),
expected_ecount,
"ecount mismatch for B({m}, {n})"
);
let arcs = dump_arcs(&g);
let mut out_deg = vec![0u32; vc];
for (u, _) in arcs {
out_deg[u as usize] += 1;
}
for (v, &d) in out_deg.iter().enumerate() {
assert_eq!(d, m, "out-degree mismatch at {v} in B({m}, {n})");
}
}
}
}
#[test]
fn in_degree_uniform_for_all_supported_inputs() {
for m in 1..=4u32 {
for n in 1..=3u32 {
let g = de_bruijn(m, n).expect("valid B(m, n)");
let vc = vcount_us(&g);
let arcs = dump_arcs(&g);
let mut in_deg = vec![0u32; vc];
for (_, v) in arcs {
in_deg[v as usize] += 1;
}
for (v, &d) in in_deg.iter().enumerate() {
assert_eq!(d, m, "in-degree mismatch at {v} in B({m}, {n})");
}
}
}
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptest_invariants {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn vcount_and_ecount_are_exact(m in 1u32..=5, n in 1u32..=4) {
let Some(vcount) = m.checked_pow(n) else { return Ok(()); };
let Some(ecount) = vcount.checked_mul(m) else { return Ok(()); };
let g = de_bruijn(m, n).expect("valid input");
prop_assert_eq!(g.vcount(), vcount);
prop_assert_eq!(g.ecount() as u64, u64::from(ecount));
}
#[test]
fn vertex_in_and_out_degree_equals_m(m in 1u32..=4, n in 1u32..=3) {
let g = de_bruijn(m, n).expect("valid input");
let vcount = g.vcount();
let vc = usize::try_from(vcount).unwrap();
let mut out_deg = vec![0u32; vc];
let mut in_deg = vec![0u32; vc];
let ec = u32::try_from(g.ecount()).unwrap();
for e in 0..ec {
let u = g.edge_source(e).expect("edge in range");
let v = g.edge_target(e).expect("edge in range");
out_deg[u as usize] += 1;
in_deg[v as usize] += 1;
}
for v in 0..vc {
prop_assert_eq!(out_deg[v], m);
prop_assert_eq!(in_deg[v], m);
}
}
#[test]
fn rewrite_rule_holds_for_every_arc(m in 1u32..=4, n in 1u32..=3) {
let g = de_bruijn(m, n).expect("valid input");
let vcount = g.vcount();
let ec = u32::try_from(g.ecount()).unwrap();
for e in 0..ec {
let u = g.edge_source(e).expect("edge in range");
let v = g.edge_target(e).expect("edge in range");
let basis = (u * m) % vcount;
prop_assert!(v >= basis && v < basis + m);
}
}
}
}