From f931ff9f6ebac87a971c15fc0af85a0327c84627 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Fri, 23 Jan 2026 15:21:04 -0600 Subject: [PATCH 01/16] working --- .../src/algorithms/pathing/bellman_ford.rs | 147 ++++++++++++++++++ raphtory/src/algorithms/pathing/mod.rs | 1 + 2 files changed, 148 insertions(+) create mode 100644 raphtory/src/algorithms/pathing/bellman_ford.rs diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs new file mode 100644 index 0000000000..73b19f00e9 --- /dev/null +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -0,0 +1,147 @@ +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::{ + cmp::Ordering, + collections::{BinaryHeap, HashMap, HashSet}, +}; + +pub fn bellman_ford_single_source_shortest_paths( + g: &G, + source: T, + 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 cost_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 n_nodes = g.count_nodes(); + + let mut source_shortest_paths_hashmap = HashMap::new(); + for i in 0..n_nodes { + source_shortest_paths_hashmap.insert(i, (cost_val.clone(), IndexSet::new())); + } + shortest_paths.insert(source_node.node, source_shortest_paths_hashmap); + + for node in g.nodes() { + if node.node == source_node.node { + continue; + } + let mut node_shortest_paths_hashmap = HashMap::new(); + node_shortest_paths_hashmap.insert(0, (max_val.clone(), IndexSet::new())); + shortest_paths.insert(node.node, node_shortest_paths_hashmap); + } + + for i in 0..(n_nodes - 1) { + for node in g.nodes() { + if node.node == source_node.node { + continue; + } + let mut min_cost = max_val.clone(); + let mut min_path= IndexSet::default(); + let edges = g.node(node.node).unwrap().out_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_shortest_paths = shortest_paths.get(&neighbor_vid).unwrap(); + let (neighbor_shortest_path_cost, neighbor_shortest_path) = + neighbor_shortest_paths.get(&(i - 1)).unwrap(); + let new_cost = neighbor_shortest_path_cost.clone().add(edge_val).unwrap(); + if new_cost < min_cost { + min_cost = new_cost; + min_path = neighbor_shortest_path.clone(); + min_path.insert(node.node); + } + } + if let Some(node_shortest_paths_hashmap) = shortest_paths.get_mut(&node.node) { + node_shortest_paths_hashmap.insert(i, (min_cost, min_path)); + } + } + } + let (index, values): (IndexSet<_, ahash::RandomState>, Vec<_>) = shortest_paths + .into_iter() + .map(|(id, nodes_path_hashmap)| { + let (cost, path) = nodes_path_hashmap.remove(&(n_nodes - 1)).unwrap(); + let nodes = + Nodes::new_filtered(g.clone(), g.clone(), NO_FILTER, Some(Index::new(path))); + (id, (cost.as_f64().unwrap(), 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; From 8fef4531c0f4455a60440adc3f246a134199a4b1 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Fri, 23 Jan 2026 16:35:33 -0600 Subject: [PATCH 02/16] working --- .../src/algorithms/pathing/bellman_ford.rs | 142 +++++++++++------- 1 file changed, 88 insertions(+), 54 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 73b19f00e9..7cef16f15c 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -1,3 +1,4 @@ +use crate::db::graph::nodes; use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; use crate::{ core::entities::nodes::node_ref::NodeRef, @@ -17,10 +18,61 @@ use raphtory_api::core::{ Direction, }; use std::{ - cmp::Ordering, - collections::{BinaryHeap, HashMap, HashSet}, + collections::{HashMap}, }; +fn find_shortest_paths(g: &G, source_node_vid: VID, cost_val: Prop, max_val: Prop, weight: Option<&str>, shortest_paths: &mut HashMap)>>) { + let n_nodes = g.count_nodes(); + let mut source_shortest_paths_hashmap = HashMap::new(); + for i in 0..n_nodes { + source_shortest_paths_hashmap.insert(i, (cost_val.clone(), IndexSet::default())); + } + shortest_paths.insert(source_node_vid, source_shortest_paths_hashmap); + + for node in g.nodes() { + if node.node == source_node_vid { + continue; + } + let mut node_shortest_paths_hashmap = HashMap::new(); + node_shortest_paths_hashmap.insert(0, (max_val.clone(), IndexSet::default())); + shortest_paths.insert(node.node, node_shortest_paths_hashmap); + } + + for i in 0..(n_nodes - 1) { + for node in g.nodes() { + if node.node == source_node_vid { + continue; + } + let mut min_cost = max_val.clone(); + let mut min_path= IndexSet::default(); + let edges = g.node(node.node).unwrap().out_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_shortest_paths = shortest_paths.get(&neighbor_vid).unwrap(); + let (neighbor_shortest_path_cost, neighbor_shortest_path) = + neighbor_shortest_paths.get(&(i - 1)).unwrap(); + let new_cost = neighbor_shortest_path_cost.clone().add(edge_val).unwrap(); + if new_cost < min_cost { + min_cost = new_cost; + min_path = neighbor_shortest_path.clone(); + min_path.insert(node.node); + } + } + if let Some(node_shortest_paths_hashmap) = shortest_paths.get_mut(&node.node) { + node_shortest_paths_hashmap.insert(i, (min_cost, min_path)); + } + } + } +} + + pub fn bellman_ford_single_source_shortest_paths( g: &G, source: T, @@ -79,66 +131,48 @@ pub fn bellman_ford_single_source_shortest_paths)>> = HashMap::new(); - + let mut incoming_shortest_paths: HashMap)>> = HashMap::new(); + let mut outgoing_shortest_paths: HashMap)>> = HashMap::new(); let n_nodes = g.count_nodes(); - let mut source_shortest_paths_hashmap = HashMap::new(); - for i in 0..n_nodes { - source_shortest_paths_hashmap.insert(i, (cost_val.clone(), IndexSet::new())); - } - shortest_paths.insert(source_node.node, source_shortest_paths_hashmap); - - for node in g.nodes() { - if node.node == source_node.node { - continue; - } - let mut node_shortest_paths_hashmap = HashMap::new(); - node_shortest_paths_hashmap.insert(0, (max_val.clone(), IndexSet::new())); - shortest_paths.insert(node.node, node_shortest_paths_hashmap); - } - - for i in 0..(n_nodes - 1) { - for node in g.nodes() { - if node.node == source_node.node { - continue; - } - let mut min_cost = max_val.clone(); - let mut min_path= IndexSet::default(); - let edges = g.node(node.node).unwrap().out_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_shortest_paths = shortest_paths.get(&neighbor_vid).unwrap(); - let (neighbor_shortest_path_cost, neighbor_shortest_path) = - neighbor_shortest_paths.get(&(i - 1)).unwrap(); - let new_cost = neighbor_shortest_path_cost.clone().add(edge_val).unwrap(); - if new_cost < min_cost { - min_cost = new_cost; - min_path = neighbor_shortest_path.clone(); - min_path.insert(node.node); - } - } - if let Some(node_shortest_paths_hashmap) = shortest_paths.get_mut(&node.node) { - node_shortest_paths_hashmap.insert(i, (min_cost, min_path)); - } - } - } - let (index, values): (IndexSet<_, ahash::RandomState>, Vec<_>) = shortest_paths - .into_iter() + let (index, values): (IndexSet<_, ahash::RandomState>, Vec<_>) = if matches!(direction, Direction::IN | Direction::OUT) { + let shortest_paths = match direction { + Direction::IN => { + &mut incoming_shortest_paths + }, + Direction::OUT => { + &mut outgoing_shortest_paths + }, + _ => unreachable!(), + }; + find_shortest_paths(g, source_node.node, cost_val.clone(), max_val.clone(), weight, shortest_paths); + shortest_paths + .iter_mut() .map(|(id, nodes_path_hashmap)| { let (cost, path) = nodes_path_hashmap.remove(&(n_nodes - 1)).unwrap(); let nodes = Nodes::new_filtered(g.clone(), g.clone(), NO_FILTER, Some(Index::new(path))); (id, (cost.as_f64().unwrap(), nodes)) }) - .unzip(); + .unzip() + } else { + find_shortest_paths(g, source_node.node, cost_val.clone(), max_val.clone(), weight, &mut incoming_shortest_paths); + find_shortest_paths(g, source_node.node, cost_val.clone(), max_val.clone(), weight, &mut outgoing_shortest_paths); + incoming_shortest_paths.iter_mut().map(|(id, nodes_path_hashmap_in)| { + let nodes_path_hashmap_out = outgoing_shortest_paths.get_mut(id).unwrap(); + let (cost_out, path_out) = nodes_path_hashmap_out.remove(&(n_nodes - 1)).unwrap(); + let (cost_in, path_in) = nodes_path_hashmap_in.remove(&(n_nodes - 1)).unwrap(); + let (cost, path) = if cost_in < cost_out { + (cost_in, path_in) + } else { + (cost_out, path_out) + }; + let nodes = + Nodes::new_filtered(g.clone(), g.clone(), NO_FILTER, Some(Index::new(path))); + (id, (cost.as_f64().unwrap(), nodes)) + }).unzip() + }; + Ok(NodeState::new( g.clone(), values.into(), From 9d65c278178443b3c1c8d47503b80b1ffce6f061 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Fri, 23 Jan 2026 17:08:51 -0600 Subject: [PATCH 03/16] working --- .../src/algorithms/pathing/bellman_ford.rs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 7cef16f15c..7c76fc684e 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -76,6 +76,7 @@ fn find_shortest_paths(g: &G, source_node_vid: VID, cost_ pub fn bellman_ford_single_source_shortest_paths( g: &G, source: T, + targets: Vec, weight: Option<&str>, direction: Direction, ) -> Result), G>, GraphError> { @@ -99,6 +100,13 @@ pub fn bellman_ford_single_source_shortest_paths Date: Fri, 23 Jan 2026 17:13:56 -0600 Subject: [PATCH 04/16] working --- raphtory/tests/algo_tests/pathing.rs | 303 +++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) diff --git a/raphtory/tests/algo_tests/pathing.rs b/raphtory/tests/algo_tests/pathing.rs index c11872df6a..4e41df7cb9 100644 --- a/raphtory/tests/algo_tests/pathing.rs +++ b/raphtory/tests/algo_tests/pathing.rs @@ -301,6 +301,309 @@ 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", 1.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, 8.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, 3.0f64); + assert_eq!(results.get_by_node("F").unwrap().0, 6.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", 4u64)]), + (1, 1, 3, vec![("weight", 4u64)]), + (2, 2, 3, vec![("weight", 2u64)]), + (3, 3, 4, vec![("weight", 3u64)]), + (4, 3, 5, vec![("weight", 1u64)]), + (5, 3, 6, vec![("weight", 6u64)]), + (6, 4, 6, vec![("weight", 2u64)]), + (7, 5, 6, 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![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, 8f64); + 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, 3f64); + assert_eq!(results.get_by_node("6").unwrap().0, 6f64); + 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_u64() { + let edges = vec![ + (0, "A", "B", vec![("weight", 4u64)]), + (1, "A", "C", vec![("weight", 4u64)]), + (2, "B", "C", vec![("weight", 2u64)]), + (3, "C", "D", vec![("weight", 3u64)]), + (4, "C", "E", vec![("weight", 1u64)]), + (5, "C", "F", vec![("weight", 6u64)]), + (6, "D", "F", vec![("weight", 2u64)]), + (7, "E", "F", 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", "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, 8f64); + 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, 3f64); + assert_eq!(results.get_by_node("F").unwrap().0, 6f64); + 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"] + ); + }); + } +} + #[cfg(test)] mod sssp_tests { use raphtory::{ From 88c45212151d39b8f7a250fe303f150c74f3fef1 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Fri, 23 Jan 2026 18:17:57 -0600 Subject: [PATCH 05/16] working --- .../src/algorithms/pathing/bellman_ford.rs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 7c76fc684e..eb51e211a5 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -17,11 +17,12 @@ use raphtory_api::core::{ }, Direction, }; +use std::panic; use std::{ collections::{HashMap}, }; -fn find_shortest_paths(g: &G, source_node_vid: VID, cost_val: Prop, max_val: Prop, weight: Option<&str>, shortest_paths: &mut HashMap)>>) { +fn find_shortest_paths(g: &G, source_node_vid: VID, cost_val: Prop, max_val: Prop, weight: Option<&str>, direction: Direction, shortest_paths: &mut HashMap)>>) { let n_nodes = g.count_nodes(); let mut source_shortest_paths_hashmap = HashMap::new(); for i in 0..n_nodes { @@ -38,14 +39,17 @@ fn find_shortest_paths(g: &G, source_node_vid: VID, cost_ shortest_paths.insert(node.node, node_shortest_paths_hashmap); } - for i in 0..(n_nodes - 1) { + for i in 1..(n_nodes) { for node in g.nodes() { if node.node == source_node_vid { continue; } - let mut min_cost = max_val.clone(); - let mut min_path= IndexSet::default(); - let edges = g.node(node.node).unwrap().out_edges(); + let (mut min_cost, mut min_path) = shortest_paths.get(&node.node).unwrap().get(&(i - 1)).unwrap().clone(); + let edges = match direction { + Direction::IN => node.in_edges(), + Direction::OUT => node.out_edges(), + _ => panic!("Unsupported direction"), + }; for edge in edges { let edge_val = match weight { None => Prop::U8(1), @@ -153,7 +157,7 @@ pub fn bellman_ford_single_source_shortest_paths unreachable!(), }; - find_shortest_paths(g, source_node.node, cost_val.clone(), max_val.clone(), weight, shortest_paths); + find_shortest_paths(g, source_node.node, cost_val.clone(), max_val.clone(), weight, direction, shortest_paths); shortest_paths .iter_mut() .filter_map(|(id, nodes_path_hashmap)| { @@ -167,8 +171,8 @@ pub fn bellman_ford_single_source_shortest_paths Date: Fri, 23 Jan 2026 20:21:42 -0600 Subject: [PATCH 06/16] working --- .../src/algorithms/pathing/bellman_ford.rs | 147 +++++++----------- 1 file changed, 56 insertions(+), 91 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index eb51e211a5..717b503b28 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -1,4 +1,3 @@ -use crate::db::graph::nodes; use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; use crate::{ core::entities::nodes::node_ref::NodeRef, @@ -17,65 +16,10 @@ use raphtory_api::core::{ }, Direction, }; -use std::panic; use std::{ collections::{HashMap}, }; -fn find_shortest_paths(g: &G, source_node_vid: VID, cost_val: Prop, max_val: Prop, weight: Option<&str>, direction: Direction, shortest_paths: &mut HashMap)>>) { - let n_nodes = g.count_nodes(); - let mut source_shortest_paths_hashmap = HashMap::new(); - for i in 0..n_nodes { - source_shortest_paths_hashmap.insert(i, (cost_val.clone(), IndexSet::default())); - } - shortest_paths.insert(source_node_vid, source_shortest_paths_hashmap); - - for node in g.nodes() { - if node.node == source_node_vid { - continue; - } - let mut node_shortest_paths_hashmap = HashMap::new(); - node_shortest_paths_hashmap.insert(0, (max_val.clone(), IndexSet::default())); - shortest_paths.insert(node.node, node_shortest_paths_hashmap); - } - - for i in 1..(n_nodes) { - for node in g.nodes() { - if node.node == source_node_vid { - continue; - } - let (mut min_cost, mut min_path) = shortest_paths.get(&node.node).unwrap().get(&(i - 1)).unwrap().clone(); - let edges = match direction { - Direction::IN => node.in_edges(), - Direction::OUT => node.out_edges(), - _ => panic!("Unsupported direction"), - }; - 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_shortest_paths = shortest_paths.get(&neighbor_vid).unwrap(); - let (neighbor_shortest_path_cost, neighbor_shortest_path) = - neighbor_shortest_paths.get(&(i - 1)).unwrap(); - let new_cost = neighbor_shortest_path_cost.clone().add(edge_val).unwrap(); - if new_cost < min_cost { - min_cost = new_cost; - min_path = neighbor_shortest_path.clone(); - min_path.insert(node.node); - } - } - if let Some(node_shortest_paths_hashmap) = shortest_paths.get_mut(&node.node) { - node_shortest_paths_hashmap.insert(i, (min_cost, min_path)); - } - } - } -} - pub fn bellman_ford_single_source_shortest_paths( g: &G, @@ -143,22 +87,63 @@ pub fn bellman_ford_single_source_shortest_paths)>> = HashMap::new(); - let mut outgoing_shortest_paths: HashMap)>> = HashMap::new(); + let mut shortest_paths: HashMap)>> = HashMap::new(); let n_nodes = g.count_nodes(); + let mut source_shortest_paths_hashmap = HashMap::new(); + let mut source_path = IndexSet::default(); + source_path.insert(source_node.node); + for i in 0..n_nodes { + source_shortest_paths_hashmap.insert(i, (cost_val.clone(), source_path.clone())); + } + shortest_paths.insert(source_node.node, source_shortest_paths_hashmap); + + for node in g.nodes() { + if node.node == source_node.node { + continue; + } + let mut node_shortest_paths_hashmap = HashMap::new(); + node_shortest_paths_hashmap.insert(0, (max_val.clone(), IndexSet::default())); + shortest_paths.insert(node.node, node_shortest_paths_hashmap); + } + + for i in 1..(n_nodes) { + for node in g.nodes() { + if node.node == source_node.node { + continue; + } + let (mut min_cost, mut min_path) = shortest_paths.get(&node.node).unwrap().get(&(i - 1)).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_shortest_paths = shortest_paths.get(&neighbor_vid).unwrap(); + let (neighbor_shortest_path_cost, neighbor_shortest_path) = + neighbor_shortest_paths.get(&(i - 1)).unwrap(); + if neighbor_shortest_path_cost == &max_val { + continue; + } + let new_cost = neighbor_shortest_path_cost.clone().add(edge_val).unwrap(); + if new_cost < min_cost { + min_cost = new_cost; + min_path = neighbor_shortest_path.clone(); + min_path.insert(node.node); + } + } + shortest_paths.get_mut(&node.node).unwrap().insert(i, (min_cost, min_path)); + } + } - let (index, values): (IndexSet<_, ahash::RandomState>, Vec<_>) = if matches!(direction, Direction::IN | Direction::OUT) { - let shortest_paths = match direction { - Direction::IN => { - &mut incoming_shortest_paths - }, - Direction::OUT => { - &mut outgoing_shortest_paths - }, - _ => unreachable!(), - }; - find_shortest_paths(g, source_node.node, cost_val.clone(), max_val.clone(), weight, direction, shortest_paths); - shortest_paths + let (index, values): (IndexSet<_, ahash::RandomState>, Vec<_>) = shortest_paths .iter_mut() .filter_map(|(id, nodes_path_hashmap)| { if !target_nodes[id.index()] { @@ -169,27 +154,7 @@ pub fn bellman_ford_single_source_shortest_paths Date: Fri, 23 Jan 2026 21:56:26 -0600 Subject: [PATCH 07/16] working --- .../src/algorithms/pathing/bellman_ford.rs | 16 ++++++ raphtory/tests/algo_tests/pathing.rs | 54 +++++++++---------- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 717b503b28..837ce87d5f 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -1,3 +1,4 @@ +/// Bellman-Ford algorithm use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; use crate::{ core::entities::nodes::node_ref::NodeRef, @@ -21,6 +22,21 @@ use std::{ }; +/// 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 cost and a vector of nodes representing the shortest path. +/// pub fn bellman_ford_single_source_shortest_paths( g: &G, source: T, diff --git a/raphtory/tests/algo_tests/pathing.rs b/raphtory/tests/algo_tests/pathing.rs index 4e41df7cb9..319cea42d9 100644 --- a/raphtory/tests/algo_tests/pathing.rs +++ b/raphtory/tests/algo_tests/pathing.rs @@ -326,7 +326,7 @@ mod bellman_ford_tests { (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", 1.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)]), @@ -355,7 +355,7 @@ mod bellman_ford_tests { vec!["A", "C", "D"] ); - assert_eq!(results.get_by_node("F").unwrap().0, 8.0f64); + 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"] @@ -371,8 +371,8 @@ mod bellman_ford_tests { ); let results = results.unwrap(); assert_eq!(results.get_by_node("D").unwrap().0, 5.0f64); - assert_eq!(results.get_by_node("E").unwrap().0, 3.0f64); - assert_eq!(results.get_by_node("F").unwrap().0, 6.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"] @@ -412,14 +412,14 @@ mod bellman_ford_tests { #[test] fn test_bellman_ford_multiple_targets_node_ids() { let edges = vec![ - (0, 1, 2, vec![("weight", 4u64)]), - (1, 1, 3, vec![("weight", 4u64)]), - (2, 2, 3, vec![("weight", 2u64)]), - (3, 3, 4, vec![("weight", 3u64)]), - (4, 3, 5, vec![("weight", 1u64)]), - (5, 3, 6, vec![("weight", 6u64)]), - (6, 4, 6, vec![("weight", 2u64)]), - (7, 5, 6, vec![("weight", 3u64)]), + (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(); @@ -443,7 +443,7 @@ mod bellman_ford_tests { vec!["1", "3", "4"] ); - assert_eq!(results.get_by_node("6").unwrap().0, 8f64); + 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"] @@ -459,8 +459,8 @@ mod bellman_ford_tests { ); let results = results.unwrap(); assert_eq!(results.get_by_node("4").unwrap().0, 5f64); - assert_eq!(results.get_by_node("5").unwrap().0, 3f64); - assert_eq!(results.get_by_node("6").unwrap().0, 6f64); + 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"] @@ -477,16 +477,16 @@ mod bellman_ford_tests { } #[test] - fn test_bellman_ford_multiple_targets_u64() { + fn test_bellman_ford_multiple_targets_i64() { let edges = vec![ - (0, "A", "B", vec![("weight", 4u64)]), - (1, "A", "C", vec![("weight", 4u64)]), - (2, "B", "C", vec![("weight", 2u64)]), - (3, "C", "D", vec![("weight", 3u64)]), - (4, "C", "E", vec![("weight", 1u64)]), - (5, "C", "F", vec![("weight", 6u64)]), - (6, "D", "F", vec![("weight", 2u64)]), - (7, "E", "F", vec![("weight", 3u64)]), + (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(); @@ -511,7 +511,7 @@ mod bellman_ford_tests { vec!["A", "C", "D"] ); - assert_eq!(results.get_by_node("F").unwrap().0, 8f64); + 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"] @@ -527,8 +527,8 @@ mod bellman_ford_tests { ); let results = results.unwrap(); assert_eq!(results.get_by_node("D").unwrap().0, 5f64); - assert_eq!(results.get_by_node("E").unwrap().0, 3f64); - assert_eq!(results.get_by_node("F").unwrap().0, 6f64); + 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"] From 1c09f3ec8c87e8de86921a175962783e5a96a473 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Sat, 24 Jan 2026 18:18:41 -0600 Subject: [PATCH 08/16] working --- .../src/algorithms/covering/dominating_set.rs | 176 ++++++++++++++++++ raphtory/src/algorithms/covering/mod.rs | 1 + raphtory/src/algorithms/mod.rs | 1 + .../src/algorithms/pathing/bellman_ford.rs | 90 +++++---- raphtory/tests/algo_tests/pathing.rs | 26 +++ 5 files changed, 257 insertions(+), 37 deletions(-) create mode 100644 raphtory/src/algorithms/covering/dominating_set.rs create mode 100644 raphtory/src/algorithms/covering/mod.rs diff --git a/raphtory/src/algorithms/covering/dominating_set.rs b/raphtory/src/algorithms/covering/dominating_set.rs new file mode 100644 index 0000000000..d37eb72851 --- /dev/null +++ b/raphtory/src/algorithms/covering/dominating_set.rs @@ -0,0 +1,176 @@ +use crate::{db::api::{view::StaticGraphViewOps}, prelude::{GraphViewOps, NodeViewOps}}; +use raphtory_api::core::{ + entities::{ + VID, + }, + Direction, +}; +use std::{ + collections::HashSet +}; + + +pub fn compute_dominating_set(g: &G, direction: Direction) -> HashSet { + let mut dominating_set = HashSet::new(); + let mut uncovered_set = HashSet::new(); + for node in g.nodes() { + uncovered_set.insert(node.node); + } + while !uncovered_set.is_empty() { + let mut max_vid = None; + let mut max_score = -1; + for v in g.nodes() { + let vid = v.node; + if dominating_set.contains(&vid) { + continue; + } + + let neighbors = match direction { + Direction::IN => v.in_neighbours(), + Direction::OUT => v.out_neighbours(), + Direction::BOTH => v.neighbours(), + }; + + let mut score = 0; + if uncovered_set.contains(&vid) { + score += 1; + } + for neighbor in neighbors { + if uncovered_set.contains(&neighbor.node) { + score += 1; + } + } + + if max_vid.is_none() || score > max_score { + max_vid = Some(vid); + max_score = score; + } + } + + let max_vid = max_vid.unwrap(); + let max_node = g.node(max_vid).unwrap(); + let max_node_neighbors = match direction { + Direction::IN => max_node.in_neighbours(), + Direction::OUT => max_node.out_neighbours(), + Direction::BOTH => max_node.neighbours(), + }; + + uncovered_set.remove(&max_vid); + for neighbor in max_node_neighbors { + uncovered_set.remove(&neighbor.node); + } + dominating_set.insert(max_vid); + } + dominating_set +} + +pub fn is_dominating_set(g: &G, ds: &HashSet, direction: Direction) -> bool { + let mut covered = HashSet::new(); + for &v in ds { + covered.insert(v); + let node = g.node(v).unwrap(); + match direction { + Direction::IN => { + for n in node.in_neighbours() { covered.insert(n.node); } + }, + Direction::OUT => { + for n in node.out_neighbours() { covered.insert(n.node); } + }, + Direction::BOTH => { + for n in node.neighbours() { covered.insert(n.node); } + } + } + } + g.count_nodes() == covered.len() +} + + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + use raphtory_api::core::Direction; + + #[test] + fn test_dominating_set_line_graph() { + let g = Graph::new(); + // 1 -> 2 -> 3 + g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + + let ds = compute_dominating_set(&g, Direction::OUT); + assert!(is_dominating_set(&g, &ds, Direction::OUT)); + } + + #[test] + fn test_dominating_set_star_graph() { + let g = Graph::new(); + // 1 -> 2, 1 -> 3, 1 -> 4 + g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(0, 1, 3, NO_PROPS, None).unwrap(); + g.add_edge(0, 1, 4, NO_PROPS, None).unwrap(); + + let ds = compute_dominating_set(&g, Direction::OUT); + assert!(is_dominating_set(&g, &ds, Direction::OUT)); + let vid_1 = g.node(1).unwrap().node; + assert!(ds.contains(&vid_1)); + assert_eq!(ds.len(), 1); + } + + #[test] + fn test_dominating_set_incoming() { + let g = Graph::new(); + // 2 -> 1, 3 -> 1, 4 -> 1 + g.add_edge(0, 2, 1, NO_PROPS, None).unwrap(); + g.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); + g.add_edge(0, 4, 1, NO_PROPS, None).unwrap(); + + let ds = compute_dominating_set(&g, Direction::IN); + assert!(is_dominating_set(&g, &ds, Direction::IN)); + let vid_1 = g.node(1).unwrap().node; + assert!(ds.contains(&vid_1)); + assert_eq!(ds.len(), 1); + } + + #[test] + fn test_dominating_set_disconnected() { + let g = Graph::new(); + g.add_node(0, 1, NO_PROPS, None).unwrap(); + g.add_node(0, 2, NO_PROPS, None).unwrap(); + + let ds = compute_dominating_set(&g, Direction::BOTH); + assert!(is_dominating_set(&g, &ds, Direction::BOTH)); + assert_eq!(ds.len(), 2); + } + + #[test] + fn test_dominating_set_cycle_graph() { + let g = Graph::new(); + // 1 -> 2 -> 3 -> 1 + g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + g.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); + + let ds = compute_dominating_set(&g, Direction::OUT); + assert!(is_dominating_set(&g, &ds, Direction::OUT)); + assert!(ds.len() >= 2); + } + + #[test] + fn test_dominating_set_complete_graph() { + let g = Graph::new(); + // Complete graph K4 + for i in 1..=4 { + for j in 1..=4 { + if i != j { + g.add_edge(0, i, j, NO_PROPS, None).unwrap(); + } + } + } + + let ds = compute_dominating_set(&g, Direction::OUT); + assert!(is_dominating_set(&g, &ds, Direction::OUT)); + // In a complete graph, any single node dominates all others. + assert_eq!(ds.len(), 1); + } +} \ No newline at end of file diff --git a/raphtory/src/algorithms/covering/mod.rs b/raphtory/src/algorithms/covering/mod.rs new file mode 100644 index 0000000000..57b62a98b7 --- /dev/null +++ b/raphtory/src/algorithms/covering/mod.rs @@ -0,0 +1 @@ +pub mod dominating_set; \ No newline at end of file diff --git a/raphtory/src/algorithms/mod.rs b/raphtory/src/algorithms/mod.rs index 9cfdb23900..25541e6632 100644 --- a/raphtory/src/algorithms/mod.rs +++ b/raphtory/src/algorithms/mod.rs @@ -32,6 +32,7 @@ pub mod community_detection; pub mod bipartite; pub mod components; pub mod cores; +pub mod covering; pub mod dynamics; pub mod embeddings; pub mod layout; diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 837ce87d5f..261e03baf6 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -64,13 +64,6 @@ pub fn bellman_ford_single_source_shortest_paths)>> = HashMap::new(); - let n_nodes = g.count_nodes(); - let mut source_shortest_paths_hashmap = HashMap::new(); - let mut source_path = IndexSet::default(); - source_path.insert(source_node.node); - for i in 0..n_nodes { - source_shortest_paths_hashmap.insert(i, (cost_val.clone(), source_path.clone())); - } - shortest_paths.insert(source_node.node, source_shortest_paths_hashmap); + 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 { - continue; + dist.insert(source_node.node, cost_val.clone()); + } else { + dist.insert(node.node, max_val.clone()); } - let mut node_shortest_paths_hashmap = HashMap::new(); - node_shortest_paths_hashmap.insert(0, (max_val.clone(), IndexSet::default())); - shortest_paths.insert(node.node, node_shortest_paths_hashmap); } - for i in 1..(n_nodes) { + for i in 1..(n_nodes + 1) { for node in g.nodes() { if node.node == source_node.node { continue; } - let (mut min_cost, mut min_path) = shortest_paths.get(&node.node).unwrap().get(&(i - 1)).unwrap().clone(); + let mut min_cost = 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(), @@ -142,33 +132,59 @@ pub fn bellman_ford_single_source_shortest_paths, Vec<_>) = shortest_paths - .iter_mut() - .filter_map(|(id, nodes_path_hashmap)| { - if !target_nodes[id.index()] { - return None; + 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; } - let (cost, path) = nodes_path_hashmap.remove(&(n_nodes - 1)).unwrap(); + 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, (cost, path))| { let nodes = Nodes::new_filtered(g.clone(), g.clone(), NO_FILTER, Some(Index::new(path))); - Some((id, (cost.as_f64().unwrap(), nodes))) + (id, (cost, nodes)) }) .unzip(); diff --git a/raphtory/tests/algo_tests/pathing.rs b/raphtory/tests/algo_tests/pathing.rs index 319cea42d9..f8e645173d 100644 --- a/raphtory/tests/algo_tests/pathing.rs +++ b/raphtory/tests/algo_tests/pathing.rs @@ -602,6 +602,32 @@ mod bellman_ford_tests { ); }); } + + #[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)] From 3e527c33831fd427fe920a7a2a975913c5441b0f Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Sun, 25 Jan 2026 20:16:33 -0600 Subject: [PATCH 09/16] working --- .../src/algorithms/pathing/bellman_ford.rs | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 261e03baf6..6bf4ccd68d 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -35,7 +35,7 @@ use std::{ /// # Returns /// /// Returns a `HashMap` where the key is the target node and the value is a tuple containing -/// the total cost and a vector of nodes representing the shortest path. +/// the total dist and a vector of nodes representing the shortest path. /// pub fn bellman_ford_single_source_shortest_paths( g: &G, @@ -66,7 +66,7 @@ pub fn bellman_ford_single_source_shortest_paths Prop::F32(0f32), PropType::F64 => Prop::F64(0f64), PropType::U8 => Prop::U8(0u8), @@ -105,7 +105,7 @@ pub fn bellman_ford_single_source_shortest_paths node.out_edges(), @@ -136,20 +136,44 @@ pub fn bellman_ford_single_source_shortest_paths 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(); + 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) { @@ -181,10 +205,10 @@ pub fn bellman_ford_single_source_shortest_paths, Vec<_>) = shortest_paths .into_iter() - .map(|(id, (cost, path))| { + .map(|(id, (dist, path))| { let nodes = Nodes::new_filtered(g.clone(), g.clone(), NO_FILTER, Some(Index::new(path))); - (id, (cost, nodes)) + (id, (dist, nodes)) }) .unzip(); From 7a0c8c28b4ddb232d1094a594e5d2a61eb84eebd Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Sun, 25 Jan 2026 20:17:31 -0600 Subject: [PATCH 10/16] working --- .../src/algorithms/covering/dominating_set.rs | 176 ------------------ raphtory/src/algorithms/covering/mod.rs | 1 - 2 files changed, 177 deletions(-) delete mode 100644 raphtory/src/algorithms/covering/dominating_set.rs delete mode 100644 raphtory/src/algorithms/covering/mod.rs diff --git a/raphtory/src/algorithms/covering/dominating_set.rs b/raphtory/src/algorithms/covering/dominating_set.rs deleted file mode 100644 index d37eb72851..0000000000 --- a/raphtory/src/algorithms/covering/dominating_set.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::{db::api::{view::StaticGraphViewOps}, prelude::{GraphViewOps, NodeViewOps}}; -use raphtory_api::core::{ - entities::{ - VID, - }, - Direction, -}; -use std::{ - collections::HashSet -}; - - -pub fn compute_dominating_set(g: &G, direction: Direction) -> HashSet { - let mut dominating_set = HashSet::new(); - let mut uncovered_set = HashSet::new(); - for node in g.nodes() { - uncovered_set.insert(node.node); - } - while !uncovered_set.is_empty() { - let mut max_vid = None; - let mut max_score = -1; - for v in g.nodes() { - let vid = v.node; - if dominating_set.contains(&vid) { - continue; - } - - let neighbors = match direction { - Direction::IN => v.in_neighbours(), - Direction::OUT => v.out_neighbours(), - Direction::BOTH => v.neighbours(), - }; - - let mut score = 0; - if uncovered_set.contains(&vid) { - score += 1; - } - for neighbor in neighbors { - if uncovered_set.contains(&neighbor.node) { - score += 1; - } - } - - if max_vid.is_none() || score > max_score { - max_vid = Some(vid); - max_score = score; - } - } - - let max_vid = max_vid.unwrap(); - let max_node = g.node(max_vid).unwrap(); - let max_node_neighbors = match direction { - Direction::IN => max_node.in_neighbours(), - Direction::OUT => max_node.out_neighbours(), - Direction::BOTH => max_node.neighbours(), - }; - - uncovered_set.remove(&max_vid); - for neighbor in max_node_neighbors { - uncovered_set.remove(&neighbor.node); - } - dominating_set.insert(max_vid); - } - dominating_set -} - -pub fn is_dominating_set(g: &G, ds: &HashSet, direction: Direction) -> bool { - let mut covered = HashSet::new(); - for &v in ds { - covered.insert(v); - let node = g.node(v).unwrap(); - match direction { - Direction::IN => { - for n in node.in_neighbours() { covered.insert(n.node); } - }, - Direction::OUT => { - for n in node.out_neighbours() { covered.insert(n.node); } - }, - Direction::BOTH => { - for n in node.neighbours() { covered.insert(n.node); } - } - } - } - g.count_nodes() == covered.len() -} - - -#[cfg(test)] -mod tests { - use super::*; - use crate::prelude::*; - use raphtory_api::core::Direction; - - #[test] - fn test_dominating_set_line_graph() { - let g = Graph::new(); - // 1 -> 2 -> 3 - g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - g.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); - - let ds = compute_dominating_set(&g, Direction::OUT); - assert!(is_dominating_set(&g, &ds, Direction::OUT)); - } - - #[test] - fn test_dominating_set_star_graph() { - let g = Graph::new(); - // 1 -> 2, 1 -> 3, 1 -> 4 - g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - g.add_edge(0, 1, 3, NO_PROPS, None).unwrap(); - g.add_edge(0, 1, 4, NO_PROPS, None).unwrap(); - - let ds = compute_dominating_set(&g, Direction::OUT); - assert!(is_dominating_set(&g, &ds, Direction::OUT)); - let vid_1 = g.node(1).unwrap().node; - assert!(ds.contains(&vid_1)); - assert_eq!(ds.len(), 1); - } - - #[test] - fn test_dominating_set_incoming() { - let g = Graph::new(); - // 2 -> 1, 3 -> 1, 4 -> 1 - g.add_edge(0, 2, 1, NO_PROPS, None).unwrap(); - g.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); - g.add_edge(0, 4, 1, NO_PROPS, None).unwrap(); - - let ds = compute_dominating_set(&g, Direction::IN); - assert!(is_dominating_set(&g, &ds, Direction::IN)); - let vid_1 = g.node(1).unwrap().node; - assert!(ds.contains(&vid_1)); - assert_eq!(ds.len(), 1); - } - - #[test] - fn test_dominating_set_disconnected() { - let g = Graph::new(); - g.add_node(0, 1, NO_PROPS, None).unwrap(); - g.add_node(0, 2, NO_PROPS, None).unwrap(); - - let ds = compute_dominating_set(&g, Direction::BOTH); - assert!(is_dominating_set(&g, &ds, Direction::BOTH)); - assert_eq!(ds.len(), 2); - } - - #[test] - fn test_dominating_set_cycle_graph() { - let g = Graph::new(); - // 1 -> 2 -> 3 -> 1 - g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - g.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); - g.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); - - let ds = compute_dominating_set(&g, Direction::OUT); - assert!(is_dominating_set(&g, &ds, Direction::OUT)); - assert!(ds.len() >= 2); - } - - #[test] - fn test_dominating_set_complete_graph() { - let g = Graph::new(); - // Complete graph K4 - for i in 1..=4 { - for j in 1..=4 { - if i != j { - g.add_edge(0, i, j, NO_PROPS, None).unwrap(); - } - } - } - - let ds = compute_dominating_set(&g, Direction::OUT); - assert!(is_dominating_set(&g, &ds, Direction::OUT)); - // In a complete graph, any single node dominates all others. - assert_eq!(ds.len(), 1); - } -} \ No newline at end of file diff --git a/raphtory/src/algorithms/covering/mod.rs b/raphtory/src/algorithms/covering/mod.rs deleted file mode 100644 index 57b62a98b7..0000000000 --- a/raphtory/src/algorithms/covering/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod dominating_set; \ No newline at end of file From aa09e6498c236a3f6f497382238729db853c58f7 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Sun, 25 Jan 2026 20:17:51 -0600 Subject: [PATCH 11/16] working --- raphtory/src/algorithms/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/raphtory/src/algorithms/mod.rs b/raphtory/src/algorithms/mod.rs index 25541e6632..9cfdb23900 100644 --- a/raphtory/src/algorithms/mod.rs +++ b/raphtory/src/algorithms/mod.rs @@ -32,7 +32,6 @@ pub mod community_detection; pub mod bipartite; pub mod components; pub mod cores; -pub mod covering; pub mod dynamics; pub mod embeddings; pub mod layout; From 112d84fff61d936fdf6790ea95810f88c683dacb Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Sun, 25 Jan 2026 20:31:43 -0600 Subject: [PATCH 12/16] working --- raphtory/src/algorithms/pathing/bellman_ford.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 6bf4ccd68d..336985390b 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -167,6 +167,9 @@ pub fn bellman_ford_single_source_shortest_paths Date: Sun, 25 Jan 2026 20:37:29 -0600 Subject: [PATCH 13/16] working --- raphtory/src/algorithms/pathing/bellman_ford.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 336985390b..afbcba9d80 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -111,7 +111,7 @@ pub fn bellman_ford_single_source_shortest_paths Date: Sun, 25 Jan 2026 20:41:27 -0600 Subject: [PATCH 14/16] working --- raphtory/src/algorithms/pathing/bellman_ford.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index afbcba9d80..046e726245 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -138,9 +138,6 @@ pub fn bellman_ford_single_source_shortest_paths Date: Sun, 25 Jan 2026 20:59:24 -0600 Subject: [PATCH 15/16] working --- raphtory/src/algorithms/pathing/bellman_ford.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 046e726245..1f6c544007 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -111,7 +111,7 @@ pub fn bellman_ford_single_source_shortest_paths Date: Sun, 25 Jan 2026 22:23:21 -0600 Subject: [PATCH 16/16] working --- raphtory/src/algorithms/pathing/bellman_ford.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 1f6c544007..039dc470e3 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -112,6 +112,7 @@ pub fn bellman_ford_single_source_shortest_paths