Skip to main content

rust_igraph/algorithms/operators/
permute_vertices.rs

1//! Vertex permutation operators (ALGO-OP-009).
2//!
3//! Creates a new graph by remapping vertex IDs according to a permutation.
4//! Also provides [`invert_permutation`] for computing the inverse of a
5//! permutation vector.
6
7use crate::core::error::IgraphError;
8use crate::core::{Graph, IgraphResult, VertexId};
9
10/// Invert a permutation vector.
11///
12/// Given a permutation `p` of `[0, n)`, returns the inverse permutation
13/// `inv` such that `inv[p[i]] == i` for all `i`. The function also
14/// validates that `p` is a proper permutation (no out-of-range values,
15/// no duplicates).
16///
17/// Counterpart of `igraph_invert_permutation` from
18/// `references/igraph/src/operators/permute.c:39-58`.
19///
20/// # Errors
21///
22/// * [`IgraphError::InvalidArgument`] — some entry is out of range
23///   `[0, n)`, or the vector contains duplicate entries.
24///
25/// # Examples
26///
27/// ```
28/// use rust_igraph::invert_permutation;
29///
30/// let perm = [2, 0, 1];
31/// let inv = invert_permutation(&perm).unwrap();
32/// assert_eq!(inv, vec![1, 2, 0]);
33/// // Verify: inv[perm[i]] == i
34/// for i in 0..3 {
35///     assert_eq!(inv[perm[i] as usize], i as u32);
36/// }
37/// ```
38pub fn invert_permutation(permutation: &[VertexId]) -> IgraphResult<Vec<VertexId>> {
39    let n = permutation.len();
40    let n_u32 = u32::try_from(n).map_err(|_| {
41        IgraphError::InvalidArgument("invert_permutation: length overflows u32".to_string())
42    })?;
43
44    let mut inverse = vec![u32::MAX; n];
45
46    for (i, &j) in permutation.iter().enumerate() {
47        if j >= n_u32 {
48            return Err(IgraphError::InvalidArgument(format!(
49                "invert_permutation: invalid index {j} in permutation (must be < {n_u32})"
50            )));
51        }
52        if inverse[j as usize] != u32::MAX {
53            return Err(IgraphError::InvalidArgument(format!(
54                "invert_permutation: duplicate entry {j} in permutation"
55            )));
56        }
57        #[allow(clippy::cast_possible_truncation)]
58        {
59            inverse[j as usize] = i as u32;
60        }
61    }
62
63    Ok(inverse)
64}
65
66/// Creates a new graph with vertices permuted according to the given mapping.
67///
68/// `permutation[i]` specifies which old vertex becomes new vertex `i`.
69/// In other words, new vertex `i` gets all edges that old vertex
70/// `permutation[i]` had, with endpoints remapped accordingly.
71///
72/// The permutation must be a valid bijection on `[0, n)`.
73///
74/// # Arguments
75///
76/// * `graph` — the input graph.
77/// * `permutation` — slice of length `n` where `permutation[new] = old`.
78///
79/// # Errors
80///
81/// Returns `InvalidArgument` if:
82/// - `permutation.len() != graph.vcount()`
83/// - Any value is out of range `[0, n)`
84/// - Any value appears more than once (not a valid permutation)
85///
86/// # Examples
87///
88/// ```
89/// use rust_igraph::{Graph, permute_vertices};
90///
91/// let mut g = Graph::with_vertices(3);
92/// g.add_edge(0, 1).unwrap();
93/// g.add_edge(1, 2).unwrap();
94///
95/// // Reverse the vertex ordering: new 0 = old 2, new 1 = old 1, new 2 = old 0
96/// let perm = [2, 1, 0];
97/// let pg = permute_vertices(&g, &perm).unwrap();
98/// assert_eq!(pg.vcount(), 3);
99/// assert_eq!(pg.ecount(), 2);
100/// ```
101pub fn permute_vertices(graph: &Graph, permutation: &[VertexId]) -> IgraphResult<Graph> {
102    let n = graph.vcount();
103
104    if permutation.len() != n as usize {
105        return Err(IgraphError::InvalidArgument(format!(
106            "permutation length {} does not match vertex count {n}",
107            permutation.len()
108        )));
109    }
110
111    // Validate permutation and build inverse (old → new)
112    let mut inverse = vec![u32::MAX; n as usize];
113    for (new_id, &old_id) in permutation.iter().enumerate() {
114        if old_id >= n {
115            return Err(IgraphError::InvalidArgument(format!(
116                "permutation contains invalid vertex id {old_id} (graph has {n} vertices)"
117            )));
118        }
119        if inverse[old_id as usize] != u32::MAX {
120            return Err(IgraphError::InvalidArgument(format!(
121                "duplicate entry {old_id} in permutation"
122            )));
123        }
124        #[allow(clippy::cast_possible_truncation)]
125        let new_id_u32 = new_id as u32;
126        inverse[old_id as usize] = new_id_u32;
127    }
128
129    // Build new edge list with remapped vertex IDs
130    let ecount = graph.ecount();
131    let directed = graph.is_directed();
132    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
133
134    for eid in 0..ecount {
135        #[allow(clippy::cast_possible_truncation)]
136        let eid_u32 = eid as u32;
137        let (src, tgt) = graph.edge(eid_u32)?;
138        let new_src = inverse[src as usize];
139        let new_tgt = inverse[tgt as usize];
140        edges.push((new_src, new_tgt));
141    }
142
143    let mut result = Graph::new(n, directed)?;
144    result.add_edges(edges)?;
145    Ok(result)
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_identity_permutation() {
154        let mut g = Graph::with_vertices(4);
155        g.add_edge(0, 1).unwrap();
156        g.add_edge(1, 2).unwrap();
157        g.add_edge(2, 3).unwrap();
158
159        let pg = permute_vertices(&g, &[0, 1, 2, 3]).unwrap();
160        assert_eq!(pg.vcount(), 4);
161        assert_eq!(pg.ecount(), 3);
162        for eid in 0..3u32 {
163            assert_eq!(g.edge(eid).unwrap(), pg.edge(eid).unwrap());
164        }
165    }
166
167    #[test]
168    fn test_reverse_permutation() {
169        let mut g = Graph::with_vertices(3);
170        g.add_edge(0, 1).unwrap();
171        g.add_edge(1, 2).unwrap();
172
173        // new vertex 0 = old vertex 2, new vertex 1 = old vertex 1, new vertex 2 = old vertex 0
174        let pg = permute_vertices(&g, &[2, 1, 0]).unwrap();
175        assert_eq!(pg.vcount(), 3);
176        assert_eq!(pg.ecount(), 2);
177        // Old edge (0,1) → new edge (2,1) → stored as (1,2) (undirected canonical)
178        assert_eq!(pg.edge(0).unwrap(), (1, 2));
179        // Old edge (1,2) → new edge (1,0) → stored as (0,1)
180        assert_eq!(pg.edge(1).unwrap(), (0, 1));
181    }
182
183    #[test]
184    fn test_directed() {
185        let mut g = Graph::new(3, true).unwrap();
186        g.add_edge(0, 1).unwrap();
187        g.add_edge(1, 2).unwrap();
188
189        let pg = permute_vertices(&g, &[2, 0, 1]).unwrap();
190        assert!(pg.is_directed());
191        // old 0 → new 1, old 1 → new 2, old 2 → new 0 (inverse of [2,0,1])
192        // Edge (0,1): new = (1,2)
193        assert_eq!(pg.edge(0).unwrap(), (1, 2));
194        // Edge (1,2): new = (2,0)
195        assert_eq!(pg.edge(1).unwrap(), (2, 0));
196    }
197
198    #[test]
199    fn test_empty_graph() {
200        let g = Graph::with_vertices(0);
201        let pg = permute_vertices(&g, &[]).unwrap();
202        assert_eq!(pg.vcount(), 0);
203    }
204
205    #[test]
206    fn test_wrong_length() {
207        let g = Graph::with_vertices(3);
208        let result = permute_vertices(&g, &[0, 1]);
209        assert!(result.is_err());
210    }
211
212    #[test]
213    fn test_out_of_range() {
214        let g = Graph::with_vertices(3);
215        let result = permute_vertices(&g, &[0, 1, 5]);
216        assert!(result.is_err());
217    }
218
219    #[test]
220    fn test_duplicate() {
221        let g = Graph::with_vertices(3);
222        let result = permute_vertices(&g, &[0, 1, 1]);
223        assert!(result.is_err());
224    }
225
226    #[test]
227    fn test_self_loop() {
228        let mut g = Graph::with_vertices(3);
229        g.add_edge(1, 1).unwrap();
230
231        let pg = permute_vertices(&g, &[2, 0, 1]).unwrap();
232        // old 1 → new 2, self-loop (1,1) becomes (2,2)
233        // inverse: old 0 → new 1, old 1 → new 2, old 2 → new 0
234        assert_eq!(pg.edge(0).unwrap(), (2, 2));
235    }
236
237    #[test]
238    fn test_cycle_permutation() {
239        let mut g = Graph::with_vertices(4);
240        g.add_edge(0, 1).unwrap();
241        g.add_edge(1, 2).unwrap();
242        g.add_edge(2, 3).unwrap();
243        g.add_edge(3, 0).unwrap();
244
245        // Cyclic shift: perm[new] = old → new 0 = old 1, new 1 = old 2, ...
246        let pg = permute_vertices(&g, &[1, 2, 3, 0]).unwrap();
247        assert_eq!(pg.vcount(), 4);
248        assert_eq!(pg.ecount(), 4);
249        // inverse: old 0 → new 3, old 1 → new 0, old 2 → new 1, old 3 → new 2
250        // Edge (0,1) → (3, 0) → stored as (0, 3) (undirected canonical)
251        assert_eq!(pg.edge(0).unwrap(), (0, 3));
252        // Edge (1,2) → (0, 1) → stored as (0, 1)
253        assert_eq!(pg.edge(1).unwrap(), (0, 1));
254    }
255
256    // ── invert_permutation tests ────────────────────────────────────
257
258    #[test]
259    fn invert_identity() {
260        let inv = invert_permutation(&[0, 1, 2, 3]).unwrap();
261        assert_eq!(inv, vec![0, 1, 2, 3]);
262    }
263
264    #[test]
265    fn invert_reverse() {
266        let inv = invert_permutation(&[2, 1, 0]).unwrap();
267        assert_eq!(inv, vec![2, 1, 0]);
268    }
269
270    #[test]
271    fn invert_cycle() {
272        let inv = invert_permutation(&[1, 2, 0]).unwrap();
273        assert_eq!(inv, vec![2, 0, 1]);
274    }
275
276    #[test]
277    fn invert_empty() {
278        let inv = invert_permutation(&[]).unwrap();
279        assert!(inv.is_empty());
280    }
281
282    #[test]
283    fn invert_single() {
284        let inv = invert_permutation(&[0]).unwrap();
285        assert_eq!(inv, vec![0]);
286    }
287
288    #[test]
289    fn invert_roundtrip() {
290        let perm = [3, 0, 2, 1];
291        let inv = invert_permutation(&perm).unwrap();
292        let inv2 = invert_permutation(&inv).unwrap();
293        assert_eq!(inv2, perm.to_vec());
294    }
295
296    #[test]
297    fn invert_out_of_range() {
298        let err = invert_permutation(&[0, 5, 1]).unwrap_err();
299        assert!(matches!(err, IgraphError::InvalidArgument(_)));
300    }
301
302    #[test]
303    fn invert_duplicate() {
304        let err = invert_permutation(&[0, 1, 1]).unwrap_err();
305        assert!(matches!(err, IgraphError::InvalidArgument(_)));
306    }
307}