diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs new file mode 100644 index 0000000000..039dc470e3 --- /dev/null +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -0,0 +1,225 @@ +/// Bellman-Ford algorithm +use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; +use crate::{ + core::entities::nodes::node_ref::NodeRef, + db::{ + api::state::{ops::filter::NO_FILTER, Index, NodeState}, + graph::nodes::Nodes, + }, + errors::GraphError, + prelude::*, +}; +use indexmap::IndexSet; +use raphtory_api::core::{ + entities::{ + properties::prop::{PropType, PropUnwrap}, + VID, + }, + Direction, +}; +use std::{ + collections::{HashMap}, +}; + + +/// Finds the shortest paths from a single source to multiple targets in a graph. +/// +/// # Arguments +/// +/// * `graph`: The graph to search in. +/// * `source`: The source node. +/// * `targets`: A vector of target nodes. +/// * `weight`: Option, The name of the weight property for the edges. If not set then defaults all edges to weight=1. +/// * `direction`: The direction of the edges of the shortest path. Defaults to both directions (undirected graph). +/// +/// # Returns +/// +/// Returns a `HashMap` where the key is the target node and the value is a tuple containing +/// the total dist and a vector of nodes representing the shortest path. +/// +pub fn bellman_ford_single_source_shortest_paths( + g: &G, + source: T, + targets: Vec, + weight: Option<&str>, + direction: Direction, +) -> Result), G>, GraphError> { + let source_ref = source.as_node_ref(); + let source_node = match g.node(source_ref) { + Some(src) => src, + None => { + let gid = match source_ref { + NodeRef::Internal(vid) => g.node_id(vid), + NodeRef::External(gid) => gid.to_owned(), + }; + return Err(GraphError::NodeMissingError(gid)); + } + }; + let mut weight_type = PropType::U8; + if let Some(weight) = weight { + if let Some((_, dtype)) = g.edge_meta().get_prop_id_and_type(weight, false) { + weight_type = dtype; + } else { + return Err(GraphError::PropertyMissingError(weight.to_string())); + } + } + + // Turn below into a generic function, then add a closure to ensure the prop is correctly unwrapped + // after the calc is done + let dist_val = match weight_type { + PropType::F32 => Prop::F32(0f32), + PropType::F64 => Prop::F64(0f64), + PropType::U8 => Prop::U8(0u8), + PropType::U16 => Prop::U16(0u16), + PropType::U32 => Prop::U32(0u32), + PropType::U64 => Prop::U64(0u64), + PropType::I32 => Prop::I32(0i32), + PropType::I64 => Prop::I64(0i64), + p_type => { + return Err(GraphError::InvalidProperty { + reason: format!("Weight type: {:?}, not supported", p_type), + }) + } + }; + let max_val = match weight_type { + PropType::F32 => Prop::F32(f32::MAX), + PropType::F64 => Prop::F64(f64::MAX), + PropType::U8 => Prop::U8(u8::MAX), + PropType::U16 => Prop::U16(u16::MAX), + PropType::U32 => Prop::U32(u32::MAX), + PropType::U64 => Prop::U64(u64::MAX), + PropType::I32 => Prop::I32(i32::MAX), + PropType::I64 => Prop::I64(i64::MAX), + p_type => { + return Err(GraphError::InvalidProperty { + reason: format!("Weight type: {:?}, not supported", p_type), + }) + } + }; + let mut shortest_paths: HashMap)> = HashMap::new(); + let mut dist: HashMap = HashMap::new(); + let mut predecessor: HashMap = HashMap::new(); + + let n_nodes = g.count_nodes(); + + for node in g.nodes() { + predecessor.insert(node.node, node.node); + if node.node == source_node.node { + dist.insert(source_node.node, dist_val.clone()); + } else { + dist.insert(node.node, max_val.clone()); + } + } + + for _ in 1..n_nodes { + let mut changed = false; + for node in g.nodes() { + if node.node == source_node.node { + continue; + } + let mut min_dist = dist.get(&node.node).unwrap().clone(); + let mut min_node = predecessor.get(&node.node).unwrap().clone(); + let edges = match direction { + Direction::IN => node.out_edges(), + Direction::OUT => node.in_edges(), + Direction::BOTH => node.edges(), + }; + for edge in edges { + let edge_val = match weight { + None => Prop::U8(1), + Some(weight) => match edge.properties().get(weight) { + Some(prop) => prop, + _ => continue, + }, + }; + let neighbor_vid = edge.nbr().node; + let neighbor_dist = dist.get(&neighbor_vid).unwrap(); + if neighbor_dist == &max_val { + continue; + } + let new_dist = neighbor_dist.clone().add(edge_val).unwrap(); + if new_dist < min_dist { + min_dist = new_dist; + min_node = neighbor_vid; + changed = true; + } + } + dist.insert(node.node, min_dist); + predecessor.insert(node.node, min_node); + } + if !changed { + break; + } + } + + for node in g.nodes() { + let edges = match direction { + Direction::IN => node.out_edges(), + Direction::OUT => node.in_edges(), + Direction::BOTH => node.edges(), + }; + let node_dist = dist.get(&node.node).unwrap(); + for edge in edges { + let edge_val = match weight { + None => Prop::U8(1), + Some(weight) => match edge.properties().get(weight) { + Some(prop) => prop, + _ => continue, + }, + }; + let neighbor_vid = edge.nbr().node; + let neighbor_dist = dist.get(&neighbor_vid).unwrap(); + if neighbor_dist == &max_val { + continue; + } + let new_dist = neighbor_dist.clone().add(edge_val).unwrap(); + if new_dist < *node_dist { + return Err(GraphError::InvalidProperty { reason: "Negative cycle detected".to_string() }); + } + } + } + + for target in targets.into_iter() { + let target_ref = target.as_node_ref(); + let target_node = match g.node(target_ref) { + Some(tgt) => tgt, + None => { + let gid = match target_ref { + NodeRef::Internal(vid) => g.node_id(vid), + NodeRef::External(gid) => gid.to_owned(), + }; + return Err(GraphError::NodeMissingError(gid)); + } + }; + let mut path = IndexSet::default(); + path.insert(target_node.node); + let mut current_node_id = target_node.node; + while let Some(prev_node) = predecessor.get(¤t_node_id) { + if *prev_node == current_node_id { + break; + } + path.insert(*prev_node); + current_node_id = *prev_node; + } + path.reverse(); + shortest_paths.insert( + target_node.node, + (dist.get(&target_node.node).unwrap().as_f64().unwrap(), path), + ); + } + + let (index, values): (IndexSet<_, ahash::RandomState>, Vec<_>) = shortest_paths + .into_iter() + .map(|(id, (dist, path))| { + let nodes = + Nodes::new_filtered(g.clone(), g.clone(), NO_FILTER, Some(Index::new(path))); + (id, (dist, nodes)) + }) + .unzip(); + + Ok(NodeState::new( + g.clone(), + values.into(), + Some(Index::new(index)), + )) +} diff --git a/raphtory/src/algorithms/pathing/mod.rs b/raphtory/src/algorithms/pathing/mod.rs index 95063769bc..7df2e85a8d 100644 --- a/raphtory/src/algorithms/pathing/mod.rs +++ b/raphtory/src/algorithms/pathing/mod.rs @@ -1,3 +1,4 @@ +pub mod bellman_ford; pub mod dijkstra; pub mod single_source_shortest_path; pub mod temporal_reachability; diff --git a/raphtory/tests/algo_tests/pathing.rs b/raphtory/tests/algo_tests/pathing.rs index c11872df6a..f8e645173d 100644 --- a/raphtory/tests/algo_tests/pathing.rs +++ b/raphtory/tests/algo_tests/pathing.rs @@ -301,6 +301,335 @@ mod dijkstra_tests { } } +#[cfg(test)] +mod bellman_ford_tests { + use raphtory::{ + algorithms::pathing::bellman_ford::bellman_ford_single_source_shortest_paths, + db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::*, + test_storage, + }; + use raphtory_api::core::Direction; + + fn load_graph(edges: Vec<(i64, &str, &str, Vec<(&str, f32)>)>) -> Graph { + let graph = Graph::new(); + + for (t, src, dst, props) in edges { + graph.add_edge(t, src, dst, props, None).unwrap(); + } + graph + } + + fn basic_graph() -> Graph { + load_graph(vec![ + (0, "A", "B", vec![("weight", 4.0f32)]), + (1, "A", "C", vec![("weight", 4.0f32)]), + (2, "B", "C", vec![("weight", 2.0f32)]), + (3, "C", "D", vec![("weight", 3.0f32)]), + (4, "C", "E", vec![("weight", -2.0f32)]), + (5, "C", "F", vec![("weight", 6.0f32)]), + (6, "D", "F", vec![("weight", 2.0f32)]), + (7, "E", "F", vec![("weight", 3.0f32)]), + ]) + } + + #[test] + fn test_bellman_ford_multiple_targets() { + let graph = basic_graph(); + + test_storage!(&graph, |graph| { + let targets: Vec<&str> = vec!["D", "F"]; + let results = bellman_ford_single_source_shortest_paths( + graph, + "A", + targets, + Some("weight"), + Direction::OUT, + ); + + let results = results.unwrap(); + + assert_eq!(results.get_by_node("D").unwrap().0, 7.0f64); + assert_eq!( + results.get_by_node("D").unwrap().1.name(), + vec!["A", "C", "D"] + ); + + assert_eq!(results.get_by_node("F").unwrap().0, 5.0f64); + assert_eq!( + results.get_by_node("F").unwrap().1.name(), + vec!["A", "C", "E", "F"] + ); + + let targets: Vec<&str> = vec!["D", "E", "F"]; + let results = bellman_ford_single_source_shortest_paths( + graph, + "B", + targets, + Some("weight"), + Direction::OUT, + ); + let results = results.unwrap(); + assert_eq!(results.get_by_node("D").unwrap().0, 5.0f64); + assert_eq!(results.get_by_node("E").unwrap().0, 0.0f64); + assert_eq!(results.get_by_node("F").unwrap().0, 3.0f64); + assert_eq!( + results.get_by_node("D").unwrap().1.name(), + vec!["B", "C", "D"] + ); + assert_eq!( + results.get_by_node("E").unwrap().1.name(), + vec!["B", "C", "E"] + ); + assert_eq!( + results.get_by_node("F").unwrap().1.name(), + vec!["B", "C", "E", "F"] + ); + }); + } + + #[test] + fn test_bellman_ford_no_weight() { + let graph = basic_graph(); + + test_storage!(&graph, |graph| { + let targets: Vec<&str> = vec!["C", "E", "F"]; + let results = + bellman_ford_single_source_shortest_paths(graph, "A", targets, None, Direction::OUT) + .unwrap(); + assert_eq!(results.get_by_node("C").unwrap().1.name(), vec!["A", "C"]); + assert_eq!( + results.get_by_node("E").unwrap().1.name(), + vec!["A", "C", "E"] + ); + assert_eq!( + results.get_by_node("F").unwrap().1.name(), + vec!["A", "C", "F"] + ); + }); + } + + #[test] + fn test_bellman_ford_multiple_targets_node_ids() { + let edges = vec![ + (0, 1, 2, vec![("weight", 4i64)]), + (1, 1, 3, vec![("weight", 4i64)]), + (2, 2, 3, vec![("weight", 2i64)]), + (3, 3, 4, vec![("weight", 3i64)]), + (4, 3, 5, vec![("weight", -2i64)]), + (5, 3, 6, vec![("weight", 6i64)]), + (6, 4, 6, vec![("weight", 2i64)]), + (7, 5, 6, vec![("weight", 3i64)]), + ]; + + let graph = Graph::new(); + for (t, src, dst, props) in edges { + graph.add_edge(t, src, dst, props, None).unwrap(); + } + + test_storage!(&graph, |graph| { + let targets = vec![4, 6]; + let results = bellman_ford_single_source_shortest_paths( + graph, + 1, + targets, + Some("weight"), + Direction::OUT, + ); + let results = results.unwrap(); + assert_eq!(results.get_by_node("4").unwrap().0, 7f64); + assert_eq!( + results.get_by_node("4").unwrap().1.name(), + vec!["1", "3", "4"] + ); + + assert_eq!(results.get_by_node("6").unwrap().0, 5f64); + assert_eq!( + results.get_by_node("6").unwrap().1.name(), + vec!["1", "3", "5", "6"] + ); + + let targets = vec![4, 5, 6]; + let results = bellman_ford_single_source_shortest_paths( + graph, + 2, + targets, + Some("weight"), + Direction::OUT, + ); + let results = results.unwrap(); + assert_eq!(results.get_by_node("4").unwrap().0, 5f64); + assert_eq!(results.get_by_node("5").unwrap().0, 0f64); + assert_eq!(results.get_by_node("6").unwrap().0, 3f64); + assert_eq!( + results.get_by_node("4").unwrap().1.name(), + vec!["2", "3", "4"] + ); + assert_eq!( + results.get_by_node("5").unwrap().1.name(), + vec!["2", "3", "5"] + ); + assert_eq!( + results.get_by_node("6").unwrap().1.name(), + vec!["2", "3", "5", "6"] + ); + }); + } + + #[test] + fn test_bellman_ford_multiple_targets_i64() { + let edges = vec![ + (0, "A", "B", vec![("weight", 4i64)]), + (1, "A", "C", vec![("weight", 4i64)]), + (2, "B", "C", vec![("weight", 2i64)]), + (3, "C", "D", vec![("weight", 3i64)]), + (4, "C", "E", vec![("weight", -2i64)]), + (5, "C", "F", vec![("weight", 6i64)]), + (6, "D", "F", vec![("weight", 2i64)]), + (7, "E", "F", vec![("weight", 3i64)]), + ]; + + let graph = Graph::new(); + + for (t, src, dst, props) in edges { + graph.add_edge(t, src, dst, props, None).unwrap(); + } + + test_storage!(&graph, |graph| { + let targets: Vec<&str> = vec!["D", "F"]; + let results = bellman_ford_single_source_shortest_paths( + graph, + "A", + targets, + Some("weight"), + Direction::OUT, + ); + let results = results.unwrap(); + assert_eq!(results.get_by_node("D").unwrap().0, 7f64); + assert_eq!( + results.get_by_node("D").unwrap().1.name(), + vec!["A", "C", "D"] + ); + + assert_eq!(results.get_by_node("F").unwrap().0, 5f64); + assert_eq!( + results.get_by_node("F").unwrap().1.name(), + vec!["A", "C", "E", "F"] + ); + + let targets: Vec<&str> = vec!["D", "E", "F"]; + let results = bellman_ford_single_source_shortest_paths( + graph, + "B", + targets, + Some("weight"), + Direction::OUT, + ); + let results = results.unwrap(); + assert_eq!(results.get_by_node("D").unwrap().0, 5f64); + assert_eq!(results.get_by_node("E").unwrap().0, 0f64); + assert_eq!(results.get_by_node("F").unwrap().0, 3f64); + assert_eq!( + results.get_by_node("D").unwrap().1.name(), + vec!["B", "C", "D"] + ); + assert_eq!( + results.get_by_node("E").unwrap().1.name(), + vec!["B", "C", "E"] + ); + assert_eq!( + results.get_by_node("F").unwrap().1.name(), + vec!["B", "C", "E", "F"] + ); + }); + } + + #[test] + fn test_bellman_ford_undirected() { + let edges = vec![ + (0, "C", "A", vec![("weight", 4u64)]), + (1, "A", "B", vec![("weight", 4u64)]), + (3, "C", "D", vec![("weight", 3u64)]), + ]; + + let graph = Graph::new(); + + for (t, src, dst, props) in edges { + graph.add_edge(t, src, dst, props, None).unwrap(); + } + + test_storage!(&graph, |graph| { + let targets: Vec<&str> = vec!["D"]; + let results = bellman_ford_single_source_shortest_paths( + graph, + "A", + targets, + Some("weight"), + Direction::BOTH, + ); + + let results = results.unwrap(); + assert_eq!(results.get_by_node("D").unwrap().0, 7f64); + assert_eq!( + results.get_by_node("D").unwrap().1.name(), + vec!["A", "C", "D"] + ); + }); + } + + #[test] + fn test_bellman_ford_no_weight_undirected() { + let edges = vec![ + (0, "C", "A", vec![("weight", 4u64)]), + (1, "A", "B", vec![("weight", 4u64)]), + (3, "C", "D", vec![("weight", 3u64)]), + ]; + + let graph = Graph::new(); + + for (t, src, dst, props) in edges { + graph.add_edge(t, src, dst, props, None).unwrap(); + } + + test_storage!(&graph, |graph| { + let targets: Vec<&str> = vec!["D"]; + let results = + bellman_ford_single_source_shortest_paths(graph, "A", targets, None, Direction::BOTH) + .unwrap(); + assert_eq!( + results.get_by_node("D").unwrap().1.name(), + vec!["A", "C", "D"] + ); + }); + } + + #[test] + fn test_bellman_ford_negative_cycle() { + let edges = vec![ + (0, "A", "B", vec![("weight", 1i64)]), + (1, "B", "C", vec![("weight", -5i64)]), + (2, "C", "A", vec![("weight", 2i64)]), + ]; + + let graph = Graph::new(); + for (t, src, dst, props) in edges { + graph.add_edge(t, src, dst, props, None).unwrap(); + } + + test_storage!(&graph, |graph| { + let targets: Vec<&str> = vec!["C"]; + let result = bellman_ford_single_source_shortest_paths( + graph, + "A", + targets, + Some("weight"), + Direction::OUT, + ); + assert!(result.is_err()); + }); + } +} + #[cfg(test)] mod sssp_tests { use raphtory::{