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#[cfg(test)]
120mod tests {
121    use super::*;
122
123    fn sorted_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
124        let m = u32::try_from(g.ecount()).unwrap();
125        let mut v: Vec<_> = (0..m).map(|e| g.edge(e).unwrap()).collect();
126        v.sort_unstable();
127        v
128    }
129
130    #[test]
131    fn empty_intersect_empty() {
132        let a = Graph::with_vertices(0);
133        let b = Graph::with_vertices(0);
134        let i = intersection(&a, &b).unwrap();
135        assert_eq!(i.vcount(), 0);
136        assert_eq!(i.ecount(), 0);
137        assert!(!i.is_directed());
138    }
139
140    #[test]
141    fn vcount_is_max_of_inputs() {
142        let a = Graph::with_vertices(3);
143        let b = Graph::with_vertices(7);
144        let i = intersection(&a, &b).unwrap();
145        assert_eq!(i.vcount(), 7);
146        assert_eq!(i.ecount(), 0);
147    }
148
149    #[test]
150    fn triangle_intersect_path_doc_example() {
151        let mut a = Graph::with_vertices(3);
152        a.add_edge(0, 1).unwrap();
153        a.add_edge(1, 2).unwrap();
154        a.add_edge(2, 0).unwrap();
155        let mut b = Graph::with_vertices(3);
156        b.add_edge(0, 1).unwrap();
157        b.add_edge(1, 2).unwrap();
158        let i = intersection(&a, &b).unwrap();
159        assert_eq!(i.vcount(), 3);
160        assert_eq!(i.ecount(), 2);
161        assert_eq!(sorted_edges(&i), vec![(0, 1), (1, 2)]);
162    }
163
164    #[test]
165    fn min_multiplicity_when_left_has_more() {
166        // left: 3× (0,1); right: 1× (0,1). Result: 1× (0,1).
167        let mut a = Graph::with_vertices(2);
168        a.add_edge(0, 1).unwrap();
169        a.add_edge(0, 1).unwrap();
170        a.add_edge(0, 1).unwrap();
171        let mut b = Graph::with_vertices(2);
172        b.add_edge(0, 1).unwrap();
173        let i = intersection(&a, &b).unwrap();
174        assert_eq!(i.ecount(), 1);
175    }
176
177    #[test]
178    fn min_multiplicity_when_right_has_more() {
179        // left: 2× (0,1); right: 5× (0,1). Result: 2× (0,1).
180        let mut a = Graph::with_vertices(2);
181        a.add_edge(0, 1).unwrap();
182        a.add_edge(0, 1).unwrap();
183        let mut b = Graph::with_vertices(2);
184        for _ in 0..5 {
185            b.add_edge(0, 1).unwrap();
186        }
187        let i = intersection(&a, &b).unwrap();
188        assert_eq!(i.ecount(), 2);
189    }
190
191    #[test]
192    fn disjoint_edge_sets_yields_empty() {
193        // No shared pair → empty graph (max-vcount preserved).
194        let mut a = Graph::with_vertices(4);
195        a.add_edge(0, 1).unwrap();
196        let mut b = Graph::with_vertices(4);
197        b.add_edge(2, 3).unwrap();
198        let i = intersection(&a, &b).unwrap();
199        assert_eq!(i.vcount(), 4);
200        assert_eq!(i.ecount(), 0);
201    }
202
203    #[test]
204    fn idempotent_with_self() {
205        // intersection(a, a) ≡ a (min(k, k) = k).
206        let mut a = Graph::with_vertices(4);
207        a.add_edge(0, 1).unwrap();
208        a.add_edge(1, 2).unwrap();
209        a.add_edge(0, 2).unwrap();
210        a.add_edge(0, 2).unwrap(); // multi-edge
211        let i = intersection(&a, &a).unwrap();
212        assert_eq!(i.vcount(), a.vcount());
213        assert_eq!(i.ecount(), a.ecount());
214        assert_eq!(sorted_edges(&i), sorted_edges(&a));
215    }
216
217    #[test]
218    fn directed_keeps_orientation_separate() {
219        // left: 0→1 + 1→0; right: 0→1 only. Intersection: 0→1.
220        let mut a = Graph::new(2, true).unwrap();
221        a.add_edge(0, 1).unwrap();
222        a.add_edge(1, 0).unwrap();
223        let mut b = Graph::new(2, true).unwrap();
224        b.add_edge(0, 1).unwrap();
225        let i = intersection(&a, &b).unwrap();
226        assert!(i.is_directed());
227        assert_eq!(i.ecount(), 1);
228        assert_eq!(i.edge(0).unwrap(), (0, 1));
229    }
230
231    #[test]
232    fn directed_min_multiplicity_per_orientation() {
233        // left: 3× (0→1), 2× (1→0); right: 2× (0→1), 4× (1→0).
234        // Result: 2× (0→1), 2× (1→0) → 4 edges total.
235        let mut a = Graph::new(2, true).unwrap();
236        for _ in 0..3 {
237            a.add_edge(0, 1).unwrap();
238        }
239        for _ in 0..2 {
240            a.add_edge(1, 0).unwrap();
241        }
242        let mut b = Graph::new(2, true).unwrap();
243        for _ in 0..2 {
244            b.add_edge(0, 1).unwrap();
245        }
246        for _ in 0..4 {
247            b.add_edge(1, 0).unwrap();
248        }
249        let i = intersection(&a, &b).unwrap();
250        assert_eq!(i.ecount(), 4);
251        let s = sorted_edges(&i);
252        assert_eq!(s.iter().filter(|&&p| p == (0, 1)).count(), 2);
253        assert_eq!(s.iter().filter(|&&p| p == (1, 0)).count(), 2);
254    }
255
256    #[test]
257    fn loops_are_preserved_with_min_multiplicity() {
258        // left: 3× (0,0); right: 1× (0,0). Result: 1× (0,0).
259        let mut a = Graph::with_vertices(1);
260        for _ in 0..3 {
261            a.add_edge(0, 0).unwrap();
262        }
263        let mut b = Graph::with_vertices(1);
264        b.add_edge(0, 0).unwrap();
265        let i = intersection(&a, &b).unwrap();
266        assert_eq!(i.ecount(), 1);
267        assert_eq!(i.edge(0).unwrap(), (0, 0));
268    }
269
270    #[test]
271    fn unaligned_vertex_sizes_use_max() {
272        // a has 2 vertices, b has 5 vertices, no shared edges.
273        let mut a = Graph::with_vertices(2);
274        a.add_edge(0, 1).unwrap();
275        let mut b = Graph::with_vertices(5);
276        b.add_edge(3, 4).unwrap();
277        let i = intersection(&a, &b).unwrap();
278        assert_eq!(i.vcount(), 5);
279        assert_eq!(i.ecount(), 0);
280    }
281
282    #[test]
283    fn mixed_directedness_errors() {
284        let a = Graph::with_vertices(2);
285        let b = Graph::new(2, true).unwrap();
286        assert!(intersection(&a, &b).is_err());
287    }
288
289    #[test]
290    fn undirected_canonicalises_swapped_endpoints() {
291        // left has (1,0); right has (0,1). Both encode the same
292        // undirected pair → intersection has 1× that pair.
293        let mut a = Graph::with_vertices(2);
294        a.add_edge(1, 0).unwrap();
295        let mut b = Graph::with_vertices(2);
296        b.add_edge(0, 1).unwrap();
297        let i = intersection(&a, &b).unwrap();
298        assert_eq!(i.ecount(), 1);
299    }
300
301    #[test]
302    fn order_independent() {
303        // intersection(a, b) and intersection(b, a) produce the same
304        // edge multiset (commutative).
305        let mut a = Graph::with_vertices(4);
306        a.add_edge(0, 1).unwrap();
307        a.add_edge(0, 1).unwrap();
308        a.add_edge(1, 2).unwrap();
309        a.add_edge(2, 3).unwrap();
310        let mut b = Graph::with_vertices(4);
311        b.add_edge(0, 1).unwrap();
312        b.add_edge(1, 2).unwrap();
313        b.add_edge(1, 2).unwrap();
314        let ab = intersection(&a, &b).unwrap();
315        let ba = intersection(&b, &a).unwrap();
316        assert_eq!(sorted_edges(&ab), sorted_edges(&ba));
317    }
318}