rust_igraph/algorithms/operators/
rewire.rs1use std::collections::HashSet;
8
9use crate::core::{Graph, IgraphResult, VertexId};
10
11pub 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 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 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 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 if !loops && (src1 == tgt1 || src2 == tgt2) {
88 continue;
89 }
90
91 if src1 == src2 || tgt1 == tgt2 {
93 continue;
94 }
95
96 if !loops && (src1 == tgt2 || tgt1 == src2) {
98 continue;
99 }
100
101 if !directed && src1 == tgt1 && src2 == tgt2 {
103 continue;
104 }
105
106 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 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 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 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 let rg = rewire(&g, 1000, true, 42).unwrap();
373 assert_eq!(rg.ecount(), 4);
374 }
375}