Skip to main content

rust_igraph/algorithms/operators/
rewire_edges.rs

1//! Probabilistic edge rewiring (ALGO-OP-012, ALGO-OP-028).
2//!
3//! Rewires each edge endpoint with a given probability.
4//! Also provides directed-edge rewiring that preserves either
5//! the in-degree or out-degree sequence.
6
7use crate::core::{Graph, IgraphResult, VertexId};
8
9/// Rewires graph edges with constant probability.
10///
11/// Each endpoint of each edge is independently rewired to a uniformly
12/// random vertex with probability `prob`. The result may contain self-loops
13/// and multi-edges depending on the `loops` parameter.
14///
15/// Uses the provided RNG seed for reproducibility.
16///
17/// # Arguments
18///
19/// * `graph` — the input graph.
20/// * `prob` — rewiring probability in `[0.0, 1.0]`.
21/// * `loops` — if `true`, rewired edges may form self-loops; otherwise,
22///   the new endpoint is guaranteed different from the other endpoint.
23/// * `seed` — random seed for deterministic output.
24///
25/// # Examples
26///
27/// ```
28/// use rust_igraph::{Graph, rewire_edges};
29///
30/// let mut g = Graph::with_vertices(10);
31/// for i in 0..9u32 {
32///     g.add_edge(i, i + 1).unwrap();
33/// }
34///
35/// let rg = rewire_edges(&g, 0.5, false, 42).unwrap();
36/// assert_eq!(rg.vcount(), 10);
37/// assert_eq!(rg.ecount(), 9);
38/// ```
39pub fn rewire_edges(graph: &Graph, prob: f64, loops: bool, seed: u64) -> IgraphResult<Graph> {
40    let n = graph.vcount();
41    let directed = graph.is_directed();
42    let ecount = graph.ecount();
43
44    if n == 0 || ecount == 0 || prob == 0.0 {
45        // Return a structural copy
46        let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
47        for eid in 0..ecount {
48            #[allow(clippy::cast_possible_truncation)]
49            let eid_u32 = eid as u32;
50            edges.push(graph.edge(eid_u32)?);
51        }
52        let mut result = Graph::new(n, directed)?;
53        result.add_edges(edges)?;
54        return Ok(result);
55    }
56
57    // Collect edge list
58    let mut edge_list: Vec<[VertexId; 2]> = Vec::with_capacity(ecount);
59    for eid in 0..ecount {
60        #[allow(clippy::cast_possible_truncation)]
61        let eid_u32 = eid as u32;
62        let (src, tgt) = graph.edge(eid_u32)?;
63        edge_list.push([src, tgt]);
64    }
65
66    // Simple splitmix64 RNG for reproducibility
67    let mut rng_state = seed;
68
69    let endpoints = ecount * 2;
70    // Use geometric distribution to skip to next rewired endpoint
71    let mut to_rewire = geom_sample(&mut rng_state, prob);
72
73    while to_rewire < endpoints {
74        let edge_idx = to_rewire / 2;
75        let endpoint_idx = to_rewire % 2; // 0 = src, 1 = tgt
76
77        let other_idx = 1 - endpoint_idx;
78        let other_vertex = edge_list[edge_idx][other_idx];
79
80        let new_vertex = if loops {
81            random_vertex(&mut rng_state, n)
82        } else {
83            random_vertex_excluding(&mut rng_state, n, other_vertex)
84        };
85
86        edge_list[edge_idx][endpoint_idx] = new_vertex;
87        to_rewire += geom_sample(&mut rng_state, prob) + 1;
88    }
89
90    // Build result graph
91    let edges: Vec<(VertexId, VertexId)> = edge_list.iter().map(|e| (e[0], e[1])).collect();
92
93    let mut result = Graph::new(n, directed)?;
94    result.add_edges(edges)?;
95    Ok(result)
96}
97
98/// Which endpoint of a directed edge to rewire.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum RewireDirectedMode {
101    /// Rewire the target (head) of each edge; preserves out-degree sequence.
102    Out,
103    /// Rewire the source (tail) of each edge; preserves in-degree sequence.
104    In,
105}
106
107/// Rewires one endpoint of directed edges with constant probability.
108///
109/// For directed graphs, rewires either the source or target of each edge
110/// independently with probability `prob`. This preserves the in-degree
111/// sequence (when rewiring targets, i.e. `Out` mode) or the out-degree
112/// sequence (when rewiring sources, i.e. `In` mode).
113///
114/// For undirected graphs, falls back to [`rewire_edges`] which rewires
115/// both endpoints.
116///
117/// # Arguments
118///
119/// * `graph` — the input graph.
120/// * `prob` — rewiring probability in `[0.0, 1.0]`.
121/// * `loops` — if `true`, rewired edges may form self-loops.
122/// * `mode` — which endpoint to rewire (`Out` = target, `In` = source).
123/// * `seed` — random seed for deterministic output.
124///
125/// # Examples
126///
127/// ```
128/// use rust_igraph::{Graph, rewire_directed_edges, RewireDirectedMode};
129///
130/// let mut g = Graph::new(5, true).unwrap();
131/// g.add_edge(0, 1).unwrap();
132/// g.add_edge(1, 2).unwrap();
133/// g.add_edge(2, 3).unwrap();
134///
135/// let rg = rewire_directed_edges(&g, 0.5, false, RewireDirectedMode::Out, 42).unwrap();
136/// assert_eq!(rg.vcount(), 5);
137/// assert_eq!(rg.ecount(), 3);
138/// assert!(rg.is_directed());
139/// ```
140pub fn rewire_directed_edges(
141    graph: &Graph,
142    prob: f64,
143    loops: bool,
144    mode: RewireDirectedMode,
145    seed: u64,
146) -> IgraphResult<Graph> {
147    use crate::core::error::IgraphError;
148
149    if !(0.0..=1.0).contains(&prob) {
150        return Err(IgraphError::InvalidArgument(
151            "rewiring probability must be in [0.0, 1.0]".to_string(),
152        ));
153    }
154
155    // For undirected graphs, fall back to rewire_edges (rewires both endpoints)
156    if !graph.is_directed() {
157        return rewire_edges(graph, prob, loops, seed);
158    }
159
160    let n = graph.vcount();
161    let ecount = graph.ecount();
162
163    if n == 0 || ecount == 0 || prob == 0.0 {
164        let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
165        for eid in 0..ecount {
166            #[allow(clippy::cast_possible_truncation)]
167            let eid_u32 = eid as u32;
168            edges.push(graph.edge(eid_u32)?);
169        }
170        let mut result = Graph::new(n, true)?;
171        result.add_edges(edges)?;
172        return Ok(result);
173    }
174
175    // offset: 0 = rewire source (IN mode), 1 = rewire target (OUT mode)
176    let offset = match mode {
177        RewireDirectedMode::In => 0,
178        RewireDirectedMode::Out => 1,
179    };
180
181    let mut edge_list: Vec<[VertexId; 2]> = Vec::with_capacity(ecount);
182    for eid in 0..ecount {
183        #[allow(clippy::cast_possible_truncation)]
184        let eid_u32 = eid as u32;
185        let (src, tgt) = graph.edge(eid_u32)?;
186        edge_list.push([src, tgt]);
187    }
188
189    let mut rng_state = seed;
190
191    let mut to_rewire = geom_sample(&mut rng_state, prob);
192    while to_rewire < ecount {
193        let other_vertex = edge_list[to_rewire][1 - offset];
194
195        let new_vertex = if loops {
196            random_vertex(&mut rng_state, n)
197        } else {
198            random_vertex_excluding(&mut rng_state, n, other_vertex)
199        };
200
201        edge_list[to_rewire][offset] = new_vertex;
202        to_rewire += geom_sample(&mut rng_state, prob) + 1;
203    }
204
205    let edges: Vec<(VertexId, VertexId)> = edge_list.iter().map(|e| (e[0], e[1])).collect();
206
207    let mut result = Graph::new(n, true)?;
208    result.add_edges(edges)?;
209    Ok(result)
210}
211
212fn splitmix64(state: &mut u64) -> u64 {
213    *state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
214    let mut z = *state;
215    z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
216    z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
217    z ^ (z >> 31)
218}
219
220fn random_f64(state: &mut u64) -> f64 {
221    let r = splitmix64(state);
222    // Convert to [0, 1) using top 53 bits
223    (r >> 11) as f64 / (1u64 << 53) as f64
224}
225
226fn random_vertex(state: &mut u64, n: u32) -> VertexId {
227    let r = splitmix64(state);
228    #[allow(clippy::cast_possible_truncation)]
229    let v = (r % u64::from(n)) as u32;
230    v
231}
232
233fn random_vertex_excluding(state: &mut u64, n: u32, exclude: VertexId) -> VertexId {
234    let r = splitmix64(state);
235    #[allow(clippy::cast_possible_truncation)]
236    let mut v = (r % u64::from(n - 1)) as u32;
237    if v >= exclude {
238        v += 1;
239    }
240    v
241}
242
243/// Sample from geometric distribution: number of failures before first success.
244fn geom_sample(state: &mut u64, prob: f64) -> usize {
245    if prob >= 1.0 {
246        return 0;
247    }
248    let u = random_f64(state);
249    if u == 0.0 {
250        return 0;
251    }
252    // floor(log(1-u) / log(1-p))
253    let result = ((1.0 - u).ln() / (1.0 - prob).ln()).floor();
254    // Clamp to reasonable range
255    if result < 0.0 || result.is_nan() {
256        0
257    } else if result > usize::MAX as f64 {
258        usize::MAX
259    } else {
260        result as usize
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_prob_zero() {
270        let mut g = Graph::with_vertices(5);
271        g.add_edge(0, 1).unwrap();
272        g.add_edge(1, 2).unwrap();
273
274        let rg = rewire_edges(&g, 0.0, false, 42).unwrap();
275        assert_eq!(rg.vcount(), 5);
276        assert_eq!(rg.ecount(), 2);
277        assert_eq!(rg.edge(0).unwrap(), g.edge(0).unwrap());
278        assert_eq!(rg.edge(1).unwrap(), g.edge(1).unwrap());
279    }
280
281    #[test]
282    fn test_prob_one_no_loops() {
283        let mut g = Graph::with_vertices(10);
284        for i in 0..9u32 {
285            g.add_edge(i, i + 1).unwrap();
286        }
287
288        let rg = rewire_edges(&g, 1.0, false, 123).unwrap();
289        assert_eq!(rg.vcount(), 10);
290        assert_eq!(rg.ecount(), 9);
291        // No self-loops
292        for eid in 0..9u32 {
293            let (s, t) = rg.edge(eid).unwrap();
294            assert_ne!(s, t);
295        }
296    }
297
298    #[test]
299    fn test_deterministic() {
300        let mut g = Graph::with_vertices(10);
301        for i in 0..9u32 {
302            g.add_edge(i, i + 1).unwrap();
303        }
304
305        let r1 = rewire_edges(&g, 0.5, false, 42).unwrap();
306        let r2 = rewire_edges(&g, 0.5, false, 42).unwrap();
307        // Same seed → same result
308        for eid in 0..9u32 {
309            assert_eq!(r1.edge(eid).unwrap(), r2.edge(eid).unwrap());
310        }
311    }
312
313    #[test]
314    fn test_different_seeds() {
315        let mut g = Graph::with_vertices(100);
316        for i in 0..99u32 {
317            g.add_edge(i, i + 1).unwrap();
318        }
319
320        let r1 = rewire_edges(&g, 0.5, false, 1).unwrap();
321        let r2 = rewire_edges(&g, 0.5, false, 2).unwrap();
322        // Different seeds → likely different results
323        let mut same_count = 0;
324        for eid in 0..99u32 {
325            if r1.edge(eid).unwrap() == r2.edge(eid).unwrap() {
326                same_count += 1;
327            }
328        }
329        // With 99 edges and prob 0.5, extremely unlikely all same
330        assert!(same_count < 99);
331    }
332
333    #[test]
334    fn test_empty_graph() {
335        let g = Graph::with_vertices(0);
336        let rg = rewire_edges(&g, 0.5, false, 42).unwrap();
337        assert_eq!(rg.vcount(), 0);
338    }
339
340    #[test]
341    fn test_no_edges() {
342        let g = Graph::with_vertices(5);
343        let rg = rewire_edges(&g, 0.5, false, 42).unwrap();
344        assert_eq!(rg.ecount(), 0);
345    }
346
347    #[test]
348    fn test_directed() {
349        let mut g = Graph::new(5, true).unwrap();
350        g.add_edge(0, 1).unwrap();
351        g.add_edge(1, 2).unwrap();
352        g.add_edge(2, 3).unwrap();
353
354        let rg = rewire_edges(&g, 0.5, false, 99).unwrap();
355        assert!(rg.is_directed());
356        assert_eq!(rg.ecount(), 3);
357    }
358
359    #[test]
360    fn test_loops_allowed() {
361        // With prob=1.0 and loops allowed, some edges may become self-loops
362        let mut g = Graph::with_vertices(3);
363        g.add_edge(0, 1).unwrap();
364        g.add_edge(1, 2).unwrap();
365
366        let rg = rewire_edges(&g, 1.0, true, 42).unwrap();
367        assert_eq!(rg.vcount(), 3);
368        assert_eq!(rg.ecount(), 2);
369    }
370
371    #[test]
372    fn test_preserves_edge_count() {
373        let mut g = Graph::with_vertices(20);
374        for i in 0..19u32 {
375            g.add_edge(i, i + 1).unwrap();
376        }
377
378        for seed in 0..10u64 {
379            let rg = rewire_edges(&g, 0.3, false, seed).unwrap();
380            assert_eq!(rg.ecount(), 19);
381        }
382    }
383
384    // --- rewire_directed_edges tests ---
385
386    #[test]
387    fn test_directed_rewire_prob_zero() {
388        let mut g = Graph::new(5, true).unwrap();
389        g.add_edge(0, 1).unwrap();
390        g.add_edge(1, 2).unwrap();
391        g.add_edge(2, 3).unwrap();
392
393        let rg = rewire_directed_edges(&g, 0.0, false, RewireDirectedMode::Out, 42).unwrap();
394        assert_eq!(rg.ecount(), 3);
395        for eid in 0..3u32 {
396            assert_eq!(rg.edge(eid).unwrap(), g.edge(eid).unwrap());
397        }
398    }
399
400    #[test]
401    fn test_directed_rewire_preserves_out_degree() {
402        // In OUT mode, we rewire targets → out-degree is preserved
403        let mut g = Graph::new(10, true).unwrap();
404        for i in 0..9u32 {
405            g.add_edge(i, i + 1).unwrap();
406        }
407
408        let rg = rewire_directed_edges(&g, 1.0, false, RewireDirectedMode::Out, 42).unwrap();
409        assert_eq!(rg.ecount(), 9);
410        assert!(rg.is_directed());
411
412        // Each original source still has the same out-degree
413        for v in 0..10u32 {
414            let orig_out: usize = (0..9).filter(|&eid| g.edge(eid).unwrap().0 == v).count();
415            let new_out: usize = (0..9).filter(|&eid| rg.edge(eid).unwrap().0 == v).count();
416            assert_eq!(orig_out, new_out);
417        }
418    }
419
420    #[test]
421    fn test_directed_rewire_preserves_in_degree() {
422        // In IN mode, we rewire sources → in-degree is preserved
423        let mut g = Graph::new(10, true).unwrap();
424        for i in 0..9u32 {
425            g.add_edge(i, i + 1).unwrap();
426        }
427
428        let rg = rewire_directed_edges(&g, 1.0, false, RewireDirectedMode::In, 42).unwrap();
429        assert_eq!(rg.ecount(), 9);
430
431        // Each original target still has the same in-degree
432        for v in 0..10u32 {
433            let orig_in: usize = (0..9).filter(|&eid| g.edge(eid).unwrap().1 == v).count();
434            let new_in: usize = (0..9).filter(|&eid| rg.edge(eid).unwrap().1 == v).count();
435            assert_eq!(orig_in, new_in);
436        }
437    }
438
439    #[test]
440    fn test_directed_rewire_no_self_loops() {
441        let mut g = Graph::new(5, true).unwrap();
442        g.add_edge(0, 1).unwrap();
443        g.add_edge(1, 2).unwrap();
444        g.add_edge(2, 3).unwrap();
445        g.add_edge(3, 4).unwrap();
446
447        for seed in 0..20u64 {
448            let rg = rewire_directed_edges(&g, 1.0, false, RewireDirectedMode::Out, seed).unwrap();
449            for eid in 0..4u32 {
450                let (s, t) = rg.edge(eid).unwrap();
451                assert_ne!(s, t);
452            }
453        }
454    }
455
456    #[test]
457    fn test_directed_rewire_undirected_fallback() {
458        // Undirected graph falls back to rewire_edges
459        let mut g = Graph::with_vertices(5);
460        g.add_edge(0, 1).unwrap();
461        g.add_edge(1, 2).unwrap();
462
463        let rg = rewire_directed_edges(&g, 0.5, false, RewireDirectedMode::Out, 42).unwrap();
464        assert!(!rg.is_directed());
465        assert_eq!(rg.ecount(), 2);
466    }
467
468    #[test]
469    fn test_directed_rewire_invalid_prob() {
470        let g = Graph::new(5, true).unwrap();
471        assert!(rewire_directed_edges(&g, -0.1, false, RewireDirectedMode::Out, 42).is_err());
472        assert!(rewire_directed_edges(&g, 1.1, false, RewireDirectedMode::Out, 42).is_err());
473    }
474
475    #[test]
476    fn test_directed_rewire_deterministic() {
477        let mut g = Graph::new(10, true).unwrap();
478        for i in 0..9u32 {
479            g.add_edge(i, i + 1).unwrap();
480        }
481
482        let r1 = rewire_directed_edges(&g, 0.5, false, RewireDirectedMode::Out, 99).unwrap();
483        let r2 = rewire_directed_edges(&g, 0.5, false, RewireDirectedMode::Out, 99).unwrap();
484        for eid in 0..9u32 {
485            assert_eq!(r1.edge(eid).unwrap(), r2.edge(eid).unwrap());
486        }
487    }
488
489    #[test]
490    fn test_directed_rewire_empty() {
491        let g = Graph::new(0, true).unwrap();
492        let rg = rewire_directed_edges(&g, 0.5, false, RewireDirectedMode::Out, 42).unwrap();
493        assert_eq!(rg.vcount(), 0);
494    }
495}