rust_igraph/algorithms/paths/
random_walk.rs1use crate::algorithms::paths::dijkstra::DijkstraMode;
23use crate::core::graph::EdgeId;
24use crate::core::rng::SplitMix64;
25use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
26
27pub(super) fn validate_weights(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<()> {
28 let Some(w) = weights else {
29 return Ok(());
30 };
31 let m = graph.ecount();
32 if w.len() != m {
33 return Err(IgraphError::InvalidArgument(format!(
34 "weights vector size ({}) differs from edge count ({})",
35 w.len(),
36 m
37 )));
38 }
39 for (e, &v) in w.iter().enumerate() {
40 if v.is_nan() {
41 return Err(IgraphError::InvalidArgument(format!(
42 "weight at edge {e} is NaN"
43 )));
44 }
45 if v < 0.0 {
46 return Err(IgraphError::InvalidArgument(format!(
47 "weight at edge {e} is negative ({v}); random walk weights must be non-negative"
48 )));
49 }
50 }
51 Ok(())
52}
53
54fn incident_for_mode(graph: &Graph, v: VertexId, mode: DijkstraMode) -> IgraphResult<Vec<EdgeId>> {
55 if !graph.is_directed() {
56 return graph.incident(v);
57 }
58 match mode {
59 DijkstraMode::Out => graph.incident(v),
60 DijkstraMode::In => graph.incident_in(v),
61 DijkstraMode::All => {
62 let mut out = graph.incident(v)?;
63 out.extend(graph.incident_in(v)?);
64 Ok(out)
65 }
66 }
67}
68
69pub fn random_walk(
111 graph: &Graph,
112 weights: Option<&[f64]>,
113 start: VertexId,
114 mode: DijkstraMode,
115 steps: u32,
116 seed: u64,
117) -> IgraphResult<(Vec<VertexId>, Vec<EdgeId>)> {
118 let n = graph.vcount();
119 if start >= n {
120 return Err(IgraphError::VertexOutOfRange { id: start, n });
121 }
122 validate_weights(graph, weights)?;
123
124 let mut rng = SplitMix64::new(seed);
125 let mut vs: Vec<VertexId> = Vec::with_capacity(steps as usize + 1);
126 let mut es: Vec<EdgeId> = Vec::with_capacity(steps as usize);
127 vs.push(start);
128
129 let mut current = start;
130 for _ in 0..steps {
131 let incidents = incident_for_mode(graph, current, mode)?;
132 if incidents.is_empty() {
133 break;
134 }
135
136 let next_eid = match weights {
137 None => {
138 let idx = rng.gen_index(incidents.len());
139 Some(incidents[idx])
140 }
141 Some(ws) => {
142 let mut total = 0.0_f64;
146 for &eid in &incidents {
147 let w = ws[eid as usize];
148 if w.is_finite() && w > 0.0 {
149 total += w;
150 }
151 }
152 if total <= 0.0 {
153 None
154 } else {
155 let target = rng.gen_unit() * total;
156 let mut acc = 0.0_f64;
157 let mut chosen: Option<EdgeId> = None;
158 for &eid in &incidents {
159 let w = ws[eid as usize];
160 if !(w.is_finite() && w > 0.0) {
161 continue;
162 }
163 acc += w;
164 if acc >= target {
165 chosen = Some(eid);
166 break;
167 }
168 }
169 if chosen.is_none() {
173 for &eid in incidents.iter().rev() {
174 let w = ws[eid as usize];
175 if w.is_finite() && w > 0.0 {
176 chosen = Some(eid);
177 break;
178 }
179 }
180 }
181 chosen
182 }
183 }
184 };
185
186 let Some(eid) = next_eid else {
187 break;
188 };
189 let next_vertex = graph.edge_other(eid, current)?;
190 es.push(eid);
191 vs.push(next_vertex);
192 current = next_vertex;
193 }
194
195 Ok((vs, es))
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn unit_weights_4_cycle_walk_length() {
204 let mut g = Graph::with_vertices(4);
205 g.add_edge(0, 1).unwrap();
206 g.add_edge(1, 2).unwrap();
207 g.add_edge(2, 3).unwrap();
208 g.add_edge(3, 0).unwrap();
209 let (vs, es) = random_walk(&g, None, 0, DijkstraMode::Out, 5, 42).unwrap();
210 assert_eq!(vs.len(), 6);
211 assert_eq!(es.len(), 5);
212 assert_eq!(vs[0], 0);
213 for w in vs.windows(2) {
216 let (a, b) = (w[0], w[1]);
217 assert!(
218 (g.find_eid(a, b).unwrap()).is_some() || (g.find_eid(b, a).unwrap()).is_some(),
219 "non-adjacent step {a}→{b}"
220 );
221 }
222 }
223
224 #[test]
225 fn stuck_returns_shorter_walk() {
226 let mut g = Graph::new(3, true).unwrap();
228 g.add_edge(0, 1).unwrap();
229 g.add_edge(2, 1).unwrap();
230 let (vs, es) = random_walk(&g, None, 0, DijkstraMode::Out, 10, 7).unwrap();
231 assert_eq!(vs, vec![0, 1]);
233 assert_eq!(es.len(), 1);
234 }
235
236 #[test]
237 fn zero_steps_returns_singleton() {
238 let mut g = Graph::with_vertices(3);
239 g.add_edge(0, 1).unwrap();
240 let (vs, es) = random_walk(&g, None, 1, DijkstraMode::Out, 0, 0).unwrap();
241 assert_eq!(vs, vec![1]);
242 assert!(es.is_empty());
243 }
244
245 #[test]
246 fn isolated_vertex_stuck_immediately() {
247 let g = Graph::with_vertices(3);
248 let (vs, es) = random_walk(&g, None, 1, DijkstraMode::Out, 5, 0).unwrap();
249 assert_eq!(vs, vec![1]);
250 assert!(es.is_empty());
251 }
252
253 #[test]
254 fn deterministic_with_same_seed() {
255 let mut g = Graph::with_vertices(5);
256 for i in 0..5u32 {
257 g.add_edge(i, (i + 1) % 5).unwrap();
258 g.add_edge(i, (i + 2) % 5).unwrap();
259 }
260 let r1 = random_walk(&g, None, 0, DijkstraMode::Out, 20, 12345).unwrap();
261 let r2 = random_walk(&g, None, 0, DijkstraMode::Out, 20, 12345).unwrap();
262 assert_eq!(r1, r2);
263 }
264
265 #[test]
266 fn different_seeds_yield_different_walks() {
267 let mut g = Graph::with_vertices(5);
268 for i in 0..5u32 {
269 g.add_edge(i, (i + 1) % 5).unwrap();
270 g.add_edge(i, (i + 2) % 5).unwrap();
271 }
272 let r1 = random_walk(&g, None, 0, DijkstraMode::Out, 20, 1).unwrap();
273 let r2 = random_walk(&g, None, 0, DijkstraMode::Out, 20, 2).unwrap();
274 assert_ne!(r1, r2);
277 }
278
279 #[test]
280 fn weighted_walk_picks_only_positive_weight_edges() {
281 let mut g = Graph::with_vertices(3);
283 g.add_edge(0, 1).unwrap();
284 g.add_edge(0, 2).unwrap();
285 let weights = [1.0_f64, 0.0];
286 for seed in 0..16u64 {
290 let (vs, _) = random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 1, seed).unwrap();
291 assert_eq!(vs, vec![0, 1], "seed {seed}");
292 }
293 }
294
295 #[test]
296 fn weighted_zero_total_weight_stops_early() {
297 let mut g = Graph::with_vertices(3);
299 g.add_edge(0, 1).unwrap();
300 g.add_edge(0, 2).unwrap();
301 let weights = [0.0_f64, 0.0];
302 let (vs, es) = random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 5, 0).unwrap();
303 assert_eq!(vs, vec![0]);
304 assert!(es.is_empty());
305 }
306
307 #[test]
308 fn negative_weight_errors() {
309 let mut g = Graph::with_vertices(2);
310 g.add_edge(0, 1).unwrap();
311 let weights = [-1.0_f64];
312 assert!(random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 1, 0).is_err());
313 }
314
315 #[test]
316 fn nan_weight_errors() {
317 let mut g = Graph::with_vertices(2);
318 g.add_edge(0, 1).unwrap();
319 let weights = [f64::NAN];
320 assert!(random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 1, 0).is_err());
321 }
322
323 #[test]
324 fn out_of_range_start_errors() {
325 let g = Graph::with_vertices(3);
326 assert!(random_walk(&g, None, 99, DijkstraMode::Out, 1, 0).is_err());
327 }
328
329 #[test]
330 fn weights_size_mismatch_errors() {
331 let mut g = Graph::with_vertices(2);
332 g.add_edge(0, 1).unwrap();
333 let weights = [1.0_f64, 2.0];
334 assert!(random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 1, 0).is_err());
335 }
336
337 #[test]
338 fn directed_in_mode_walks_reverse_edges() {
339 let mut g = Graph::new(3, true).unwrap();
340 g.add_edge(0, 1).unwrap();
341 g.add_edge(1, 2).unwrap();
342 let (vs, _) = random_walk(&g, None, 2, DijkstraMode::In, 2, 0).unwrap();
344 assert_eq!(vs, vec![2, 1, 0]);
345 }
346}