1use std::{
5 collections::HashSet,
6 hash::{BuildHasher, Hash},
7 iter::from_fn,
8};
9
10use indexmap::IndexSet;
11use petgraph::{
12 Direction::Outgoing,
13 visit::{EdgeRef, IntoEdgeReferences, IntoEdgesDirected, NodeCount},
14};
15use rand::seq::SliceRandom;
16use smallvec::SmallVec;
17
18#[allow(clippy::too_many_arguments)]
96pub fn all_simple_paths_multi<'a, TargetColl, G, S, F, C>(
97 graph: G,
98 from: G::NodeId,
99 to: &'a HashSet<G::NodeId, S>,
100 excluded_nodes: Option<&'a HashSet<G::NodeId, S>>,
101 min_intermediate_nodes: usize,
102 max_intermediate_nodes: Option<usize>,
103 initial_cost: C,
104 min_cost: Option<C>,
105 cost_fn: F,
106) -> impl Iterator<Item = (TargetColl, C)> + 'a
107where
108 G: NodeCount + IntoEdgesDirected + 'a,
109 <G as IntoEdgesDirected>::EdgesDirected: 'a,
110 G::NodeId: Eq + Hash,
111 TargetColl: FromIterator<G::NodeId>,
112 S: BuildHasher + Default,
113 C: Clone + PartialOrd + 'a,
114 F: Fn(C, &<<G as IntoEdgeReferences>::EdgeRef as EdgeRef>::Weight, usize) -> C + 'a,
115{
116 let max_nodes = if let Some(l) = max_intermediate_nodes {
117 l + 2
118 } else {
119 graph.node_count()
120 };
121
122 let min_nodes = min_intermediate_nodes + 2;
123
124 let mut visited: IndexSet<G::NodeId, S> = IndexSet::from_iter(Some(from));
126 let mut rng = rand::rng();
129 let mut initial: SmallVec<[_; 16]> = graph.edges_directed(from, Outgoing).collect();
130 initial.shuffle(&mut rng);
131 let mut stack = Vec::with_capacity(max_nodes);
132 stack.push(initial.into_iter());
133 let mut costs: Vec<C> = Vec::with_capacity(max_nodes);
135 costs.push(initial_cost);
136
137 from_fn(move || {
138 while let Some(edges) = stack.last_mut() {
139 if let Some(edge) = edges.next() {
140 let child = edge.target();
141
142 if visited.contains(&child) || excluded_nodes.is_some_and(|excl| excl.contains(&child)) {
146 continue;
147 }
148
149 let current_nodes = visited.len();
151 let new_cost = cost_fn(costs.last().unwrap().clone(), edge.weight(), current_nodes - 1);
152
153 if let Some(ref min) = min_cost
155 && new_cost < *min
156 {
157 continue;
158 }
159
160 let mut valid_path: Option<(TargetColl, C)> = None;
161
162 if to.contains(&child) && (current_nodes + 1) >= min_nodes {
164 valid_path = Some((
165 visited.iter().cloned().chain(Some(child)).collect::<TargetColl>(),
166 new_cost.clone(),
167 ));
168 }
169
170 if (current_nodes < (max_nodes - 1)) && to.iter().any(|n| *n != child && !visited.contains(n)) {
172 visited.insert(child);
173 let mut child_edges: SmallVec<[_; 16]> = graph.edges_directed(child, Outgoing).collect();
174 child_edges.shuffle(&mut rng);
175 stack.push(child_edges.into_iter());
176 costs.push(new_cost);
177 }
178
179 if valid_path.is_some() {
181 return valid_path;
182 }
183 } else {
184 stack.pop();
186 visited.pop();
187 costs.pop();
188 }
189 }
190 None
191 })
192}
193
194#[cfg(test)]
195mod test {
196 use std::collections::{HashSet, hash_map::RandomState};
197
198 use petgraph::prelude::{DiGraph, UnGraph};
199
200 use super::all_simple_paths_multi;
201
202 fn sorted_paths<T, I: Iterator<Item = (Vec<petgraph::graph::NodeIndex>, T)>>(iter: I) -> Vec<Vec<usize>> {
204 let mut paths: Vec<Vec<usize>> = iter.map(|(v, _)| v.into_iter().map(|i| i.index()).collect()).collect();
205 paths.sort();
206 paths
207 }
208
209 #[test]
210 fn undirected_graph_should_find_all_paths_to_multiple_targets() {
211 let graph = UnGraph::<i32, i32>::from_edges([(0, 1), (1, 2), (2, 3), (2, 4)]);
212 let targets = HashSet::from_iter([3.into(), 4.into()]);
213 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
214 &graph,
215 0.into(),
216 &targets,
217 None,
218 0,
219 None,
220 0,
221 None,
222 |c, _, _| c,
223 ));
224 insta::assert_yaml_snapshot!(paths);
225 }
226
227 #[test]
228 fn directed_graph_should_find_all_paths_to_multiple_targets() {
229 let graph = DiGraph::<i32, ()>::from_edges([(0, 1), (1, 2), (2, 3), (2, 4)]);
230 let targets = HashSet::from_iter([3.into(), 4.into()]);
231 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
232 &graph,
233 0.into(),
234 &targets,
235 None,
236 0,
237 None,
238 0,
239 None,
240 |c, _, _| c,
241 ));
242 insta::assert_yaml_snapshot!(paths);
243 }
244
245 #[test]
246 fn undirected_graph_should_respect_max_intermediate_nodes() {
247 let graph = UnGraph::<i32, ()>::from_edges([(0, 1), (1, 2), (2, 3), (2, 4)]);
248 let targets = HashSet::from_iter([3.into(), 4.into()]);
249 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
250 &graph,
251 0.into(),
252 &targets,
253 None,
254 0,
255 Some(2),
256 0,
257 None,
258 |c, _, _| c,
259 ));
260 insta::assert_yaml_snapshot!(paths);
261 }
262
263 #[test]
264 fn max_intermediate_nodes_should_not_be_exceeded_when_target_connects_to_target() {
265 let graph = DiGraph::<i32, ()>::from_edges([(0, 1), (1, 2), (2, 3)]);
271 let targets = HashSet::from_iter([2.into(), 3.into()]);
272 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
273 &graph,
274 0.into(),
275 &targets,
276 None,
277 0,
278 Some(1),
279 0,
280 None,
281 |c, _, _| c,
282 ));
283 insta::assert_yaml_snapshot!(paths);
284 }
285
286 #[test]
287 fn directed_graph_should_respect_max_intermediate_nodes() {
288 let graph = DiGraph::<i32, ()>::from_edges([(0, 1), (1, 2), (2, 3), (2, 4)]);
289 let targets = HashSet::from_iter([3.into(), 4.into()]);
290 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
291 &graph,
292 0.into(),
293 &targets,
294 None,
295 0,
296 Some(2),
297 0,
298 None,
299 |c, _, _| c,
300 ));
301 insta::assert_yaml_snapshot!(paths);
302 }
303
304 #[test]
305 fn inline_targets_should_yield_both_short_and_long_paths() {
306 let graph = UnGraph::<i32, ()>::from_edges([(0, 1), (1, 2), (2, 3)]);
307 let targets = HashSet::from_iter([2.into(), 3.into()]);
308 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
309 &graph,
310 0.into(),
311 &targets,
312 None,
313 0,
314 None,
315 0,
316 None,
317 |c, _, _| c,
318 ));
319 insta::assert_yaml_snapshot!(paths);
320 }
321
322 #[test]
323 fn cyclic_graph_should_yield_only_simple_paths() {
324 let graph = DiGraph::<i32, ()>::from_edges([(0, 1), (1, 2), (2, 0), (1, 3)]);
325 let targets = HashSet::from_iter([2.into(), 3.into()]);
326 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
327 &graph,
328 0.into(),
329 &targets,
330 None,
331 0,
332 None,
333 0,
334 None,
335 |c, _, _| c,
336 ));
337 insta::assert_yaml_snapshot!(paths);
338 }
339
340 #[test]
341 fn source_in_target_set_should_not_yield_zero_length_path() {
342 let graph = UnGraph::<i32, ()>::from_edges([(0, 1), (1, 2)]);
343 let targets = HashSet::from_iter([0.into(), 1.into(), 2.into()]);
344 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
345 &graph,
346 0.into(),
347 &targets,
348 None,
349 0,
350 None,
351 0,
352 None,
353 |c, _, _| c,
354 ));
355 insta::assert_yaml_snapshot!(paths);
356 }
357
358 #[test]
359 fn non_trivial_graph_should_find_all_simple_paths() {
360 let graph = DiGraph::<i32, ()>::from_edges([
361 (0, 1),
362 (1, 2),
363 (2, 3),
364 (3, 4),
365 (0, 5),
366 (1, 5),
367 (1, 3),
368 (5, 4),
369 (4, 2),
370 (4, 3),
371 ]);
372 let targets = HashSet::from_iter([2.into(), 3.into()]);
373 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
374 &graph,
375 1.into(),
376 &targets,
377 None,
378 0,
379 None,
380 0,
381 None,
382 |c, _, _| c,
383 ));
384 insta::assert_yaml_snapshot!(paths);
385 }
386
387 #[test]
388 fn min_intermediate_nodes_should_exclude_shorter_paths() {
389 let graph = UnGraph::<i32, ()>::from_edges([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
390 let targets = HashSet::from_iter([1.into(), 3.into()]);
391 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
392 &graph,
393 0.into(),
394 &targets,
395 None,
396 2,
397 None,
398 0,
399 None,
400 |c, _, _| c,
401 ));
402 insta::assert_yaml_snapshot!(paths);
403 }
404
405 #[test]
406 fn multiplicative_cost_should_accumulate_along_path() {
407 let mut graph = DiGraph::<(), f64>::new();
410 let n: Vec<_> = (0..5).map(|_| graph.add_node(())).collect();
411 graph.extend_with_edges([
412 (n[0], n[1], 0.9),
413 (n[1], n[2], 0.8),
414 (n[2], n[3], 0.7),
415 (n[1], n[4], 0.6),
416 ]);
417
418 let targets = HashSet::from_iter([n[3], n[4]]);
419 let results: Vec<(Vec<_>, f64)> = all_simple_paths_multi::<_, _, RandomState, _, _>(
420 &graph,
421 n[0],
422 &targets,
423 None,
424 0,
425 None,
426 1.0,
427 None,
428 |c, w, _| c * w,
429 )
430 .map(|(v, cost): (Vec<_>, f64)| (v.into_iter().map(|i| i.index()).collect(), cost))
431 .collect();
432
433 assert_eq!(results.len(), 2);
436 for (path, cost) in &results {
437 match path.as_slice() {
438 [0, 1, 2, 3] => assert!((cost - 0.504).abs() < 1e-9),
439 [0, 1, 4] => assert!((cost - 0.54).abs() < 1e-9),
440 other => panic!("unexpected path: {other:?}"),
441 }
442 }
443 }
444
445 #[test]
446 fn min_cost_should_prune_path_falling_below_threshold() {
447 let mut graph = DiGraph::<(), f64>::new();
452 let n: Vec<_> = (0..5).map(|_| graph.add_node(())).collect();
453 graph.extend_with_edges([
454 (n[0], n[1], 0.9),
455 (n[1], n[2], 0.8),
456 (n[2], n[3], 0.7),
457 (n[1], n[4], 0.6),
458 ]);
459
460 let targets = HashSet::from_iter([n[3], n[4]]);
461 let results: Vec<(Vec<_>, f64)> = all_simple_paths_multi::<_, _, RandomState, _, _>(
462 &graph,
463 n[0],
464 &targets,
465 None,
466 0,
467 None,
468 1.0,
469 Some(0.51),
470 |c, w, _| c * w,
471 )
472 .map(|(v, cost): (Vec<_>, f64)| (v.into_iter().map(|i| i.index()).collect(), cost))
473 .collect();
474
475 assert_eq!(results.len(), 1);
476 assert_eq!(results[0].0, vec![0, 1, 4]);
477 assert!((results[0].1 - 0.54).abs() < 1e-9);
478 }
479
480 #[test]
481 fn min_cost_should_prune_entire_branch_on_low_first_edge() {
482 let mut graph = DiGraph::<(), f64>::new();
487 let n: Vec<_> = (0..4).map(|_| graph.add_node(())).collect();
488 graph.extend_with_edges([
489 (n[0], n[1], 0.1),
490 (n[1], n[2], 0.9),
491 (n[0], n[3], 0.9),
492 (n[3], n[2], 0.9),
493 ]);
494
495 let targets = HashSet::from_iter([n[2]]);
496 let results: Vec<(Vec<_>, f64)> = all_simple_paths_multi::<_, _, RandomState, _, _>(
497 &graph,
498 n[0],
499 &targets,
500 None,
501 0,
502 None,
503 1.0,
504 Some(0.5),
505 |c, w, _| c * w,
506 )
507 .map(|(v, cost): (Vec<_>, f64)| (v.into_iter().map(|i| i.index()).collect(), cost))
508 .collect();
509
510 assert_eq!(results.len(), 1);
511 assert_eq!(results[0].0, vec![0, 3, 2]);
512 assert!((results[0].1 - 0.81).abs() < 1e-9);
513 }
514
515 #[test]
516 fn min_cost_should_yield_empty_when_all_paths_below_threshold() {
517 let mut graph = DiGraph::<(), f64>::new();
521 let n: Vec<_> = (0..3).map(|_| graph.add_node(())).collect();
522 graph.extend_with_edges([(n[0], n[1], 0.3), (n[1], n[2], 0.3)]);
523
524 let targets = HashSet::from_iter([n[2]]);
525 let results: Vec<(Vec<_>, f64)> = all_simple_paths_multi::<_, _, RandomState, _, _>(
526 &graph,
527 n[0],
528 &targets,
529 None,
530 0,
531 None,
532 1.0,
533 Some(0.5),
534 |c, w, _| c * w,
535 )
536 .map(|(v, cost): (Vec<_>, f64)| (v.into_iter().map(|i| i.index()).collect(), cost))
537 .collect();
538
539 assert!(results.is_empty());
540 }
541
542 #[test]
543 fn excluded_nodes_should_prune_branches_containing_them() {
544 let graph = DiGraph::<i32, ()>::from_edges([(0, 1), (1, 2), (2, 3), (1, 4), (4, 3)]);
548 let targets = HashSet::from_iter([3.into()]);
549 let excluded: HashSet<petgraph::graph::NodeIndex, RandomState> = HashSet::from_iter([2.into()]);
550 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
551 &graph,
552 0.into(),
553 &targets,
554 Some(&excluded),
555 0,
556 None,
557 0,
558 None,
559 |c, _, _| c,
560 ));
561 assert_eq!(paths, vec![vec![0, 1, 4, 3]]);
562 }
563
564 #[test]
565 fn excluded_target_yields_no_paths() {
566 let graph = DiGraph::<i32, ()>::from_edges([(0, 1), (1, 2)]);
568 let targets = HashSet::from_iter([2.into()]);
569 let excluded: HashSet<petgraph::graph::NodeIndex, RandomState> = HashSet::from_iter([2.into()]);
570 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
571 &graph,
572 0.into(),
573 &targets,
574 Some(&excluded),
575 0,
576 None,
577 0,
578 None,
579 |c, _, _| c,
580 ));
581 assert!(paths.is_empty());
582 }
583
584 #[test]
585 fn complete_graph_should_yield_all_and_only_simple_paths() {
586 use petgraph::graph::NodeIndex;
587
588 let edges: Vec<(u32, u32)> = (0u32..5)
590 .flat_map(|a| (0u32..5).filter(move |&b| b != a).map(move |b| (a, b)))
591 .collect();
592 let graph = DiGraph::<i32, ()>::from_edges(edges);
593
594 let src: NodeIndex = 0.into();
595 let dst: NodeIndex = 4.into();
596 let targets = HashSet::from_iter([dst]);
597
598 let all_paths: Vec<(Vec<NodeIndex>, i32)> = all_simple_paths_multi::<_, _, RandomState, _, _>(
600 &graph,
601 src,
602 &targets,
603 None,
604 0,
605 None,
606 0,
607 None,
608 |c, _, _| c,
609 )
610 .collect();
611
612 assert_eq!(all_paths.len(), 16, "expected 16 simple paths in K5 from 0 to 4");
615
616 for (path, _) in &all_paths {
617 assert_eq!(path.first(), Some(&src), "path must start at src: {path:?}");
618 assert_eq!(path.last(), Some(&dst), "path must end at dst: {path:?}");
619 let interior = &path[1..path.len() - 1];
621 assert!(!interior.contains(&src), "src repeated inside path: {path:?}");
622 assert!(!interior.contains(&dst), "dst repeated inside path: {path:?}");
623 let unique: HashSet<&NodeIndex, RandomState> = path.iter().collect();
625 assert_eq!(unique.len(), path.len(), "duplicate node in path: {path:?}");
626 }
627
628 let excluded: HashSet<NodeIndex, RandomState> = HashSet::from_iter([NodeIndex::from(2u32)]);
630 let restricted: Vec<(Vec<NodeIndex>, i32)> = all_simple_paths_multi::<_, _, RandomState, _, _>(
631 &graph,
632 src,
633 &targets,
634 Some(&excluded),
635 0,
636 None,
637 0,
638 None,
639 |c, _, _| c,
640 )
641 .collect();
642
643 assert_eq!(restricted.len(), 5, "expected 5 paths when node 2 is excluded");
646
647 for (path, _) in &restricted {
648 assert!(
649 !path.contains(&NodeIndex::from(2u32)),
650 "excluded node 2 in path: {path:?}"
651 );
652 let unique: HashSet<&NodeIndex, RandomState> = path.iter().collect();
653 assert_eq!(unique.len(), path.len(), "duplicate node in path: {path:?}");
654 }
655
656 let all_set: Vec<Vec<usize>> = all_paths
658 .iter()
659 .map(|(p, _)| p.iter().map(|n| n.index()).collect())
660 .collect();
661 for (path, _) in &restricted {
662 let as_usize: Vec<usize> = path.iter().map(|n| n.index()).collect();
663 assert!(
664 all_set.contains(&as_usize),
665 "restricted path not in all-paths: {path:?}"
666 );
667 }
668 }
669
670 #[test]
671 fn excluded_from_should_be_ignored() {
672 let graph = DiGraph::<i32, ()>::from_edges([(0, 1), (1, 2)]);
674 let targets = HashSet::from_iter([2.into()]);
675 let excluded: HashSet<petgraph::graph::NodeIndex, RandomState> = HashSet::from_iter([0.into()]);
676 let paths = sorted_paths(all_simple_paths_multi::<_, _, RandomState, _, _>(
677 &graph,
678 0.into(),
679 &targets,
680 Some(&excluded),
681 0,
682 None,
683 0,
684 None,
685 |c, _, _| c,
686 ));
687 assert_eq!(paths, vec![vec![0, 1, 2]]);
688 }
689}