Skip to main content

rust_igraph/algorithms/operators/
union.rs

1//! Union of two graphs (ALGO-OP-004).
2//!
3//! Counterpart of `igraph_union()` from
4//! `references/igraph/src/operators/union.c:69-75`. Vertex sets are
5//! aligned by index — the result has
6//! `max(left.vcount(), right.vcount())` vertices. Edges are unioned by
7//! *maximum multiplicity*: for each (canonicalised) endpoint pair
8//! `(u, v)`, the result contains `max(count_left, count_right)` edges.
9//! For undirected inputs the canonicalised pair is `(min(u,v),
10//! max(u,v))`; for directed inputs the pair is taken as-is, so
11//! `(u, v)` and `(v, u)` are tallied separately.
12//!
13//! Phase-1 minimal slice: two-graph variant only. Multi-arg
14//! `union_many` and the edge-mapping outputs (`edge_map1` /
15//! `edge_map2`) ship later.
16
17use std::collections::BTreeMap;
18
19use crate::core::graph::EdgeId;
20use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
21
22/// Returns the union of `left` and `right`.
23///
24/// Vertex sets are aligned by index — the result has
25/// `max(left.vcount(), right.vcount())` vertices. For each endpoint
26/// pair `(u, v)`, the multiplicity in the result equals the larger of
27/// the multiplicities in the two inputs.
28///
29/// Both inputs must agree on directedness; an undirected edge
30/// `(u, v)` is canonicalised to `(min(u, v), max(u, v))` before
31/// counting, while directed edges are tallied as-is.
32///
33/// Output edges are emitted in lexicographic `(src, tgt)` order; two
34/// edges sharing the same canonicalised pair appear consecutively.
35///
36/// # Errors
37/// - [`IgraphError::InvalidArgument`] if directedness diverges.
38///
39/// # Examples
40///
41/// ```
42/// use rust_igraph::{Graph, union};
43///
44/// // Triangle ∪ path: edges {(0,1), (1,2), (2,0)} ∪ {(0,1), (1,3)}
45/// // → {(0,1), (1,2), (2,0), (1,3)} on 4 vertices.
46/// let mut a = Graph::with_vertices(3);
47/// a.add_edge(0, 1).unwrap();
48/// a.add_edge(1, 2).unwrap();
49/// a.add_edge(2, 0).unwrap();
50/// let mut b = Graph::with_vertices(4);
51/// b.add_edge(0, 1).unwrap();
52/// b.add_edge(1, 3).unwrap();
53///
54/// let u = union(&a, &b).unwrap();
55/// assert_eq!(u.vcount(), 4);
56/// assert_eq!(u.ecount(), 4);
57/// ```
58pub fn union(left: &Graph, right: &Graph) -> IgraphResult<Graph> {
59    if left.is_directed() != right.is_directed() {
60        return Err(IgraphError::InvalidArgument(
61            "union: cannot mix directed and undirected graphs".to_string(),
62        ));
63    }
64    let directed = left.is_directed();
65    let n = std::cmp::max(left.vcount(), right.vcount());
66
67    let canon = |u: VertexId, v: VertexId| -> (VertexId, VertexId) {
68        if directed || u <= v { (u, v) } else { (v, u) }
69    };
70
71    let mut count_left: BTreeMap<(VertexId, VertexId), u32> = BTreeMap::new();
72    let mut count_right: BTreeMap<(VertexId, VertexId), u32> = BTreeMap::new();
73
74    let m_l = u32::try_from(left.ecount())
75        .map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
76    for e in 0..m_l {
77        let (u, v) = left.edge(e as EdgeId)?;
78        *count_left.entry(canon(u, v)).or_insert(0) += 1;
79    }
80    let m_r = u32::try_from(right.ecount())
81        .map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
82    for e in 0..m_r {
83        let (u, v) = right.edge(e as EdgeId)?;
84        *count_right.entry(canon(u, v)).or_insert(0) += 1;
85    }
86
87    // Walk the merged key set in lex order and emit the per-pair
88    // multiplicity-max edges. Using BTreeMap keeps output edges
89    // deterministic (sorted by `(src, tgt)`).
90    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
91    let mut iter_l = count_left.iter().peekable();
92    let mut iter_r = count_right.iter().peekable();
93    loop {
94        let next = match (iter_l.peek(), iter_r.peek()) {
95            (None, None) => break,
96            (Some((kl, _)), None) => Some((**kl, true, false)),
97            (None, Some((kr, _))) => Some((**kr, false, true)),
98            (Some((kl, _)), Some((kr, _))) => match kl.cmp(kr) {
99                std::cmp::Ordering::Less => Some((**kl, true, false)),
100                std::cmp::Ordering::Greater => Some((**kr, false, true)),
101                std::cmp::Ordering::Equal => Some((**kl, true, true)),
102            },
103        };
104        let Some((k, take_l, take_r)) = next else {
105            break;
106        };
107        let cl = if take_l {
108            *iter_l.next().expect("peek matched").1
109        } else {
110            0
111        };
112        let cr = if take_r {
113            *iter_r.next().expect("peek matched").1
114        } else {
115            0
116        };
117        let m = std::cmp::max(cl, cr);
118        for _ in 0..m {
119            edges.push(k);
120        }
121    }
122
123    let mut out = Graph::new(n, directed)?;
124    out.add_edges(edges)?;
125    Ok(out)
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    fn sorted_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
133        let m = u32::try_from(g.ecount()).unwrap();
134        let mut v: Vec<_> = (0..m).map(|e| g.edge(e).unwrap()).collect();
135        v.sort_unstable();
136        v
137    }
138
139    #[test]
140    fn empty_union_empty() {
141        let a = Graph::with_vertices(0);
142        let b = Graph::with_vertices(0);
143        let u = union(&a, &b).unwrap();
144        assert_eq!(u.vcount(), 0);
145        assert_eq!(u.ecount(), 0);
146        assert!(!u.is_directed());
147    }
148
149    #[test]
150    fn vcount_is_max_of_inputs() {
151        // a: 3 isolated vertices, b: 6 isolated vertices.
152        let a = Graph::with_vertices(3);
153        let b = Graph::with_vertices(6);
154        let u = union(&a, &b).unwrap();
155        assert_eq!(u.vcount(), 6);
156        assert_eq!(u.ecount(), 0);
157    }
158
159    #[test]
160    fn triangle_union_path_doc_example() {
161        let mut a = Graph::with_vertices(3);
162        a.add_edge(0, 1).unwrap();
163        a.add_edge(1, 2).unwrap();
164        a.add_edge(2, 0).unwrap();
165        let mut b = Graph::with_vertices(4);
166        b.add_edge(0, 1).unwrap();
167        b.add_edge(1, 3).unwrap();
168        let u = union(&a, &b).unwrap();
169        assert_eq!(u.vcount(), 4);
170        assert_eq!(u.ecount(), 4);
171        assert_eq!(sorted_edges(&u), vec![(0, 1), (0, 2), (1, 2), (1, 3)]);
172    }
173
174    #[test]
175    fn max_multiplicity_when_left_has_more() {
176        // left: 3× (0,1); right: 1× (0,1). Result: 3× (0,1).
177        let mut a = Graph::with_vertices(2);
178        a.add_edge(0, 1).unwrap();
179        a.add_edge(0, 1).unwrap();
180        a.add_edge(0, 1).unwrap();
181        let mut b = Graph::with_vertices(2);
182        b.add_edge(0, 1).unwrap();
183        let u = union(&a, &b).unwrap();
184        assert_eq!(u.ecount(), 3);
185    }
186
187    #[test]
188    fn max_multiplicity_when_right_has_more() {
189        // left: 1× (0,1); right: 5× (0,1). Result: 5× (0,1).
190        let mut a = Graph::with_vertices(2);
191        a.add_edge(0, 1).unwrap();
192        let mut b = Graph::with_vertices(2);
193        for _ in 0..5 {
194            b.add_edge(0, 1).unwrap();
195        }
196        let u = union(&a, &b).unwrap();
197        assert_eq!(u.ecount(), 5);
198    }
199
200    #[test]
201    fn disjoint_edge_sets_become_simple_union() {
202        // a: edges (0,1); b: edges (2,3) on 4 vertices.
203        let mut a = Graph::with_vertices(4);
204        a.add_edge(0, 1).unwrap();
205        let mut b = Graph::with_vertices(4);
206        b.add_edge(2, 3).unwrap();
207        let u = union(&a, &b).unwrap();
208        assert_eq!(sorted_edges(&u), vec![(0, 1), (2, 3)]);
209    }
210
211    #[test]
212    fn idempotent_with_self() {
213        // union(a, a) ≡ a (max-multiplicity: max(k, k) = k).
214        let mut a = Graph::with_vertices(4);
215        a.add_edge(0, 1).unwrap();
216        a.add_edge(1, 2).unwrap();
217        a.add_edge(0, 2).unwrap();
218        a.add_edge(0, 2).unwrap(); // multi-edge
219        let u = union(&a, &a).unwrap();
220        assert_eq!(u.vcount(), a.vcount());
221        assert_eq!(u.ecount(), a.ecount());
222        assert_eq!(sorted_edges(&u), sorted_edges(&a));
223    }
224
225    #[test]
226    fn directed_keeps_orientation_separate() {
227        // left: (0→1); right: (1→0). Both edges land in the result.
228        let mut a = Graph::new(2, true).unwrap();
229        a.add_edge(0, 1).unwrap();
230        let mut b = Graph::new(2, true).unwrap();
231        b.add_edge(1, 0).unwrap();
232        let u = union(&a, &b).unwrap();
233        assert!(u.is_directed());
234        assert_eq!(u.ecount(), 2);
235        assert_eq!(sorted_edges(&u), vec![(0, 1), (1, 0)]);
236    }
237
238    #[test]
239    fn directed_max_multiplicity_per_orientation() {
240        // left: 2× (0→1); right: 3× (1→0); both kept independently.
241        let mut a = Graph::new(2, true).unwrap();
242        a.add_edge(0, 1).unwrap();
243        a.add_edge(0, 1).unwrap();
244        let mut b = Graph::new(2, true).unwrap();
245        for _ in 0..3 {
246            b.add_edge(1, 0).unwrap();
247        }
248        let u = union(&a, &b).unwrap();
249        assert_eq!(u.ecount(), 5);
250        let s = sorted_edges(&u);
251        assert_eq!(s.iter().filter(|&&p| p == (0, 1)).count(), 2);
252        assert_eq!(s.iter().filter(|&&p| p == (1, 0)).count(), 3);
253    }
254
255    #[test]
256    fn loops_are_preserved() {
257        // left: 2× (0,0); right: 1× (0,0). Result: 2× (0,0).
258        let mut a = Graph::with_vertices(1);
259        a.add_edge(0, 0).unwrap();
260        a.add_edge(0, 0).unwrap();
261        let mut b = Graph::with_vertices(1);
262        b.add_edge(0, 0).unwrap();
263        let u = union(&a, &b).unwrap();
264        assert_eq!(u.ecount(), 2);
265        assert!(u.edge(0).unwrap() == (0, 0));
266    }
267
268    #[test]
269    fn unaligned_vertex_sizes_use_max() {
270        // a has 2 vertices, b has 5 vertices. Result must have 5 vertices.
271        let mut a = Graph::with_vertices(2);
272        a.add_edge(0, 1).unwrap();
273        let mut b = Graph::with_vertices(5);
274        b.add_edge(3, 4).unwrap();
275        let u = union(&a, &b).unwrap();
276        assert_eq!(u.vcount(), 5);
277        assert_eq!(sorted_edges(&u), vec![(0, 1), (3, 4)]);
278    }
279
280    #[test]
281    fn mixed_directedness_errors() {
282        let a = Graph::with_vertices(2);
283        let b = Graph::new(2, true).unwrap();
284        assert!(union(&a, &b).is_err());
285    }
286
287    #[test]
288    fn undirected_canonicalises_swapped_endpoints() {
289        // Both edges encode the same undirected pair (1,0) and (0,1).
290        let mut a = Graph::with_vertices(2);
291        a.add_edge(1, 0).unwrap();
292        let mut b = Graph::with_vertices(2);
293        b.add_edge(0, 1).unwrap();
294        let u = union(&a, &b).unwrap();
295        // max(1,1) = 1.
296        assert_eq!(u.ecount(), 1);
297        let endpoints = u.edge(0).unwrap();
298        assert!(endpoints == (0, 1) || endpoints == (1, 0));
299    }
300
301    #[test]
302    fn larger_left_vertex_count_preserved() {
303        // Mirror the previous test with sides swapped.
304        let mut a = Graph::with_vertices(5);
305        a.add_edge(3, 4).unwrap();
306        let mut b = Graph::with_vertices(2);
307        b.add_edge(0, 1).unwrap();
308        let u = union(&a, &b).unwrap();
309        assert_eq!(u.vcount(), 5);
310        assert_eq!(sorted_edges(&u), vec![(0, 1), (3, 4)]);
311    }
312}