Skip to main content

rust_igraph/algorithms/operators/
intersection.rs

1//! Intersection of two graphs (ALGO-OP-005).
2//!
3//! Counterpart of `igraph_intersection()` from
4//! `references/igraph/src/operators/intersection.c:71-77`. Vertex sets
5//! are aligned by index — the result has
6//! `max(left.vcount(), right.vcount())` vertices (matching upstream's
7//! "common edges, larger vertex set" contract). Edges are intersected
8//! by *minimum multiplicity*: for each (canonicalised) endpoint pair
9//! `(u, v)`, the result contains `min(count_left, count_right)` edges,
10//! so a pair only survives when present in BOTH inputs.
11//!
12//! For undirected inputs the canonicalised pair is `(min(u,v),
13//! max(u,v))`; for directed inputs the pair is taken as-is, so
14//! `(u, v)` and `(v, u)` are tallied separately.
15//!
16//! Phase-1 minimal slice: two-graph variant only. Multi-arg
17//! `intersection_many` and the edge-mapping outputs (`edge_map1` /
18//! `edge_map2`) ship later.
19
20use std::collections::BTreeMap;
21
22use crate::core::graph::EdgeId;
23use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
24
25/// Returns the intersection of `left` and `right`.
26///
27/// Vertex sets are aligned by index — the result has
28/// `max(left.vcount(), right.vcount())` vertices. For each endpoint
29/// pair `(u, v)`, the multiplicity in the result equals the smaller of
30/// the multiplicities in the two inputs (so pairs unique to one side
31/// are dropped).
32///
33/// Both inputs must agree on directedness; an undirected edge
34/// `(u, v)` is canonicalised to `(min(u, v), max(u, v))` before
35/// counting, while directed edges are tallied as-is.
36///
37/// Output edges are emitted in lexicographic `(src, tgt)` order; two
38/// edges sharing the same canonicalised pair appear consecutively.
39///
40/// # Errors
41/// - [`IgraphError::InvalidArgument`] if directedness diverges.
42///
43/// # Examples
44///
45/// ```
46/// use rust_igraph::{Graph, intersection};
47///
48/// // Triangle ∩ path: edges {(0,1), (1,2), (2,0)} ∩ {(0,1), (1,2)}
49/// // → {(0,1), (1,2)} on max(3, 3) = 3 vertices.
50/// let mut a = Graph::with_vertices(3);
51/// a.add_edge(0, 1).unwrap();
52/// a.add_edge(1, 2).unwrap();
53/// a.add_edge(2, 0).unwrap();
54/// let mut b = Graph::with_vertices(3);
55/// b.add_edge(0, 1).unwrap();
56/// b.add_edge(1, 2).unwrap();
57///
58/// let i = intersection(&a, &b).unwrap();
59/// assert_eq!(i.vcount(), 3);
60/// assert_eq!(i.ecount(), 2);
61/// ```
62pub fn intersection(left: &Graph, right: &Graph) -> IgraphResult<Graph> {
63    if left.is_directed() != right.is_directed() {
64        return Err(IgraphError::InvalidArgument(
65            "intersection: cannot mix directed and undirected graphs".to_string(),
66        ));
67    }
68    let directed = left.is_directed();
69    let n = std::cmp::max(left.vcount(), right.vcount());
70
71    let canon = |u: VertexId, v: VertexId| -> (VertexId, VertexId) {
72        if directed || u <= v { (u, v) } else { (v, u) }
73    };
74
75    let mut count_left: BTreeMap<(VertexId, VertexId), u32> = BTreeMap::new();
76    let mut count_right: BTreeMap<(VertexId, VertexId), u32> = BTreeMap::new();
77
78    let m_l = u32::try_from(left.ecount())
79        .map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
80    for e in 0..m_l {
81        let (u, v) = left.edge(e as EdgeId)?;
82        *count_left.entry(canon(u, v)).or_insert(0) += 1;
83    }
84    let m_r = u32::try_from(right.ecount())
85        .map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
86    for e in 0..m_r {
87        let (u, v) = right.edge(e as EdgeId)?;
88        *count_right.entry(canon(u, v)).or_insert(0) += 1;
89    }
90
91    // Walk the smaller map and look up matches in the other; only pairs
92    // present in BOTH contribute. Iterating the smaller side in BTreeMap
93    // order keeps output deterministic without materialising the merged
94    // key set.
95    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
96    let (driver, lookup) = if count_left.len() <= count_right.len() {
97        (&count_left, &count_right)
98    } else {
99        (&count_right, &count_left)
100    };
101    for (k, &cd) in driver {
102        if let Some(&co) = lookup.get(k) {
103            let m = std::cmp::min(cd, co);
104            for _ in 0..m {
105                edges.push(*k);
106            }
107        }
108    }
109    // The driver may be `count_right`, in which case we walked the
110    // intersection in `right`'s key order — that's still the same set
111    // of pairs. But for stable output across left/right swaps, sort.
112    edges.sort_unstable();
113
114    let mut out = Graph::new(n, directed)?;
115    out.add_edges(edges)?;
116    Ok(out)
117}
118
119/// Returns the intersection of multiple graphs.
120///
121/// Generalises [`intersection`] to an arbitrary number of inputs. The
122/// result has `max(g.vcount() for g in graphs)` vertices. For each
123/// endpoint pair `(u, v)`, the multiplicity in the result is the
124/// minimum over all input graphs (a pair only survives if it appears
125/// in *every* input).
126///
127/// If `graphs` is empty, returns an empty directed graph (matching
128/// igraph C convention). All inputs must agree on directedness.
129///
130/// # Errors
131/// - [`IgraphError::InvalidArgument`] if directedness diverges.
132///
133/// # Examples
134///
135/// ```
136/// use rust_igraph::{Graph, intersection_many};
137///
138/// let mut a = Graph::with_vertices(3);
139/// a.add_edge(0, 1).unwrap();
140/// a.add_edge(1, 2).unwrap();
141/// let mut b = Graph::with_vertices(3);
142/// b.add_edge(0, 1).unwrap();
143/// b.add_edge(2, 0).unwrap();
144/// let mut c = Graph::with_vertices(3);
145/// c.add_edge(0, 1).unwrap();
146/// c.add_edge(1, 2).unwrap();
147/// c.add_edge(2, 0).unwrap();
148///
149/// let i = intersection_many(&[&a, &b, &c]).unwrap();
150/// assert_eq!(i.ecount(), 1); // only (0,1) is in all three
151/// ```
152pub fn intersection_many(graphs: &[&Graph]) -> IgraphResult<Graph> {
153    if graphs.is_empty() {
154        return Graph::new(0, true);
155    }
156
157    let directed = graphs[0].is_directed();
158    for g in &graphs[1..] {
159        if g.is_directed() != directed {
160            return Err(IgraphError::InvalidArgument(
161                "intersection_many: cannot mix directed and undirected graphs".to_string(),
162            ));
163        }
164    }
165
166    let n = graphs.iter().map(|g| g.vcount()).max().unwrap_or(0);
167
168    let canon = |u: VertexId, v: VertexId| -> (VertexId, VertexId) {
169        if directed || u <= v { (u, v) } else { (v, u) }
170    };
171
172    // Build edge counts for the first graph
173    let mut result_counts: BTreeMap<(VertexId, VertexId), u32> = BTreeMap::new();
174    let m = graphs[0].ecount();
175    for eid in 0..m {
176        #[allow(clippy::cast_possible_truncation)]
177        let eid_u32 = eid as u32;
178        let (u, v) = graphs[0].edge(eid_u32)?;
179        *result_counts.entry(canon(u, v)).or_insert(0) += 1;
180    }
181
182    // For each subsequent graph, take the per-pair minimum
183    for g in &graphs[1..] {
184        let mut counts: BTreeMap<(VertexId, VertexId), u32> = BTreeMap::new();
185        let gm = g.ecount();
186        for eid in 0..gm {
187            #[allow(clippy::cast_possible_truncation)]
188            let eid_u32 = eid as u32;
189            let (u, v) = g.edge(eid_u32)?;
190            *counts.entry(canon(u, v)).or_insert(0) += 1;
191        }
192
193        // Intersect: keep only pairs present in both, with min multiplicity
194        result_counts.retain(|pair, mult| {
195            if let Some(&cnt) = counts.get(pair) {
196                *mult = std::cmp::min(*mult, cnt);
197                true
198            } else {
199                false
200            }
201        });
202    }
203
204    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
205    for (pair, mult) in &result_counts {
206        for _ in 0..*mult {
207            edges.push(*pair);
208        }
209    }
210
211    let mut out = Graph::new(n, directed)?;
212    out.add_edges(edges)?;
213    Ok(out)
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    fn sorted_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
221        let m = u32::try_from(g.ecount()).unwrap();
222        let mut v: Vec<_> = (0..m).map(|e| g.edge(e).unwrap()).collect();
223        v.sort_unstable();
224        v
225    }
226
227    #[test]
228    fn empty_intersect_empty() {
229        let a = Graph::with_vertices(0);
230        let b = Graph::with_vertices(0);
231        let i = intersection(&a, &b).unwrap();
232        assert_eq!(i.vcount(), 0);
233        assert_eq!(i.ecount(), 0);
234        assert!(!i.is_directed());
235    }
236
237    #[test]
238    fn vcount_is_max_of_inputs() {
239        let a = Graph::with_vertices(3);
240        let b = Graph::with_vertices(7);
241        let i = intersection(&a, &b).unwrap();
242        assert_eq!(i.vcount(), 7);
243        assert_eq!(i.ecount(), 0);
244    }
245
246    #[test]
247    fn triangle_intersect_path_doc_example() {
248        let mut a = Graph::with_vertices(3);
249        a.add_edge(0, 1).unwrap();
250        a.add_edge(1, 2).unwrap();
251        a.add_edge(2, 0).unwrap();
252        let mut b = Graph::with_vertices(3);
253        b.add_edge(0, 1).unwrap();
254        b.add_edge(1, 2).unwrap();
255        let i = intersection(&a, &b).unwrap();
256        assert_eq!(i.vcount(), 3);
257        assert_eq!(i.ecount(), 2);
258        assert_eq!(sorted_edges(&i), vec![(0, 1), (1, 2)]);
259    }
260
261    #[test]
262    fn min_multiplicity_when_left_has_more() {
263        // left: 3× (0,1); right: 1× (0,1). Result: 1× (0,1).
264        let mut a = Graph::with_vertices(2);
265        a.add_edge(0, 1).unwrap();
266        a.add_edge(0, 1).unwrap();
267        a.add_edge(0, 1).unwrap();
268        let mut b = Graph::with_vertices(2);
269        b.add_edge(0, 1).unwrap();
270        let i = intersection(&a, &b).unwrap();
271        assert_eq!(i.ecount(), 1);
272    }
273
274    #[test]
275    fn min_multiplicity_when_right_has_more() {
276        // left: 2× (0,1); right: 5× (0,1). Result: 2× (0,1).
277        let mut a = Graph::with_vertices(2);
278        a.add_edge(0, 1).unwrap();
279        a.add_edge(0, 1).unwrap();
280        let mut b = Graph::with_vertices(2);
281        for _ in 0..5 {
282            b.add_edge(0, 1).unwrap();
283        }
284        let i = intersection(&a, &b).unwrap();
285        assert_eq!(i.ecount(), 2);
286    }
287
288    #[test]
289    fn disjoint_edge_sets_yields_empty() {
290        // No shared pair → empty graph (max-vcount preserved).
291        let mut a = Graph::with_vertices(4);
292        a.add_edge(0, 1).unwrap();
293        let mut b = Graph::with_vertices(4);
294        b.add_edge(2, 3).unwrap();
295        let i = intersection(&a, &b).unwrap();
296        assert_eq!(i.vcount(), 4);
297        assert_eq!(i.ecount(), 0);
298    }
299
300    #[test]
301    fn idempotent_with_self() {
302        // intersection(a, a) ≡ a (min(k, k) = k).
303        let mut a = Graph::with_vertices(4);
304        a.add_edge(0, 1).unwrap();
305        a.add_edge(1, 2).unwrap();
306        a.add_edge(0, 2).unwrap();
307        a.add_edge(0, 2).unwrap(); // multi-edge
308        let i = intersection(&a, &a).unwrap();
309        assert_eq!(i.vcount(), a.vcount());
310        assert_eq!(i.ecount(), a.ecount());
311        assert_eq!(sorted_edges(&i), sorted_edges(&a));
312    }
313
314    #[test]
315    fn directed_keeps_orientation_separate() {
316        // left: 0→1 + 1→0; right: 0→1 only. Intersection: 0→1.
317        let mut a = Graph::new(2, true).unwrap();
318        a.add_edge(0, 1).unwrap();
319        a.add_edge(1, 0).unwrap();
320        let mut b = Graph::new(2, true).unwrap();
321        b.add_edge(0, 1).unwrap();
322        let i = intersection(&a, &b).unwrap();
323        assert!(i.is_directed());
324        assert_eq!(i.ecount(), 1);
325        assert_eq!(i.edge(0).unwrap(), (0, 1));
326    }
327
328    #[test]
329    fn directed_min_multiplicity_per_orientation() {
330        // left: 3× (0→1), 2× (1→0); right: 2× (0→1), 4× (1→0).
331        // Result: 2× (0→1), 2× (1→0) → 4 edges total.
332        let mut a = Graph::new(2, true).unwrap();
333        for _ in 0..3 {
334            a.add_edge(0, 1).unwrap();
335        }
336        for _ in 0..2 {
337            a.add_edge(1, 0).unwrap();
338        }
339        let mut b = Graph::new(2, true).unwrap();
340        for _ in 0..2 {
341            b.add_edge(0, 1).unwrap();
342        }
343        for _ in 0..4 {
344            b.add_edge(1, 0).unwrap();
345        }
346        let i = intersection(&a, &b).unwrap();
347        assert_eq!(i.ecount(), 4);
348        let s = sorted_edges(&i);
349        assert_eq!(s.iter().filter(|&&p| p == (0, 1)).count(), 2);
350        assert_eq!(s.iter().filter(|&&p| p == (1, 0)).count(), 2);
351    }
352
353    #[test]
354    fn loops_are_preserved_with_min_multiplicity() {
355        // left: 3× (0,0); right: 1× (0,0). Result: 1× (0,0).
356        let mut a = Graph::with_vertices(1);
357        for _ in 0..3 {
358            a.add_edge(0, 0).unwrap();
359        }
360        let mut b = Graph::with_vertices(1);
361        b.add_edge(0, 0).unwrap();
362        let i = intersection(&a, &b).unwrap();
363        assert_eq!(i.ecount(), 1);
364        assert_eq!(i.edge(0).unwrap(), (0, 0));
365    }
366
367    #[test]
368    fn unaligned_vertex_sizes_use_max() {
369        // a has 2 vertices, b has 5 vertices, no shared edges.
370        let mut a = Graph::with_vertices(2);
371        a.add_edge(0, 1).unwrap();
372        let mut b = Graph::with_vertices(5);
373        b.add_edge(3, 4).unwrap();
374        let i = intersection(&a, &b).unwrap();
375        assert_eq!(i.vcount(), 5);
376        assert_eq!(i.ecount(), 0);
377    }
378
379    #[test]
380    fn mixed_directedness_errors() {
381        let a = Graph::with_vertices(2);
382        let b = Graph::new(2, true).unwrap();
383        assert!(intersection(&a, &b).is_err());
384    }
385
386    #[test]
387    fn undirected_canonicalises_swapped_endpoints() {
388        // left has (1,0); right has (0,1). Both encode the same
389        // undirected pair → intersection has 1× that pair.
390        let mut a = Graph::with_vertices(2);
391        a.add_edge(1, 0).unwrap();
392        let mut b = Graph::with_vertices(2);
393        b.add_edge(0, 1).unwrap();
394        let i = intersection(&a, &b).unwrap();
395        assert_eq!(i.ecount(), 1);
396    }
397
398    #[test]
399    fn order_independent() {
400        // intersection(a, b) and intersection(b, a) produce the same
401        // edge multiset (commutative).
402        let mut a = Graph::with_vertices(4);
403        a.add_edge(0, 1).unwrap();
404        a.add_edge(0, 1).unwrap();
405        a.add_edge(1, 2).unwrap();
406        a.add_edge(2, 3).unwrap();
407        let mut b = Graph::with_vertices(4);
408        b.add_edge(0, 1).unwrap();
409        b.add_edge(1, 2).unwrap();
410        b.add_edge(1, 2).unwrap();
411        let ab = intersection(&a, &b).unwrap();
412        let ba = intersection(&b, &a).unwrap();
413        assert_eq!(sorted_edges(&ab), sorted_edges(&ba));
414    }
415
416    // --- intersection_many tests ---
417
418    #[test]
419    fn intersection_many_empty_list() {
420        let i = intersection_many(&[]).unwrap();
421        assert_eq!(i.vcount(), 0);
422        assert!(i.is_directed());
423    }
424
425    #[test]
426    fn intersection_many_single_graph() {
427        let mut a = Graph::with_vertices(3);
428        a.add_edge(0, 1).unwrap();
429        a.add_edge(1, 2).unwrap();
430        let i = intersection_many(&[&a]).unwrap();
431        assert_eq!(i.vcount(), 3);
432        assert_eq!(i.ecount(), 2);
433        assert_eq!(sorted_edges(&i), sorted_edges(&a));
434    }
435
436    #[test]
437    fn intersection_many_three_graphs() {
438        let mut a = Graph::with_vertices(3);
439        a.add_edge(0, 1).unwrap();
440        a.add_edge(1, 2).unwrap();
441        let mut b = Graph::with_vertices(3);
442        b.add_edge(0, 1).unwrap();
443        b.add_edge(2, 0).unwrap();
444        let mut c = Graph::with_vertices(3);
445        c.add_edge(0, 1).unwrap();
446        c.add_edge(1, 2).unwrap();
447        c.add_edge(2, 0).unwrap();
448
449        let i = intersection_many(&[&a, &b, &c]).unwrap();
450        assert_eq!(i.vcount(), 3);
451        assert_eq!(i.ecount(), 1); // only (0,1) present in all
452        assert_eq!(sorted_edges(&i), vec![(0, 1)]);
453    }
454
455    #[test]
456    fn intersection_many_min_multiplicity() {
457        let mut a = Graph::with_vertices(2);
458        a.add_edge(0, 1).unwrap();
459        a.add_edge(0, 1).unwrap();
460        a.add_edge(0, 1).unwrap();
461        let mut b = Graph::with_vertices(2);
462        b.add_edge(0, 1).unwrap();
463        b.add_edge(0, 1).unwrap();
464        let mut c = Graph::with_vertices(2);
465        c.add_edge(0, 1).unwrap();
466        c.add_edge(0, 1).unwrap();
467        c.add_edge(0, 1).unwrap();
468        c.add_edge(0, 1).unwrap();
469
470        let i = intersection_many(&[&a, &b, &c]).unwrap();
471        assert_eq!(i.ecount(), 2); // min(3, 2, 4) = 2
472    }
473
474    #[test]
475    fn intersection_many_no_common_edge() {
476        let mut a = Graph::with_vertices(3);
477        a.add_edge(0, 1).unwrap();
478        let mut b = Graph::with_vertices(3);
479        b.add_edge(1, 2).unwrap();
480        let mut c = Graph::with_vertices(3);
481        c.add_edge(2, 0).unwrap();
482
483        let i = intersection_many(&[&a, &b, &c]).unwrap();
484        assert_eq!(i.ecount(), 0);
485    }
486
487    #[test]
488    fn intersection_many_mixed_directedness_fails() {
489        let a = Graph::with_vertices(2);
490        let b = Graph::new(2, true).unwrap();
491        assert!(intersection_many(&[&a, &b]).is_err());
492    }
493
494    #[test]
495    fn intersection_many_directed() {
496        let mut a = Graph::new(3, true).unwrap();
497        a.add_edge(0, 1).unwrap();
498        a.add_edge(1, 2).unwrap();
499        let mut b = Graph::new(3, true).unwrap();
500        b.add_edge(0, 1).unwrap();
501        b.add_edge(2, 1).unwrap();
502
503        let i = intersection_many(&[&a, &b]).unwrap();
504        assert!(i.is_directed());
505        assert_eq!(i.ecount(), 1); // only (0,1) common
506        assert_eq!(sorted_edges(&i), vec![(0, 1)]);
507    }
508}