Skip to main content

rust_igraph/algorithms/constructors/
de_bruijn.rs

1//! De Bruijn graph constructor (ALGO-CN-012).
2//!
3//! Counterpart of `igraph_de_bruijn()` in
4//! `references/igraph/src/constructors/de_bruijn.c:58-113`.
5//!
6//! The De Bruijn graph `B(m, n)` is the **directed** graph on `m^n`
7//! vertices whose vertices are length-`n` strings drawn from an alphabet
8//! of `m` symbols. There is an arc from vertex `v = (a_1, …, a_n)` to
9//! vertex `w = (a_2, …, a_n, b)` for every alphabet symbol `b` — that
10//! is, `w` is obtained from `v` by dropping the first symbol and
11//! appending one. Each vertex therefore has out-degree exactly `m` and
12//! in-degree exactly `m` (when `n ≥ 1`); the total arc count is
13//! `m^(n+1) = m · vcount`.
14//!
15//! Vertices are encoded as integers in `[0, m^n)` via the little-endian
16//! base-`m` interpretation that upstream uses. The arc-rewrite then
17//! reduces to integer arithmetic: from `i`, the `m` successors are
18//! `((i * m) mod m^n) + b` for `b ∈ [0, m)`.
19//!
20//! Degenerate forms:
21//!
22//! * `n == 0` → exactly `1` directed vertex with no arcs (matches
23//!   upstream — there is exactly one length-0 string and the rewrite has
24//!   no symbol to append).
25//! * `m == 0` → exactly `0` vertices (null graph).
26//! * `m == 1, n == 1` → 1 vertex with a single self-loop (the lone
27//!   string maps to itself).
28//! * `B(2, 1)` is the directed graph on `{0, 1}` with all four arcs
29//!   (including both self-loops).
30//!
31//! Time complexity: `O(|V| + |E|) = O(m^(n+1))`.
32
33use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
34
35/// Build the De Bruijn graph `B(m, n)`.
36///
37/// The result is **always directed**, matches the C convention
38/// (`IGRAPH_DIRECTED`). Self-loops appear naturally whenever the
39/// rewrite `(i, ((i * m) mod m^n) + b)` lands back on `i` — most
40/// notably for `m == 1, n ≥ 1`, where the single vertex `0` has a
41/// self-loop, and along the main diagonal of `B(m, 1)`.
42///
43/// # Errors
44///
45/// * [`IgraphError::InvalidArgument`] — `m.pow(n)` would overflow `u32`
46///   (vertex count too large to address with the graph's `u32` vertex
47///   id width). The check uses [`u32::checked_pow`] so the failure mode
48///   is deterministic and never produces a silently-wrapped graph.
49/// * [`IgraphError::InvalidArgument`] — the edge count `m^(n+1)`
50///   overflows `usize` so the edge buffer cannot be sized safely.
51///
52/// # Examples
53///
54/// ```
55/// use rust_igraph::de_bruijn;
56///
57/// // B(2, 2) — 4 vertices, 8 arcs, 4 of which are self-loops or pairs.
58/// let g = de_bruijn(2, 2).unwrap();
59/// assert_eq!(g.vcount(), 4);
60/// assert_eq!(g.ecount(), 8);
61/// assert!(g.is_directed());
62/// ```
63pub fn de_bruijn(m: u32, n: u32) -> IgraphResult<Graph> {
64    // Degenerate forms first — matches the upstream order in
65    // `igraph_de_bruijn`.
66    if n == 0 {
67        // There is exactly one length-0 string.
68        return Graph::new(1, true);
69    }
70    if m == 0 {
71        return Graph::new(0, true);
72    }
73
74    let vcount = m.checked_pow(n).ok_or_else(|| {
75        IgraphError::InvalidArgument(format!("de_bruijn: m^n overflows u32 (m = {m}, n = {n})"))
76    })?;
77
78    // Edge count = vcount * m = m^(n+1). Compute through usize so the
79    // overflow check is meaningful for the edge buffer.
80    let vcount_us = usize::try_from(vcount).map_err(|_| {
81        IgraphError::InvalidArgument(format!(
82            "de_bruijn: vcount {vcount} cannot fit usize on this target"
83        ))
84    })?;
85    let m_us = usize::try_from(m).map_err(|_| {
86        IgraphError::InvalidArgument(format!("de_bruijn: m {m} cannot fit usize on this target"))
87    })?;
88    let ecount = vcount_us.checked_mul(m_us).ok_or_else(|| {
89        IgraphError::InvalidArgument(format!(
90            "de_bruijn: m^(n+1) overflows usize (m = {m}, n = {n})"
91        ))
92    })?;
93
94    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
95
96    // For each vertex i, the m successors are ((i * m) mod vcount) + b
97    // for b ∈ [0, m). Use u64 for the intermediate product so we don't
98    // need a checked_mul inside the inner loop.
99    let vcount_u64 = u64::from(vcount);
100    let m_u64 = u64::from(m);
101    for i in 0..vcount {
102        let basis = (u64::from(i) * m_u64) % vcount_u64;
103        for b in 0..m {
104            // `basis + b` is < `vcount` by construction, so the u32 cast
105            // is always safe.
106            #[allow(clippy::cast_possible_truncation)]
107            let target = (basis + u64::from(b)) as u32;
108            edges.push((i, target));
109        }
110    }
111
112    let mut g = Graph::new(vcount, true)?;
113    g.add_edges(edges)?;
114    Ok(g)
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use std::collections::BTreeSet;
121
122    fn dump_arcs(g: &Graph) -> Vec<(VertexId, VertexId)> {
123        let ec = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
124        let mut out = Vec::with_capacity(g.ecount());
125        for e in 0..ec {
126            // Directed graph: preserve source → target orientation.
127            let u = g.edge_source(e).expect("edge in range");
128            let v = g.edge_target(e).expect("edge in range");
129            out.push((u, v));
130        }
131        out
132    }
133
134    fn vcount_us(g: &Graph) -> usize {
135        usize::try_from(g.vcount()).expect("vcount fits usize")
136    }
137
138    #[test]
139    fn n_zero_yields_singleton_directed() {
140        let g = de_bruijn(5, 0).expect("n=0 valid");
141        assert_eq!(g.vcount(), 1);
142        assert_eq!(g.ecount(), 0);
143        assert!(g.is_directed());
144    }
145
146    #[test]
147    fn m_zero_yields_null_directed() {
148        let g = de_bruijn(0, 4).expect("m=0 valid");
149        assert_eq!(g.vcount(), 0);
150        assert_eq!(g.ecount(), 0);
151        assert!(g.is_directed());
152    }
153
154    #[test]
155    fn m_one_n_one_is_single_self_loop() {
156        let g = de_bruijn(1, 1).expect("B(1,1) valid");
157        assert_eq!(g.vcount(), 1);
158        assert_eq!(g.ecount(), 1);
159        assert_eq!(dump_arcs(&g), vec![(0, 0)]);
160    }
161
162    #[test]
163    fn b_2_1_is_directed_k2_with_loops() {
164        // 2 vertices, 4 arcs: 0→0, 0→1, 1→0, 1→1.
165        let g = de_bruijn(2, 1).expect("B(2,1) valid");
166        assert_eq!(g.vcount(), 2);
167        assert_eq!(g.ecount(), 4);
168        let arcs: BTreeSet<_> = dump_arcs(&g).into_iter().collect();
169        let expected: BTreeSet<_> = [(0, 0), (0, 1), (1, 0), (1, 1)].into_iter().collect();
170        assert_eq!(arcs, expected);
171    }
172
173    #[test]
174    fn b_2_2_exact_arc_list() {
175        // vcount = 4, basis = (i * 2) mod 4.
176        // i=0 basis=0 → (0,0) (0,1)
177        // i=1 basis=2 → (1,2) (1,3)
178        // i=2 basis=0 → (2,0) (2,1)
179        // i=3 basis=2 → (3,2) (3,3)
180        let g = de_bruijn(2, 2).expect("B(2,2) valid");
181        assert_eq!(g.vcount(), 4);
182        assert_eq!(g.ecount(), 8);
183        let expected = vec![
184            (0, 0),
185            (0, 1),
186            (1, 2),
187            (1, 3),
188            (2, 0),
189            (2, 1),
190            (3, 2),
191            (3, 3),
192        ];
193        assert_eq!(dump_arcs(&g), expected);
194    }
195
196    #[test]
197    fn b_3_2_exact_arc_count_and_degrees() {
198        // vcount = 9, ecount = 27.
199        let g = de_bruijn(3, 2).expect("B(3,2) valid");
200        assert_eq!(g.vcount(), 9);
201        assert_eq!(g.ecount(), 27);
202        // Each vertex has out-degree 3 and in-degree 3.
203        // Count via raw arc list.
204        let arcs = dump_arcs(&g);
205        let mut out_deg = [0u32; 9];
206        let mut in_deg = [0u32; 9];
207        for (u, v) in arcs {
208            out_deg[u as usize] += 1;
209            in_deg[v as usize] += 1;
210        }
211        for (v, (&o, &i)) in out_deg.iter().zip(in_deg.iter()).enumerate() {
212            assert_eq!(o, 3, "out-degree at {v}");
213            assert_eq!(i, 3, "in-degree at {v}");
214        }
215    }
216
217    #[test]
218    fn b_2_3_total_counts() {
219        let g = de_bruijn(2, 3).expect("B(2,3) valid");
220        assert_eq!(g.vcount(), 8);
221        assert_eq!(g.ecount(), 16);
222    }
223
224    #[test]
225    fn b_4_3_total_counts() {
226        let g = de_bruijn(4, 3).expect("B(4,3) valid");
227        assert_eq!(g.vcount(), 64);
228        assert_eq!(g.ecount(), 256);
229    }
230
231    #[test]
232    fn rewrite_rule_holds() {
233        // Verify on B(3, 2): for every arc (i, j), j must lie in
234        // [(i * m) mod vcount, (i * m) mod vcount + m).
235        let m: u32 = 3;
236        let n: u32 = 2;
237        let g = de_bruijn(m, n).expect("valid");
238        let vcount = g.vcount();
239        for (u, v) in dump_arcs(&g) {
240            let basis = (u * m) % vcount;
241            assert!(
242                v >= basis && v < basis + m,
243                "arc ({u} → {v}) violates rewrite (basis = {basis}, m = {m})"
244            );
245        }
246    }
247
248    #[test]
249    fn vcount_overflow_rejected() {
250        // m = 2, n = 32 → 2^32 = 4_294_967_296 > u32::MAX → reject.
251        let err = de_bruijn(2, 32).expect_err("vcount overflow");
252        assert!(matches!(err, IgraphError::InvalidArgument(_)));
253    }
254
255    #[test]
256    fn out_degree_uniform_for_all_supported_inputs() {
257        for m in 1..=4u32 {
258            for n in 1..=3u32 {
259                let g = de_bruijn(m, n).expect("valid B(m, n)");
260                let vc = vcount_us(&g);
261                let expected_ecount = (m as usize).pow(n + 1);
262                assert_eq!(
263                    g.ecount(),
264                    expected_ecount,
265                    "ecount mismatch for B({m}, {n})"
266                );
267                let arcs = dump_arcs(&g);
268                let mut out_deg = vec![0u32; vc];
269                for (u, _) in arcs {
270                    out_deg[u as usize] += 1;
271                }
272                for (v, &d) in out_deg.iter().enumerate() {
273                    assert_eq!(d, m, "out-degree mismatch at {v} in B({m}, {n})");
274                }
275            }
276        }
277    }
278
279    #[test]
280    fn in_degree_uniform_for_all_supported_inputs() {
281        for m in 1..=4u32 {
282            for n in 1..=3u32 {
283                let g = de_bruijn(m, n).expect("valid B(m, n)");
284                let vc = vcount_us(&g);
285                let arcs = dump_arcs(&g);
286                let mut in_deg = vec![0u32; vc];
287                for (_, v) in arcs {
288                    in_deg[v as usize] += 1;
289                }
290                for (v, &d) in in_deg.iter().enumerate() {
291                    assert_eq!(d, m, "in-degree mismatch at {v} in B({m}, {n})");
292                }
293            }
294        }
295    }
296}
297
298#[cfg(all(test, feature = "proptest-harness"))]
299mod proptest_invariants {
300    use super::*;
301    use proptest::prelude::*;
302
303    proptest! {
304        #![proptest_config(ProptestConfig::with_cases(64))]
305
306        /// `vcount == m^n` and `ecount == m^(n+1)` across all supported
307        /// `(m, n)` pairs that fit in `u32`.
308        #[test]
309        fn vcount_and_ecount_are_exact(m in 1u32..=5, n in 1u32..=4) {
310            // Skip configurations that would blow past u32 vcount.
311            let Some(vcount) = m.checked_pow(n) else { return Ok(()); };
312            let Some(ecount) = vcount.checked_mul(m) else { return Ok(()); };
313            let g = de_bruijn(m, n).expect("valid input");
314            prop_assert_eq!(g.vcount(), vcount);
315            prop_assert_eq!(g.ecount() as u64, u64::from(ecount));
316        }
317
318        /// Every vertex has out-degree `m` and in-degree `m`.
319        #[test]
320        fn vertex_in_and_out_degree_equals_m(m in 1u32..=4, n in 1u32..=3) {
321            let g = de_bruijn(m, n).expect("valid input");
322            let vcount = g.vcount();
323            let vc = usize::try_from(vcount).unwrap();
324            let mut out_deg = vec![0u32; vc];
325            let mut in_deg = vec![0u32; vc];
326            let ec = u32::try_from(g.ecount()).unwrap();
327            for e in 0..ec {
328                let u = g.edge_source(e).expect("edge in range");
329                let v = g.edge_target(e).expect("edge in range");
330                out_deg[u as usize] += 1;
331                in_deg[v as usize] += 1;
332            }
333            for v in 0..vc {
334                prop_assert_eq!(out_deg[v], m);
335                prop_assert_eq!(in_deg[v], m);
336            }
337        }
338
339        /// Every arc `(u, v)` satisfies the rewrite `v ∈ [basis, basis + m)`
340        /// where `basis = (u * m) mod vcount`.
341        #[test]
342        fn rewrite_rule_holds_for_every_arc(m in 1u32..=4, n in 1u32..=3) {
343            let g = de_bruijn(m, n).expect("valid input");
344            let vcount = g.vcount();
345            let ec = u32::try_from(g.ecount()).unwrap();
346            for e in 0..ec {
347                let u = g.edge_source(e).expect("edge in range");
348                let v = g.edge_target(e).expect("edge in range");
349                let basis = (u * m) % vcount;
350                prop_assert!(v >= basis && v < basis + m);
351            }
352        }
353    }
354}