Skip to main content

rust_igraph/algorithms/operators/
rewire.rs

1//! Degree-preserving edge rewiring (ALGO-OP-016).
2//!
3//! Randomly rewires edges while preserving each vertex's degree. Uses
4//! the switching algorithm: pick two edges, swap endpoints if the swap
5//! doesn't violate constraints (no multi-edges, optionally no self-loops).
6
7use std::collections::HashSet;
8
9use crate::core::{Graph, IgraphResult, VertexId};
10
11/// Randomly rewires a graph while preserving its degree sequence.
12///
13/// Performs `num_trials` switching attempts. Each trial picks two random
14/// edges `(a, b)` and `(c, d)` and attempts to replace them with `(a, d)`
15/// and `(c, b)`. The swap is rejected if it would create a multi-edge or
16/// (when `loops` is false) a self-loop.
17///
18/// Returns a new graph; the original is not modified.
19///
20/// # Arguments
21///
22/// * `graph` — the input graph.
23/// * `num_trials` — number of rewiring trials (not all succeed).
24/// * `loops` — if `true`, self-loops may be created; if `false`, swaps
25///   that would create self-loops are rejected.
26/// * `seed` — random seed for deterministic output.
27///
28/// # Examples
29///
30/// ```
31/// use rust_igraph::{Graph, rewire};
32///
33/// let mut g = Graph::with_vertices(6);
34/// g.add_edge(0, 1).unwrap();
35/// g.add_edge(1, 2).unwrap();
36/// g.add_edge(2, 3).unwrap();
37/// g.add_edge(3, 4).unwrap();
38/// g.add_edge(4, 5).unwrap();
39///
40/// let rg = rewire(&g, 100, false, 42).unwrap();
41/// assert_eq!(rg.vcount(), 6);
42/// assert_eq!(rg.ecount(), 5);
43/// // Degree sequence is preserved
44/// ```
45pub fn rewire(graph: &Graph, num_trials: usize, loops: bool, seed: u64) -> IgraphResult<Graph> {
46    let n = graph.vcount();
47    let ecount = graph.ecount();
48    let directed = graph.is_directed();
49
50    if ecount < 2 {
51        return copy_graph(graph);
52    }
53
54    // Collect edges as mutable array
55    let mut edge_list: Vec<[VertexId; 2]> = Vec::with_capacity(ecount);
56    for eid in 0..ecount {
57        #[allow(clippy::cast_possible_truncation)]
58        let (src, tgt) = graph.edge(eid as u32)?;
59        edge_list.push([src, tgt]);
60    }
61
62    // Build edge set for O(1) existence checks
63    let mut edge_set: HashSet<(VertexId, VertexId)> = HashSet::with_capacity(ecount);
64    for e in &edge_list {
65        edge_set.insert(canonical_edge(e[0], e[1], directed));
66    }
67
68    let mut rng_state = seed;
69
70    for _ in 0..num_trials {
71        // Pick two distinct random edge indices
72        let idx1 = random_index(&mut rng_state, ecount);
73        let mut idx2 = random_index(&mut rng_state, ecount - 1);
74        if idx2 >= idx1 {
75            idx2 += 1;
76        }
77
78        let src1 = edge_list[idx1][0];
79        let tgt1 = edge_list[idx1][1];
80        let (src2, tgt2) = if !directed && random_bool(&mut rng_state) {
81            (edge_list[idx2][1], edge_list[idx2][0])
82        } else {
83            (edge_list[idx2][0], edge_list[idx2][1])
84        };
85
86        // Check: skip loops if not allowed
87        if !loops && (src1 == tgt1 || src2 == tgt2) {
88            continue;
89        }
90
91        // Check: swap would have no effect
92        if src1 == src2 || tgt1 == tgt2 {
93            continue;
94        }
95
96        // Check: swap would create a self-loop
97        if !loops && (src1 == tgt2 || tgt1 == src2) {
98            continue;
99        }
100
101        // For undirected: if both are self-loops, swap creates multi-edge
102        if !directed && src1 == tgt1 && src2 == tgt2 {
103            continue;
104        }
105
106        // Check: new edges don't already exist (no multi-edges)
107        let new_edge1 = canonical_edge(src1, tgt2, directed);
108        let new_edge2 = canonical_edge(src2, tgt1, directed);
109
110        if edge_set.contains(&new_edge1) {
111            continue;
112        }
113        if edge_set.contains(&new_edge2) {
114            continue;
115        }
116        if new_edge1 == new_edge2 {
117            continue;
118        }
119
120        // Perform the swap
121        let old_edge1 = canonical_edge(src1, tgt1, directed);
122        let old_edge2 = canonical_edge(src2, tgt2, directed);
123
124        edge_set.remove(&old_edge1);
125        edge_set.remove(&old_edge2);
126        edge_set.insert(new_edge1);
127        edge_set.insert(new_edge2);
128
129        edge_list[idx1] = [src1, tgt2];
130        edge_list[idx2] = [src2, tgt1];
131    }
132
133    // Build result graph
134    let edges: Vec<(VertexId, VertexId)> = edge_list.iter().map(|e| (e[0], e[1])).collect();
135    let mut result = Graph::new(n, directed)?;
136    result.add_edges(edges)?;
137    Ok(result)
138}
139
140fn canonical_edge(src: VertexId, tgt: VertexId, directed: bool) -> (VertexId, VertexId) {
141    if directed || src <= tgt {
142        (src, tgt)
143    } else {
144        (tgt, src)
145    }
146}
147
148fn copy_graph(graph: &Graph) -> IgraphResult<Graph> {
149    let n = graph.vcount();
150    let ecount = graph.ecount();
151    let directed = graph.is_directed();
152    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
153    for eid in 0..ecount {
154        #[allow(clippy::cast_possible_truncation)]
155        edges.push(graph.edge(eid as u32)?);
156    }
157    let mut result = Graph::new(n, directed)?;
158    result.add_edges(edges)?;
159    Ok(result)
160}
161
162fn splitmix64(state: &mut u64) -> u64 {
163    *state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
164    let mut z = *state;
165    z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
166    z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
167    z ^ (z >> 31)
168}
169
170fn random_index(state: &mut u64, n: usize) -> usize {
171    let r = splitmix64(state);
172    #[allow(clippy::cast_possible_truncation)]
173    {
174        (r as usize) % n
175    }
176}
177
178fn random_bool(state: &mut u64) -> bool {
179    splitmix64(state) & 1 == 1
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    fn degree_sequence(graph: &Graph) -> Vec<u32> {
187        let n = graph.vcount();
188        let mut deg = vec![0u32; n as usize];
189        let ecount = graph.ecount();
190        for eid in 0..ecount {
191            #[allow(clippy::cast_possible_truncation)]
192            let (src, tgt) = graph.edge(eid as u32).unwrap();
193            deg[src as usize] += 1;
194            if graph.is_directed() || src != tgt {
195                deg[tgt as usize] += 1;
196            }
197        }
198        deg
199    }
200
201    #[test]
202    fn test_preserves_degree_sequence() {
203        let mut g = Graph::with_vertices(10);
204        g.add_edge(0, 1).unwrap();
205        g.add_edge(1, 2).unwrap();
206        g.add_edge(2, 3).unwrap();
207        g.add_edge(3, 4).unwrap();
208        g.add_edge(4, 5).unwrap();
209        g.add_edge(5, 6).unwrap();
210        g.add_edge(6, 7).unwrap();
211        g.add_edge(7, 8).unwrap();
212        g.add_edge(8, 9).unwrap();
213
214        let original_deg = degree_sequence(&g);
215        let rg = rewire(&g, 200, false, 42).unwrap();
216        let rewired_deg = degree_sequence(&rg);
217
218        assert_eq!(original_deg, rewired_deg);
219        assert_eq!(rg.ecount(), g.ecount());
220    }
221
222    #[test]
223    fn test_preserves_edge_count() {
224        let mut g = Graph::with_vertices(8);
225        g.add_edge(0, 1).unwrap();
226        g.add_edge(1, 2).unwrap();
227        g.add_edge(2, 3).unwrap();
228        g.add_edge(3, 0).unwrap();
229        g.add_edge(4, 5).unwrap();
230        g.add_edge(5, 6).unwrap();
231        g.add_edge(6, 7).unwrap();
232        g.add_edge(7, 4).unwrap();
233
234        let rg = rewire(&g, 100, false, 99).unwrap();
235        assert_eq!(rg.ecount(), 8);
236    }
237
238    #[test]
239    fn test_no_self_loops() {
240        let mut g = Graph::with_vertices(6);
241        g.add_edge(0, 1).unwrap();
242        g.add_edge(1, 2).unwrap();
243        g.add_edge(2, 3).unwrap();
244        g.add_edge(3, 4).unwrap();
245        g.add_edge(4, 5).unwrap();
246        g.add_edge(5, 0).unwrap();
247
248        let rg = rewire(&g, 500, false, 123).unwrap();
249        for eid in 0..rg.ecount() {
250            #[allow(clippy::cast_possible_truncation)]
251            let (s, t) = rg.edge(eid as u32).unwrap();
252            assert_ne!(s, t, "self-loop at edge {eid}");
253        }
254    }
255
256    #[test]
257    fn test_deterministic() {
258        let mut g = Graph::with_vertices(10);
259        for i in 0..9u32 {
260            g.add_edge(i, i + 1).unwrap();
261        }
262
263        let r1 = rewire(&g, 50, false, 42).unwrap();
264        let r2 = rewire(&g, 50, false, 42).unwrap();
265
266        for eid in 0..r1.ecount() {
267            #[allow(clippy::cast_possible_truncation)]
268            let e = eid as u32;
269            assert_eq!(r1.edge(e).unwrap(), r2.edge(e).unwrap());
270        }
271    }
272
273    #[test]
274    fn test_different_seeds() {
275        let mut g = Graph::with_vertices(20);
276        for i in 0..19u32 {
277            g.add_edge(i, i + 1).unwrap();
278        }
279        g.add_edge(19, 0).unwrap();
280
281        let r1 = rewire(&g, 200, false, 1).unwrap();
282        let r2 = rewire(&g, 200, false, 2).unwrap();
283
284        let mut differ = false;
285        for eid in 0..r1.ecount() {
286            #[allow(clippy::cast_possible_truncation)]
287            let e = eid as u32;
288            if r1.edge(e).unwrap() != r2.edge(e).unwrap() {
289                differ = true;
290                break;
291            }
292        }
293        assert!(differ, "different seeds should produce different results");
294    }
295
296    #[test]
297    fn test_single_edge() {
298        let mut g = Graph::with_vertices(3);
299        g.add_edge(0, 1).unwrap();
300
301        let rg = rewire(&g, 100, false, 42).unwrap();
302        assert_eq!(rg.ecount(), 1);
303        assert_eq!(rg.edge(0).unwrap(), (0, 1));
304    }
305
306    #[test]
307    fn test_zero_trials() {
308        let mut g = Graph::with_vertices(5);
309        g.add_edge(0, 1).unwrap();
310        g.add_edge(1, 2).unwrap();
311        g.add_edge(2, 3).unwrap();
312
313        let rg = rewire(&g, 0, false, 42).unwrap();
314        for eid in 0..3u32 {
315            assert_eq!(rg.edge(eid).unwrap(), g.edge(eid).unwrap());
316        }
317    }
318
319    #[test]
320    fn test_directed() {
321        let mut g = Graph::new(6, true).unwrap();
322        g.add_edge(0, 1).unwrap();
323        g.add_edge(1, 2).unwrap();
324        g.add_edge(2, 3).unwrap();
325        g.add_edge(3, 4).unwrap();
326        g.add_edge(4, 5).unwrap();
327
328        let original_deg = degree_sequence(&g);
329        let rg = rewire(&g, 100, false, 77).unwrap();
330        assert!(rg.is_directed());
331        assert_eq!(rg.ecount(), 5);
332        assert_eq!(degree_sequence(&rg), original_deg);
333    }
334
335    #[test]
336    fn test_empty_graph() {
337        let g = Graph::with_vertices(0);
338        let rg = rewire(&g, 100, false, 42).unwrap();
339        assert_eq!(rg.vcount(), 0);
340        assert_eq!(rg.ecount(), 0);
341    }
342
343    #[test]
344    fn test_no_multi_edges() {
345        let mut g = Graph::with_vertices(4);
346        g.add_edge(0, 1).unwrap();
347        g.add_edge(1, 2).unwrap();
348        g.add_edge(2, 3).unwrap();
349        g.add_edge(3, 0).unwrap();
350
351        let rg = rewire(&g, 1000, false, 42).unwrap();
352
353        // Check no multi-edges
354        let mut seen = HashSet::new();
355        for eid in 0..rg.ecount() {
356            #[allow(clippy::cast_possible_truncation)]
357            let (s, t) = rg.edge(eid as u32).unwrap();
358            let key = if s <= t { (s, t) } else { (t, s) };
359            assert!(seen.insert(key), "multi-edge found: {key:?}");
360        }
361    }
362
363    #[test]
364    fn test_loops_allowed() {
365        let mut g = Graph::with_vertices(4);
366        g.add_edge(0, 1).unwrap();
367        g.add_edge(1, 2).unwrap();
368        g.add_edge(2, 3).unwrap();
369        g.add_edge(3, 0).unwrap();
370
371        // With loops and enough trials, some self-loops may appear
372        let rg = rewire(&g, 1000, true, 42).unwrap();
373        assert_eq!(rg.ecount(), 4);
374    }
375}