Skip to main content

rust_igraph/algorithms/operators/
difference.rs

1//! Difference of two graphs (ALGO-OP-006).
2//!
3//! Counterpart of `igraph_difference()` from
4//! `references/igraph/src/operators/difference.c:54-181`. The result
5//! has exactly `orig.vcount()` vertices (the **left** operand only —
6//! this is intentionally asymmetric, unlike
7//! [`crate::union`] / [`crate::intersection`] which take the max). For
8//! each canonicalised endpoint pair the result keeps
9//! `max(0, count_orig - count_sub)` copies, i.e. multiset subtraction
10//! clamped to zero.
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 subtracted independently.
15//!
16//! Phase-1 minimal slice: two-graph variant only. The upstream
17//! `edge_ids` permutation output (used by C for attribute permutation)
18//! is not yet exposed.
19
20use std::collections::BTreeMap;
21
22use crate::core::graph::EdgeId;
23use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
24
25/// Returns `orig \ sub`: the multiset difference of the edges.
26///
27/// The result has `orig.vcount()` vertices — vertices in `sub` beyond
28/// `orig.vcount()` are simply ignored. For each endpoint pair `(u, v)`,
29/// the multiplicity in the result equals
30/// `max(0, count_orig - count_sub)`.
31///
32/// Both inputs must agree on directedness; an undirected edge
33/// `(u, v)` is canonicalised to `(min(u, v), max(u, v))` before
34/// counting, while directed edges are tallied as-is.
35///
36/// Output edges are emitted in lexicographic `(src, tgt)` order; two
37/// edges sharing the same canonicalised pair appear consecutively.
38///
39/// # Errors
40/// - [`IgraphError::InvalidArgument`] if directedness diverges.
41///
42/// # Examples
43///
44/// ```
45/// use rust_igraph::{Graph, difference};
46///
47/// // Triangle \ path: edges {(0,1), (1,2), (2,0)} \ {(0,1), (1,2)}
48/// // → {(0,2)} on 3 vertices (canonicalised (0,2) for undirected).
49/// let mut a = Graph::with_vertices(3);
50/// a.add_edge(0, 1).unwrap();
51/// a.add_edge(1, 2).unwrap();
52/// a.add_edge(2, 0).unwrap();
53/// let mut b = Graph::with_vertices(3);
54/// b.add_edge(0, 1).unwrap();
55/// b.add_edge(1, 2).unwrap();
56///
57/// let d = difference(&a, &b).unwrap();
58/// assert_eq!(d.vcount(), 3);
59/// assert_eq!(d.ecount(), 1);
60/// ```
61pub fn difference(orig: &Graph, sub: &Graph) -> IgraphResult<Graph> {
62    if orig.is_directed() != sub.is_directed() {
63        return Err(IgraphError::InvalidArgument(
64            "difference: cannot mix directed and undirected graphs".to_string(),
65        ));
66    }
67    let directed = orig.is_directed();
68    let n = orig.vcount();
69
70    let canon = |u: VertexId, v: VertexId| -> (VertexId, VertexId) {
71        if directed || u <= v { (u, v) } else { (v, u) }
72    };
73
74    let mut count_orig: BTreeMap<(VertexId, VertexId), u32> = BTreeMap::new();
75    let mut count_sub: BTreeMap<(VertexId, VertexId), u32> = BTreeMap::new();
76
77    let m_o = u32::try_from(orig.ecount())
78        .map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
79    for e in 0..m_o {
80        let (u, v) = orig.edge(e as EdgeId)?;
81        *count_orig.entry(canon(u, v)).or_insert(0) += 1;
82    }
83    let m_s = u32::try_from(sub.ecount())
84        .map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
85    for e in 0..m_s {
86        let (u, v) = sub.edge(e as EdgeId)?;
87        // Pairs touching vertices not in `orig` cannot match any orig
88        // pair anyway, but we still tally — the lookup below simply
89        // returns 0 for non-keys.
90        *count_sub.entry(canon(u, v)).or_insert(0) += 1;
91    }
92
93    // Walk orig's keys (already in lex order) and subtract sub's
94    // multiplicity, clamping to 0. Pairs unique to `sub` never appear
95    // in the output.
96    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
97    for (k, &co) in &count_orig {
98        let cs = count_sub.get(k).copied().unwrap_or(0);
99        let m = co.saturating_sub(cs);
100        for _ in 0..m {
101            edges.push(*k);
102        }
103    }
104
105    let mut out = Graph::new(n, directed)?;
106    out.add_edges(edges)?;
107    Ok(out)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    fn sorted_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
115        let m = u32::try_from(g.ecount()).unwrap();
116        let mut v: Vec<_> = (0..m).map(|e| g.edge(e).unwrap()).collect();
117        v.sort_unstable();
118        v
119    }
120
121    #[test]
122    fn empty_minus_empty() {
123        let a = Graph::with_vertices(0);
124        let b = Graph::with_vertices(0);
125        let d = difference(&a, &b).unwrap();
126        assert_eq!(d.vcount(), 0);
127        assert_eq!(d.ecount(), 0);
128        assert!(!d.is_directed());
129    }
130
131    #[test]
132    fn vcount_is_orig_only() {
133        // sub has more vertices than orig — result still uses orig.vcount().
134        let a = Graph::with_vertices(3);
135        let b = Graph::with_vertices(7);
136        let d = difference(&a, &b).unwrap();
137        assert_eq!(d.vcount(), 3);
138        assert_eq!(d.ecount(), 0);
139    }
140
141    #[test]
142    fn vcount_when_orig_larger() {
143        let a = Graph::with_vertices(7);
144        let b = Graph::with_vertices(3);
145        let d = difference(&a, &b).unwrap();
146        assert_eq!(d.vcount(), 7);
147    }
148
149    #[test]
150    fn triangle_minus_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 d = difference(&a, &b).unwrap();
159        assert_eq!(d.vcount(), 3);
160        assert_eq!(d.ecount(), 1);
161        assert_eq!(sorted_edges(&d), vec![(0, 2)]);
162    }
163
164    #[test]
165    fn self_difference_is_empty() {
166        // diff(a, a) ≡ empty edge set on a.vcount() vertices.
167        let mut a = Graph::with_vertices(4);
168        a.add_edge(0, 1).unwrap();
169        a.add_edge(1, 2).unwrap();
170        a.add_edge(0, 2).unwrap();
171        a.add_edge(0, 2).unwrap(); // multi-edge
172        let d = difference(&a, &a).unwrap();
173        assert_eq!(d.vcount(), 4);
174        assert_eq!(d.ecount(), 0);
175    }
176
177    #[test]
178    fn difference_with_empty_is_identity_edges() {
179        let mut a = Graph::with_vertices(3);
180        a.add_edge(0, 1).unwrap();
181        a.add_edge(1, 2).unwrap();
182        let b = Graph::with_vertices(3);
183        let d = difference(&a, &b).unwrap();
184        assert_eq!(d.vcount(), 3);
185        assert_eq!(sorted_edges(&d), vec![(0, 1), (1, 2)]);
186    }
187
188    #[test]
189    fn empty_minus_anything_is_empty() {
190        let a = Graph::with_vertices(3);
191        let mut b = Graph::with_vertices(3);
192        b.add_edge(0, 1).unwrap();
193        b.add_edge(1, 2).unwrap();
194        let d = difference(&a, &b).unwrap();
195        assert_eq!(d.vcount(), 3);
196        assert_eq!(d.ecount(), 0);
197    }
198
199    #[test]
200    fn pair_unique_to_sub_does_not_appear() {
201        // sub has (2,3) which orig lacks — must not appear in result.
202        let mut a = Graph::with_vertices(4);
203        a.add_edge(0, 1).unwrap();
204        let mut b = Graph::with_vertices(4);
205        b.add_edge(2, 3).unwrap();
206        let d = difference(&a, &b).unwrap();
207        assert_eq!(d.vcount(), 4);
208        assert_eq!(sorted_edges(&d), vec![(0, 1)]);
209    }
210
211    #[test]
212    fn multiplicity_clamps_to_zero() {
213        // orig: 2× (0,1); sub: 5× (0,1). Result: 0 edges.
214        let mut a = Graph::with_vertices(2);
215        a.add_edge(0, 1).unwrap();
216        a.add_edge(0, 1).unwrap();
217        let mut b = Graph::with_vertices(2);
218        for _ in 0..5 {
219            b.add_edge(0, 1).unwrap();
220        }
221        let d = difference(&a, &b).unwrap();
222        assert_eq!(d.ecount(), 0);
223    }
224
225    #[test]
226    fn multiplicity_partial_subtraction() {
227        // orig: 5× (0,1); sub: 2× (0,1). Result: 3× (0,1).
228        let mut a = Graph::with_vertices(2);
229        for _ in 0..5 {
230            a.add_edge(0, 1).unwrap();
231        }
232        let mut b = Graph::with_vertices(2);
233        b.add_edge(0, 1).unwrap();
234        b.add_edge(0, 1).unwrap();
235        let d = difference(&a, &b).unwrap();
236        assert_eq!(d.ecount(), 3);
237        assert!(sorted_edges(&d).iter().all(|&p| p == (0, 1)));
238    }
239
240    #[test]
241    fn directed_orientations_are_independent() {
242        // orig: 0→1 + 1→0; sub: 0→1 only. Result: 1→0.
243        let mut a = Graph::new(2, true).unwrap();
244        a.add_edge(0, 1).unwrap();
245        a.add_edge(1, 0).unwrap();
246        let mut b = Graph::new(2, true).unwrap();
247        b.add_edge(0, 1).unwrap();
248        let d = difference(&a, &b).unwrap();
249        assert!(d.is_directed());
250        assert_eq!(d.ecount(), 1);
251        assert_eq!(d.edge(0).unwrap(), (1, 0));
252    }
253
254    #[test]
255    fn directed_per_orientation_multiplicity() {
256        // orig: 4× (0→1), 2× (1→0); sub: 1× (0→1), 3× (1→0).
257        // Result: 3× (0→1), 0× (1→0) → 3 edges total.
258        let mut a = Graph::new(2, true).unwrap();
259        for _ in 0..4 {
260            a.add_edge(0, 1).unwrap();
261        }
262        for _ in 0..2 {
263            a.add_edge(1, 0).unwrap();
264        }
265        let mut b = Graph::new(2, true).unwrap();
266        b.add_edge(0, 1).unwrap();
267        for _ in 0..3 {
268            b.add_edge(1, 0).unwrap();
269        }
270        let d = difference(&a, &b).unwrap();
271        assert_eq!(d.ecount(), 3);
272        assert!(sorted_edges(&d).iter().all(|&p| p == (0, 1)));
273    }
274
275    #[test]
276    fn loops_subtract_correctly() {
277        // orig: 3× (0,0); sub: 1× (0,0). Result: 2× (0,0).
278        let mut a = Graph::with_vertices(1);
279        for _ in 0..3 {
280            a.add_edge(0, 0).unwrap();
281        }
282        let mut b = Graph::with_vertices(1);
283        b.add_edge(0, 0).unwrap();
284        let d = difference(&a, &b).unwrap();
285        assert_eq!(d.ecount(), 2);
286        assert!(sorted_edges(&d).iter().all(|&p| p == (0, 0)));
287    }
288
289    #[test]
290    fn mixed_directedness_errors() {
291        let a = Graph::with_vertices(2);
292        let b = Graph::new(2, true).unwrap();
293        assert!(difference(&a, &b).is_err());
294    }
295
296    #[test]
297    fn undirected_canonicalises_swapped_endpoints() {
298        // orig has (1,0); sub has (0,1). Both encode the same
299        // undirected pair → result is empty.
300        let mut a = Graph::with_vertices(2);
301        a.add_edge(1, 0).unwrap();
302        let mut b = Graph::with_vertices(2);
303        b.add_edge(0, 1).unwrap();
304        let d = difference(&a, &b).unwrap();
305        assert_eq!(d.ecount(), 0);
306    }
307
308    #[test]
309    fn sub_high_index_vertex_ignored() {
310        // sub has an edge (3,4) but orig only has 3 vertices, so the
311        // pair simply isn't a key in count_orig → ignored.
312        let mut a = Graph::with_vertices(3);
313        a.add_edge(0, 1).unwrap();
314        a.add_edge(1, 2).unwrap();
315        let mut b = Graph::with_vertices(5);
316        b.add_edge(0, 1).unwrap();
317        b.add_edge(3, 4).unwrap();
318        let d = difference(&a, &b).unwrap();
319        assert_eq!(d.vcount(), 3);
320        assert_eq!(sorted_edges(&d), vec![(1, 2)]);
321    }
322}