Skip to main content

rust_igraph/algorithms/operators/
contract_vertices.rs

1//! Vertex contraction operator (ALGO-OP-010).
2//!
3//! Merges groups of vertices into single vertices, remapping edges.
4
5use crate::core::error::IgraphError;
6use crate::core::{Graph, IgraphResult, VertexId};
7
8/// Contracts vertices according to a grouping, merging edges.
9///
10/// Each vertex `v` is mapped to the group `mapping[v]`. All vertices in the
11/// same group are merged into a single vertex in the result graph. Edges are
12/// remapped accordingly — this typically produces self-loops (from in-group
13/// edges) and multi-edges (from multiple inter-group connections). Use
14/// [`crate::simplify`] afterwards to remove these if desired.
15///
16/// The number of vertices in the result equals `max(mapping) + 1`. To avoid
17/// orphan vertices (IDs with no corresponding input vertex), use consecutive
18/// integers starting from zero.
19///
20/// # Arguments
21///
22/// * `graph` — the input graph.
23/// * `mapping` — slice of length `vcount` where `mapping[old] = new_group`.
24///
25/// # Errors
26///
27/// Returns `InvalidArgument` if `mapping.len() != graph.vcount()`.
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, contract_vertices};
33///
34/// let mut g = Graph::with_vertices(4);
35/// g.add_edge(0, 1).unwrap();
36/// g.add_edge(1, 2).unwrap();
37/// g.add_edge(2, 3).unwrap();
38///
39/// // Merge vertices 0+1 → group 0, vertices 2+3 → group 1
40/// let mapping = [0, 0, 1, 1];
41/// let cg = contract_vertices(&g, &mapping).unwrap();
42/// assert_eq!(cg.vcount(), 2);
43/// // Edge (0,1) → self-loop on 0, edge (1,2) → (0,1), edge (2,3) → self-loop on 1
44/// assert_eq!(cg.ecount(), 3);
45/// ```
46pub fn contract_vertices(graph: &Graph, mapping: &[VertexId]) -> IgraphResult<Graph> {
47    let n = graph.vcount();
48
49    if mapping.len() != n as usize {
50        return Err(IgraphError::InvalidArgument(format!(
51            "mapping length {} does not match vertex count {n}",
52            mapping.len()
53        )));
54    }
55
56    // Determine number of vertices in the result
57    let new_vcount = if n == 0 {
58        0u32
59    } else {
60        mapping.iter().copied().max().unwrap_or(0) + 1
61    };
62
63    // Remap all edges
64    let ecount = graph.ecount();
65    let directed = graph.is_directed();
66    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
67
68    for eid in 0..ecount {
69        #[allow(clippy::cast_possible_truncation)]
70        let eid_u32 = eid as u32;
71        let (src, tgt) = graph.edge(eid_u32)?;
72        let new_src = mapping[src as usize];
73        let new_tgt = mapping[tgt as usize];
74        edges.push((new_src, new_tgt));
75    }
76
77    let mut result = Graph::new(new_vcount, directed)?;
78    result.add_edges(edges)?;
79    Ok(result)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_basic_contraction() {
88        let mut g = Graph::with_vertices(4);
89        g.add_edge(0, 1).unwrap();
90        g.add_edge(1, 2).unwrap();
91        g.add_edge(2, 3).unwrap();
92
93        // Merge 0+1 → 0, 2+3 → 1
94        let cg = contract_vertices(&g, &[0, 0, 1, 1]).unwrap();
95        assert_eq!(cg.vcount(), 2);
96        assert_eq!(cg.ecount(), 3);
97    }
98
99    #[test]
100    fn test_identity_mapping() {
101        let mut g = Graph::with_vertices(3);
102        g.add_edge(0, 1).unwrap();
103        g.add_edge(1, 2).unwrap();
104
105        let cg = contract_vertices(&g, &[0, 1, 2]).unwrap();
106        assert_eq!(cg.vcount(), 3);
107        assert_eq!(cg.ecount(), 2);
108        for eid in 0..2u32 {
109            assert_eq!(g.edge(eid).unwrap(), cg.edge(eid).unwrap());
110        }
111    }
112
113    #[test]
114    fn test_all_into_one() {
115        let mut g = Graph::with_vertices(4);
116        g.add_edge(0, 1).unwrap();
117        g.add_edge(1, 2).unwrap();
118        g.add_edge(2, 3).unwrap();
119        g.add_edge(3, 0).unwrap();
120
121        // All vertices merge into group 0
122        let cg = contract_vertices(&g, &[0, 0, 0, 0]).unwrap();
123        assert_eq!(cg.vcount(), 1);
124        assert_eq!(cg.ecount(), 4); // all become self-loops
125    }
126
127    #[test]
128    fn test_directed() {
129        let mut g = Graph::new(4, true).unwrap();
130        g.add_edge(0, 1).unwrap();
131        g.add_edge(2, 3).unwrap();
132
133        // Merge 0+2 → 0, 1+3 → 1
134        let cg = contract_vertices(&g, &[0, 1, 0, 1]).unwrap();
135        assert!(cg.is_directed());
136        assert_eq!(cg.vcount(), 2);
137        assert_eq!(cg.ecount(), 2);
138        assert_eq!(cg.edge(0).unwrap(), (0, 1));
139        assert_eq!(cg.edge(1).unwrap(), (0, 1));
140    }
141
142    #[test]
143    fn test_empty_graph() {
144        let g = Graph::with_vertices(0);
145        let cg = contract_vertices(&g, &[]).unwrap();
146        assert_eq!(cg.vcount(), 0);
147        assert_eq!(cg.ecount(), 0);
148    }
149
150    #[test]
151    fn test_no_edges() {
152        let g = Graph::with_vertices(5);
153        let cg = contract_vertices(&g, &[0, 0, 1, 1, 2]).unwrap();
154        assert_eq!(cg.vcount(), 3);
155        assert_eq!(cg.ecount(), 0);
156    }
157
158    #[test]
159    fn test_wrong_length() {
160        let g = Graph::with_vertices(3);
161        let result = contract_vertices(&g, &[0, 1]);
162        assert!(result.is_err());
163    }
164
165    #[test]
166    fn test_self_loop_preserved() {
167        let mut g = Graph::with_vertices(3);
168        g.add_edge(1, 1).unwrap();
169
170        let cg = contract_vertices(&g, &[0, 0, 1]).unwrap();
171        assert_eq!(cg.vcount(), 2);
172        assert_eq!(cg.ecount(), 1);
173        assert_eq!(cg.edge(0).unwrap(), (0, 0));
174    }
175
176    #[test]
177    fn test_non_consecutive_mapping() {
178        // mapping values don't need to cover all IDs in [0, max]
179        let mut g = Graph::with_vertices(3);
180        g.add_edge(0, 1).unwrap();
181        g.add_edge(1, 2).unwrap();
182
183        // Map to 0, 5, 5 — result has 6 vertices (max+1 = 6)
184        let cg = contract_vertices(&g, &[0, 5, 5]).unwrap();
185        assert_eq!(cg.vcount(), 6);
186        assert_eq!(cg.ecount(), 2);
187    }
188}