rust_igraph/algorithms/paths/
random_walk.rs1use crate::algorithms::paths::dijkstra::DijkstraMode;
23use crate::core::graph::EdgeId;
24use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
25
26struct SplitMix64(u64);
30
31impl SplitMix64 {
32 fn new(seed: u64) -> Self {
33 Self(seed)
34 }
35 fn next_u64(&mut self) -> u64 {
36 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
37 let mut z = self.0;
38 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
39 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
40 z ^ (z >> 31)
41 }
42 fn gen_index(&mut self, bound: usize) -> usize {
44 let r = self.next_u64() % (bound as u64);
47 usize::try_from(r).expect("bound fits in usize by construction")
48 }
49 fn gen_unit(&mut self) -> f64 {
51 let bits = self.next_u64() >> 11; #[allow(clippy::cast_precision_loss)]
56 let numerator = bits as f64;
57 numerator * (1.0_f64 / 9_007_199_254_740_992.0_f64)
58 }
59}
60
61fn validate_weights(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<()> {
62 let Some(w) = weights else {
63 return Ok(());
64 };
65 let m = graph.ecount();
66 if w.len() != m {
67 return Err(IgraphError::InvalidArgument(format!(
68 "weights vector size ({}) differs from edge count ({})",
69 w.len(),
70 m
71 )));
72 }
73 for (e, &v) in w.iter().enumerate() {
74 if v.is_nan() {
75 return Err(IgraphError::InvalidArgument(format!(
76 "weight at edge {e} is NaN"
77 )));
78 }
79 if v < 0.0 {
80 return Err(IgraphError::InvalidArgument(format!(
81 "weight at edge {e} is negative ({v}); random walk weights must be non-negative"
82 )));
83 }
84 }
85 Ok(())
86}
87
88fn incident_for_mode(graph: &Graph, v: VertexId, mode: DijkstraMode) -> IgraphResult<Vec<EdgeId>> {
89 if !graph.is_directed() {
90 return graph.incident(v);
91 }
92 match mode {
93 DijkstraMode::Out => graph.incident(v),
94 DijkstraMode::In => graph.incident_in(v),
95 DijkstraMode::All => {
96 let mut out = graph.incident(v)?;
97 out.extend(graph.incident_in(v)?);
98 Ok(out)
99 }
100 }
101}
102
103pub fn random_walk(
145 graph: &Graph,
146 weights: Option<&[f64]>,
147 start: VertexId,
148 mode: DijkstraMode,
149 steps: u32,
150 seed: u64,
151) -> IgraphResult<(Vec<VertexId>, Vec<EdgeId>)> {
152 let n = graph.vcount();
153 if start >= n {
154 return Err(IgraphError::VertexOutOfRange { id: start, n });
155 }
156 validate_weights(graph, weights)?;
157
158 let mut rng = SplitMix64::new(seed);
159 let mut vs: Vec<VertexId> = Vec::with_capacity(steps as usize + 1);
160 let mut es: Vec<EdgeId> = Vec::with_capacity(steps as usize);
161 vs.push(start);
162
163 let mut current = start;
164 for _ in 0..steps {
165 let incidents = incident_for_mode(graph, current, mode)?;
166 if incidents.is_empty() {
167 break;
168 }
169
170 let next_eid = match weights {
171 None => {
172 let idx = rng.gen_index(incidents.len());
173 Some(incidents[idx])
174 }
175 Some(ws) => {
176 let mut total = 0.0_f64;
180 for &eid in &incidents {
181 let w = ws[eid as usize];
182 if w.is_finite() && w > 0.0 {
183 total += w;
184 }
185 }
186 if total <= 0.0 {
187 None
188 } else {
189 let target = rng.gen_unit() * total;
190 let mut acc = 0.0_f64;
191 let mut chosen: Option<EdgeId> = None;
192 for &eid in &incidents {
193 let w = ws[eid as usize];
194 if !(w.is_finite() && w > 0.0) {
195 continue;
196 }
197 acc += w;
198 if acc >= target {
199 chosen = Some(eid);
200 break;
201 }
202 }
203 if chosen.is_none() {
207 for &eid in incidents.iter().rev() {
208 let w = ws[eid as usize];
209 if w.is_finite() && w > 0.0 {
210 chosen = Some(eid);
211 break;
212 }
213 }
214 }
215 chosen
216 }
217 }
218 };
219
220 let Some(eid) = next_eid else {
221 break;
222 };
223 let next_vertex = graph.edge_other(eid, current)?;
224 es.push(eid);
225 vs.push(next_vertex);
226 current = next_vertex;
227 }
228
229 Ok((vs, es))
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn unit_weights_4_cycle_walk_length() {
238 let mut g = Graph::with_vertices(4);
239 g.add_edge(0, 1).unwrap();
240 g.add_edge(1, 2).unwrap();
241 g.add_edge(2, 3).unwrap();
242 g.add_edge(3, 0).unwrap();
243 let (vs, es) = random_walk(&g, None, 0, DijkstraMode::Out, 5, 42).unwrap();
244 assert_eq!(vs.len(), 6);
245 assert_eq!(es.len(), 5);
246 assert_eq!(vs[0], 0);
247 for w in vs.windows(2) {
250 let (a, b) = (w[0], w[1]);
251 assert!(
252 (g.find_eid(a, b).unwrap()).is_some() || (g.find_eid(b, a).unwrap()).is_some(),
253 "non-adjacent step {a}→{b}"
254 );
255 }
256 }
257
258 #[test]
259 fn stuck_returns_shorter_walk() {
260 let mut g = Graph::new(3, true).unwrap();
262 g.add_edge(0, 1).unwrap();
263 g.add_edge(2, 1).unwrap();
264 let (vs, es) = random_walk(&g, None, 0, DijkstraMode::Out, 10, 7).unwrap();
265 assert_eq!(vs, vec![0, 1]);
267 assert_eq!(es.len(), 1);
268 }
269
270 #[test]
271 fn zero_steps_returns_singleton() {
272 let mut g = Graph::with_vertices(3);
273 g.add_edge(0, 1).unwrap();
274 let (vs, es) = random_walk(&g, None, 1, DijkstraMode::Out, 0, 0).unwrap();
275 assert_eq!(vs, vec![1]);
276 assert!(es.is_empty());
277 }
278
279 #[test]
280 fn isolated_vertex_stuck_immediately() {
281 let g = Graph::with_vertices(3);
282 let (vs, es) = random_walk(&g, None, 1, DijkstraMode::Out, 5, 0).unwrap();
283 assert_eq!(vs, vec![1]);
284 assert!(es.is_empty());
285 }
286
287 #[test]
288 fn deterministic_with_same_seed() {
289 let mut g = Graph::with_vertices(5);
290 for i in 0..5u32 {
291 g.add_edge(i, (i + 1) % 5).unwrap();
292 g.add_edge(i, (i + 2) % 5).unwrap();
293 }
294 let r1 = random_walk(&g, None, 0, DijkstraMode::Out, 20, 12345).unwrap();
295 let r2 = random_walk(&g, None, 0, DijkstraMode::Out, 20, 12345).unwrap();
296 assert_eq!(r1, r2);
297 }
298
299 #[test]
300 fn different_seeds_yield_different_walks() {
301 let mut g = Graph::with_vertices(5);
302 for i in 0..5u32 {
303 g.add_edge(i, (i + 1) % 5).unwrap();
304 g.add_edge(i, (i + 2) % 5).unwrap();
305 }
306 let r1 = random_walk(&g, None, 0, DijkstraMode::Out, 20, 1).unwrap();
307 let r2 = random_walk(&g, None, 0, DijkstraMode::Out, 20, 2).unwrap();
308 assert_ne!(r1, r2);
311 }
312
313 #[test]
314 fn weighted_walk_picks_only_positive_weight_edges() {
315 let mut g = Graph::with_vertices(3);
317 g.add_edge(0, 1).unwrap();
318 g.add_edge(0, 2).unwrap();
319 let weights = [1.0_f64, 0.0];
320 for seed in 0..16u64 {
324 let (vs, _) = random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 1, seed).unwrap();
325 assert_eq!(vs, vec![0, 1], "seed {seed}");
326 }
327 }
328
329 #[test]
330 fn weighted_zero_total_weight_stops_early() {
331 let mut g = Graph::with_vertices(3);
333 g.add_edge(0, 1).unwrap();
334 g.add_edge(0, 2).unwrap();
335 let weights = [0.0_f64, 0.0];
336 let (vs, es) = random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 5, 0).unwrap();
337 assert_eq!(vs, vec![0]);
338 assert!(es.is_empty());
339 }
340
341 #[test]
342 fn negative_weight_errors() {
343 let mut g = Graph::with_vertices(2);
344 g.add_edge(0, 1).unwrap();
345 let weights = [-1.0_f64];
346 assert!(random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 1, 0).is_err());
347 }
348
349 #[test]
350 fn nan_weight_errors() {
351 let mut g = Graph::with_vertices(2);
352 g.add_edge(0, 1).unwrap();
353 let weights = [f64::NAN];
354 assert!(random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 1, 0).is_err());
355 }
356
357 #[test]
358 fn out_of_range_start_errors() {
359 let g = Graph::with_vertices(3);
360 assert!(random_walk(&g, None, 99, DijkstraMode::Out, 1, 0).is_err());
361 }
362
363 #[test]
364 fn weights_size_mismatch_errors() {
365 let mut g = Graph::with_vertices(2);
366 g.add_edge(0, 1).unwrap();
367 let weights = [1.0_f64, 2.0];
368 assert!(random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 1, 0).is_err());
369 }
370
371 #[test]
372 fn directed_in_mode_walks_reverse_edges() {
373 let mut g = Graph::new(3, true).unwrap();
374 g.add_edge(0, 1).unwrap();
375 g.add_edge(1, 2).unwrap();
376 let (vs, _) = random_walk(&g, None, 2, DijkstraMode::In, 2, 0).unwrap();
378 assert_eq!(vs, vec![2, 1, 0]);
379 }
380}