use super::traversal::TraversalDirection;
use crate::types::{ETypeId, Edge, NodeId};
use crate::util::heap::IndexedMinHeap;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct PathResult {
pub path: Vec<NodeId>,
pub edges: Vec<(NodeId, ETypeId, NodeId)>,
pub total_weight: f64,
pub found: bool,
}
impl PathResult {
pub fn not_found() -> Self {
Self {
path: Vec::new(),
edges: Vec::new(),
total_weight: f64::INFINITY,
found: false,
}
}
}
#[derive(Debug, Clone)]
struct PathState {
node_id: NodeId,
cost: f64, depth: usize, parent: Option<NodeId>,
edge: Option<(NodeId, ETypeId, NodeId)>, }
#[derive(Clone)]
struct AStarState {
g_score: f64,
f_score: f64,
depth: usize,
parent: Option<NodeId>,
edge: Option<(NodeId, ETypeId, NodeId)>,
}
#[derive(Debug, Clone)]
pub struct PathConfig {
pub source: NodeId,
pub targets: HashSet<NodeId>,
pub allowed_etypes: HashSet<ETypeId>,
pub direction: TraversalDirection,
pub max_depth: usize,
}
impl PathConfig {
pub fn new(source: NodeId, target: NodeId) -> Self {
let mut targets = HashSet::new();
targets.insert(target);
Self {
source,
targets,
allowed_etypes: HashSet::new(),
direction: TraversalDirection::Out,
max_depth: 100,
}
}
pub fn with_targets(source: NodeId, targets: impl IntoIterator<Item = NodeId>) -> Self {
Self {
source,
targets: targets.into_iter().collect(),
allowed_etypes: HashSet::new(),
direction: TraversalDirection::Out,
max_depth: 100,
}
}
pub fn via(mut self, etype: ETypeId) -> Self {
self.allowed_etypes.insert(etype);
self
}
pub fn max_depth(mut self, depth: usize) -> Self {
self.max_depth = depth;
self
}
pub fn direction(mut self, direction: TraversalDirection) -> Self {
self.direction = direction;
self
}
}
pub fn dijkstra<F, W>(config: PathConfig, neighbors: F, edge_weight: W) -> PathResult
where
F: Fn(NodeId, TraversalDirection, Option<ETypeId>) -> Vec<Edge>,
W: Fn(NodeId, ETypeId, NodeId) -> f64,
{
let source_id = config.source;
let mut distances: HashMap<NodeId, PathState> = HashMap::new();
let mut visited: HashSet<NodeId> = HashSet::new();
let mut queue = IndexedMinHeap::new();
distances.insert(
source_id,
PathState {
node_id: source_id,
cost: 0.0,
depth: 0,
parent: None,
edge: None,
},
);
queue.insert(source_id, 0.0);
while let Some(current_id) = queue.extract_min() {
if visited.contains(¤t_id) {
continue;
}
visited.insert(current_id);
if config.targets.contains(¤t_id) {
return reconstruct_path(&distances, current_id, source_id);
}
let Some(current_state) = distances.get(¤t_id).cloned() else {
continue;
};
if current_state.depth >= config.max_depth {
continue;
}
let directions = match config.direction {
TraversalDirection::Both => vec![TraversalDirection::Out, TraversalDirection::In],
dir => vec![dir],
};
for dir in directions {
let etype_filter = if config.allowed_etypes.is_empty() {
None
} else {
None
};
let neighbors = neighbors(current_id, dir, etype_filter);
for edge in neighbors {
if !config.allowed_etypes.is_empty() && !config.allowed_etypes.contains(&edge.etype) {
continue;
}
let neighbor_id = match dir {
TraversalDirection::Out => edge.dst,
TraversalDirection::In => edge.src,
TraversalDirection::Both => {
if edge.src == current_id {
edge.dst
} else {
edge.src
}
}
};
if visited.contains(&neighbor_id) {
continue;
}
let weight = edge_weight(edge.src, edge.etype, edge.dst);
let new_cost = current_state.cost + weight;
let existing_cost = distances.get(&neighbor_id).map(|s| s.cost);
let should_update = existing_cost.map(|c| new_cost < c).unwrap_or(true);
if should_update {
let had_existing = existing_cost.is_some();
distances.insert(
neighbor_id,
PathState {
node_id: neighbor_id,
cost: new_cost,
depth: current_state.depth + 1,
parent: Some(current_id),
edge: Some((edge.src, edge.etype, edge.dst)),
},
);
if had_existing {
queue.decrease_priority(neighbor_id, new_cost);
} else {
queue.insert(neighbor_id, new_cost);
}
}
}
}
}
PathResult::not_found()
}
pub fn a_star<F, W, H>(config: PathConfig, neighbors: F, edge_weight: W, heuristic: H) -> PathResult
where
F: Fn(NodeId, TraversalDirection, Option<ETypeId>) -> Vec<Edge>,
W: Fn(NodeId, ETypeId, NodeId) -> f64,
H: Fn(NodeId, NodeId) -> f64,
{
let source_id = config.source;
let Some(primary_target) = first_target(&config) else {
return PathResult::not_found();
};
let mut states: HashMap<NodeId, AStarState> = HashMap::new();
let mut visited: HashSet<NodeId> = HashSet::new();
let mut queue = IndexedMinHeap::new();
init_astar_state(
source_id,
primary_target,
&heuristic,
&mut states,
&mut queue,
);
while let Some(current_id) = queue.extract_min() {
if visited.contains(¤t_id) {
continue;
}
visited.insert(current_id);
if config.targets.contains(¤t_id) {
let path_states = build_astar_path_states(&states);
return reconstruct_path(&path_states, current_id, source_id);
}
let Some(current_state) = states.get(¤t_id).cloned() else {
continue;
};
if current_state.depth >= config.max_depth {
continue;
}
for dir in traversal_directions(config.direction) {
let neighbors = neighbors(current_id, dir, None);
for edge in neighbors {
if !config.allowed_etypes.is_empty() && !config.allowed_etypes.contains(&edge.etype) {
continue;
}
let neighbor_id = neighbor_id_for_edge(current_id, dir, &edge);
if visited.contains(&neighbor_id) {
continue;
}
let weight = edge_weight(edge.src, edge.etype, edge.dst);
let tentative_g = current_state.g_score + weight;
let existing_g_score = states.get(&neighbor_id).map(|s| s.g_score);
let should_update = existing_g_score.map(|g| tentative_g < g).unwrap_or(true);
if should_update {
let had_existing = existing_g_score.is_some();
let h = heuristic(neighbor_id, primary_target);
let f = tentative_g + h;
states.insert(
neighbor_id,
AStarState {
g_score: tentative_g,
f_score: f,
depth: current_state.depth + 1,
parent: Some(current_id),
edge: Some((edge.src, edge.etype, edge.dst)),
},
);
if had_existing {
queue.decrease_priority(neighbor_id, f);
} else {
queue.insert(neighbor_id, f);
}
}
}
}
}
PathResult::not_found()
}
fn init_astar_state<H>(
source_id: NodeId,
primary_target: NodeId,
heuristic: &H,
states: &mut HashMap<NodeId, AStarState>,
queue: &mut IndexedMinHeap<NodeId>,
) where
H: Fn(NodeId, NodeId) -> f64,
{
let h = heuristic(source_id, primary_target);
states.insert(
source_id,
AStarState {
g_score: 0.0,
f_score: h,
depth: 0,
parent: None,
edge: None,
},
);
queue.insert(source_id, h);
}
fn build_astar_path_states(states: &HashMap<NodeId, AStarState>) -> HashMap<NodeId, PathState> {
states
.iter()
.map(|(&id, state)| {
(
id,
PathState {
node_id: id,
cost: state.g_score,
depth: state.depth,
parent: state.parent,
edge: state.edge,
},
)
})
.collect()
}
fn traversal_directions(direction: TraversalDirection) -> Vec<TraversalDirection> {
match direction {
TraversalDirection::Both => vec![TraversalDirection::Out, TraversalDirection::In],
dir => vec![dir],
}
}
fn neighbor_id_for_edge(current_id: NodeId, dir: TraversalDirection, edge: &Edge) -> NodeId {
match dir {
TraversalDirection::Out => edge.dst,
TraversalDirection::In => edge.src,
TraversalDirection::Both => {
if edge.src == current_id {
edge.dst
} else {
edge.src
}
}
}
}
fn reconstruct_path(
states: &HashMap<NodeId, PathState>,
target_id: NodeId,
source_id: NodeId,
) -> PathResult {
let mut path = Vec::new();
let mut edges = Vec::new();
let mut current_id = Some(target_id);
let mut path_states = Vec::new();
while let Some(id) = current_id {
let Some(state) = states.get(&id) else {
break;
};
path_states.push(state.clone());
if id == source_id {
break;
}
current_id = state.parent;
}
let reached_source = match path_states.last() {
Some(last) => last.node_id == source_id,
None => false,
};
if !reached_source {
return PathResult::not_found();
}
path_states.reverse();
for (i, state) in path_states.iter().enumerate() {
path.push(state.node_id);
if i > 0 {
if let Some(edge) = state.edge {
edges.push(edge);
}
}
}
let total_weight = path_states.last().map(|s| s.cost).unwrap_or(0.0);
PathResult {
path,
edges,
total_weight,
found: true,
}
}
pub struct PathFindingBuilder<F, W> {
source: NodeId,
targets: HashSet<NodeId>,
allowed_etypes: HashSet<ETypeId>,
direction: TraversalDirection,
max_depth: usize,
neighbors: F,
edge_weight: W,
}
impl<F, W> PathFindingBuilder<F, W>
where
F: Fn(NodeId, TraversalDirection, Option<ETypeId>) -> Vec<Edge>,
W: Fn(NodeId, ETypeId, NodeId) -> f64,
{
pub fn new(source: NodeId, neighbors: F, edge_weight: W) -> Self {
Self {
source,
targets: HashSet::new(),
allowed_etypes: HashSet::new(),
direction: TraversalDirection::Out,
max_depth: 100,
neighbors,
edge_weight,
}
}
pub fn to(mut self, target: NodeId) -> Self {
self.targets.clear();
self.targets.insert(target);
self
}
pub fn to_any(mut self, targets: impl IntoIterator<Item = NodeId>) -> Self {
self.targets = targets.into_iter().collect();
self
}
pub fn via(mut self, etype: ETypeId) -> Self {
self.allowed_etypes.insert(etype);
self
}
pub fn max_depth(mut self, depth: usize) -> Self {
self.max_depth = depth;
self
}
pub fn direction(mut self, direction: TraversalDirection) -> Self {
self.direction = direction;
self
}
pub fn dijkstra(self) -> PathResult {
if self.targets.is_empty() {
return PathResult::not_found();
}
let config = PathConfig {
source: self.source,
targets: self.targets,
allowed_etypes: self.allowed_etypes,
direction: self.direction,
max_depth: self.max_depth,
};
dijkstra(config, self.neighbors, self.edge_weight)
}
pub fn a_star<H>(self, heuristic: H) -> PathResult
where
H: Fn(NodeId, NodeId) -> f64,
{
if self.targets.is_empty() {
return PathResult::not_found();
}
let config = PathConfig {
source: self.source,
targets: self.targets,
allowed_etypes: self.allowed_etypes,
direction: self.direction,
max_depth: self.max_depth,
};
a_star(config, self.neighbors, self.edge_weight, heuristic)
}
pub fn k_shortest(self, k: usize) -> Vec<PathResult> {
if self.targets.is_empty() || k == 0 {
return Vec::new();
}
let config = PathConfig {
source: self.source,
targets: self.targets,
allowed_etypes: self.allowed_etypes,
direction: self.direction,
max_depth: self.max_depth,
};
yen_k_shortest(config, k, self.neighbors, self.edge_weight)
}
pub fn all_paths(self) -> Vec<PathResult> {
self.k_shortest(100)
}
}
pub fn bfs<F>(config: PathConfig, neighbors: F) -> PathResult
where
F: Fn(NodeId, TraversalDirection, Option<ETypeId>) -> Vec<Edge>,
{
dijkstra(config, neighbors, |_, _, _| 1.0)
}
pub fn yen_k_shortest<F, W>(
config: PathConfig,
k: usize,
neighbors: F,
edge_weight: W,
) -> Vec<PathResult>
where
F: Fn(NodeId, TraversalDirection, Option<ETypeId>) -> Vec<Edge>,
W: Fn(NodeId, ETypeId, NodeId) -> f64,
{
if k == 0 {
return Vec::new();
}
let target = match first_target(&config) {
Some(target) => target,
None => return Vec::new(),
};
let mut result_paths: Vec<PathResult> = Vec::with_capacity(k);
let first_path = dijkstra(config.clone(), &neighbors, &edge_weight);
if !first_path.found {
return Vec::new();
}
result_paths.push(first_path);
if k == 1 {
return result_paths;
}
let mut candidates: Vec<PathResult> = Vec::new();
for path_idx in 0..k - 1 {
if path_idx >= result_paths.len() {
break;
}
let prev_path = &result_paths[path_idx];
let prev_path_nodes = &prev_path.path;
let max_spur_idx = prev_path_nodes.len().saturating_sub(1);
for (spur_idx, &spur_node) in prev_path_nodes.iter().enumerate().take(max_spur_idx) {
let (root_path, root_edges) = root_segments(prev_path, spur_idx);
let root_weight = root_weight(&root_edges, &edge_weight);
let mut excluded_edges = HashSet::new();
extend_excluded_edges(&mut excluded_edges, &result_paths, &root_path, spur_idx);
extend_excluded_edges(&mut excluded_edges, &candidates, &root_path, spur_idx);
let root_nodes = root_nodes(&root_path, spur_idx);
let filtered_neighbors = |node: NodeId, dir: TraversalDirection, etype: Option<ETypeId>| {
neighbors(node, dir, etype)
.into_iter()
.filter(|edge| {
if node == spur_node && excluded_edges.contains(&(edge.src, edge.etype, edge.dst)) {
return false;
}
let neighbor = if dir == TraversalDirection::In {
edge.src
} else {
edge.dst
};
!root_nodes.contains(&neighbor)
})
.collect()
};
let spur_config = build_spur_config(&config, spur_node, target, spur_idx);
let spur_path = dijkstra(spur_config, filtered_neighbors, &edge_weight);
if spur_path.found {
let candidate = combine_paths(root_path, root_edges, root_weight, spur_path);
if !is_duplicate_path(&candidate, &result_paths)
&& !is_duplicate_path(&candidate, &candidates)
{
candidates.push(candidate);
}
}
}
if !candidates.is_empty() {
if let Some(best) = pop_best_candidate(&mut candidates) {
result_paths.push(best);
}
} else {
break;
}
}
result_paths
}
fn first_target(config: &PathConfig) -> Option<NodeId> {
config.targets.iter().next().copied()
}
fn root_segments(
prev_path: &PathResult,
spur_idx: usize,
) -> (Vec<NodeId>, Vec<(NodeId, ETypeId, NodeId)>) {
let root_path: Vec<NodeId> = prev_path.path[..=spur_idx].to_vec();
let root_edges: Vec<(NodeId, ETypeId, NodeId)> = if spur_idx > 0 {
prev_path.edges[..spur_idx].to_vec()
} else {
Vec::new()
};
(root_path, root_edges)
}
fn root_weight<W>(root_edges: &[(NodeId, ETypeId, NodeId)], edge_weight: &W) -> f64
where
W: Fn(NodeId, ETypeId, NodeId) -> f64,
{
root_edges
.iter()
.map(|(s, e, d)| edge_weight(*s, *e, *d))
.sum()
}
fn extend_excluded_edges(
excluded_edges: &mut HashSet<(NodeId, ETypeId, NodeId)>,
paths: &[PathResult],
root_path: &[NodeId],
spur_idx: usize,
) {
for path in paths {
if path.path.len() > spur_idx && path.path[..=spur_idx] == root_path[..] {
if let Some(&edge) = path.edges.get(spur_idx) {
excluded_edges.insert(edge);
}
}
}
}
fn root_nodes(root_path: &[NodeId], spur_idx: usize) -> HashSet<NodeId> {
root_path[..spur_idx].iter().copied().collect()
}
fn build_spur_config(
config: &PathConfig,
spur_node: NodeId,
target: NodeId,
spur_idx: usize,
) -> PathConfig {
let mut targets = HashSet::new();
targets.insert(target);
PathConfig {
source: spur_node,
targets,
allowed_etypes: config.allowed_etypes.clone(),
direction: config.direction,
max_depth: config.max_depth.saturating_sub(spur_idx),
}
}
fn combine_paths(
mut root_path: Vec<NodeId>,
mut root_edges: Vec<(NodeId, ETypeId, NodeId)>,
root_weight: f64,
spur_path: PathResult,
) -> PathResult {
root_path.extend(spur_path.path.into_iter().skip(1));
root_edges.extend(spur_path.edges);
PathResult {
path: root_path,
edges: root_edges,
total_weight: root_weight + spur_path.total_weight,
found: true,
}
}
fn is_duplicate_path(candidate: &PathResult, paths: &[PathResult]) -> bool {
paths.iter().any(|p| p.path == candidate.path)
}
fn pop_best_candidate(candidates: &mut Vec<PathResult>) -> Option<PathResult> {
candidates.sort_by(|a, b| {
a.total_weight
.partial_cmp(&b.total_weight)
.unwrap_or(std::cmp::Ordering::Equal)
});
if candidates.is_empty() {
None
} else {
Some(candidates.remove(0))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mock_graph() -> impl Fn(NodeId, TraversalDirection, Option<ETypeId>) -> Vec<Edge> {
move |node_id: NodeId, direction: TraversalDirection, _etype: Option<ETypeId>| {
let mut edges = Vec::new();
match direction {
TraversalDirection::Out => match node_id {
1 => {
edges.push(Edge {
src: 1,
etype: 1,
dst: 2,
});
edges.push(Edge {
src: 1,
etype: 1,
dst: 4,
});
}
2 => {
edges.push(Edge {
src: 2,
etype: 1,
dst: 3,
});
edges.push(Edge {
src: 2,
etype: 1,
dst: 5,
});
}
4 => {
edges.push(Edge {
src: 4,
etype: 1,
dst: 5,
});
}
_ => {}
},
TraversalDirection::In => match node_id {
2 => edges.push(Edge {
src: 1,
etype: 1,
dst: 2,
}),
3 => edges.push(Edge {
src: 2,
etype: 1,
dst: 3,
}),
4 => edges.push(Edge {
src: 1,
etype: 1,
dst: 4,
}),
5 => {
edges.push(Edge {
src: 2,
etype: 1,
dst: 5,
});
edges.push(Edge {
src: 4,
etype: 1,
dst: 5,
});
}
_ => {}
},
TraversalDirection::Both => {
let out = mock_graph()(node_id, TraversalDirection::Out, None);
let in_edges = mock_graph()(node_id, TraversalDirection::In, None);
edges.extend(out);
edges.extend(in_edges);
}
}
edges
}
}
fn weight_fn(src: NodeId, _etype: ETypeId, dst: NodeId) -> f64 {
if src == 1 && dst == 4 {
2.0
} else {
1.0
}
}
#[test]
fn test_dijkstra_direct_path() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 2).via(1);
let result = dijkstra(config, neighbors, |_, _, _| 1.0);
assert!(result.found);
assert_eq!(result.path, vec![1, 2]);
assert_eq!(result.total_weight, 1.0);
}
#[test]
fn test_dijkstra_two_hop() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 3).via(1);
let result = dijkstra(config, neighbors, |_, _, _| 1.0);
assert!(result.found);
assert_eq!(result.path, vec![1, 2, 3]);
assert_eq!(result.total_weight, 2.0);
}
#[test]
fn test_dijkstra_weighted() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 5).via(1);
let result = dijkstra(config, neighbors, weight_fn);
assert!(result.found);
assert_eq!(result.path, vec![1, 2, 5]);
assert_eq!(result.total_weight, 2.0);
}
#[test]
fn test_dijkstra_no_path() {
let neighbors = mock_graph();
let config = PathConfig::new(3, 1).via(1);
let result = dijkstra(config, neighbors, |_, _, _| 1.0);
assert!(!result.found);
assert!(result.path.is_empty());
}
#[test]
fn test_dijkstra_max_depth() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 3).via(1).max_depth(1);
let result = dijkstra(config, neighbors, |_, _, _| 1.0);
assert!(!result.found); }
#[test]
fn test_dijkstra_multiple_targets() {
let neighbors = mock_graph();
let config = PathConfig::with_targets(1, vec![3, 4]).via(1);
let result = dijkstra(config, neighbors, |_, _, _| 1.0);
assert!(result.found);
assert_eq!(result.path, vec![1, 4]);
}
#[test]
fn test_a_star() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 5).via(1);
let result = a_star(config, neighbors, weight_fn, |_, _| 0.0);
assert!(result.found);
assert_eq!(result.path, vec![1, 2, 5]);
}
#[test]
fn test_bfs() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 5).via(1);
let result = bfs(config, neighbors);
assert!(result.found);
assert_eq!(result.path.len(), 3);
assert_eq!(result.path[0], 1);
assert_eq!(result.path[2], 5);
}
#[test]
fn test_builder() {
let neighbors = mock_graph();
let result = PathFindingBuilder::new(1, neighbors, |_, _, _| 1.0)
.to(3)
.via(1)
.max_depth(10)
.dijkstra();
assert!(result.found);
assert_eq!(result.path, vec![1, 2, 3]);
}
#[test]
fn test_builder_no_target() {
let neighbors = mock_graph();
let result = PathFindingBuilder::new(1, neighbors, |_, _, _| 1.0)
.via(1)
.dijkstra();
assert!(!result.found);
}
#[test]
fn test_same_source_target() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 1).via(1);
let result = dijkstra(config, neighbors, |_, _, _| 1.0);
assert!(result.found);
assert_eq!(result.path, vec![1]);
assert_eq!(result.total_weight, 0.0);
}
#[test]
fn test_yen_single_path() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 3).via(1);
let paths = yen_k_shortest(config, 1, neighbors, |_, _, _| 1.0);
assert_eq!(paths.len(), 1);
assert!(paths[0].found);
assert_eq!(paths[0].path, vec![1, 2, 3]);
}
#[test]
fn test_yen_two_paths_to_node_5() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 5).via(1);
let paths = yen_k_shortest(config, 3, neighbors, weight_fn);
assert!(paths.len() >= 2);
assert_eq!(paths[0].path, vec![1, 2, 5]);
assert_eq!(paths[0].total_weight, 2.0);
assert_eq!(paths[1].path, vec![1, 4, 5]);
assert_eq!(paths[1].total_weight, 3.0);
}
#[test]
fn test_yen_paths_sorted_by_weight() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 5).via(1);
let paths = yen_k_shortest(config, 10, neighbors, weight_fn);
for i in 1..paths.len() {
assert!(
paths[i].total_weight >= paths[i - 1].total_weight,
"Paths should be sorted by weight"
);
}
}
#[test]
fn test_yen_no_path() {
let neighbors = mock_graph();
let config = PathConfig::new(3, 1).via(1);
let paths = yen_k_shortest(config, 3, neighbors, |_, _, _| 1.0);
assert!(paths.is_empty());
}
#[test]
fn test_yen_k_zero() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 5).via(1);
let paths = yen_k_shortest(config, 0, neighbors, |_, _, _| 1.0);
assert!(paths.is_empty());
}
#[test]
fn test_yen_no_duplicate_paths() {
let neighbors = mock_graph();
let config = PathConfig::new(1, 5).via(1);
let paths = yen_k_shortest(config, 10, neighbors, |_, _, _| 1.0);
for i in 0..paths.len() {
for j in i + 1..paths.len() {
assert_ne!(
paths[i].path, paths[j].path,
"Should not have duplicate paths"
);
}
}
}
#[test]
fn test_yen_builder() {
let neighbors = mock_graph();
let paths = PathFindingBuilder::new(1, neighbors, weight_fn)
.to(5)
.via(1)
.k_shortest(3);
assert!(paths.len() >= 2);
assert_eq!(paths[0].path, vec![1, 2, 5]);
assert_eq!(paths[1].path, vec![1, 4, 5]);
}
#[test]
fn test_yen_all_paths() {
let neighbors = mock_graph();
let paths = PathFindingBuilder::new(1, neighbors, |_, _, _| 1.0)
.to(5)
.via(1)
.all_paths();
assert!(paths.len() >= 2);
}
}