Skip to main content

rust_igraph/algorithms/constructors/
kautz.rs

1//! Kautz graph constructor (ALGO-CN-013).
2//!
3//! Counterpart of `igraph_kautz()` in
4//! `references/igraph/src/constructors/kautz.c:61-210`.
5//!
6//! The Kautz graph `K(m, n)` is the **directed** graph whose vertices are
7//! length-`n+1` strings over an alphabet of `m+1` letters, with the
8//! constraint that no two consecutive letters in the string may be
9//! equal. There is an arc from `v = (a_0, …, a_n)` to
10//! `w = (a_1, …, a_n, b)` for every alphabet letter `b ≠ a_n`. Each
11//! vertex therefore has out-degree exactly `m` and in-degree exactly `m`;
12//! the total arc count is `m · (m+1) · m^n`.
13//!
14//! Vertex count is `(m+1) · m^n`. Vertices are encoded as base-`(m+1)`
15//! integers in `[0, (m+1)^(n+1))`, but only those whose digit sequence
16//! avoids consecutive repeats are valid — we build two index tables
17//! (`index1`, `index2`) to map between the sparse string codes and the
18//! dense `[0, vcount)` vertex ids, just like the upstream C source.
19//!
20//! Degenerate forms (match upstream order in `kautz.c` lines 81-86):
21//!
22//! * `n == 0` → `(m+1)`-vertex directed complete graph with no
23//!   self-loops (`K_{m+1}` directed). For `m == 0 ∧ n == 0` this
24//!   collapses to a singleton. The C source delegates to
25//!   `igraph_full`; we inline the emission to avoid pulling in a
26//!   not-yet-ported full-graph helper.
27//! * `m == 0 ∧ n ≥ 1` → empty 0-vertex directed graph (no valid string
28//!   survives the consecutive-distinct constraint when only one symbol
29//!   exists).
30//!
31//! Time complexity: `O(|V| + |E|)` — both the index-table construction
32//! and the arc emission are linear in the output.
33
34use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
35
36/// Build the Kautz graph `K(m, n)`.
37///
38/// The result is **always directed** (matches `IGRAPH_DIRECTED` in the C
39/// constructor). For `n ≥ 1` every vertex has out-degree and in-degree
40/// equal to `m`.
41///
42/// # Errors
43///
44/// * [`IgraphError::InvalidArgument`] — `(m+1)^(n+1)` overflows `u32`,
45///   so the sparse string-code space cannot be addressed with the
46///   graph's `u32` vertex id width. The check uses [`u32::checked_pow`]
47///   so the failure mode is deterministic.
48/// * [`IgraphError::InvalidArgument`] — vcount `(m+1) · m^n` or ecount
49///   `m · vcount` overflows `usize` so the index tables / edge buffer
50///   cannot be sized safely.
51///
52/// # Examples
53///
54/// ```
55/// use rust_igraph::kautz;
56///
57/// // K(2, 1) — directed graph on 6 vertices; every vertex has
58/// // out-degree 2 and in-degree 2 → 12 arcs total.
59/// let g = kautz(2, 1).unwrap();
60/// assert_eq!(g.vcount(), 6);
61/// assert_eq!(g.ecount(), 12);
62/// assert!(g.is_directed());
63/// ```
64pub fn kautz(m: u32, n: u32) -> IgraphResult<Graph> {
65    // n == 0 → directed K_{m+1} with no loops. m == 0 ∧ n == 0
66    // collapses to a singleton (inner loop emits no arcs).
67    if n == 0 {
68        return kautz_n_zero(m);
69    }
70    // m == 0 with n ≥ 1 → no valid strings (single-symbol alphabet
71    // cannot satisfy the no-consecutive-equal constraint).
72    if m == 0 {
73        return Graph::new(0, true);
74    }
75
76    let dims = KautzDims::compute(m, n)?;
77    let (index1, index2) = build_kautz_indices(m, &dims)?;
78    let edges = emit_kautz_arcs(m, &dims, &index1, &index2);
79
80    let mut g = Graph::new(dims.vcount, true)?;
81    g.add_edges(edges)?;
82    Ok(g)
83}
84
85/// `n == 0` specialisation: directed complete graph on `m+1` vertices
86/// with no self-loops. The C source delegates to `igraph_full`; we
87/// inline so this constructor stays standalone.
88fn kautz_n_zero(m: u32) -> IgraphResult<Graph> {
89    let m_plus_1 = m.checked_add(1).ok_or_else(|| {
90        IgraphError::InvalidArgument(format!("kautz: m + 1 overflows u32 (m = {m})"))
91    })?;
92    let mp1_us = usize::try_from(m_plus_1).map_err(|_| {
93        IgraphError::InvalidArgument(format!("kautz: m + 1 = {m_plus_1} cannot fit usize"))
94    })?;
95    let ec = mp1_us
96        .checked_mul(mp1_us.saturating_sub(1))
97        .ok_or_else(|| {
98            IgraphError::InvalidArgument(format!("kautz: (m+1)·m overflows usize (m = {m})"))
99        })?;
100    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ec);
101    for i in 0..m_plus_1 {
102        for j in 0..m_plus_1 {
103            if i != j {
104                edges.push((i, j));
105            }
106        }
107    }
108    let mut g = Graph::new(m_plus_1, true)?;
109    g.add_edges(edges)?;
110    Ok(g)
111}
112
113/// Computed dimensions of `K(m, n)` after every overflow gate has
114/// passed. All `*_us` fields are pre-cast for indexing.
115struct KautzDims {
116    m_plus_1: u32,
117    vcount: u32,
118    allstrings: u32,
119    vcount_us: usize,
120    allstrings_us: usize,
121    ecount: usize,
122    n_us: usize,
123    n_plus_1_us: usize,
124}
125
126impl KautzDims {
127    fn compute(m: u32, n: u32) -> IgraphResult<Self> {
128        let m_plus_1 = m.checked_add(1).ok_or_else(|| {
129            IgraphError::InvalidArgument(format!("kautz: m + 1 overflows u32 (m = {m})"))
130        })?;
131        let n_plus_1 = n.checked_add(1).ok_or_else(|| {
132            IgraphError::InvalidArgument(format!("kautz: n + 1 overflows u32 (n = {n})"))
133        })?;
134        let m_to_n = m.checked_pow(n).ok_or_else(|| {
135            IgraphError::InvalidArgument(format!("kautz: m^n overflows u32 (m = {m}, n = {n})"))
136        })?;
137        let vcount = m_plus_1.checked_mul(m_to_n).ok_or_else(|| {
138            IgraphError::InvalidArgument(format!(
139                "kautz: (m+1)·m^n overflows u32 (m = {m}, n = {n})"
140            ))
141        })?;
142        let allstrings = m_plus_1.checked_pow(n_plus_1).ok_or_else(|| {
143            IgraphError::InvalidArgument(format!(
144                "kautz: (m+1)^(n+1) overflows u32 (m = {m}, n = {n})"
145            ))
146        })?;
147        let vcount_us = usize::try_from(vcount).map_err(|_| {
148            IgraphError::InvalidArgument(format!("kautz: vcount {vcount} cannot fit usize"))
149        })?;
150        let allstrings_us = usize::try_from(allstrings).map_err(|_| {
151            IgraphError::InvalidArgument(format!("kautz: allstrings {allstrings} cannot fit usize"))
152        })?;
153        let m_us = usize::try_from(m)
154            .map_err(|_| IgraphError::InvalidArgument(format!("kautz: m {m} cannot fit usize")))?;
155        let n_us = usize::try_from(n)
156            .map_err(|_| IgraphError::InvalidArgument(format!("kautz: n {n} cannot fit usize")))?;
157        let n_plus_1_us = usize::try_from(n_plus_1).map_err(|_| {
158            IgraphError::InvalidArgument(format!("kautz: n + 1 = {n_plus_1} cannot fit usize"))
159        })?;
160        let ecount = vcount_us.checked_mul(m_us).ok_or_else(|| {
161            IgraphError::InvalidArgument(format!(
162                "kautz: m·vcount overflows usize (m = {m}, n = {n})"
163            ))
164        })?;
165        Ok(Self {
166            m_plus_1,
167            vcount,
168            allstrings,
169            vcount_us,
170            allstrings_us,
171            ecount,
172            n_us,
173            n_plus_1_us,
174        })
175    }
176}
177
178/// Walks all length-`n+1` strings over `{0..=m}` with no two
179/// consecutive equal letters in lex order, building both the sparse
180/// code→1-based-id map (`index1`) and the dense id→code map (`index2`).
181fn build_kautz_indices(m: u32, dims: &KautzDims) -> IgraphResult<(Vec<u32>, Vec<u32>)> {
182    // Positional weights: table[i] = (m+1)^(n-i). Each entry
183    // ≤ allstrings/m_plus_1 < u32::MAX by the precheck.
184    let mut table: Vec<u32> = vec![0; dims.n_plus_1_us];
185    let mut weight: u64 = 1;
186    for i in (0..dims.n_plus_1_us).rev() {
187        table[i] = u32::try_from(weight).map_err(|_| {
188            IgraphError::InvalidArgument(format!(
189                "kautz: table[{i}] overflows u32 (internal invariant)"
190            ))
191        })?;
192        weight = weight.saturating_mul(u64::from(dims.m_plus_1));
193    }
194
195    let mut index1: Vec<u32> = vec![0; dims.allstrings_us];
196    let mut index2: Vec<u32> = vec![0; dims.vcount_us];
197    let mut digits: Vec<u32> = vec![0; dims.n_plus_1_us];
198
199    let mut actb: usize = 0;
200    let mut actvalue: u32 = 0;
201    let mut idx: usize = 0;
202
203    loop {
204        // Complete digits[0..=actb] into a valid string by filling
205        // digits[actb+1..=n] with alternating 0/1 starting from
206        // (digits[actb] == 0 ? 1 : 0).
207        let mut z: u32 = u32::from(digits[actb] == 0);
208        for k in (actb + 1)..=dims.n_us {
209            digits[k] = z;
210            actvalue += z * table[k];
211            z = 1 - z;
212        }
213        actb = dims.n_us;
214
215        index1[actvalue as usize] = u32::try_from(idx + 1).map_err(|_| {
216            IgraphError::InvalidArgument(
217                "kautz: idx + 1 overflows u32 (internal invariant)".to_string(),
218            )
219        })?;
220        index2[idx] = actvalue;
221        idx += 1;
222        if idx >= dims.vcount_us {
223            break;
224        }
225
226        // Advance to the next valid prefix.
227        let advanced = advance_kautz_cursor(m, &table, &mut digits, &mut actb, &mut actvalue);
228        if !advanced {
229            // Defensive: unreachable when vcount is correct (there
230            // are exactly vcount valid strings).
231            break;
232        }
233    }
234
235    Ok((index1, index2))
236}
237
238/// Tries to bump the cursor to the next valid prefix. Returns `false`
239/// only when no further valid prefix exists (an internal invariant
240/// violation; never happens for well-formed `(m, n)`).
241fn advance_kautz_cursor(
242    m: u32,
243    table: &[u32],
244    digits: &mut [u32],
245    actb: &mut usize,
246    actvalue: &mut u32,
247) -> bool {
248    loop {
249        let mut next = digits[*actb] + 1;
250        if *actb != 0 && digits[*actb - 1] == next {
251            next += 1;
252        }
253        if next <= m {
254            *actvalue += (next - digits[*actb]) * table[*actb];
255            digits[*actb] = next;
256            return true;
257        }
258        *actvalue -= digits[*actb] * table[*actb];
259        if *actb == 0 {
260            return false;
261        }
262        *actb -= 1;
263    }
264}
265
266/// Source-major, digit-ascending arc emission. For every vertex `i`
267/// with sparse code `fromvalue`, the `m` successors live at
268/// `basis + digit` for `digit ∈ [0, m]` with `digit != lastdigit`.
269fn emit_kautz_arcs(
270    m: u32,
271    dims: &KautzDims,
272    index1: &[u32],
273    index2: &[u32],
274) -> Vec<(VertexId, VertexId)> {
275    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(dims.ecount);
276    let m_plus_1_u64 = u64::from(dims.m_plus_1);
277    let allstrings_u64 = u64::from(dims.allstrings);
278    for i in 0..dims.vcount {
279        let fromvalue = index2[i as usize];
280        let lastdigit = fromvalue % dims.m_plus_1;
281        // fromvalue · (m+1) may exceed u32, but its reduction mod
282        // allstrings lives in [0, allstrings) ≤ u32::MAX.
283        let basis_u64 = (u64::from(fromvalue) * m_plus_1_u64) % allstrings_u64;
284        #[allow(clippy::cast_possible_truncation)]
285        let basis = basis_u64 as u32;
286        for digit in 0..=m {
287            if digit == lastdigit {
288                continue;
289            }
290            let tovalue = basis + digit;
291            let sentinel = index1[tovalue as usize];
292            if sentinel == 0 {
293                continue;
294            }
295            edges.push((i, sentinel - 1));
296        }
297    }
298    edges
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use std::collections::BTreeSet;
305
306    fn dump_arcs(g: &Graph) -> Vec<(VertexId, VertexId)> {
307        let ec = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
308        let mut out = Vec::with_capacity(g.ecount());
309        for e in 0..ec {
310            let u = g.edge_source(e).expect("edge in range");
311            let v = g.edge_target(e).expect("edge in range");
312            out.push((u, v));
313        }
314        out
315    }
316
317    #[test]
318    fn n_zero_m_zero_singleton() {
319        // K(0, 0) — single vertex, no arcs (one length-1 string).
320        let g = kautz(0, 0).expect("K(0,0)");
321        assert_eq!(g.vcount(), 1);
322        assert_eq!(g.ecount(), 0);
323        assert!(g.is_directed());
324    }
325
326    #[test]
327    fn n_zero_m_five_directed_k6() {
328        // K(5, 0) — directed K_6 with no loops, 30 arcs.
329        let g = kautz(5, 0).expect("K(5,0)");
330        assert_eq!(g.vcount(), 6);
331        assert_eq!(g.ecount(), 30);
332        let arcs: BTreeSet<_> = dump_arcs(&g).into_iter().collect();
333        let mut expected = BTreeSet::new();
334        for i in 0u32..6 {
335            for j in 0u32..6 {
336                if i != j {
337                    expected.insert((i, j));
338                }
339            }
340        }
341        assert_eq!(arcs, expected);
342    }
343
344    #[test]
345    fn m_zero_n_ten_empty() {
346        let g = kautz(0, 10).expect("K(0,10)");
347        assert_eq!(g.vcount(), 0);
348        assert_eq!(g.ecount(), 0);
349        assert!(g.is_directed());
350    }
351
352    #[test]
353    fn k_2_1_counts() {
354        let g = kautz(2, 1).expect("K(2,1)");
355        assert_eq!(g.vcount(), 6);
356        assert_eq!(g.ecount(), 12);
357        assert!(g.is_directed());
358    }
359
360    #[test]
361    fn k_2_1_in_out_degrees() {
362        // K(2, 1): out-degree = in-degree = m = 2 for every vertex.
363        let g = kautz(2, 1).expect("K(2,1)");
364        let mut out_deg = [0u32; 6];
365        let mut in_deg = [0u32; 6];
366        for (u, v) in dump_arcs(&g) {
367            out_deg[u as usize] += 1;
368            in_deg[v as usize] += 1;
369        }
370        for (v, (&o, &i)) in out_deg.iter().zip(in_deg.iter()).enumerate() {
371            assert_eq!(o, 2, "out-degree at {v}");
372            assert_eq!(i, 2, "in-degree at {v}");
373        }
374    }
375
376    #[test]
377    fn k_3_2_counts() {
378        // K(3, 2): (m+1)·m^n = 4·9 = 36 vertices, m·vcount = 108 arcs.
379        let g = kautz(3, 2).expect("K(3,2)");
380        assert_eq!(g.vcount(), 36);
381        assert_eq!(g.ecount(), 108);
382    }
383
384    #[test]
385    fn k_2_2_counts() {
386        // K(2, 2): 3·4 = 12 vertices, 2·12 = 24 arcs.
387        let g = kautz(2, 2).expect("K(2,2)");
388        assert_eq!(g.vcount(), 12);
389        assert_eq!(g.ecount(), 24);
390    }
391
392    #[test]
393    fn vcount_overflow_rejected() {
394        // (m+1)^(n+1) for m=2, n=31 is 3^32 which overflows u32.
395        let err = kautz(2, 31).expect_err("overflow expected");
396        assert!(matches!(err, IgraphError::InvalidArgument(_)));
397    }
398}
399
400#[cfg(all(test, feature = "proptest-harness"))]
401mod proptest_invariants {
402    use super::*;
403    use proptest::prelude::*;
404    use std::collections::BTreeSet;
405
406    proptest! {
407        #![proptest_config(ProptestConfig::with_cases(48))]
408
409        /// `vcount == (m+1)·m^n` and `ecount == m·vcount` across the
410        /// supported `(m, n)` grid that stays well clear of u32 overflow.
411        #[test]
412        fn vcount_and_ecount_are_exact(m in 1u32..=5, n in 1u32..=4) {
413            let Some(m_to_n) = m.checked_pow(n) else { return Ok(()); };
414            let Some(vcount) = (m + 1).checked_mul(m_to_n) else { return Ok(()); };
415            let Some(ecount) = vcount.checked_mul(m) else { return Ok(()); };
416            let g = kautz(m, n).expect("valid input");
417            prop_assert_eq!(g.vcount(), vcount);
418            prop_assert_eq!(g.ecount() as u64, u64::from(ecount));
419            prop_assert!(g.is_directed());
420        }
421
422        /// Every vertex has out-degree `m` and in-degree `m`.
423        #[test]
424        fn vertex_in_and_out_degree_equals_m(m in 1u32..=4, n in 1u32..=3) {
425            let g = kautz(m, n).expect("valid input");
426            let vc = usize::try_from(g.vcount()).unwrap();
427            let mut out_deg = vec![0u32; vc];
428            let mut in_deg = vec![0u32; vc];
429            let ec = u32::try_from(g.ecount()).unwrap();
430            for e in 0..ec {
431                let u = g.edge_source(e).expect("edge in range");
432                let v = g.edge_target(e).expect("edge in range");
433                out_deg[u as usize] += 1;
434                in_deg[v as usize] += 1;
435            }
436            for v in 0..vc {
437                prop_assert_eq!(out_deg[v], m);
438                prop_assert_eq!(in_deg[v], m);
439            }
440        }
441
442        /// Kautz graphs are loopless and simple: no `(v, v)` arcs and no
443        /// repeated `(u, v)` pairs.
444        #[test]
445        fn loopless_and_simple(m in 1u32..=4, n in 1u32..=3) {
446            let g = kautz(m, n).expect("valid input");
447            let ec = u32::try_from(g.ecount()).unwrap();
448            let mut seen: BTreeSet<(VertexId, VertexId)> = BTreeSet::new();
449            for e in 0..ec {
450                let u = g.edge_source(e).expect("edge in range");
451                let v = g.edge_target(e).expect("edge in range");
452                prop_assert_ne!(u, v, "Kautz graph must not contain self-loops");
453                prop_assert!(seen.insert((u, v)), "duplicate arc ({u}, {v})");
454            }
455        }
456    }
457}