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/// Returns the union of multiple graphs.
129///
130/// Generalises [`union`] to an arbitrary number of inputs. The result
131/// has `max(g.vcount() for g in graphs)` vertices. For each endpoint
132/// pair `(u, v)`, the multiplicity in the result equals the maximum
133/// over all input graphs.
134///
135/// If `graphs` is empty, returns an empty directed graph (matching
136/// igraph C convention). All inputs must agree on directedness.
137///
138/// # Errors
139/// - [`IgraphError::InvalidArgument`] if directedness diverges.
140///
141/// # Examples
142///
143/// ```
144/// use rust_igraph::{Graph, union_many};
145///
146/// let mut a = Graph::with_vertices(3);
147/// a.add_edge(0, 1).unwrap();
148/// let mut b = Graph::with_vertices(4);
149/// b.add_edge(1, 2).unwrap();
150/// let mut c = Graph::with_vertices(2);
151/// c.add_edge(0, 1).unwrap();
152///
153/// let u = union_many(&[&a, &b, &c]).unwrap();
154/// assert_eq!(u.vcount(), 4);
155/// assert_eq!(u.ecount(), 2); // (0,1) and (1,2)
156/// ```
157pub fn union_many(graphs: &[&Graph]) -> IgraphResult<Graph> {
158    if graphs.is_empty() {
159        return Graph::new(0, true);
160    }
161
162    let directed = graphs[0].is_directed();
163    for g in &graphs[1..] {
164        if g.is_directed() != directed {
165            return Err(IgraphError::InvalidArgument(
166                "union_many: cannot mix directed and undirected graphs".to_string(),
167            ));
168        }
169    }
170
171    let n = graphs.iter().map(|g| g.vcount()).max().unwrap_or(0);
172
173    let canon = |u: VertexId, v: VertexId| -> (VertexId, VertexId) {
174        if directed || u <= v { (u, v) } else { (v, u) }
175    };
176
177    // Collect per-pair maximum multiplicity across all graphs
178    let mut max_mult: BTreeMap<(VertexId, VertexId), u32> = BTreeMap::new();
179
180    for g in graphs {
181        let mut counts: BTreeMap<(VertexId, VertexId), u32> = BTreeMap::new();
182        let m = g.ecount();
183        for eid in 0..m {
184            #[allow(clippy::cast_possible_truncation)]
185            let eid_u32 = eid as u32;
186            let (u, v) = g.edge(eid_u32)?;
187            *counts.entry(canon(u, v)).or_insert(0) += 1;
188        }
189        for (pair, cnt) in counts {
190            let entry = max_mult.entry(pair).or_insert(0);
191            if cnt > *entry {
192                *entry = cnt;
193            }
194        }
195    }
196
197    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
198    for (pair, mult) in &max_mult {
199        for _ in 0..*mult {
200            edges.push(*pair);
201        }
202    }
203
204    let mut out = Graph::new(n, directed)?;
205    out.add_edges(edges)?;
206    Ok(out)
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    fn sorted_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
214        let m = u32::try_from(g.ecount()).unwrap();
215        let mut v: Vec<_> = (0..m).map(|e| g.edge(e).unwrap()).collect();
216        v.sort_unstable();
217        v
218    }
219
220    #[test]
221    fn empty_union_empty() {
222        let a = Graph::with_vertices(0);
223        let b = Graph::with_vertices(0);
224        let u = union(&a, &b).unwrap();
225        assert_eq!(u.vcount(), 0);
226        assert_eq!(u.ecount(), 0);
227        assert!(!u.is_directed());
228    }
229
230    #[test]
231    fn vcount_is_max_of_inputs() {
232        // a: 3 isolated vertices, b: 6 isolated vertices.
233        let a = Graph::with_vertices(3);
234        let b = Graph::with_vertices(6);
235        let u = union(&a, &b).unwrap();
236        assert_eq!(u.vcount(), 6);
237        assert_eq!(u.ecount(), 0);
238    }
239
240    #[test]
241    fn triangle_union_path_doc_example() {
242        let mut a = Graph::with_vertices(3);
243        a.add_edge(0, 1).unwrap();
244        a.add_edge(1, 2).unwrap();
245        a.add_edge(2, 0).unwrap();
246        let mut b = Graph::with_vertices(4);
247        b.add_edge(0, 1).unwrap();
248        b.add_edge(1, 3).unwrap();
249        let u = union(&a, &b).unwrap();
250        assert_eq!(u.vcount(), 4);
251        assert_eq!(u.ecount(), 4);
252        assert_eq!(sorted_edges(&u), vec![(0, 1), (0, 2), (1, 2), (1, 3)]);
253    }
254
255    #[test]
256    fn max_multiplicity_when_left_has_more() {
257        // left: 3× (0,1); right: 1× (0,1). Result: 3× (0,1).
258        let mut a = Graph::with_vertices(2);
259        a.add_edge(0, 1).unwrap();
260        a.add_edge(0, 1).unwrap();
261        a.add_edge(0, 1).unwrap();
262        let mut b = Graph::with_vertices(2);
263        b.add_edge(0, 1).unwrap();
264        let u = union(&a, &b).unwrap();
265        assert_eq!(u.ecount(), 3);
266    }
267
268    #[test]
269    fn max_multiplicity_when_right_has_more() {
270        // left: 1× (0,1); right: 5× (0,1). Result: 5× (0,1).
271        let mut a = Graph::with_vertices(2);
272        a.add_edge(0, 1).unwrap();
273        let mut b = Graph::with_vertices(2);
274        for _ in 0..5 {
275            b.add_edge(0, 1).unwrap();
276        }
277        let u = union(&a, &b).unwrap();
278        assert_eq!(u.ecount(), 5);
279    }
280
281    #[test]
282    fn disjoint_edge_sets_become_simple_union() {
283        // a: edges (0,1); b: edges (2,3) on 4 vertices.
284        let mut a = Graph::with_vertices(4);
285        a.add_edge(0, 1).unwrap();
286        let mut b = Graph::with_vertices(4);
287        b.add_edge(2, 3).unwrap();
288        let u = union(&a, &b).unwrap();
289        assert_eq!(sorted_edges(&u), vec![(0, 1), (2, 3)]);
290    }
291
292    #[test]
293    fn idempotent_with_self() {
294        // union(a, a) ≡ a (max-multiplicity: max(k, k) = k).
295        let mut a = Graph::with_vertices(4);
296        a.add_edge(0, 1).unwrap();
297        a.add_edge(1, 2).unwrap();
298        a.add_edge(0, 2).unwrap();
299        a.add_edge(0, 2).unwrap(); // multi-edge
300        let u = union(&a, &a).unwrap();
301        assert_eq!(u.vcount(), a.vcount());
302        assert_eq!(u.ecount(), a.ecount());
303        assert_eq!(sorted_edges(&u), sorted_edges(&a));
304    }
305
306    #[test]
307    fn directed_keeps_orientation_separate() {
308        // left: (0→1); right: (1→0). Both edges land in the result.
309        let mut a = Graph::new(2, true).unwrap();
310        a.add_edge(0, 1).unwrap();
311        let mut b = Graph::new(2, true).unwrap();
312        b.add_edge(1, 0).unwrap();
313        let u = union(&a, &b).unwrap();
314        assert!(u.is_directed());
315        assert_eq!(u.ecount(), 2);
316        assert_eq!(sorted_edges(&u), vec![(0, 1), (1, 0)]);
317    }
318
319    #[test]
320    fn directed_max_multiplicity_per_orientation() {
321        // left: 2× (0→1); right: 3× (1→0); both kept independently.
322        let mut a = Graph::new(2, true).unwrap();
323        a.add_edge(0, 1).unwrap();
324        a.add_edge(0, 1).unwrap();
325        let mut b = Graph::new(2, true).unwrap();
326        for _ in 0..3 {
327            b.add_edge(1, 0).unwrap();
328        }
329        let u = union(&a, &b).unwrap();
330        assert_eq!(u.ecount(), 5);
331        let s = sorted_edges(&u);
332        assert_eq!(s.iter().filter(|&&p| p == (0, 1)).count(), 2);
333        assert_eq!(s.iter().filter(|&&p| p == (1, 0)).count(), 3);
334    }
335
336    #[test]
337    fn loops_are_preserved() {
338        // left: 2× (0,0); right: 1× (0,0). Result: 2× (0,0).
339        let mut a = Graph::with_vertices(1);
340        a.add_edge(0, 0).unwrap();
341        a.add_edge(0, 0).unwrap();
342        let mut b = Graph::with_vertices(1);
343        b.add_edge(0, 0).unwrap();
344        let u = union(&a, &b).unwrap();
345        assert_eq!(u.ecount(), 2);
346        assert!(u.edge(0).unwrap() == (0, 0));
347    }
348
349    #[test]
350    fn unaligned_vertex_sizes_use_max() {
351        // a has 2 vertices, b has 5 vertices. Result must have 5 vertices.
352        let mut a = Graph::with_vertices(2);
353        a.add_edge(0, 1).unwrap();
354        let mut b = Graph::with_vertices(5);
355        b.add_edge(3, 4).unwrap();
356        let u = union(&a, &b).unwrap();
357        assert_eq!(u.vcount(), 5);
358        assert_eq!(sorted_edges(&u), vec![(0, 1), (3, 4)]);
359    }
360
361    #[test]
362    fn mixed_directedness_errors() {
363        let a = Graph::with_vertices(2);
364        let b = Graph::new(2, true).unwrap();
365        assert!(union(&a, &b).is_err());
366    }
367
368    #[test]
369    fn undirected_canonicalises_swapped_endpoints() {
370        // Both edges encode the same undirected pair (1,0) and (0,1).
371        let mut a = Graph::with_vertices(2);
372        a.add_edge(1, 0).unwrap();
373        let mut b = Graph::with_vertices(2);
374        b.add_edge(0, 1).unwrap();
375        let u = union(&a, &b).unwrap();
376        // max(1,1) = 1.
377        assert_eq!(u.ecount(), 1);
378        let endpoints = u.edge(0).unwrap();
379        assert!(endpoints == (0, 1) || endpoints == (1, 0));
380    }
381
382    #[test]
383    fn larger_left_vertex_count_preserved() {
384        // Mirror the previous test with sides swapped.
385        let mut a = Graph::with_vertices(5);
386        a.add_edge(3, 4).unwrap();
387        let mut b = Graph::with_vertices(2);
388        b.add_edge(0, 1).unwrap();
389        let u = union(&a, &b).unwrap();
390        assert_eq!(u.vcount(), 5);
391        assert_eq!(sorted_edges(&u), vec![(0, 1), (3, 4)]);
392    }
393
394    // --- union_many tests ---
395
396    #[test]
397    fn union_many_empty_list() {
398        let u = union_many(&[]).unwrap();
399        assert_eq!(u.vcount(), 0);
400        assert!(u.is_directed());
401    }
402
403    #[test]
404    fn union_many_single_graph() {
405        let mut a = Graph::with_vertices(3);
406        a.add_edge(0, 1).unwrap();
407        a.add_edge(1, 2).unwrap();
408        let u = union_many(&[&a]).unwrap();
409        assert_eq!(u.vcount(), 3);
410        assert_eq!(u.ecount(), 2);
411        assert_eq!(sorted_edges(&u), sorted_edges(&a));
412    }
413
414    #[test]
415    fn union_many_three_graphs() {
416        let mut a = Graph::with_vertices(3);
417        a.add_edge(0, 1).unwrap();
418        let mut b = Graph::with_vertices(4);
419        b.add_edge(1, 2).unwrap();
420        let mut c = Graph::with_vertices(5);
421        c.add_edge(3, 4).unwrap();
422
423        let u = union_many(&[&a, &b, &c]).unwrap();
424        assert_eq!(u.vcount(), 5);
425        assert_eq!(u.ecount(), 3);
426        assert_eq!(sorted_edges(&u), vec![(0, 1), (1, 2), (3, 4)]);
427    }
428
429    #[test]
430    fn union_many_max_multiplicity() {
431        let mut a = Graph::with_vertices(2);
432        a.add_edge(0, 1).unwrap();
433        a.add_edge(0, 1).unwrap();
434        let mut b = Graph::with_vertices(2);
435        b.add_edge(0, 1).unwrap();
436        b.add_edge(0, 1).unwrap();
437        b.add_edge(0, 1).unwrap();
438        let mut c = Graph::with_vertices(2);
439        c.add_edge(0, 1).unwrap();
440
441        let u = union_many(&[&a, &b, &c]).unwrap();
442        assert_eq!(u.ecount(), 3); // max(2, 3, 1) = 3
443    }
444
445    #[test]
446    fn union_many_mixed_directedness_fails() {
447        let a = Graph::with_vertices(2);
448        let b = Graph::new(2, true).unwrap();
449        assert!(union_many(&[&a, &b]).is_err());
450    }
451
452    #[test]
453    fn union_many_directed() {
454        let mut a = Graph::new(3, true).unwrap();
455        a.add_edge(0, 1).unwrap();
456        let mut b = Graph::new(3, true).unwrap();
457        b.add_edge(1, 0).unwrap();
458        let mut c = Graph::new(3, true).unwrap();
459        c.add_edge(1, 2).unwrap();
460
461        let u = union_many(&[&a, &b, &c]).unwrap();
462        assert!(u.is_directed());
463        assert_eq!(u.ecount(), 3);
464        assert_eq!(sorted_edges(&u), vec![(0, 1), (1, 0), (1, 2)]);
465    }
466}