Skip to main content

rust_igraph/algorithms/constructors/
full.rs

1//! Full graph (complete graph) constructor (ALGO-CN-014).
2//!
3//! Counterpart of `igraph_full()` in
4//! `references/igraph/src/constructors/full.c:54-124`.
5//!
6//! In a *full* graph every possible edge is present: every vertex is
7//! connected to every other vertex. The two boolean flags shape the
8//! ecount:
9//!
10//! | directed | loops | ecount         | emission pattern                       |
11//! |----------|-------|----------------|----------------------------------------|
12//! | false    | false | `n·(n−1)/2`    | `(i, j)` for every `0 ≤ i < j < n`     |
13//! | false    | true  | `n·(n+1)/2`    | `(i, j)` for every `0 ≤ i ≤ j < n`     |
14//! | true     | false | `n·(n−1)`      | `(i, j)` for every `i ≠ j`             |
15//! | true     | true  | `n²`           | `(i, j)` for every `0 ≤ i, j < n`      |
16//!
17//! Emission order matches upstream byte-for-byte (source-major,
18//! target-ascending; for the `directed && !loops` case the inner loop
19//! walks `j ∈ [0, i)` first then `j ∈ (i, n)`).
20//!
21//! Time complexity: `O(|V|^2) = O(|E|)`.
22//!
23//! Degenerate inputs:
24//!
25//! * `n == 0` → empty graph (vcount = 0, ecount = 0).
26//! * `n == 1 ∧ loops == false` → singleton, no edges.
27//! * `n == 1 ∧ loops == true` → singleton with a single self-loop
28//!   `(0, 0)`.
29
30use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
31
32/// Build the full (complete) graph on `n` vertices.
33///
34/// The semantics follow `igraph_full(graph, n, directed, loops)`:
35///
36/// * `directed = false, loops = false` → `K_n` with `n·(n−1)/2` edges.
37/// * `directed = false, loops = true`  → `K_n` plus one self-loop per
38///   vertex (`n·(n+1)/2` edges total).
39/// * `directed = true,  loops = false` → directed `K_n` with both
40///   `(i, j)` and `(j, i)` arcs for every `i ≠ j` (`n·(n−1)` arcs).
41/// * `directed = true,  loops = true`  → above plus a self-loop on every
42///   vertex (`n²` arcs total).
43///
44/// # Errors
45///
46/// * [`IgraphError::InvalidArgument`] — the upper bound on the edge
47///   count (`n²` in the worst case) overflows `usize`, so the edge
48///   buffer cannot be sized safely. Computed via [`usize::checked_mul`]
49///   so the failure mode is deterministic.
50///
51/// # Examples
52///
53/// ```
54/// use rust_igraph::full_graph;
55///
56/// // K_4 — undirected complete graph on 4 vertices, 6 edges.
57/// let k4 = full_graph(4, false, false).unwrap();
58/// assert_eq!(k4.vcount(), 4);
59/// assert_eq!(k4.ecount(), 6);
60/// assert!(!k4.is_directed());
61///
62/// // Directed K_3 with loops — 9 arcs (3 × 3).
63/// let dk3 = full_graph(3, true, true).unwrap();
64/// assert_eq!(dk3.vcount(), 3);
65/// assert_eq!(dk3.ecount(), 9);
66/// assert!(dk3.is_directed());
67/// ```
68pub fn full_graph(n: u32, directed: bool, loops: bool) -> IgraphResult<Graph> {
69    if n == 0 {
70        return Graph::new(0, directed);
71    }
72
73    let n_us = usize::try_from(n)
74        .map_err(|_| IgraphError::InvalidArgument(format!("full_graph: n {n} cannot fit usize")))?;
75    let ecount = expected_ecount(n_us, directed, loops)?;
76
77    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
78
79    match (directed, loops) {
80        (true, true) => {
81            // n² arcs, source-major, j walks the full row.
82            for i in 0..n {
83                for j in 0..n {
84                    edges.push((i, j));
85                }
86            }
87        }
88        (true, false) => {
89            // n·(n−1) arcs, source-major; j walks [0, i) then (i, n).
90            for i in 0..n {
91                for j in 0..i {
92                    edges.push((i, j));
93                }
94                for j in (i + 1)..n {
95                    edges.push((i, j));
96                }
97            }
98        }
99        (false, true) => {
100            // n·(n+1)/2 edges, canonical (i ≤ j).
101            for i in 0..n {
102                for j in i..n {
103                    edges.push((i, j));
104                }
105            }
106        }
107        (false, false) => {
108            // n·(n−1)/2 edges, canonical (i < j).
109            for i in 0..n {
110                for j in (i + 1)..n {
111                    edges.push((i, j));
112                }
113            }
114        }
115    }
116
117    debug_assert_eq!(edges.len(), ecount);
118
119    let mut g = Graph::new(n, directed)?;
120    g.add_edges(edges)?;
121    Ok(g)
122}
123
124/// Worst-case (= exact) edge count for the requested `(directed, loops)`
125/// configuration. Returns `InvalidArgument` if the product would
126/// overflow `usize`.
127fn expected_ecount(n: usize, directed: bool, loops: bool) -> IgraphResult<usize> {
128    match (directed, loops) {
129        (true, true) => n.checked_mul(n).ok_or_else(|| {
130            IgraphError::InvalidArgument(format!("full_graph: n² overflows usize (n = {n})"))
131        }),
132        (true, false) => {
133            // n·(n−1). When n == 0 we already returned above.
134            n.checked_mul(n - 1).ok_or_else(|| {
135                IgraphError::InvalidArgument(format!(
136                    "full_graph: n·(n−1) overflows usize (n = {n})"
137                ))
138            })
139        }
140        (false, true) => {
141            // n·(n+1)/2. Compute n·(n+1) safely, then halve.
142            let prod = n
143                .checked_mul(n.checked_add(1).ok_or_else(|| {
144                    IgraphError::InvalidArgument(format!(
145                        "full_graph: n+1 overflows usize (n = {n})"
146                    ))
147                })?)
148                .ok_or_else(|| {
149                    IgraphError::InvalidArgument(format!(
150                        "full_graph: n·(n+1) overflows usize (n = {n})"
151                    ))
152                })?;
153            Ok(prod / 2)
154        }
155        (false, false) => {
156            // n·(n−1)/2.
157            let prod = n.checked_mul(n - 1).ok_or_else(|| {
158                IgraphError::InvalidArgument(format!(
159                    "full_graph: n·(n−1) overflows usize (n = {n})"
160                ))
161            })?;
162            Ok(prod / 2)
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use std::collections::BTreeSet;
171
172    fn dump_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
173        let ec = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
174        let mut out = Vec::with_capacity(g.ecount());
175        for e in 0..ec {
176            let u = g.edge_source(e).expect("edge in range");
177            let v = g.edge_target(e).expect("edge in range");
178            out.push((u, v));
179        }
180        out
181    }
182
183    #[test]
184    fn null_graph_zero_vertices() {
185        let g = full_graph(0, false, false).expect("K_0");
186        assert_eq!(g.vcount(), 0);
187        assert_eq!(g.ecount(), 0);
188        assert!(!g.is_directed());
189    }
190
191    #[test]
192    fn singleton_no_loops() {
193        let g = full_graph(1, false, false).expect("K_1, no loops");
194        assert_eq!(g.vcount(), 1);
195        assert_eq!(g.ecount(), 0);
196    }
197
198    #[test]
199    fn singleton_with_loops() {
200        let g = full_graph(1, false, true).expect("K_1, with loops");
201        assert_eq!(g.vcount(), 1);
202        assert_eq!(g.ecount(), 1);
203        assert_eq!(dump_edges(&g), vec![(0, 0)]);
204    }
205
206    #[test]
207    fn undirected_no_loops_k10_emission_order() {
208        // Cross-reference with references/igraph/tests/unit/full.out
209        // "Undirected, no loops".
210        let g = full_graph(10, false, false).expect("K_10");
211        assert_eq!(g.vcount(), 10);
212        assert_eq!(g.ecount(), 45);
213        let edges = dump_edges(&g);
214        // First five edges in upstream emission order.
215        assert_eq!(edges[0], (0, 1));
216        assert_eq!(edges[1], (0, 2));
217        assert_eq!(edges[8], (0, 9));
218        assert_eq!(edges[9], (1, 2));
219        // Last edge: (8, 9).
220        assert_eq!(*edges.last().unwrap(), (8, 9));
221    }
222
223    #[test]
224    fn directed_no_loops_k10_emission_order() {
225        let g = full_graph(10, true, false).expect("directed K_10");
226        assert_eq!(g.vcount(), 10);
227        assert_eq!(g.ecount(), 90);
228        let arcs = dump_edges(&g);
229        // For source i, arcs are (i, 0..i) followed by (i, i+1..n).
230        assert_eq!(arcs[0], (0, 1));
231        assert_eq!(arcs[8], (0, 9));
232        // Source 1's row: (1, 0), (1, 2), …, (1, 9).
233        assert_eq!(arcs[9], (1, 0));
234        assert_eq!(arcs[10], (1, 2));
235        // Last arc: (9, 8).
236        assert_eq!(*arcs.last().unwrap(), (9, 8));
237    }
238
239    #[test]
240    fn undirected_with_loops_k10_counts() {
241        let g = full_graph(10, false, true).expect("undirected K_10 + loops");
242        assert_eq!(g.vcount(), 10);
243        // n(n+1)/2 = 55.
244        assert_eq!(g.ecount(), 55);
245        let edges: BTreeSet<_> = dump_edges(&g).into_iter().collect();
246        let mut expected: BTreeSet<(VertexId, VertexId)> = BTreeSet::new();
247        for i in 0u32..10 {
248            for j in i..10 {
249                expected.insert((i, j));
250            }
251        }
252        assert_eq!(edges, expected);
253    }
254
255    #[test]
256    fn directed_with_loops_k10_counts() {
257        let g = full_graph(10, true, true).expect("directed K_10 + loops");
258        assert_eq!(g.vcount(), 10);
259        assert_eq!(g.ecount(), 100);
260        let arcs: BTreeSet<_> = dump_edges(&g).into_iter().collect();
261        let mut expected: BTreeSet<(VertexId, VertexId)> = BTreeSet::new();
262        for i in 0u32..10 {
263            for j in 0u32..10 {
264                expected.insert((i, j));
265            }
266        }
267        assert_eq!(arcs, expected);
268    }
269
270    #[test]
271    fn directed_no_loops_matches_directed_complete_kautz_n_zero() {
272        // K_{m+1} from CN-013 (kautz with n=0) is exactly
273        // full_graph(m+1, directed=true, loops=false). Verify the same
274        // arc multiset over the m+1 = 4 case.
275        let g_full = full_graph(4, true, false).expect("directed K_4");
276        let arcs: BTreeSet<_> = dump_edges(&g_full).into_iter().collect();
277        let mut expected = BTreeSet::new();
278        for i in 0u32..4 {
279            for j in 0u32..4 {
280                if i != j {
281                    expected.insert((i, j));
282                }
283            }
284        }
285        assert_eq!(arcs, expected);
286    }
287
288    #[test]
289    fn ecount_overflow_rejected() {
290        // n² ≥ usize::MAX is impossible on 64-bit, but on 32-bit hosts
291        // `n = 65_537` would overflow. Pick a value that overflows both:
292        // n² overflows u32 at n ≥ 65_536, but we test the u32 → usize
293        // path is solid by feeding a parameter that fits u32 yet would
294        // produce an absurdly large edge buffer. For portability we
295        // limit the test to the `(true, true)` branch by exercising the
296        // helper directly.
297        let r = expected_ecount(usize::MAX / 2, true, true);
298        assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
299    }
300}
301
302#[cfg(all(test, feature = "proptest-harness"))]
303mod proptest_invariants {
304    use super::*;
305    use proptest::prelude::*;
306    use std::collections::BTreeSet;
307
308    proptest! {
309        #![proptest_config(ProptestConfig::with_cases(64))]
310
311        /// Edge count matches the closed-form for every `(directed, loops)`
312        /// combination.
313        #[test]
314        fn ecount_matches_closed_form(n in 0u32..=12u32, directed in proptest::bool::ANY, loops in proptest::bool::ANY) {
315            let g = full_graph(n, directed, loops).expect("valid input");
316            let n_us = n as usize;
317            let expected = match (directed, loops) {
318                (true, true) => n_us * n_us,
319                (true, false) => n_us * n_us.saturating_sub(1),
320                (false, true) => n_us * (n_us + 1) / 2,
321                (false, false) => n_us * n_us.saturating_sub(1) / 2,
322            };
323            prop_assert_eq!(g.vcount(), n);
324            prop_assert_eq!(g.ecount(), expected);
325            prop_assert_eq!(g.is_directed(), directed);
326        }
327
328        /// Without loops, no `(v, v)` edge ever appears. With loops, every
329        /// vertex has exactly one self-loop.
330        #[test]
331        fn loop_flag_controls_self_loops(n in 1u32..=10u32, directed in proptest::bool::ANY) {
332            for &loops in &[false, true] {
333                let g = full_graph(n, directed, loops).expect("valid input");
334                let ec = u32::try_from(g.ecount()).unwrap();
335                let mut loop_count = 0u32;
336                for e in 0..ec {
337                    let u = g.edge_source(e).expect("edge in range");
338                    let v = g.edge_target(e).expect("edge in range");
339                    if u == v {
340                        loop_count += 1;
341                    }
342                }
343                if loops {
344                    prop_assert_eq!(loop_count, n, "with loops every vertex has exactly one self-loop");
345                } else {
346                    prop_assert_eq!(loop_count, 0u32, "loops=false must yield no self-loops");
347                }
348            }
349        }
350
351        /// The undirected non-looped variant is exactly the unordered pair set
352        /// `{ (i, j) : 0 ≤ i < j < n }` — emission order is checked separately
353        /// in the deterministic tests above, here we only check the multiset.
354        #[test]
355        fn undirected_no_loops_yields_pair_set(n in 0u32..=8u32) {
356            let g = full_graph(n, false, false).expect("valid input");
357            let ec = u32::try_from(g.ecount()).unwrap();
358            let mut got: BTreeSet<(VertexId, VertexId)> = BTreeSet::new();
359            for e in 0..ec {
360                let u = g.edge_source(e).expect("edge in range");
361                let v = g.edge_target(e).expect("edge in range");
362                let canon = if u <= v { (u, v) } else { (v, u) };
363                prop_assert!(got.insert(canon), "duplicate undirected edge ({u}, {v})");
364            }
365            let mut expected = BTreeSet::new();
366            for i in 0u32..n {
367                for j in (i + 1)..n {
368                    expected.insert((i, j));
369                }
370            }
371            prop_assert_eq!(got, expected);
372        }
373    }
374}