use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub fn kautz(m: u32, n: u32) -> IgraphResult<Graph> {
if n == 0 {
return kautz_n_zero(m);
}
if m == 0 {
return Graph::new(0, true);
}
let dims = KautzDims::compute(m, n)?;
let (index1, index2) = build_kautz_indices(m, &dims)?;
let edges = emit_kautz_arcs(m, &dims, &index1, &index2);
let mut g = Graph::new(dims.vcount, true)?;
g.add_edges(edges)?;
Ok(g)
}
fn kautz_n_zero(m: u32) -> IgraphResult<Graph> {
let m_plus_1 = m.checked_add(1).ok_or_else(|| {
IgraphError::InvalidArgument(format!("kautz: m + 1 overflows u32 (m = {m})"))
})?;
let mp1_us = usize::try_from(m_plus_1).map_err(|_| {
IgraphError::InvalidArgument(format!("kautz: m + 1 = {m_plus_1} cannot fit usize"))
})?;
let ec = mp1_us
.checked_mul(mp1_us.saturating_sub(1))
.ok_or_else(|| {
IgraphError::InvalidArgument(format!("kautz: (m+1)·m overflows usize (m = {m})"))
})?;
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ec);
for i in 0..m_plus_1 {
for j in 0..m_plus_1 {
if i != j {
edges.push((i, j));
}
}
}
let mut g = Graph::new(m_plus_1, true)?;
g.add_edges(edges)?;
Ok(g)
}
struct KautzDims {
m_plus_1: u32,
vcount: u32,
allstrings: u32,
vcount_us: usize,
allstrings_us: usize,
ecount: usize,
n_us: usize,
n_plus_1_us: usize,
}
impl KautzDims {
fn compute(m: u32, n: u32) -> IgraphResult<Self> {
let m_plus_1 = m.checked_add(1).ok_or_else(|| {
IgraphError::InvalidArgument(format!("kautz: m + 1 overflows u32 (m = {m})"))
})?;
let n_plus_1 = n.checked_add(1).ok_or_else(|| {
IgraphError::InvalidArgument(format!("kautz: n + 1 overflows u32 (n = {n})"))
})?;
let m_to_n = m.checked_pow(n).ok_or_else(|| {
IgraphError::InvalidArgument(format!("kautz: m^n overflows u32 (m = {m}, n = {n})"))
})?;
let vcount = m_plus_1.checked_mul(m_to_n).ok_or_else(|| {
IgraphError::InvalidArgument(format!(
"kautz: (m+1)·m^n overflows u32 (m = {m}, n = {n})"
))
})?;
let allstrings = m_plus_1.checked_pow(n_plus_1).ok_or_else(|| {
IgraphError::InvalidArgument(format!(
"kautz: (m+1)^(n+1) overflows u32 (m = {m}, n = {n})"
))
})?;
let vcount_us = usize::try_from(vcount).map_err(|_| {
IgraphError::InvalidArgument(format!("kautz: vcount {vcount} cannot fit usize"))
})?;
let allstrings_us = usize::try_from(allstrings).map_err(|_| {
IgraphError::InvalidArgument(format!("kautz: allstrings {allstrings} cannot fit usize"))
})?;
let m_us = usize::try_from(m)
.map_err(|_| IgraphError::InvalidArgument(format!("kautz: m {m} cannot fit usize")))?;
let n_us = usize::try_from(n)
.map_err(|_| IgraphError::InvalidArgument(format!("kautz: n {n} cannot fit usize")))?;
let n_plus_1_us = usize::try_from(n_plus_1).map_err(|_| {
IgraphError::InvalidArgument(format!("kautz: n + 1 = {n_plus_1} cannot fit usize"))
})?;
let ecount = vcount_us.checked_mul(m_us).ok_or_else(|| {
IgraphError::InvalidArgument(format!(
"kautz: m·vcount overflows usize (m = {m}, n = {n})"
))
})?;
Ok(Self {
m_plus_1,
vcount,
allstrings,
vcount_us,
allstrings_us,
ecount,
n_us,
n_plus_1_us,
})
}
}
fn build_kautz_indices(m: u32, dims: &KautzDims) -> IgraphResult<(Vec<u32>, Vec<u32>)> {
let mut table: Vec<u32> = vec![0; dims.n_plus_1_us];
let mut weight: u64 = 1;
for i in (0..dims.n_plus_1_us).rev() {
table[i] = u32::try_from(weight).map_err(|_| {
IgraphError::InvalidArgument(format!(
"kautz: table[{i}] overflows u32 (internal invariant)"
))
})?;
weight = weight.saturating_mul(u64::from(dims.m_plus_1));
}
let mut index1: Vec<u32> = vec![0; dims.allstrings_us];
let mut index2: Vec<u32> = vec![0; dims.vcount_us];
let mut digits: Vec<u32> = vec![0; dims.n_plus_1_us];
let mut actb: usize = 0;
let mut actvalue: u32 = 0;
let mut idx: usize = 0;
loop {
let mut z: u32 = u32::from(digits[actb] == 0);
for k in (actb + 1)..=dims.n_us {
digits[k] = z;
actvalue += z * table[k];
z = 1 - z;
}
actb = dims.n_us;
index1[actvalue as usize] = u32::try_from(idx + 1).map_err(|_| {
IgraphError::InvalidArgument(
"kautz: idx + 1 overflows u32 (internal invariant)".to_string(),
)
})?;
index2[idx] = actvalue;
idx += 1;
if idx >= dims.vcount_us {
break;
}
let advanced = advance_kautz_cursor(m, &table, &mut digits, &mut actb, &mut actvalue);
if !advanced {
break;
}
}
Ok((index1, index2))
}
fn advance_kautz_cursor(
m: u32,
table: &[u32],
digits: &mut [u32],
actb: &mut usize,
actvalue: &mut u32,
) -> bool {
loop {
let mut next = digits[*actb] + 1;
if *actb != 0 && digits[*actb - 1] == next {
next += 1;
}
if next <= m {
*actvalue += (next - digits[*actb]) * table[*actb];
digits[*actb] = next;
return true;
}
*actvalue -= digits[*actb] * table[*actb];
if *actb == 0 {
return false;
}
*actb -= 1;
}
}
fn emit_kautz_arcs(
m: u32,
dims: &KautzDims,
index1: &[u32],
index2: &[u32],
) -> Vec<(VertexId, VertexId)> {
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(dims.ecount);
let m_plus_1_u64 = u64::from(dims.m_plus_1);
let allstrings_u64 = u64::from(dims.allstrings);
for i in 0..dims.vcount {
let fromvalue = index2[i as usize];
let lastdigit = fromvalue % dims.m_plus_1;
let basis_u64 = (u64::from(fromvalue) * m_plus_1_u64) % allstrings_u64;
#[allow(clippy::cast_possible_truncation)]
let basis = basis_u64 as u32;
for digit in 0..=m {
if digit == lastdigit {
continue;
}
let tovalue = basis + digit;
let sentinel = index1[tovalue as usize];
if sentinel == 0 {
continue;
}
edges.push((i, sentinel - 1));
}
}
edges
}
#[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
}
#[test]
fn n_zero_m_zero_singleton() {
let g = kautz(0, 0).expect("K(0,0)");
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
assert!(g.is_directed());
}
#[test]
fn n_zero_m_five_directed_k6() {
let g = kautz(5, 0).expect("K(5,0)");
assert_eq!(g.vcount(), 6);
assert_eq!(g.ecount(), 30);
let arcs: BTreeSet<_> = dump_arcs(&g).into_iter().collect();
let mut expected = BTreeSet::new();
for i in 0u32..6 {
for j in 0u32..6 {
if i != j {
expected.insert((i, j));
}
}
}
assert_eq!(arcs, expected);
}
#[test]
fn m_zero_n_ten_empty() {
let g = kautz(0, 10).expect("K(0,10)");
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
assert!(g.is_directed());
}
#[test]
fn k_2_1_counts() {
let g = kautz(2, 1).expect("K(2,1)");
assert_eq!(g.vcount(), 6);
assert_eq!(g.ecount(), 12);
assert!(g.is_directed());
}
#[test]
fn k_2_1_in_out_degrees() {
let g = kautz(2, 1).expect("K(2,1)");
let mut out_deg = [0u32; 6];
let mut in_deg = [0u32; 6];
for (u, v) in dump_arcs(&g) {
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, 2, "out-degree at {v}");
assert_eq!(i, 2, "in-degree at {v}");
}
}
#[test]
fn k_3_2_counts() {
let g = kautz(3, 2).expect("K(3,2)");
assert_eq!(g.vcount(), 36);
assert_eq!(g.ecount(), 108);
}
#[test]
fn k_2_2_counts() {
let g = kautz(2, 2).expect("K(2,2)");
assert_eq!(g.vcount(), 12);
assert_eq!(g.ecount(), 24);
}
#[test]
fn vcount_overflow_rejected() {
let err = kautz(2, 31).expect_err("overflow expected");
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptest_invariants {
use super::*;
use proptest::prelude::*;
use std::collections::BTreeSet;
proptest! {
#![proptest_config(ProptestConfig::with_cases(48))]
#[test]
fn vcount_and_ecount_are_exact(m in 1u32..=5, n in 1u32..=4) {
let Some(m_to_n) = m.checked_pow(n) else { return Ok(()); };
let Some(vcount) = (m + 1).checked_mul(m_to_n) else { return Ok(()); };
let Some(ecount) = vcount.checked_mul(m) else { return Ok(()); };
let g = kautz(m, n).expect("valid input");
prop_assert_eq!(g.vcount(), vcount);
prop_assert_eq!(g.ecount() as u64, u64::from(ecount));
prop_assert!(g.is_directed());
}
#[test]
fn vertex_in_and_out_degree_equals_m(m in 1u32..=4, n in 1u32..=3) {
let g = kautz(m, n).expect("valid input");
let vc = usize::try_from(g.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 loopless_and_simple(m in 1u32..=4, n in 1u32..=3) {
let g = kautz(m, n).expect("valid input");
let ec = u32::try_from(g.ecount()).unwrap();
let mut seen: BTreeSet<(VertexId, VertexId)> = BTreeSet::new();
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");
prop_assert_ne!(u, v, "Kautz graph must not contain self-loops");
prop_assert!(seen.insert((u, v)), "duplicate arc ({u}, {v})");
}
}
}
}