Skip to main content

rust_igraph/algorithms/operators/
disjoint_union.rs

1//! Disjoint union (ALGO-OP-002).
2//!
3//! Counterpart of `igraph_disjoint_union()` from
4//! `references/igraph/src/operators/disjoint_union.c`. Vertices of
5//! `right` are relabelled to follow `left`'s vertices: vertex `v` in
6//! `right` becomes `v + left.vcount()` in the result. Vertex and edge
7//! ordering is preserved (left's edges first, then right's edges in
8//! original order with shifted endpoints).
9//!
10//! Phase-1 minimal slice: two-graph variant only. Multi-argument
11//! `disjoint_union_many` ships later (ALGO-OP-002b). Edge / vertex
12//! attributes are dropped (Phase-1 minimal — see ALGO-AT-* milestone).
13
14use crate::core::graph::EdgeId;
15use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
16
17/// Returns the disjoint union of `left` and `right`.
18///
19/// The result has `left.vcount() + right.vcount()` vertices and
20/// `left.ecount() + right.ecount()` edges. Vertices from `right` are
21/// shifted by `left.vcount()`.
22///
23/// # Errors
24/// - [`IgraphError::InvalidArgument`] if the two graphs differ in
25///   directedness — disjoint union is only defined for graphs of the
26///   same directedness.
27///
28/// # Examples
29///
30/// ```
31/// use rust_igraph::{Graph, disjoint_union};
32///
33/// // Two triangles → 6-vertex graph with two disjoint triangles.
34/// let mut a = Graph::with_vertices(3);
35/// a.add_edge(0, 1).unwrap();
36/// a.add_edge(1, 2).unwrap();
37/// a.add_edge(2, 0).unwrap();
38/// let b = a.clone();
39///
40/// let u = disjoint_union(&a, &b).unwrap();
41/// assert_eq!(u.vcount(), 6);
42/// assert_eq!(u.ecount(), 6);
43/// // The right triangle's edges are shifted by 3.
44/// assert_eq!(u.edge(3).unwrap(), (3, 4));
45/// ```
46pub fn disjoint_union(left: &Graph, right: &Graph) -> IgraphResult<Graph> {
47    disjoint_union_many(&[left, right])
48}
49
50/// Disjoint union of any number of graphs (ALGO-OP-002b).
51///
52/// Counterpart of `igraph_disjoint_union_many()`. Vertices of the
53/// `i`-th graph are relabelled to start at `Σ_{j<i} graphs[j].vcount()`,
54/// preserving each input's internal vertex / edge ordering.
55///
56/// All inputs must have the same directedness. The empty slice yields
57/// the null graph (`vcount = 0`, undirected). A single-graph input
58/// returns a clone of that graph.
59///
60/// # Errors
61/// - [`IgraphError::InvalidArgument`] if directedness diverges across
62///   inputs.
63///
64/// # Examples
65///
66/// ```
67/// use rust_igraph::{Graph, disjoint_union_many};
68///
69/// // Three triangles → 9-vertex graph with three disjoint triangles.
70/// let mut t = Graph::with_vertices(3);
71/// t.add_edge(0, 1).unwrap();
72/// t.add_edge(1, 2).unwrap();
73/// t.add_edge(2, 0).unwrap();
74/// let u = disjoint_union_many(&[&t, &t, &t]).unwrap();
75/// assert_eq!(u.vcount(), 9);
76/// assert_eq!(u.ecount(), 9);
77/// // Third triangle's last edge is `(2, 0)` shifted by 6. The
78/// // undirected adjacency canonicalises to (min, max).
79/// assert_eq!(u.edge(8).unwrap(), (6, 8));
80/// ```
81pub fn disjoint_union_many(graphs: &[&Graph]) -> IgraphResult<Graph> {
82    if graphs.is_empty() {
83        return Graph::new(0, false);
84    }
85    let directed = graphs[0].is_directed();
86    for g in &graphs[1..] {
87        if g.is_directed() != directed {
88            return Err(IgraphError::InvalidArgument(
89                "disjoint_union_many: cannot mix directed and undirected graphs".to_string(),
90            ));
91        }
92    }
93
94    // Total vertex / edge count with overflow guards. Cumulative
95    // shifts let us relabel each graph's edges in one pass.
96    let mut shifts: Vec<u32> = Vec::with_capacity(graphs.len());
97    let mut total_v: u32 = 0;
98    let mut total_e: usize = 0;
99    for g in graphs {
100        shifts.push(total_v);
101        total_v = total_v
102            .checked_add(g.vcount())
103            .ok_or(IgraphError::Internal("vertex count overflow"))?;
104        total_e = total_e
105            .checked_add(g.ecount())
106            .ok_or(IgraphError::Internal("edge count overflow"))?;
107    }
108
109    let mut out = Graph::new(total_v, directed)?;
110    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_e);
111    for (i, g) in graphs.iter().enumerate() {
112        let shift = shifts[i];
113        let m = u32::try_from(g.ecount())
114            .map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
115        for e in 0..m {
116            let (u, v) = g.edge(e as EdgeId)?;
117            edges.push((u + shift, v + shift));
118        }
119    }
120    out.add_edges(edges)?;
121    Ok(out)
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    fn sorted_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
129        let m = u32::try_from(g.ecount()).unwrap();
130        let mut v: Vec<_> = (0..m).map(|e| g.edge(e).unwrap()).collect();
131        v.sort_unstable();
132        v
133    }
134
135    #[test]
136    fn two_triangles_undirected() {
137        let mut a = Graph::with_vertices(3);
138        a.add_edge(0, 1).unwrap();
139        a.add_edge(1, 2).unwrap();
140        a.add_edge(2, 0).unwrap();
141        let b = a.clone();
142        let u = disjoint_union(&a, &b).unwrap();
143        assert_eq!(u.vcount(), 6);
144        assert_eq!(u.ecount(), 6);
145        assert_eq!(
146            sorted_edges(&u),
147            vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5)]
148        );
149    }
150
151    #[test]
152    fn empty_left() {
153        let a = Graph::with_vertices(0);
154        let mut b = Graph::with_vertices(2);
155        b.add_edge(0, 1).unwrap();
156        let u = disjoint_union(&a, &b).unwrap();
157        assert_eq!(u.vcount(), 2);
158        assert_eq!(u.ecount(), 1);
159        assert_eq!(u.edge(0).unwrap(), (0, 1));
160    }
161
162    #[test]
163    fn empty_right() {
164        let mut a = Graph::with_vertices(2);
165        a.add_edge(0, 1).unwrap();
166        let b = Graph::with_vertices(0);
167        let u = disjoint_union(&a, &b).unwrap();
168        assert_eq!(u.vcount(), 2);
169        assert_eq!(u.ecount(), 1);
170    }
171
172    #[test]
173    fn both_empty() {
174        let a = Graph::with_vertices(0);
175        let b = Graph::with_vertices(0);
176        let u = disjoint_union(&a, &b).unwrap();
177        assert_eq!(u.vcount(), 0);
178        assert_eq!(u.ecount(), 0);
179    }
180
181    #[test]
182    fn isolated_plus_edge() {
183        // Right graph has only isolated vertices; output preserves them.
184        let mut a = Graph::with_vertices(2);
185        a.add_edge(0, 1).unwrap();
186        let b = Graph::with_vertices(3);
187        let u = disjoint_union(&a, &b).unwrap();
188        assert_eq!(u.vcount(), 5);
189        assert_eq!(u.ecount(), 1);
190    }
191
192    #[test]
193    fn directed_directed_succeeds() {
194        let mut a = Graph::new(2, true).unwrap();
195        a.add_edge(0, 1).unwrap();
196        let mut b = Graph::new(2, true).unwrap();
197        b.add_edge(1, 0).unwrap();
198        let u = disjoint_union(&a, &b).unwrap();
199        assert!(u.is_directed());
200        assert_eq!(u.vcount(), 4);
201        assert_eq!(u.ecount(), 2);
202        assert_eq!(u.edge(0).unwrap(), (0, 1));
203        assert_eq!(u.edge(1).unwrap(), (3, 2));
204    }
205
206    #[test]
207    fn mixed_directedness_errors() {
208        let a = Graph::with_vertices(2);
209        let b = Graph::new(2, true).unwrap();
210        assert!(disjoint_union(&a, &b).is_err());
211    }
212
213    #[test]
214    fn vertex_count_preserved_for_isolated_left() {
215        let a = Graph::with_vertices(5);
216        let mut b = Graph::with_vertices(2);
217        b.add_edge(0, 1).unwrap();
218        let u = disjoint_union(&a, &b).unwrap();
219        assert_eq!(u.vcount(), 7);
220        assert_eq!(u.edge(0).unwrap(), (5, 6));
221    }
222
223    #[test]
224    fn idempotent_with_self_when_relabelled() {
225        // disjoint_union(a, a) doubles vertex count and edge count.
226        let mut a = Graph::with_vertices(4);
227        a.add_edge(0, 1).unwrap();
228        a.add_edge(1, 2).unwrap();
229        a.add_edge(2, 3).unwrap();
230        let u = disjoint_union(&a, &a).unwrap();
231        assert_eq!(u.vcount(), 8);
232        assert_eq!(u.ecount(), 6);
233    }
234
235    // ----- ALGO-OP-002b: disjoint_union_many -----
236
237    #[test]
238    fn many_empty_slice_yields_null_graph() {
239        let u = disjoint_union_many(&[]).unwrap();
240        assert_eq!(u.vcount(), 0);
241        assert_eq!(u.ecount(), 0);
242        assert!(!u.is_directed());
243    }
244
245    #[test]
246    fn many_single_graph_yields_clone() {
247        let mut a = Graph::with_vertices(3);
248        a.add_edge(0, 1).unwrap();
249        a.add_edge(1, 2).unwrap();
250        let u = disjoint_union_many(&[&a]).unwrap();
251        assert_eq!(u.vcount(), a.vcount());
252        assert_eq!(u.ecount(), a.ecount());
253        assert_eq!(sorted_edges(&u), sorted_edges(&a));
254    }
255
256    #[test]
257    fn many_three_triangles_shifts_correctly() {
258        let mut t = Graph::with_vertices(3);
259        t.add_edge(0, 1).unwrap();
260        t.add_edge(1, 2).unwrap();
261        t.add_edge(2, 0).unwrap();
262        let u = disjoint_union_many(&[&t, &t, &t]).unwrap();
263        assert_eq!(u.vcount(), 9);
264        assert_eq!(u.ecount(), 9);
265        // Each triangle contributes its three edges in order.
266        assert_eq!(u.edge(0).unwrap(), (0, 1));
267        assert_eq!(u.edge(3).unwrap(), (3, 4));
268        assert_eq!(u.edge(6).unwrap(), (6, 7));
269    }
270
271    #[test]
272    fn many_matches_pairwise_left_to_right() {
273        // disjoint_union_many(&[a, b, c]) ≡ disjoint_union(disjoint_union(a, b), c)
274        let mut a = Graph::with_vertices(2);
275        a.add_edge(0, 1).unwrap();
276        let mut b = Graph::with_vertices(3);
277        b.add_edge(0, 1).unwrap();
278        b.add_edge(1, 2).unwrap();
279        let mut c = Graph::with_vertices(2);
280        c.add_edge(0, 1).unwrap();
281        let u = disjoint_union_many(&[&a, &b, &c]).unwrap();
282        let pairwise = disjoint_union(&disjoint_union(&a, &b).unwrap(), &c).unwrap();
283        assert_eq!(u.vcount(), pairwise.vcount());
284        assert_eq!(u.ecount(), pairwise.ecount());
285        assert_eq!(sorted_edges(&u), sorted_edges(&pairwise));
286    }
287
288    #[test]
289    fn many_mixed_directedness_errors() {
290        let a = Graph::with_vertices(2);
291        let b = Graph::with_vertices(2);
292        let c = Graph::new(2, true).unwrap();
293        assert!(disjoint_union_many(&[&a, &b, &c]).is_err());
294    }
295
296    #[test]
297    fn many_directed_preserves_orientation() {
298        let mut a = Graph::new(2, true).unwrap();
299        a.add_edge(0, 1).unwrap();
300        let mut b = Graph::new(2, true).unwrap();
301        b.add_edge(1, 0).unwrap();
302        let u = disjoint_union_many(&[&a, &b]).unwrap();
303        assert!(u.is_directed());
304        assert_eq!(u.vcount(), 4);
305        assert_eq!(u.ecount(), 2);
306        assert_eq!(u.edge(0).unwrap(), (0, 1));
307        assert_eq!(u.edge(1).unwrap(), (3, 2));
308    }
309}