From f931ff9f6ebac87a971c15fc0af85a0327c84627 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Fri, 23 Jan 2026 15:21:04 -0600 Subject: [PATCH 01/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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 Date: Wed, 28 Jan 2026 19:59:15 -0600 Subject: [PATCH 17/45] working --- .../src/algorithms/covering/dominating_set.rs | 432 ++++++++++++++++++ raphtory/src/algorithms/covering/mod.rs | 1 + raphtory/src/algorithms/mod.rs | 1 + 3 files changed, 434 insertions(+) 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..6dbcf37fe4 --- /dev/null +++ b/raphtory/src/algorithms/covering/dominating_set.rs @@ -0,0 +1,432 @@ +use crate::db::api::state::ops::node; +use crate::db::graph::assertions::FilterNeighbours; +/// Dijkstra's 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::hash::Hash; +use std::{ + cmp::Ordering, + collections::{BinaryHeap, HashMap, HashSet}, +}; +use rayon::prelude::*; + +#[derive(Default, Clone)] +struct NodeCoveringState { + vid: VID, + is_covered: bool, + is_active: bool, + is_candidate: bool, + has_no_coverage: bool, + support: usize, + candidates: usize, + weight_rounded: usize, + weight: usize, + add_to_dominating_set: bool +} + + +pub fn fast_distributed_dominating_set(g: &G) -> HashSet { + let mut dominating_set = HashSet::new(); + let mut covered_count = 0; + let n_nodes = g.count_nodes(); + let mut adj_list: Vec> = vec![vec![]; n_nodes]; + let mut current_node_configs = vec![NodeCoveringState::default(); n_nodes]; + let mut next_node_configs = vec![NodeCoveringState::default(); n_nodes]; + for node in g.nodes() { + let vid = node.node; + current_node_configs[vid.index()].vid = vid; + next_node_configs[vid.index()].vid = vid; + adj_list[vid.index()] = node.neighbours().iter().map(|n| n.node.index()).collect(); + } + while covered_count < n_nodes { + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.is_covered = current_node_config.is_covered; + if current_node_config.has_no_coverage { + return; + } + let mut node_weight = 0 as u64; + if !current_node_config.is_covered { + node_weight += 1; + } + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if !neighbor_config.is_covered { + node_weight += 1; + } + } + if node_weight == 0 { + next_node_config.has_no_coverage = true; + next_node_config.weight = 0; + next_node_config.weight_rounded = 0; + } else { + let node_weight_rounded = (2 as u64).pow(node_weight.ilog2()) as usize; + next_node_config.weight = node_weight as usize; + next_node_config.weight_rounded = node_weight_rounded; + } + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.has_no_coverage = current_node_config.has_no_coverage; + next_node_config.weight = current_node_config.weight; + next_node_config.weight_rounded = current_node_config.weight_rounded; + if current_node_config.has_no_coverage { + next_node_config.is_active = false; + return; + } + let mut max_weight_rounded = current_node_config.weight_rounded; + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if neighbor_config.weight_rounded > max_weight_rounded { + max_weight_rounded = neighbor_config.weight_rounded; + } + for second_neighbor_index in &adj_list[*neighbor_index] { + let second_neighbor_config = ¤t_node_configs[*second_neighbor_index]; + if second_neighbor_config.weight_rounded > max_weight_rounded { + max_weight_rounded = second_neighbor_config.weight_rounded; + } + } + } + if current_node_config.weight_rounded == max_weight_rounded { + next_node_config.is_active = true; + } else { + next_node_config.is_active = false; + } + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.is_active = current_node_config.is_active; + if current_node_config.has_no_coverage { + next_node_config.support = 0; + return; + } + let mut support = 0; + if current_node_config.is_active { + support += 1; + } + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if neighbor_config.is_active { + support += 1; + } + } + next_node_config.support = support; + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.support = current_node_config.support; + if !current_node_config.is_active{ + next_node_config.is_candidate = false; + return; + } + let mut max_support = 0; + if !current_node_config.is_covered { + max_support = current_node_config.support; + } + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if !neighbor_config.is_covered && neighbor_config.support > max_support { + max_support = neighbor_config.support; + } + } + let p = 1.0/(max_support as f64); + let r: f64 = rand::random(); + if r < p { + next_node_config.is_candidate = true; + } else { + next_node_config.is_candidate = false; + } + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.is_candidate = current_node_config.is_candidate; + if current_node_config.has_no_coverage { + next_node_config.candidates = 0; + return; + } + let mut candidates = 0; + if current_node_config.is_candidate { + candidates += 1; + } + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if neighbor_config.is_candidate { + candidates += 1; + } + } + next_node_config.candidates = candidates; + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.candidates = current_node_config.candidates; + if !current_node_config.is_candidate { + return; + } + let mut sum_candidates = 0; + if !current_node_config.is_covered { + sum_candidates += current_node_config.candidates; + } + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if !neighbor_config.is_covered { + sum_candidates += neighbor_config.candidates; + } + } + if sum_candidates <= 3 * current_node_config.weight_rounded { + next_node_config.add_to_dominating_set = true; + } + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + for i in 0..n_nodes { + let add_to_dominating_set = current_node_configs[i].add_to_dominating_set; + if add_to_dominating_set { + { + let node_config = &mut current_node_configs[i]; + dominating_set.insert(node_config.vid); + node_config.add_to_dominating_set = false; + if !node_config.is_covered { + node_config.is_covered = true; + covered_count += 1; + } + } + for neighbor_index in &adj_list[i] { + let neighbor_config = &mut current_node_configs[*neighbor_index]; + if !neighbor_config.is_covered { + neighbor_config.is_covered = true; + covered_count += 1; + } + } + } + } + } + dominating_set +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + + /// Helper function to verify if a set of nodes is a valid dominating set + fn is_dominating_set(graph: &G, ds: &HashSet) -> bool { + let mut covered = HashSet::new(); + + // Add all dominating set nodes and their neighbors to covered set + for &vid in ds { + covered.insert(vid); + if let Some(node) = graph.node(vid) { + for neighbor in node.neighbours() { + covered.insert(neighbor.node); + } + } + } + + // Check that all nodes in the graph are covered + for node in graph.nodes() { + if !covered.contains(&node.node) { + return false; + } + } + true + } + + #[test] + fn test_single_node_graph() { + let graph = Graph::new(); + graph.add_node(0, 1, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert_eq!(ds.len(), 1, "Single node should dominate itself"); + } + + #[test] + fn test_two_connected_nodes() { + let graph = Graph::new(); + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert_eq!(ds.len(), 1, "One node should dominate an edge"); + } + + #[test] + fn test_star_graph() { + let graph = Graph::new(); + // Star with center 0 and leaves 1-5 + for i in 1..=5 { + graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert_eq!(ds.len(), 1, "Star graph center should be the dominating set"); + } + + #[test] + fn test_path_graph() { + let graph = Graph::new(); + // Path: 1-2-3-4-5 + for i in 1..5 { + graph.add_edge(0, i, i + 1, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Path graph dominating set: {:?}", ds); + } + + #[test] + fn test_triangle_graph() { + let graph = Graph::new(); + // Triangle: 1-2-3-1 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(ds.len() <= 2, "At most 2 nodes needed to dominate a triangle"); + } + + #[test] + fn test_complete_graph_k4() { + let graph = Graph::new(); + // Complete graph K4 + for i in 1..=4 { + for j in (i + 1)..=4 { + graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); + } + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(!ds.is_empty()); + } + + #[test] + fn test_cycle_graph() { + let graph = Graph::new(); + // Cycle: 1-2-3-4-5-1 + for i in 1..=5 { + graph.add_edge(0, i, if i == 5 { 1 } else { i + 1 }, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Cycle graph dominating set: {:?}", ds); + } + + #[test] + fn test_disconnected_components() { + let graph = Graph::new(); + // Component 1: 1-2-3 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + // Component 2: 4-5 + graph.add_edge(0, 4, 5, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(ds.len() >= 2, "Should have at least one node per component"); + } + + #[test] + fn test_grid_graph() { + let graph = Graph::new(); + // 3x3 grid + // 1-2-3 + // | | | + // 4-5-6 + // | | | + // 7-8-9 + let edges = vec![ + (1, 2), (2, 3), (1, 4), (2, 5), (3, 6), + (4, 5), (5, 6), (4, 7), (5, 8), (6, 9), + (7, 8), (8, 9), + ]; + + for (src, dst) in edges { + graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Grid dominating set size: {}", ds.len()); + } + + #[test] + fn test_with_isolated_nodes() { + let graph = Graph::new(); + // Connected: 1-2-3 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + // Isolated nodes + graph.add_node(0, 4, NO_PROPS, None).unwrap(); + graph.add_node(0, 5, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(ds.len() >= 3, "Each isolated node must be in the dominating set"); + } + + #[test] + fn test_larger_random_structure() { + let graph = Graph::new(); + // Create a more complex structure + let edges = vec![ + (1, 2), (1, 3), (1, 4), // Node 1 hub + (2, 5), (3, 6), (4, 7), // Branches + (5, 8), (6, 8), (7, 8), // Converge to 8 + (8, 9), (8, 10), // From 8 + (9, 10), // Triangle + ]; + + for (src, dst) in edges { + graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Complex structure dominating set size: {}", ds.len()); + } +} + 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; From a4e9a8168626350df2c89e75da79365aa1ece344 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 28 Jan 2026 20:00:43 -0600 Subject: [PATCH 18/45] working --- raphtory/src/algorithms/covering/dominating_set.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/raphtory/src/algorithms/covering/dominating_set.rs b/raphtory/src/algorithms/covering/dominating_set.rs index 6dbcf37fe4..47e31574a2 100644 --- a/raphtory/src/algorithms/covering/dominating_set.rs +++ b/raphtory/src/algorithms/covering/dominating_set.rs @@ -1,6 +1,5 @@ use crate::db::api::state::ops::node; use crate::db::graph::assertions::FilterNeighbours; -/// Dijkstra's algorithm use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; use crate::{ core::entities::nodes::node_ref::NodeRef, From 97d48ad05b18f8ed845e731718eda84a3e5d5dc5 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 28 Jan 2026 22:36:20 -0600 Subject: [PATCH 19/45] working --- ....rs => fast_distributed_dominating_set.rs} | 22 ++++--------------- raphtory/src/algorithms/covering/mod.rs | 2 +- 2 files changed, 5 insertions(+), 19 deletions(-) rename raphtory/src/algorithms/covering/{dominating_set.rs => fast_distributed_dominating_set.rs} (96%) diff --git a/raphtory/src/algorithms/covering/dominating_set.rs b/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs similarity index 96% rename from raphtory/src/algorithms/covering/dominating_set.rs rename to raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs index 47e31574a2..dafb87951b 100644 --- a/raphtory/src/algorithms/covering/dominating_set.rs +++ b/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs @@ -1,28 +1,14 @@ -use crate::db::api::state::ops::node; -use crate::db::graph::assertions::FilterNeighbours; -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 crate::db::api::view::StaticGraphViewOps; +use crate::db::api::view::node::NodeViewOps; use raphtory_api::core::{ entities::{ - properties::prop::{PropType, PropUnwrap}, VID, }, - Direction, }; -use std::hash::Hash; use std::{ - cmp::Ordering, - collections::{BinaryHeap, HashMap, HashSet}, + collections::HashSet, }; +use crate::db::api::view::graph::GraphViewOps; use rayon::prelude::*; #[derive(Default, Clone)] diff --git a/raphtory/src/algorithms/covering/mod.rs b/raphtory/src/algorithms/covering/mod.rs index 57b62a98b7..a6b40bfb44 100644 --- a/raphtory/src/algorithms/covering/mod.rs +++ b/raphtory/src/algorithms/covering/mod.rs @@ -1 +1 @@ -pub mod dominating_set; \ No newline at end of file +pub mod fast_distributed_dominating_set; \ No newline at end of file From 10876f3adfbab4e2c738ae9c1bec051d67b64312 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Mon, 2 Feb 2026 19:56:54 -0600 Subject: [PATCH 20/45] working --- .../src/algorithms/covering/dominating_set.rs | 337 ++++++++++++++++++ raphtory/src/algorithms/covering/mod.rs | 1 + 2 files changed, 338 insertions(+) create mode 100644 raphtory/src/algorithms/covering/dominating_set.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..ef678cb106 --- /dev/null +++ b/raphtory/src/algorithms/covering/dominating_set.rs @@ -0,0 +1,337 @@ +use crate::{db::api::view::StaticGraphViewOps}; +use crate::db::api::view::node::NodeViewOps; +use raphtory_api::core::{ + entities::{ + VID, + }, +}; +use std::{ + collections::HashSet, +}; +use crate::db::api::view::graph::GraphViewOps; + +#[derive(Default, Clone)] +struct LinkedListNode { + next: Option, + prev: Option, + uncovered_count: usize, + vid: VID +} + +struct DominatingSetQueue { + linked_nodes: Vec, + uncovered_count_map: Vec>, + max_uncovered_count: usize, +} + +impl DominatingSetQueue { + pub fn from_graph(g: &G)-> Self { + let n_nodes = g.count_nodes(); + let mut linked_nodes = vec![LinkedListNode::default(); n_nodes]; + let mut uncovered_count_map = vec![None; n_nodes + 1]; + for node in g.nodes() { + let vid = node.node; + let index = vid.index(); + let uncovered_count = node.degree() + 1; + let current_linked_node = &mut linked_nodes[index]; + current_linked_node.uncovered_count = uncovered_count; + current_linked_node.vid = vid; + if let Some(existing_index) = uncovered_count_map[uncovered_count] { + current_linked_node.next = Some(existing_index); + linked_nodes[existing_index].prev = Some(index); + } + uncovered_count_map[uncovered_count] = Some(index); + } + Self { + linked_nodes, + uncovered_count_map, + max_uncovered_count: n_nodes, + } + } + + pub fn maximum(&mut self) -> Option { + while self.max_uncovered_count > 0 { + if let Some(index) = self.uncovered_count_map[self.max_uncovered_count] { + if let Some(next_index) = self.linked_nodes[index].next { + let next_linked_node = &mut self.linked_nodes[next_index]; + next_linked_node.prev = None; + self.uncovered_count_map[self.max_uncovered_count] = Some(next_index); + } else { + self.uncovered_count_map[self.max_uncovered_count] = None; + } + self.linked_nodes[index].next = None; + return Some(index); + } + self.max_uncovered_count -= 1; + } + None + } + // uncovered count should be less than the max uncovered count + pub fn insert(&mut self, index: usize, uncovered_count: usize) { + self.linked_nodes[index].uncovered_count = uncovered_count; + if let Some(existing_index) = self.uncovered_count_map[uncovered_count] { + self.linked_nodes[index].next = Some(existing_index); + self.linked_nodes[existing_index].prev = Some(index); + } + self.uncovered_count_map[uncovered_count] = Some(index); + } + + pub fn node_details(&self, index: usize) -> (VID, usize) { + let linked_node = &self.linked_nodes[index]; + (linked_node.vid, linked_node.uncovered_count) + } +} + + +pub fn lazy_greedy_dominating_set(g: &G) -> HashSet { + let n_nodes = g.count_nodes(); + let mut dominating_set: HashSet = HashSet::new(); + let mut covered_count = 0; + let mut covered_nodes: Vec = vec![false; n_nodes]; + let mut queue = DominatingSetQueue::from_graph(g); + while covered_count < n_nodes { + let index = queue.maximum().unwrap(); + let (vid, stale_uncovered_count) = queue.node_details(index); + let node = g.node(vid).unwrap(); + let mut actual_uncovered_count = 0; + if !covered_nodes[vid.index()] { + actual_uncovered_count += 1; + } + for neighbor in node.neighbours() { + if !covered_nodes[neighbor.node.index()] { + actual_uncovered_count += 1; + } + } + if actual_uncovered_count == stale_uncovered_count { + dominating_set.insert(vid); + if !covered_nodes[vid.index()] { + covered_nodes[vid.index()] = true; + covered_count += 1; + } + for neighbor in node.neighbours() { + if !covered_nodes[neighbor.node.index()] { + covered_nodes[neighbor.node.index()] = true; + covered_count += 1; + } + } + } else { + queue.insert(index, actual_uncovered_count); + } + } + dominating_set +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + + /// Helper function to verify if a set of nodes is a valid dominating set + fn is_dominating_set(graph: &G, ds: &HashSet) -> bool { + let mut covered = HashSet::new(); + + // Add all dominating set nodes and their neighbors to covered set + for &vid in ds { + covered.insert(vid); + if let Some(node) = graph.node(vid) { + for neighbor in node.neighbours() { + covered.insert(neighbor.node); + } + } + } + + // Check that all nodes in the graph are covered + for node in graph.nodes() { + if !covered.contains(&node.node) { + return false; + } + } + true + } + + #[test] + fn test_empty_graph() { + let graph = Graph::new(); + let ds = lazy_greedy_dominating_set(&graph); + + assert!(ds.is_empty(), "Empty graph should have an empty dominating set"); + } + + #[test] + fn test_single_node_graph() { + let graph = Graph::new(); + graph.add_node(0, 1, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Single node should dominate itself"); + assert!(ds.contains(&VID(0)), "The single node should be in the dominating set"); + } + + #[test] + fn test_two_connected_nodes() { + let graph = Graph::new(); + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "One node should be sufficient to dominate both nodes in an edge"); + } + + #[test] + fn test_star_graph() { + let graph = Graph::new(); + // Star with center 0 and leaves 1-5 + for i in 1..=5 { + graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Star graph center should be the only node in the dominating set"); + assert!(ds.contains(&VID(0)), "Center node should be in the dominating set"); + } + + #[test] + fn test_path_graph() { + let graph = Graph::new(); + // Path: 1-2-3-4-5 + for i in 1..5 { + graph.add_edge(0, i, i + 1, NO_PROPS, None).unwrap(); + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + // For a path of 5 nodes, we need at most 2 nodes + assert!(ds.len() <= 2, "Path of 5 nodes should need at most 2 nodes in dominating set"); + } + + #[test] + fn test_triangle_graph() { + let graph = Graph::new(); + // Triangle: 1-2-3-1 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Triangle should need only 1 node in dominating set"); + } + + #[test] + fn test_complete_graph() { + let graph = Graph::new(); + // Complete graph K4 + for i in 1..=4 { + for j in (i+1)..=4 { + graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); + } + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Complete graph should need only 1 node in dominating set"); + } + + #[test] + fn test_disconnected_graph() { + let graph = Graph::new(); + // Two separate edges: 1-2 and 3-4 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 4, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 2, "Two disconnected components need at least 2 nodes"); + } + + #[test] + fn test_cycle_graph() { + let graph = Graph::new(); + // Cycle: 1-2-3-4-5-1 + for i in 1..=5 { + let next = if i == 5 { 1 } else { i + 1 }; + graph.add_edge(0, i, next, NO_PROPS, None).unwrap(); + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + // For a cycle of 5 nodes, we need at most 2 nodes + assert!(ds.len() <= 2, "Cycle of 5 nodes should need at most 2 nodes in dominating set"); + } + + #[test] + fn test_bipartite_graph() { + let graph = Graph::new(); + // Complete bipartite graph K_{2,3} + for i in 1..=2 { + for j in 3..=5 { + graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); + } + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + // Can be dominated by either all nodes from one partition (2 or 3 nodes) + assert!(ds.len() <= 3); + } + + #[test] + fn test_isolated_nodes() { + let graph = Graph::new(); + // Add isolated nodes without edges + graph.add_node(0, 1, NO_PROPS, None).unwrap(); + graph.add_node(0, 2, NO_PROPS, None).unwrap(); + graph.add_node(0, 3, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 3, "All isolated nodes must be in the dominating set"); + } + + #[test] + fn test_mixed_graph() { + let graph = Graph::new(); + // Mix of connected and isolated nodes + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + graph.add_node(0, 10, NO_PROPS, None).unwrap(); // Isolated node + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert!(ds.contains(&VID(3)), "Isolated node must be in the dominating set"); + // Should have at least 2 nodes: one for the connected part, one for the isolated node + assert!(ds.len() >= 2, "Should have at least 2 nodes in dominating set"); + } + + #[test] + fn test_larger_graph() { + let graph = Graph::new(); + // Create a more complex graph structure + // Central hub connected to multiple smaller clusters + for i in 1..=3 { + graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); + for j in 1..=2 { + let node_id = i * 10 + j; + graph.add_edge(0, i, node_id, NO_PROPS, None).unwrap(); + } + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + println!("Larger graph dominating set size: {}", ds.len()); + } +} \ No newline at end of file diff --git a/raphtory/src/algorithms/covering/mod.rs b/raphtory/src/algorithms/covering/mod.rs index a6b40bfb44..38fd5df508 100644 --- a/raphtory/src/algorithms/covering/mod.rs +++ b/raphtory/src/algorithms/covering/mod.rs @@ -1 +1,2 @@ +pub mod dominating_set; pub mod fast_distributed_dominating_set; \ No newline at end of file From db01b4cb72e3a018ef606918ecdfedc84fccf7c5 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Mon, 2 Feb 2026 20:30:35 -0600 Subject: [PATCH 21/45] working --- raphtory/src/algorithms/covering/dominating_set.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/raphtory/src/algorithms/covering/dominating_set.rs b/raphtory/src/algorithms/covering/dominating_set.rs index ef678cb106..56f9d95750 100644 --- a/raphtory/src/algorithms/covering/dominating_set.rs +++ b/raphtory/src/algorithms/covering/dominating_set.rs @@ -115,7 +115,9 @@ pub fn lazy_greedy_dominating_set(g: &G) -> HashSet } } } else { - queue.insert(index, actual_uncovered_count); + if actual_uncovered_count > 0 { + queue.insert(index, actual_uncovered_count); + } } } dominating_set From 2163523198913277d985877d289c92e9609eb74b Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Tue, 3 Feb 2026 00:15:05 -0600 Subject: [PATCH 22/45] added benches --- raphtory-benchmark/benches/algobench.rs | 45 ++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/raphtory-benchmark/benches/algobench.rs b/raphtory-benchmark/benches/algobench.rs index 3465253c76..c37c31c579 100644 --- a/raphtory-benchmark/benches/algobench.rs +++ b/raphtory-benchmark/benches/algobench.rs @@ -11,8 +11,12 @@ use raphtory::{ global_temporal_three_node_motifs::global_temporal_three_node_motif, local_triangle_count::local_triangle_count, }, + covering::{ + fast_distributed_dominating_set::fast_distributed_dominating_set, + dominating_set::lazy_greedy_dominating_set + } }, - graphgen::random_attachment::random_attachment, + graphgen::{preferential_attachment::ba_preferential_attachment, random_attachment::random_attachment}, prelude::*, }; use raphtory_benchmark::common::bench; @@ -130,6 +134,44 @@ pub fn temporal_motifs(c: &mut Criterion) { group.finish(); } +pub fn dominating_set(c: &mut Criterion) { + let mut group = c.benchmark_group("dominating_set_scaling"); + group.sample_size(10); + + let sizes = [1_000, 10_000, 100_000]; + let seed: [u8; 32] = [1; 32]; + + for &size in &sizes { + let g = Graph::new(); + ba_preferential_attachment(&g, size, 2, Some(seed)); + + + group.bench_with_input( + BenchmarkId::new("fast_distributed", size), + &g, + |b, graph| { + b.iter(|| { + let result = fast_distributed_dominating_set(graph); + black_box(result); + }) + } + ); + + group.bench_with_input( + BenchmarkId::new("lazy_greedy", size), + &g, + |b, graph| { + b.iter(|| { + let result = lazy_greedy_dominating_set(graph); + black_box(result); + }) + } + ); + } + + group.finish(); +} + criterion_group!( benches, local_triangle_count_analysis, @@ -138,5 +180,6 @@ criterion_group!( graphgen_large_pagerank, graphgen_large_concomp, temporal_motifs, + dominating_set ); criterion_main!(benches); From 6159601b0834b15cf998918a3027e406fc13bb4e Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Tue, 3 Feb 2026 00:56:37 -0600 Subject: [PATCH 23/45] modified tests --- .../src/algorithms/covering/dominating_set.rs | 224 ++---------------- .../fast_distributed_dominating_set.rs | 200 ---------------- raphtory/tests/algo_tests/covering.rs | 36 +++ raphtory/tests/algo_tests/mod.rs | 2 + 4 files changed, 53 insertions(+), 409 deletions(-) create mode 100644 raphtory/tests/algo_tests/covering.rs diff --git a/raphtory/src/algorithms/covering/dominating_set.rs b/raphtory/src/algorithms/covering/dominating_set.rs index 56f9d95750..0feaa7cff4 100644 --- a/raphtory/src/algorithms/covering/dominating_set.rs +++ b/raphtory/src/algorithms/covering/dominating_set.rs @@ -123,217 +123,23 @@ pub fn lazy_greedy_dominating_set(g: &G) -> HashSet dominating_set } -#[cfg(test)] -mod tests { - use super::*; - use crate::prelude::*; - - /// Helper function to verify if a set of nodes is a valid dominating set - fn is_dominating_set(graph: &G, ds: &HashSet) -> bool { - let mut covered = HashSet::new(); - - // Add all dominating set nodes and their neighbors to covered set - for &vid in ds { - covered.insert(vid); - if let Some(node) = graph.node(vid) { - for neighbor in node.neighbours() { - covered.insert(neighbor.node); - } - } - } - - // Check that all nodes in the graph are covered - for node in graph.nodes() { - if !covered.contains(&node.node) { - return false; - } - } - true - } - - #[test] - fn test_empty_graph() { - let graph = Graph::new(); - let ds = lazy_greedy_dominating_set(&graph); - - assert!(ds.is_empty(), "Empty graph should have an empty dominating set"); - } - - #[test] - fn test_single_node_graph() { - let graph = Graph::new(); - graph.add_node(0, 1, NO_PROPS, None).unwrap(); - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - assert_eq!(ds.len(), 1, "Single node should dominate itself"); - assert!(ds.contains(&VID(0)), "The single node should be in the dominating set"); - } - - #[test] - fn test_two_connected_nodes() { - let graph = Graph::new(); - graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - assert_eq!(ds.len(), 1, "One node should be sufficient to dominate both nodes in an edge"); - } - - #[test] - fn test_star_graph() { - let graph = Graph::new(); - // Star with center 0 and leaves 1-5 - for i in 1..=5 { - graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); - } - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - assert_eq!(ds.len(), 1, "Star graph center should be the only node in the dominating set"); - assert!(ds.contains(&VID(0)), "Center node should be in the dominating set"); - } - - #[test] - fn test_path_graph() { - let graph = Graph::new(); - // Path: 1-2-3-4-5 - for i in 1..5 { - graph.add_edge(0, i, i + 1, NO_PROPS, None).unwrap(); - } - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - // For a path of 5 nodes, we need at most 2 nodes - assert!(ds.len() <= 2, "Path of 5 nodes should need at most 2 nodes in dominating set"); - } - - #[test] - fn test_triangle_graph() { - let graph = Graph::new(); - // Triangle: 1-2-3-1 - graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); - graph.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - assert_eq!(ds.len(), 1, "Triangle should need only 1 node in dominating set"); - } - - #[test] - fn test_complete_graph() { - let graph = Graph::new(); - // Complete graph K4 - for i in 1..=4 { - for j in (i+1)..=4 { - graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); - } - } - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - assert_eq!(ds.len(), 1, "Complete graph should need only 1 node in dominating set"); - } - - #[test] - fn test_disconnected_graph() { - let graph = Graph::new(); - // Two separate edges: 1-2 and 3-4 - graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - graph.add_edge(0, 3, 4, NO_PROPS, None).unwrap(); - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - assert_eq!(ds.len(), 2, "Two disconnected components need at least 2 nodes"); - } - - #[test] - fn test_cycle_graph() { - let graph = Graph::new(); - // Cycle: 1-2-3-4-5-1 - for i in 1..=5 { - let next = if i == 5 { 1 } else { i + 1 }; - graph.add_edge(0, i, next, NO_PROPS, None).unwrap(); +pub fn is_dominating_set(g: &G, dominating_set: &HashSet) -> bool { + let n_nodes = g.count_nodes(); + let mut covered_nodes: Vec = vec![false; n_nodes]; + let mut covered_count = 0; + for &vid in dominating_set { + if !covered_nodes[vid.index()] { + covered_nodes[vid.index()] = true; + covered_count += 1; } - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - // For a cycle of 5 nodes, we need at most 2 nodes - assert!(ds.len() <= 2, "Cycle of 5 nodes should need at most 2 nodes in dominating set"); - } - - #[test] - fn test_bipartite_graph() { - let graph = Graph::new(); - // Complete bipartite graph K_{2,3} - for i in 1..=2 { - for j in 3..=5 { - graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); + let node = g.node(vid).unwrap(); + for neighbor in node.neighbours() { + if !covered_nodes[neighbor.node.index()] { + covered_nodes[neighbor.node.index()] = true; + covered_count += 1; } } - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - // Can be dominated by either all nodes from one partition (2 or 3 nodes) - assert!(ds.len() <= 3); - } - - #[test] - fn test_isolated_nodes() { - let graph = Graph::new(); - // Add isolated nodes without edges - graph.add_node(0, 1, NO_PROPS, None).unwrap(); - graph.add_node(0, 2, NO_PROPS, None).unwrap(); - graph.add_node(0, 3, NO_PROPS, None).unwrap(); - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - assert_eq!(ds.len(), 3, "All isolated nodes must be in the dominating set"); - } - - #[test] - fn test_mixed_graph() { - let graph = Graph::new(); - // Mix of connected and isolated nodes - graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); - graph.add_node(0, 10, NO_PROPS, None).unwrap(); // Isolated node - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - assert!(ds.contains(&VID(3)), "Isolated node must be in the dominating set"); - // Should have at least 2 nodes: one for the connected part, one for the isolated node - assert!(ds.len() >= 2, "Should have at least 2 nodes in dominating set"); } + covered_count == n_nodes +} - #[test] - fn test_larger_graph() { - let graph = Graph::new(); - // Create a more complex graph structure - // Central hub connected to multiple smaller clusters - for i in 1..=3 { - graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); - for j in 1..=2 { - let node_id = i * 10 + j; - graph.add_edge(0, i, node_id, NO_PROPS, None).unwrap(); - } - } - - let ds = lazy_greedy_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); - println!("Larger graph dominating set size: {}", ds.len()); - } -} \ No newline at end of file diff --git a/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs b/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs index dafb87951b..37aa06bc64 100644 --- a/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs +++ b/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs @@ -214,204 +214,4 @@ pub fn fast_distributed_dominating_set(g: &G) -> HashSet< dominating_set } -#[cfg(test)] -mod tests { - use super::*; - use crate::prelude::*; - - /// Helper function to verify if a set of nodes is a valid dominating set - fn is_dominating_set(graph: &G, ds: &HashSet) -> bool { - let mut covered = HashSet::new(); - - // Add all dominating set nodes and their neighbors to covered set - for &vid in ds { - covered.insert(vid); - if let Some(node) = graph.node(vid) { - for neighbor in node.neighbours() { - covered.insert(neighbor.node); - } - } - } - - // Check that all nodes in the graph are covered - for node in graph.nodes() { - if !covered.contains(&node.node) { - return false; - } - } - true - } - - #[test] - fn test_single_node_graph() { - let graph = Graph::new(); - graph.add_node(0, 1, NO_PROPS, None).unwrap(); - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - assert_eq!(ds.len(), 1, "Single node should dominate itself"); - } - - #[test] - fn test_two_connected_nodes() { - let graph = Graph::new(); - graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - assert_eq!(ds.len(), 1, "One node should dominate an edge"); - } - - #[test] - fn test_star_graph() { - let graph = Graph::new(); - // Star with center 0 and leaves 1-5 - for i in 1..=5 { - graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); - } - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - assert_eq!(ds.len(), 1, "Star graph center should be the dominating set"); - } - - #[test] - fn test_path_graph() { - let graph = Graph::new(); - // Path: 1-2-3-4-5 - for i in 1..5 { - graph.add_edge(0, i, i + 1, NO_PROPS, None).unwrap(); - } - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - println!("Path graph dominating set: {:?}", ds); - } - - #[test] - fn test_triangle_graph() { - let graph = Graph::new(); - // Triangle: 1-2-3-1 - graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); - graph.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - assert!(ds.len() <= 2, "At most 2 nodes needed to dominate a triangle"); - } - - #[test] - fn test_complete_graph_k4() { - let graph = Graph::new(); - // Complete graph K4 - for i in 1..=4 { - for j in (i + 1)..=4 { - graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); - } - } - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - assert!(!ds.is_empty()); - } - - #[test] - fn test_cycle_graph() { - let graph = Graph::new(); - // Cycle: 1-2-3-4-5-1 - for i in 1..=5 { - graph.add_edge(0, i, if i == 5 { 1 } else { i + 1 }, NO_PROPS, None).unwrap(); - } - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - println!("Cycle graph dominating set: {:?}", ds); - } - - #[test] - fn test_disconnected_components() { - let graph = Graph::new(); - // Component 1: 1-2-3 - graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); - // Component 2: 4-5 - graph.add_edge(0, 4, 5, NO_PROPS, None).unwrap(); - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - assert!(ds.len() >= 2, "Should have at least one node per component"); - } - - #[test] - fn test_grid_graph() { - let graph = Graph::new(); - // 3x3 grid - // 1-2-3 - // | | | - // 4-5-6 - // | | | - // 7-8-9 - let edges = vec![ - (1, 2), (2, 3), (1, 4), (2, 5), (3, 6), - (4, 5), (5, 6), (4, 7), (5, 8), (6, 9), - (7, 8), (8, 9), - ]; - - for (src, dst) in edges { - graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); - } - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - println!("Grid dominating set size: {}", ds.len()); - } - - #[test] - fn test_with_isolated_nodes() { - let graph = Graph::new(); - // Connected: 1-2-3 - graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); - // Isolated nodes - graph.add_node(0, 4, NO_PROPS, None).unwrap(); - graph.add_node(0, 5, NO_PROPS, None).unwrap(); - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - assert!(ds.len() >= 3, "Each isolated node must be in the dominating set"); - } - - #[test] - fn test_larger_random_structure() { - let graph = Graph::new(); - // Create a more complex structure - let edges = vec![ - (1, 2), (1, 3), (1, 4), // Node 1 hub - (2, 5), (3, 6), (4, 7), // Branches - (5, 8), (6, 8), (7, 8), // Converge to 8 - (8, 9), (8, 10), // From 8 - (9, 10), // Triangle - ]; - - for (src, dst) in edges { - graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); - } - - let ds = fast_distributed_dominating_set(&graph); - - assert!(is_dominating_set(&graph, &ds)); - println!("Complex structure dominating set size: {}", ds.len()); - } -} diff --git a/raphtory/tests/algo_tests/covering.rs b/raphtory/tests/algo_tests/covering.rs new file mode 100644 index 0000000000..a891d124d8 --- /dev/null +++ b/raphtory/tests/algo_tests/covering.rs @@ -0,0 +1,36 @@ + +#[cfg(test)] +mod dominating_set_tests { + use raphtory::{ + algorithms::covering::{dominating_set::{is_dominating_set, lazy_greedy_dominating_set}, fast_distributed_dominating_set::fast_distributed_dominating_set}, + graphgen::erdos_renyi::erdos_renyi, + db::{api::{view::StaticGraphViewOps}, graph::graph::Graph}, + prelude::*, + }; + + fn graph() -> Graph { + let nodes_to_add = 1000; + let p = 0.5; + let seed = 42; + let g = erdos_renyi(nodes_to_add, p, Some(seed)).unwrap(); + g + } + + #[test] + fn test_lazy_greedy_dominating_set() { + let g = graph(); + let n_nodes = g.count_nodes(); + let dominating_set = lazy_greedy_dominating_set(&g); + assert!(is_dominating_set(&g, &dominating_set)); + assert!(dominating_set.len() <= (f64::ln(n_nodes as f64) as usize + 2)) + } + + #[test] + fn test_fast_distributed_dominating_set() { + let g = graph(); + let n_nodes = g.count_nodes(); + let dominating_set = fast_distributed_dominating_set(&g); + assert!(is_dominating_set(&g, &dominating_set)); + assert!(dominating_set.len() <= (6 * (f64::ln(n_nodes as f64) as usize) + 12)) + } +} \ No newline at end of file diff --git a/raphtory/tests/algo_tests/mod.rs b/raphtory/tests/algo_tests/mod.rs index 3887662709..436839f74f 100644 --- a/raphtory/tests/algo_tests/mod.rs +++ b/raphtory/tests/algo_tests/mod.rs @@ -2,7 +2,9 @@ mod centrality; mod community_detection; mod components; mod cores; +mod covering; mod embeddings; mod metrics; mod motifs; mod pathing; + From 435ff0267fdeb241fb2ea57c65f13a76e2a5e4d9 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Tue, 3 Feb 2026 01:22:08 -0600 Subject: [PATCH 24/45] changed tests --- .../src/algorithms/covering/dominating_set.rs | 193 ++++++++++++++++++ .../fast_distributed_dominating_set.rs | 180 ++++++++++++++++ raphtory/tests/algo_tests/covering.rs | 36 ---- raphtory/tests/algo_tests/mod.rs | 1 - 4 files changed, 373 insertions(+), 37 deletions(-) delete mode 100644 raphtory/tests/algo_tests/covering.rs diff --git a/raphtory/src/algorithms/covering/dominating_set.rs b/raphtory/src/algorithms/covering/dominating_set.rs index 0feaa7cff4..78a904fa57 100644 --- a/raphtory/src/algorithms/covering/dominating_set.rs +++ b/raphtory/src/algorithms/covering/dominating_set.rs @@ -143,3 +143,196 @@ pub fn is_dominating_set(g: &G, dominating_set: &HashSet< covered_count == n_nodes } + + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + + #[test] + fn test_empty_graph() { + let graph = Graph::new(); + let ds = lazy_greedy_dominating_set(&graph); + + assert!(ds.is_empty(), "Empty graph should have an empty dominating set"); + } + + #[test] + fn test_single_node_graph() { + let graph = Graph::new(); + graph.add_node(0, 1, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Single node should dominate itself"); + assert!(ds.contains(&VID(0)), "The single node should be in the dominating set"); + } + + #[test] + fn test_two_connected_nodes() { + let graph = Graph::new(); + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "One node should be sufficient to dominate both nodes in an edge"); + } + + #[test] + fn test_star_graph() { + let graph = Graph::new(); + // Star with center 0 and leaves 1-5 + for i in 1..=5 { + graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Star graph center should be the only node in the dominating set"); + assert!(ds.contains(&VID(0)), "Center node should be in the dominating set"); + } + + #[test] + fn test_path_graph() { + let graph = Graph::new(); + // Path: 1-2-3-4-5 + for i in 1..5 { + graph.add_edge(0, i, i + 1, NO_PROPS, None).unwrap(); + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + // For a path of 5 nodes, we need at most 2 nodes + assert!(ds.len() <= 2, "Path of 5 nodes should need at most 2 nodes in dominating set"); + } + + #[test] + fn test_triangle_graph() { + let graph = Graph::new(); + // Triangle: 1-2-3-1 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Triangle should need only 1 node in dominating set"); + } + + #[test] + fn test_complete_graph() { + let graph = Graph::new(); + // Complete graph K4 + for i in 1..=4 { + for j in (i+1)..=4 { + graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); + } + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Complete graph should need only 1 node in dominating set"); + } + + #[test] + fn test_disconnected_graph() { + let graph = Graph::new(); + // Two separate edges: 1-2 and 3-4 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 4, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 2, "Two disconnected components need at least 2 nodes"); + } + + #[test] + fn test_cycle_graph() { + let graph = Graph::new(); + // Cycle: 1-2-3-4-5-1 + for i in 1..=5 { + let next = if i == 5 { 1 } else { i + 1 }; + graph.add_edge(0, i, next, NO_PROPS, None).unwrap(); + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + // For a cycle of 5 nodes, we need at most 2 nodes + assert!(ds.len() <= 2, "Cycle of 5 nodes should need at most 2 nodes in dominating set"); + } + + #[test] + fn test_bipartite_graph() { + let graph = Graph::new(); + // Complete bipartite graph K_{2,3} + for i in 1..=2 { + for j in 3..=5 { + graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); + } + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + // Can be dominated by either all nodes from one partition (2 or 3 nodes) + assert!(ds.len() <= 3); + } + + #[test] + fn test_isolated_nodes() { + let graph = Graph::new(); + // Add isolated nodes without edges + graph.add_node(0, 1, NO_PROPS, None).unwrap(); + graph.add_node(0, 2, NO_PROPS, None).unwrap(); + graph.add_node(0, 3, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 3, "All isolated nodes must be in the dominating set"); + } + + #[test] + fn test_mixed_graph() { + let graph = Graph::new(); + // Mix of connected and isolated nodes + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + graph.add_node(0, 10, NO_PROPS, None).unwrap(); // Isolated node + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert!(ds.contains(&VID(3)), "Isolated node must be in the dominating set"); + // Should have at least 2 nodes: one for the connected part, one for the isolated node + assert!(ds.len() >= 2, "Should have at least 2 nodes in dominating set"); + } + + #[test] + fn test_larger_graph() { + let graph = Graph::new(); + // Create a more complex graph structure + // Central hub connected to multiple smaller clusters + for i in 1..=3 { + graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); + for j in 1..=2 { + let node_id = i * 10 + j; + graph.add_edge(0, i, node_id, NO_PROPS, None).unwrap(); + } + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + println!("Larger graph dominating set size: {}", ds.len()); + } +} diff --git a/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs b/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs index 37aa06bc64..d56886d602 100644 --- a/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs +++ b/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs @@ -215,3 +215,183 @@ pub fn fast_distributed_dominating_set(g: &G) -> HashSet< } +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + use super::super::dominating_set::is_dominating_set; + + #[test] + fn test_single_node_graph() { + let graph = Graph::new(); + graph.add_node(0, 1, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert_eq!(ds.len(), 1, "Single node should dominate itself"); + } + + #[test] + fn test_two_connected_nodes() { + let graph = Graph::new(); + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert_eq!(ds.len(), 1, "One node should dominate an edge"); + } + + #[test] + fn test_star_graph() { + let graph = Graph::new(); + // Star with center 0 and leaves 1-5 + for i in 1..=5 { + graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert_eq!(ds.len(), 1, "Star graph center should be the dominating set"); + } + + #[test] + fn test_path_graph() { + let graph = Graph::new(); + // Path: 1-2-3-4-5 + for i in 1..5 { + graph.add_edge(0, i, i + 1, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Path graph dominating set: {:?}", ds); + } + + #[test] + fn test_triangle_graph() { + let graph = Graph::new(); + // Triangle: 1-2-3-1 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(ds.len() <= 2, "At most 2 nodes needed to dominate a triangle"); + } + + #[test] + fn test_complete_graph_k4() { + let graph = Graph::new(); + // Complete graph K4 + for i in 1..=4 { + for j in (i + 1)..=4 { + graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); + } + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(!ds.is_empty()); + } + + #[test] + fn test_cycle_graph() { + let graph = Graph::new(); + // Cycle: 1-2-3-4-5-1 + for i in 1..=5 { + graph.add_edge(0, i, if i == 5 { 1 } else { i + 1 }, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Cycle graph dominating set: {:?}", ds); + } + + #[test] + fn test_disconnected_components() { + let graph = Graph::new(); + // Component 1: 1-2-3 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + // Component 2: 4-5 + graph.add_edge(0, 4, 5, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(ds.len() >= 2, "Should have at least one node per component"); + } + + #[test] + fn test_grid_graph() { + let graph = Graph::new(); + // 3x3 grid + // 1-2-3 + // | | | + // 4-5-6 + // | | | + // 7-8-9 + let edges = vec![ + (1, 2), (2, 3), (1, 4), (2, 5), (3, 6), + (4, 5), (5, 6), (4, 7), (5, 8), (6, 9), + (7, 8), (8, 9), + ]; + + for (src, dst) in edges { + graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Grid dominating set size: {}", ds.len()); + } + + #[test] + fn test_with_isolated_nodes() { + let graph = Graph::new(); + // Connected: 1-2-3 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + // Isolated nodes + graph.add_node(0, 4, NO_PROPS, None).unwrap(); + graph.add_node(0, 5, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(ds.len() >= 3, "Each isolated node must be in the dominating set"); + } + + #[test] + fn test_larger_random_structure() { + let graph = Graph::new(); + // Create a more complex structure + let edges = vec![ + (1, 2), (1, 3), (1, 4), // Node 1 hub + (2, 5), (3, 6), (4, 7), // Branches + (5, 8), (6, 8), (7, 8), // Converge to 8 + (8, 9), (8, 10), // From 8 + (9, 10), // Triangle + ]; + + for (src, dst) in edges { + graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Complex structure dominating set size: {}", ds.len()); + } +} + + diff --git a/raphtory/tests/algo_tests/covering.rs b/raphtory/tests/algo_tests/covering.rs deleted file mode 100644 index a891d124d8..0000000000 --- a/raphtory/tests/algo_tests/covering.rs +++ /dev/null @@ -1,36 +0,0 @@ - -#[cfg(test)] -mod dominating_set_tests { - use raphtory::{ - algorithms::covering::{dominating_set::{is_dominating_set, lazy_greedy_dominating_set}, fast_distributed_dominating_set::fast_distributed_dominating_set}, - graphgen::erdos_renyi::erdos_renyi, - db::{api::{view::StaticGraphViewOps}, graph::graph::Graph}, - prelude::*, - }; - - fn graph() -> Graph { - let nodes_to_add = 1000; - let p = 0.5; - let seed = 42; - let g = erdos_renyi(nodes_to_add, p, Some(seed)).unwrap(); - g - } - - #[test] - fn test_lazy_greedy_dominating_set() { - let g = graph(); - let n_nodes = g.count_nodes(); - let dominating_set = lazy_greedy_dominating_set(&g); - assert!(is_dominating_set(&g, &dominating_set)); - assert!(dominating_set.len() <= (f64::ln(n_nodes as f64) as usize + 2)) - } - - #[test] - fn test_fast_distributed_dominating_set() { - let g = graph(); - let n_nodes = g.count_nodes(); - let dominating_set = fast_distributed_dominating_set(&g); - assert!(is_dominating_set(&g, &dominating_set)); - assert!(dominating_set.len() <= (6 * (f64::ln(n_nodes as f64) as usize) + 12)) - } -} \ No newline at end of file diff --git a/raphtory/tests/algo_tests/mod.rs b/raphtory/tests/algo_tests/mod.rs index 436839f74f..26ae22df08 100644 --- a/raphtory/tests/algo_tests/mod.rs +++ b/raphtory/tests/algo_tests/mod.rs @@ -2,7 +2,6 @@ mod centrality; mod community_detection; mod components; mod cores; -mod covering; mod embeddings; mod metrics; mod motifs; From 9669ed18b11b887f700b4afbc7ac3b55c10d9d66 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Tue, 3 Feb 2026 14:42:14 -0600 Subject: [PATCH 25/45] updated benches --- raphtory-benchmark/benches/algobench.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/raphtory-benchmark/benches/algobench.rs b/raphtory-benchmark/benches/algobench.rs index c37c31c579..4971d945dc 100644 --- a/raphtory-benchmark/benches/algobench.rs +++ b/raphtory-benchmark/benches/algobench.rs @@ -136,9 +136,10 @@ pub fn temporal_motifs(c: &mut Criterion) { pub fn dominating_set(c: &mut Criterion) { let mut group = c.benchmark_group("dominating_set_scaling"); - group.sample_size(10); + group.sample_size(100); + group.sampling_mode(SamplingMode::Flat); - let sizes = [1_000, 10_000, 100_000]; + let sizes = [1_000, 10_000, 100_000, 300_000]; let seed: [u8; 32] = [1; 32]; for &size in &sizes { From fe473997358fee62fad7cfb3cf7f81cc767ab0fa Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 4 Feb 2026 02:00:11 -0600 Subject: [PATCH 26/45] working --- raphtory/src/algorithms/pathing/dijkstra.rs | 86 ++++++++++++++------- raphtory/src/algorithms/pathing/johnson.rs | 28 +++++++ raphtory/src/algorithms/pathing/mod.rs | 1 + 3 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 raphtory/src/algorithms/pathing/johnson.rs diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index 7a063ca44d..f2dc971bf4 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -1,3 +1,4 @@ +use crate::db::graph::edge::EdgeView; /// Dijkstra's algorithm use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; use crate::{ @@ -57,7 +58,7 @@ impl PartialOrd for State { /// /// 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 dijkstra_single_source_shortest_paths( g: &G, source: T, @@ -65,17 +66,6 @@ pub fn dijkstra_single_source_shortest_paths, 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) { @@ -84,14 +74,6 @@ pub fn dijkstra_single_source_shortest_paths| -> Option { + let edge_val = match weight{ + None => Some(Prop::U8(1)), + Some(weight) => match edge.properties().get(weight) { + Some(prop) => Some(prop), + _ => None + } + }; + edge_val + }; + let targets_len = targets.len(); + dijkstra_single_source_shortest_paths_algorithm(g, source, Some(targets), direction, targets_len, cost_val, max_val, weight_fn) +} + +pub(crate) fn dijkstra_single_source_shortest_paths_algorithm) -> Option>( + g: &G, + source: T, + targets: Option>, + direction: Direction, + k: usize, + cost_val: Prop, + max_val: Prop, + weight_fn: F +) -> Result), G>, GraphError> { + let mut k = k; + 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 target_nodes = if let Some(targets) = targets { + let mut target_nodes = vec![false; g.unfiltered_num_nodes()]; + for target in targets { + if let Some(target_node) = g.node(target) { + target_nodes[target_node.node.index()] = true; + } + } + target_nodes + } else { + vec![true; g.unfiltered_num_nodes()] + }; + let mut heap = BinaryHeap::new(); + heap.push(State { cost: cost_val.clone(), node: source_node.node, @@ -152,6 +183,10 @@ pub fn dijkstra_single_source_shortest_paths Prop::U8(1), - Some(weight) => match edge.properties().get(weight) { - Some(prop) => prop, - _ => continue, - }, + let edge_val = if let Some(w) = weight_fn(edge) { + w + } else { + continue; }; - let next_cost = cost.clone().add(edge_val).unwrap(); if next_cost < *dist.entry(next_node_vid).or_insert(max_val.clone()) { heap.push(State { diff --git a/raphtory/src/algorithms/pathing/johnson.rs b/raphtory/src/algorithms/pathing/johnson.rs new file mode 100644 index 0000000000..b0cfe0279f --- /dev/null +++ b/raphtory/src/algorithms/pathing/johnson.rs @@ -0,0 +1,28 @@ +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}, +}; + +// pub fn johnson_all_pairs_shortest_paths( +// g: &G, +// weight: Option<&str>, +// direction: Direction, +// ) -> Result)>, G>, GraphError> { +// } \ No newline at end of file diff --git a/raphtory/src/algorithms/pathing/mod.rs b/raphtory/src/algorithms/pathing/mod.rs index 7df2e85a8d..29d2eef14d 100644 --- a/raphtory/src/algorithms/pathing/mod.rs +++ b/raphtory/src/algorithms/pathing/mod.rs @@ -1,4 +1,5 @@ pub mod bellman_ford; pub mod dijkstra; +pub mod johnson; pub mod single_source_shortest_path; pub mod temporal_reachability; From c92956422f1dd716bd2c68d644ba04acae313fce Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 4 Feb 2026 02:39:03 -0600 Subject: [PATCH 27/45] working --- .../src/algorithms/pathing/bellman_ford.rs | 90 ++++++++----------- raphtory/src/algorithms/pathing/dijkstra.rs | 49 ++-------- raphtory/src/algorithms/pathing/johnson.rs | 14 +-- raphtory/src/algorithms/pathing/mod.rs | 31 +++++++ 4 files changed, 82 insertions(+), 102 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 039dc470e3..3fc5f6dc2a 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::edge::EdgeView; /// Bellman-Ford algorithm use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; use crate::{ @@ -20,6 +21,7 @@ use raphtory_api::core::{ use std::{ collections::{HashMap}, }; +use super::to_prop; /// Finds the shortest paths from a single source to multiple targets in a graph. @@ -37,12 +39,39 @@ use std::{ /// 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> { + // 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 = to_prop(g, weight, 0.0)?; + let max_val = to_prop(g, weight, f64::MAX)?; + let weight_fn = |edge: &EdgeView| -> Option { + let edge_val = match weight{ + None => Some(Prop::U8(1)), + Some(weight) => match edge.properties().get(weight) { + Some(prop) => Some(prop), + _ => None + } + }; + edge_val + }; + bellman_ford_single_source_shortest_paths_algorithm(g, source, targets, direction, dist_val, max_val, weight_fn) +} + +pub(crate) fn bellman_ford_single_source_shortest_paths_algorithm) -> Option>( + g: &G, + source: T, + targets: Vec, + direction: Direction, + dist_val: Prop, + max_val: Prop, + weight_fn: F ) -> Result), G>, GraphError> { let source_ref = source.as_node_ref(); let source_node = match g.node(source_ref) { @@ -55,47 +84,6 @@ pub fn bellman_ford_single_source_shortest_paths 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(); @@ -125,12 +113,10 @@ pub fn bellman_ford_single_source_shortest_paths 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 edge_val = if let Some(w) = weight_fn(&edge) { + w + } else { + continue; }; let neighbor_vid = edge.nbr().node; let neighbor_dist = dist.get(&neighbor_vid).unwrap(); @@ -160,12 +146,10 @@ pub fn bellman_ford_single_source_shortest_paths Prop::U8(1), - Some(weight) => match edge.properties().get(weight) { - Some(prop) => prop, - _ => continue, - }, + let edge_val = if let Some(w) = weight_fn(&edge) { + w + } else { + continue; }; let neighbor_vid = edge.nbr().node; let neighbor_dist = dist.get(&neighbor_vid).unwrap(); diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index f2dc971bf4..1c31f00b2b 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -22,6 +22,7 @@ use std::{ cmp::Ordering, collections::{BinaryHeap, HashMap, HashSet}, }; +use super::to_prop; /// A state in the Dijkstra algorithm with a cost and a node name. #[derive(PartialEq)] @@ -66,47 +67,9 @@ pub fn dijkstra_single_source_shortest_paths, direction: Direction, ) -> Result), G>, GraphError> { - 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 weight_fn = |edge: EdgeView| -> Option { + let cost_val = to_prop(g, weight, 0.0)?; + let max_val = to_prop(g, weight, f64::MAX)?; + let weight_fn = |edge: &EdgeView| -> Option { let edge_val = match weight{ None => Some(Prop::U8(1)), Some(weight) => match edge.properties().get(weight) { @@ -120,7 +83,7 @@ pub fn dijkstra_single_source_shortest_paths) -> Option>( +pub(crate) fn dijkstra_single_source_shortest_paths_algorithm) -> Option>( g: &G, source: T, targets: Option>, @@ -202,7 +165,7 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm( -// g: &G, -// weight: Option<&str>, -// direction: Direction, -// ) -> Result)>, G>, GraphError> { -// } \ No newline at end of file +pub fn johnson_all_pairs_shortest_paths( + g: &G, + weight: Option<&str>, + direction: Direction, +) -> Result)>, G>, GraphError> { +} \ No newline at end of file diff --git a/raphtory/src/algorithms/pathing/mod.rs b/raphtory/src/algorithms/pathing/mod.rs index 29d2eef14d..41569dbc39 100644 --- a/raphtory/src/algorithms/pathing/mod.rs +++ b/raphtory/src/algorithms/pathing/mod.rs @@ -1,5 +1,36 @@ +use raphtory_api::core::entities::properties::prop::{Prop, PropType}; +use crate::{db::api::view::StaticGraphViewOps, errors::GraphError}; + +pub(crate) fn to_prop(g: &G, weight: Option<&str>, val: f64) -> Result { + 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())); + } + } + let prop_val = match weight_type { + PropType::F32 => Prop::F32(val as f32), + PropType::F64 => Prop::F64(val as f64), + PropType::U8 => Prop::U8(val as u8), + PropType::U16 => Prop::U16(val as u16), + PropType::U32 => Prop::U32(val as u32), + PropType::U64 => Prop::U64(val as u64), + PropType::I32 => Prop::I32(val as i32), + PropType::I64 => Prop::I64(val as i64), + p_type => { + return Err(GraphError::InvalidProperty { + reason: format!("Weight type: {:?}, not supported", p_type), + }) + } + }; + Ok(prop_val) +} + pub mod bellman_ford; pub mod dijkstra; pub mod johnson; pub mod single_source_shortest_path; pub mod temporal_reachability; + From 60c3c0c310c53c5224ce36899566cb7a66fdeb66 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 4 Feb 2026 13:47:50 -0600 Subject: [PATCH 28/45] working --- .../src/algorithms/pathing/bellman_ford.rs | 65 +++++++++++-------- raphtory/src/algorithms/pathing/johnson.rs | 20 ++++++ 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 3fc5f6dc2a..c4b9c47b4b 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -1,4 +1,5 @@ use crate::db::graph::edge::EdgeView; +use crate::db::graph::node::NodeView; /// Bellman-Ford algorithm use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; use crate::{ @@ -163,34 +164,26 @@ pub(crate) fn bellman_ford_single_source_shortest_paths_algorithm 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; + if let Some(targets) = Some(targets) { + 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)); + } + }; + add_to_shortest_paths(&target_node, &mut shortest_paths, &dist, &predecessor); + } + } else { + for node in g.nodes() { + add_to_shortest_paths(&node, &mut shortest_paths, &dist, &predecessor); } - 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() @@ -207,3 +200,21 @@ pub(crate) fn bellman_ford_single_source_shortest_paths_algorithm(target_node: &NodeView, shortest_paths: &mut HashMap)>, dist: &HashMap, predecessor: &HashMap) { + 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), + ); +} diff --git a/raphtory/src/algorithms/pathing/johnson.rs b/raphtory/src/algorithms/pathing/johnson.rs index 765216d415..d6df94a12d 100644 --- a/raphtory/src/algorithms/pathing/johnson.rs +++ b/raphtory/src/algorithms/pathing/johnson.rs @@ -1,3 +1,5 @@ +use crate::db::graph::edge::EdgeView; +use crate::db::graph::node::NodeView; use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; use crate::{ core::entities::nodes::node_ref::NodeRef, @@ -27,4 +29,22 @@ pub fn johnson_all_pairs_shortest_paths( weight: Option<&str>, direction: Direction, ) -> Result)>, G>, GraphError> { + let dist_val = to_prop(g, weight, 0.0)?; + let weight_fn = |edge: &EdgeView| -> Option { + let edge_val = match weight{ + None => Some(Prop::U8(1)), + Some(weight) => match edge.properties().get(weight) { + Some(prop) => Some(prop), + _ => None + } + }; + edge_val + }; + let source_node = if let Some(source_node) = g.nodes().iter().next() { + source_node + } else { + return Ok(NodeState::new(g, NO_FILTER)); + }; + + let result = bellman_ford_single_source_shortest_paths_algorithm(g, source_node, None, direction, dist_val, max_val, weight_fn)?; } \ No newline at end of file From 41f4d87913e9f49b500d5b5813432ba72bcf9104 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 4 Feb 2026 22:28:25 -0600 Subject: [PATCH 29/45] working --- raphtory/src/algorithms/diameter/diameter.rs | 59 ++++++++ raphtory/src/algorithms/diameter/mod.rs | 1 + raphtory/src/algorithms/mod.rs | 1 + .../src/algorithms/pathing/bellman_ford.rs | 142 +++++++++--------- raphtory/src/algorithms/pathing/dijkstra.rs | 112 +++++++------- raphtory/src/algorithms/pathing/mod.rs | 2 +- .../pathing/{johnson.rs => reweighting.rs} | 12 +- 7 files changed, 201 insertions(+), 128 deletions(-) create mode 100644 raphtory/src/algorithms/diameter/diameter.rs create mode 100644 raphtory/src/algorithms/diameter/mod.rs rename raphtory/src/algorithms/pathing/{johnson.rs => reweighting.rs} (72%) diff --git a/raphtory/src/algorithms/diameter/diameter.rs b/raphtory/src/algorithms/diameter/diameter.rs new file mode 100644 index 0000000000..0ef8b2458a --- /dev/null +++ b/raphtory/src/algorithms/diameter/diameter.rs @@ -0,0 +1,59 @@ +use crate::db::graph::edge::EdgeView; +use crate::db::graph::node::NodeView; +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}, +}; +use super::super::pathing::{bellman_ford::bellman_ford_single_source_shortest_paths_algorithm, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; + +pub fn diameter_approximation( + g: &G, + weight: Option<&str>, + direction: Direction, +) { + let dist_val = to_prop(g, weight, 0.0)?; + let weight_fn = |edge: &EdgeView| -> Option { + let edge_val = match weight{ + None => Some(Prop::U8(1)), + Some(weight) => match edge.properties().get(weight) { + Some(prop) => Some(prop), + _ => None + } + }; + edge_val + }; + let source_node = if let Some(source_node) = g.nodes().iter().next() { + source_node + } else { + return;; + }; + let nonnegative_weight_fn = |edge: &EdgeView| -> Option { + let edge_val = match weight{ + None => Some(Prop::U8(1)), + Some(weight) => match edge.properties().get(weight) { + Some(prop) => Some(prop), + _ => None + } + }; + edge_val + }; + + let result = bellman_ford_single_source_shortest_paths_algorithm(g, source_node, None, direction, dist_val, max_val, weight_fn)?; +} \ No newline at end of file diff --git a/raphtory/src/algorithms/diameter/mod.rs b/raphtory/src/algorithms/diameter/mod.rs new file mode 100644 index 0000000000..6d41622dba --- /dev/null +++ b/raphtory/src/algorithms/diameter/mod.rs @@ -0,0 +1 @@ +pub mod diameter; \ No newline at end of file diff --git a/raphtory/src/algorithms/mod.rs b/raphtory/src/algorithms/mod.rs index 25541e6632..1612c0f2c3 100644 --- a/raphtory/src/algorithms/mod.rs +++ b/raphtory/src/algorithms/mod.rs @@ -33,6 +33,7 @@ pub mod bipartite; pub mod components; pub mod cores; pub mod covering; +pub mod diameter; 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 c4b9c47b4b..f9bf35a34b 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -19,6 +19,7 @@ use raphtory_api::core::{ }, Direction, }; +use std::hash::Hash; use std::{ collections::{HashMap}, }; @@ -62,52 +63,94 @@ pub fn bellman_ford_single_source_shortest_paths)> = HashMap::new(); + 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)); + } + }; + add_to_shortest_paths(&target_node, &mut shortest_paths, &distances, &predecessor); + } + 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)), + )) } pub(crate) fn bellman_ford_single_source_shortest_paths_algorithm) -> Option>( g: &G, - source: T, - targets: Vec, + source: Option, direction: Direction, dist_val: Prop, max_val: Prop, weight_fn: F -) -> 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)); - } +) -> Result<(Vec, Vec), GraphError> { + let mut dummy_node = false; + // creates a dummy node if source node is none + let source_node_vid = if let Some(source) = source { + 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)); + } + }; + source_node.node + } else { + dummy_node = true; + VID(usize::MAX) }; - 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(); + let mut dist: Vec = vec![max_val.clone(); n_nodes]; + let mut predecessor: Vec = vec![VID(usize::MAX); n_nodes]; 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()); + let node_idx = node.node.index(); + if dummy_node { + predecessor[node_idx] = source_node_vid; } else { - dist.insert(node.node, max_val.clone()); + predecessor[node_idx] = node.node; + if node.node == source_node_vid { + dist[source_node_vid.index()] = dist_val.clone(); + } else { + dist[node_idx] = max_val.clone(); + } } } for _ in 1..n_nodes { let mut changed = false; for node in g.nodes() { - if node.node == source_node.node { + if node.node == source_node_vid { continue; } - let mut min_dist = dist.get(&node.node).unwrap().clone(); - let mut min_node = predecessor.get(&node.node).unwrap().clone(); + let node_idx = node.node.index(); + let mut min_dist = dist[node_idx].clone(); + let mut min_node = predecessor[node_idx]; let edges = match direction { Direction::IN => node.out_edges(), Direction::OUT => node.in_edges(), @@ -120,8 +163,8 @@ pub(crate) fn bellman_ford_single_source_shortest_paths_algorithm node.in_edges(), Direction::BOTH => node.edges(), }; - let node_dist = dist.get(&node.node).unwrap(); + let node_dist = dist[node.node.index()]; for edge in edges { let edge_val = if let Some(w) = weight_fn(&edge) { w @@ -153,8 +196,8 @@ pub(crate) fn bellman_ford_single_source_shortest_paths_algorithm 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)); - } - }; - add_to_shortest_paths(&target_node, &mut shortest_paths, &dist, &predecessor); - } - } else { - for node in g.nodes() { - add_to_shortest_paths(&node, &mut shortest_paths, &dist, &predecessor); - } - }; - - 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)), - )) + Ok((dist, predecessor)) } fn add_to_shortest_paths(target_node: &NodeView, shortest_paths: &mut HashMap)>, dist: &HashMap, predecessor: &HashMap) { diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index 1c31f00b2b..d6f1516514 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -80,20 +80,55 @@ pub fn dijkstra_single_source_shortest_paths)> = HashMap::new(); + 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(); + let node_vid = target_node.node; + path.insert(node_vid); + let mut current_node_id = node_vid; + while let Some(prev_node) = predecessor.get(current_node_id.index()) { + path.insert(*prev_node); + current_node_id = *prev_node; + } + path.reverse(); + paths.insert(node_vid, (distances[node_vid.index()].as_f64().unwrap(), path)); + } + let (index, values): (IndexSet<_, ahash::RandomState>, Vec<_>) = paths + .into_iter() + .map(|(id, (cost, path))| { + let nodes = + Nodes::new_filtered(g.clone(), g.clone(), NO_FILTER, Some(Index::new(path))); + (id, (cost, nodes)) + }) + .unzip(); + Ok(NodeState::new( + g.clone(), + values.into(), + Some(Index::new(index)), + )) } pub(crate) fn dijkstra_single_source_shortest_paths_algorithm) -> Option>( g: &G, source: T, - targets: Option>, direction: Direction, k: usize, cost_val: Prop, max_val: Prop, weight_fn: F -) -> Result), G>, GraphError> { - let mut k = k; +) -> Result<(Vec, Vec), GraphError> { let source_ref = source.as_node_ref(); let source_node = match g.node(source_ref) { Some(src) => src, @@ -105,17 +140,7 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm = HashMap::new(); - let mut predecessor: HashMap = HashMap::new(); - let mut visited: HashSet = HashSet::new(); - let mut paths: HashMap)> = HashMap::new(); - - dist.insert(source_node.node, cost_val.clone()); + let mut dist: Vec = vec![max_val.clone(); n_nodes]; + dist[source_node.node.index()] = cost_val.clone(); + let mut predecessor: Vec = vec![VID(usize::MAX); n_nodes]; + for node in g.nodes() { + predecessor[node.node.index()] = node.node; + } + let mut visited: Vec = vec![false; n_nodes]; + let mut visited_count = 0; while let Some(State { cost, node: node_vid, }) = heap.pop() { - if target_nodes[node_vid.index()] && !paths.contains_key(&node_vid) { - let mut path = IndexSet::default(); - path.insert(node_vid); - let mut current_node_id = node_vid; - while let Some(prev_node) = predecessor.get(¤t_node_id) { - path.insert(*prev_node); - current_node_id = *prev_node; - } - path.reverse(); - paths.insert(node_vid, (cost.as_f64().unwrap(), path)); - k -= 1; - if k == 0 { - break; - } + if visited_count == k { + break; } - if !visited.insert(node_vid) { + if visited[node_vid.index()] { continue; + } else { + visited[node_vid.index()] = true; + visited_count += 1; } - let edges = match direction { Direction::OUT => g.node(node_vid).unwrap().out_edges(), Direction::IN => g.node(node_vid).unwrap().in_edges(), @@ -164,6 +181,7 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm, Vec<_>) = paths - .into_iter() - .map(|(id, (cost, path))| { - let nodes = - Nodes::new_filtered(g.clone(), g.clone(), NO_FILTER, Some(Index::new(path))); - (id, (cost, nodes)) - }) - .unzip(); - Ok(NodeState::new( - g.clone(), - values.into(), - Some(Index::new(index)), - )) + Ok((dist, predecessor)) } diff --git a/raphtory/src/algorithms/pathing/mod.rs b/raphtory/src/algorithms/pathing/mod.rs index 41569dbc39..0029f3ff80 100644 --- a/raphtory/src/algorithms/pathing/mod.rs +++ b/raphtory/src/algorithms/pathing/mod.rs @@ -30,7 +30,7 @@ pub(crate) fn to_prop(g: &G, weight: Option<&str>, val: f pub mod bellman_ford; pub mod dijkstra; -pub mod johnson; +pub mod reweighting; pub mod single_source_shortest_path; pub mod temporal_reachability; diff --git a/raphtory/src/algorithms/pathing/johnson.rs b/raphtory/src/algorithms/pathing/reweighting.rs similarity index 72% rename from raphtory/src/algorithms/pathing/johnson.rs rename to raphtory/src/algorithms/pathing/reweighting.rs index d6df94a12d..9c40a6c4e4 100644 --- a/raphtory/src/algorithms/pathing/johnson.rs +++ b/raphtory/src/algorithms/pathing/reweighting.rs @@ -21,14 +21,13 @@ use raphtory_api::core::{ use std::{ collections::{HashMap}, }; -use super::bellman_ford::bellman_ford_single_source_shortest_paths_algorithm; -use super::to_prop; +use super::super::pathing::{bellman_ford::bellman_ford_single_source_shortest_paths_algorithm, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; -pub fn johnson_all_pairs_shortest_paths( +pub fn nonegative_weight_function( g: &G, weight: Option<&str>, direction: Direction, -) -> Result)>, G>, GraphError> { +) { let dist_val = to_prop(g, weight, 0.0)?; let weight_fn = |edge: &EdgeView| -> Option { let edge_val = match weight{ @@ -43,8 +42,7 @@ pub fn johnson_all_pairs_shortest_paths( let source_node = if let Some(source_node) = g.nodes().iter().next() { source_node } else { - return Ok(NodeState::new(g, NO_FILTER)); + return; }; - - let result = bellman_ford_single_source_shortest_paths_algorithm(g, source_node, None, direction, dist_val, max_val, weight_fn)?; + let distances = bellman_ford_single_source_shortest_paths_algorithm(g, None, None, direction, dist_val, dist_val, weight_fn)?; } \ No newline at end of file From 017e9b07957f617ea663b7e57439caaf20ad33cc Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 4 Feb 2026 22:31:01 -0600 Subject: [PATCH 30/45] working --- .../src/algorithms/pathing/bellman_ford.rs | 37 +++++++-------- .../src/algorithms/pathing/reweighting.rs | 46 +++++++++---------- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index f9bf35a34b..7d3bf1a5f9 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -77,8 +77,22 @@ pub fn bellman_ford_single_source_shortest_paths, Vec<_>) = shortest_paths .into_iter() .map(|(id, (dist, path))| { @@ -201,7 +215,7 @@ pub(crate) fn bellman_ford_single_source_shortest_paths_algorithm(target_node: &NodeView, shortest_paths: &mut HashMap)>, dist: &HashMap, predecessor: &HashMap) { - 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), - ); -} diff --git a/raphtory/src/algorithms/pathing/reweighting.rs b/raphtory/src/algorithms/pathing/reweighting.rs index 9c40a6c4e4..217dc25bde 100644 --- a/raphtory/src/algorithms/pathing/reweighting.rs +++ b/raphtory/src/algorithms/pathing/reweighting.rs @@ -23,26 +23,26 @@ use std::{ }; use super::super::pathing::{bellman_ford::bellman_ford_single_source_shortest_paths_algorithm, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; -pub fn nonegative_weight_function( - g: &G, - weight: Option<&str>, - direction: Direction, -) { - let dist_val = to_prop(g, weight, 0.0)?; - let weight_fn = |edge: &EdgeView| -> Option { - let edge_val = match weight{ - None => Some(Prop::U8(1)), - Some(weight) => match edge.properties().get(weight) { - Some(prop) => Some(prop), - _ => None - } - }; - edge_val - }; - let source_node = if let Some(source_node) = g.nodes().iter().next() { - source_node - } else { - return; - }; - let distances = bellman_ford_single_source_shortest_paths_algorithm(g, None, None, direction, dist_val, dist_val, weight_fn)?; -} \ No newline at end of file +// pub fn nonegative_weight_function( +// g: &G, +// weight: Option<&str>, +// direction: Direction, +// ) { +// let dist_val = to_prop(g, weight, 0.0)?; +// let weight_fn = |edge: &EdgeView| -> Option { +// let edge_val = match weight{ +// None => Some(Prop::U8(1)), +// Some(weight) => match edge.properties().get(weight) { +// Some(prop) => Some(prop), +// _ => None +// } +// }; +// edge_val +// }; +// let source_node = if let Some(source_node) = g.nodes().iter().next() { +// source_node +// } else { +// return; +// }; +// let distances = bellman_ford_single_source_shortest_paths_algorithm(g, None, None, direction, dist_val, dist_val, weight_fn)?; +// } \ No newline at end of file From 3f234ddad845ba75c9c6a6b93f7b97a610ef2240 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 4 Feb 2026 22:35:59 -0600 Subject: [PATCH 31/45] working --- raphtory/src/algorithms/pathing/bellman_ford.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 7d3bf1a5f9..4a78c88c45 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -80,7 +80,7 @@ pub fn bellman_ford_single_source_shortest_paths node.in_edges(), Direction::BOTH => node.edges(), }; - let node_dist = dist[node.node.index()]; + let node_dist = &dist[node.node.index()]; for edge in edges { let edge_val = if let Some(w) = weight_fn(&edge) { w @@ -210,12 +210,12 @@ pub(crate) fn bellman_ford_single_source_shortest_paths_algorithm Date: Wed, 4 Feb 2026 23:03:51 -0600 Subject: [PATCH 32/45] working --- raphtory/src/algorithms/diameter/diameter.rs | 66 ++++++++++---------- raphtory/src/algorithms/pathing/dijkstra.rs | 10 ++- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/raphtory/src/algorithms/diameter/diameter.rs b/raphtory/src/algorithms/diameter/diameter.rs index 0ef8b2458a..d3e166a7da 100644 --- a/raphtory/src/algorithms/diameter/diameter.rs +++ b/raphtory/src/algorithms/diameter/diameter.rs @@ -23,37 +23,37 @@ use std::{ }; use super::super::pathing::{bellman_ford::bellman_ford_single_source_shortest_paths_algorithm, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; -pub fn diameter_approximation( - g: &G, - weight: Option<&str>, - direction: Direction, -) { - let dist_val = to_prop(g, weight, 0.0)?; - let weight_fn = |edge: &EdgeView| -> Option { - let edge_val = match weight{ - None => Some(Prop::U8(1)), - Some(weight) => match edge.properties().get(weight) { - Some(prop) => Some(prop), - _ => None - } - }; - edge_val - }; - let source_node = if let Some(source_node) = g.nodes().iter().next() { - source_node - } else { - return;; - }; - let nonnegative_weight_fn = |edge: &EdgeView| -> Option { - let edge_val = match weight{ - None => Some(Prop::U8(1)), - Some(weight) => match edge.properties().get(weight) { - Some(prop) => Some(prop), - _ => None - } - }; - edge_val - }; +// pub fn diameter_approximation( +// g: &G, +// weight: Option<&str>, +// direction: Direction, +// ) { +// let dist_val = to_prop(g, weight, 0.0)?; +// let weight_fn = |edge: &EdgeView| -> Option { +// let edge_val = match weight{ +// None => Some(Prop::U8(1)), +// Some(weight) => match edge.properties().get(weight) { +// Some(prop) => Some(prop), +// _ => None +// } +// }; +// edge_val +// }; +// let source_node = if let Some(source_node) = g.nodes().iter().next() { +// source_node +// } else { +// return;; +// }; +// let nonnegative_weight_fn = |edge: &EdgeView| -> Option { +// let edge_val = match weight{ +// None => Some(Prop::U8(1)), +// Some(weight) => match edge.properties().get(weight) { +// Some(prop) => Some(prop), +// _ => None +// } +// }; +// edge_val +// }; - let result = bellman_ford_single_source_shortest_paths_algorithm(g, source_node, None, direction, dist_val, max_val, weight_fn)?; -} \ No newline at end of file +// let result = bellman_ford_single_source_shortest_paths_algorithm(g, source_node, None, direction, dist_val, max_val, weight_fn)?; +// } \ No newline at end of file diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index d6f1516514..a4c23cd445 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -18,6 +18,7 @@ use raphtory_api::core::{ }, Direction, }; +use std::usize; use std::{ cmp::Ordering, collections::{BinaryHeap, HashMap, HashSet}, @@ -79,8 +80,7 @@ pub fn dijkstra_single_source_shortest_paths)> = HashMap::new(); for target in targets.into_iter() { let target_ref = target.as_node_ref(); @@ -99,6 +99,9 @@ pub fn dijkstra_single_source_shortest_paths Date: Wed, 4 Feb 2026 23:10:55 -0600 Subject: [PATCH 33/45] working --- raphtory/src/algorithms/pathing/dijkstra.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index a4c23cd445..9428c0254e 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -80,7 +80,8 @@ pub fn dijkstra_single_source_shortest_paths)> = HashMap::new(); for target in targets.into_iter() { let target_ref = target.as_node_ref(); From d6cdf1c9348567a93dd430822182c39ca938605b Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 4 Feb 2026 23:52:15 -0600 Subject: [PATCH 34/45] working --- raphtory/src/algorithms/pathing/mod.rs | 9 +- .../src/algorithms/pathing/reweighting.rs | 243 ++++++++++++++++-- 2 files changed, 225 insertions(+), 27 deletions(-) diff --git a/raphtory/src/algorithms/pathing/mod.rs b/raphtory/src/algorithms/pathing/mod.rs index 0029f3ff80..036a329341 100644 --- a/raphtory/src/algorithms/pathing/mod.rs +++ b/raphtory/src/algorithms/pathing/mod.rs @@ -10,7 +10,12 @@ pub(crate) fn to_prop(g: &G, weight: Option<&str>, val: f return Err(GraphError::PropertyMissingError(weight.to_string())); } } - let prop_val = match weight_type { + let prop_val = get_prop_val(weight_type, val)?; + Ok(prop_val) +} + +pub(crate) fn get_prop_val(prop_type: PropType, val: f64) -> Result { + let prop_type = match prop_type { PropType::F32 => Prop::F32(val as f32), PropType::F64 => Prop::F64(val as f64), PropType::U8 => Prop::U8(val as u8), @@ -25,7 +30,7 @@ pub(crate) fn to_prop(g: &G, weight: Option<&str>, val: f }) } }; - Ok(prop_val) + Ok(prop_type) } pub mod bellman_ford; diff --git a/raphtory/src/algorithms/pathing/reweighting.rs b/raphtory/src/algorithms/pathing/reweighting.rs index 217dc25bde..fbabcdaa6f 100644 --- a/raphtory/src/algorithms/pathing/reweighting.rs +++ b/raphtory/src/algorithms/pathing/reweighting.rs @@ -21,28 +21,221 @@ use raphtory_api::core::{ use std::{ collections::{HashMap}, }; -use super::super::pathing::{bellman_ford::bellman_ford_single_source_shortest_paths_algorithm, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; - -// pub fn nonegative_weight_function( -// g: &G, -// weight: Option<&str>, -// direction: Direction, -// ) { -// let dist_val = to_prop(g, weight, 0.0)?; -// let weight_fn = |edge: &EdgeView| -> Option { -// let edge_val = match weight{ -// None => Some(Prop::U8(1)), -// Some(weight) => match edge.properties().get(weight) { -// Some(prop) => Some(prop), -// _ => None -// } -// }; -// edge_val -// }; -// let source_node = if let Some(source_node) = g.nodes().iter().next() { -// source_node -// } else { -// return; -// }; -// let distances = bellman_ford_single_source_shortest_paths_algorithm(g, None, None, direction, dist_val, dist_val, weight_fn)?; -// } \ No newline at end of file +use super::super::pathing::{bellman_ford::bellman_ford_single_source_shortest_paths_algorithm, dijkstra::dijkstra_single_source_shortest_paths_algorithm, get_prop_val, to_prop}; + +pub fn get_johnson_reweighting_function<'a, G: StaticGraphViewOps, F: Fn(&EdgeView) -> Option>( + g: &G, + weight: Option<&'a str>, + direction: Direction, +) -> Result) -> Option + 'a, GraphError> { + let dist_val = to_prop(g, weight, 0.0)?; + let weight_fn = move |edge: &EdgeView| -> Option { + let edge_val = match weight{ + None => Some(Prop::U8(1)), + Some(weight) => match edge.properties().get(weight) { + Some(prop) => Some(prop), + _ => None + } + }; + edge_val + }; + let (distances, _) = bellman_ford_single_source_shortest_paths_algorithm(g, None::, direction, dist_val.clone(), dist_val, weight_fn)?; + let reweighting_function = move |edge: &EdgeView| -> Option { + let u = edge.src().node; + let v = edge.dst().node; + let weight_val = weight_fn(edge)?; + let dist_u = distances[u.index()].clone(); + let dist_v = distances[v.index()].clone(); + let new_weight_val_f64 = dist_u.as_f64().unwrap() + weight_val.as_f64().unwrap() - dist_v.as_f64().unwrap(); + // new weight should always be non-negative here + let new_weight_val = get_prop_val(weight_val.dtype(), new_weight_val_f64).unwrap(); + Some(new_weight_val) + }; + Ok(reweighting_function) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::{api::mutation::AdditionOps, graph::graph::Graph}; + + 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 graph_with_negative_weights() -> Graph { + load_graph(vec![ + (0, "A", "B", vec![("weight", 4.0f32)]), + (1, "A", "C", vec![("weight", 2.0f32)]), + (2, "B", "C", vec![("weight", -3.0f32)]), + (3, "C", "D", vec![("weight", 2.0f32)]), + (4, "B", "D", vec![("weight", 5.0f32)]), + ]) + } + + fn graph_with_positive_weights() -> Graph { + load_graph(vec![ + (0, "A", "B", vec![("weight", 4.0f32)]), + (1, "A", "C", vec![("weight", 2.0f32)]), + (2, "B", "C", vec![("weight", 3.0f32)]), + (3, "C", "D", vec![("weight", 2.0f32)]), + (4, "B", "D", vec![("weight", 5.0f32)]), + ]) + } + + #[test] + fn test_reweighting_negative_weights() { + let graph = graph_with_negative_weights(); + let reweight_fn = get_johnson_reweighting_function(&graph, Some("weight"), Direction::OUT) + .expect("Reweighting should succeed"); + + // Check that all reweighted edges have non-negative weights + for edge in graph.edges() { + let new_weight = reweight_fn(&edge); + assert!(new_weight.is_some()); + let weight_val = new_weight.unwrap().as_f64().unwrap(); + assert!( + weight_val >= -1e-10, + "Reweighted edge {:?} -> {:?} has negative weight: {}", + edge.src().name(), + edge.dst().name(), + weight_val + ); + } + } + + #[test] + fn test_reweighting_positive_weights() { + let graph = graph_with_positive_weights(); + let reweight_fn = get_johnson_reweighting_function(&graph, Some("weight"), Direction::OUT) + .expect("Reweighting should succeed"); + + // Check that all reweighted edges have non-negative weights + for edge in graph.edges() { + let new_weight = reweight_fn(&edge); + assert!(new_weight.is_some()); + let weight_val = new_weight.unwrap().as_f64().unwrap(); + assert!( + weight_val >= -1e-10, + "Reweighted edge {:?} -> {:?} has negative weight: {}", + edge.src().name(), + edge.dst().name(), + weight_val + ); + } + } + + #[test] + fn test_reweighting_no_weight_property() { + let graph = graph_with_negative_weights(); + // Test with None weight (should use uniform weights of 1) + let reweight_fn = get_johnson_reweighting_function(&graph, None, Direction::OUT) + .expect("Reweighting should succeed"); + + // All edges should have non-negative weights + for edge in graph.edges() { + let new_weight = reweight_fn(&edge); + assert!(new_weight.is_some()); + let weight_val = new_weight.unwrap().as_f64().unwrap(); + assert!(weight_val >= -1e-10); + } + } + + #[test] + fn test_reweighting_preserves_shortest_paths() { + let graph = graph_with_negative_weights(); + let reweight_fn = get_johnson_reweighting_function(&graph, Some("weight"), Direction::OUT) + .expect("Reweighting should succeed"); + + // Edge A->B (weight 4) should be reweighted + let edge_ab = graph.edge("A", "B").unwrap(); + let reweight_ab = reweight_fn(&edge_ab).unwrap().as_f64().unwrap(); + + // Edge B->C (weight -3) should be reweighted to non-negative + let edge_bc = graph.edge("B", "C").unwrap(); + let reweight_bc = reweight_fn(&edge_bc).unwrap().as_f64().unwrap(); + + // Both should be non-negative + assert!(reweight_ab >= -1e-10); + assert!(reweight_bc >= -1e-10); + } + + #[test] + fn test_reweighting_with_different_directions() { + let graph = graph_with_negative_weights(); + + // Test with Direction::OUT + let reweight_fn_out = get_johnson_reweighting_function(&graph, Some("weight"), Direction::OUT); + assert!(reweight_fn_out.is_ok()); + + // Test with Direction::IN + let reweight_fn_in = get_johnson_reweighting_function(&graph, Some("weight"), Direction::IN); + assert!(reweight_fn_in.is_ok()); + + // Test with Direction::BOTH + let reweight_fn_both = get_johnson_reweighting_function(&graph, Some("weight"), Direction::BOTH); + assert!(reweight_fn_both.is_ok()); + } + + #[test] + fn test_reweighting_with_integer_weights() { + let edges = vec![ + (0, 1, 2, vec![("weight", 4i64)]), + (1, 1, 3, vec![("weight", 2i64)]), + (2, 2, 3, vec![("weight", -3i64)]), + (3, 3, 4, vec![("weight", 2i64)]), + (4, 2, 4, vec![("weight", 5i64)]), + ]; + + let graph = Graph::new(); + for (t, src, dst, props) in edges { + graph.add_edge(t, src, dst, props, None).unwrap(); + } + + let reweight_fn = get_johnson_reweighting_function(&graph, Some("weight"), Direction::OUT) + .expect("Reweighting should succeed"); + + // Check that all reweighted edges have non-negative weights + for edge in graph.edges() { + let new_weight = reweight_fn(&edge); + assert!(new_weight.is_some()); + let weight_val = new_weight.unwrap().as_f64().unwrap(); + assert!( + weight_val >= -1e-10, + "Reweighted edge has negative weight: {}", + weight_val + ); + } + } + + #[test] + fn test_reweighting_specific_values() { + // Create a simple graph where we can verify exact reweighted values + let graph = load_graph(vec![ + (0, "A", "B", vec![("weight", 1.0f32)]), + (1, "B", "C", vec![("weight", -2.0f32)]), + (2, "A", "C", vec![("weight", 0.0f32)]), + ]); + + let reweight_fn = get_johnson_reweighting_function(&graph, Some("weight"), Direction::OUT) + .expect("Reweighting should succeed"); + + // All edges should be reweighted to non-negative values + for edge in graph.edges() { + let new_weight = reweight_fn(&edge); + assert!(new_weight.is_some()); + let weight_val = new_weight.unwrap().as_f64().unwrap(); + assert!( + weight_val >= -1e-10, + "Edge {:?} -> {:?} has negative weight: {}", + edge.src().name(), + edge.dst().name(), + weight_val + ); + } + } +} \ No newline at end of file From f5bcf7e4e81b193577ab3ac4bcce2677ba661054 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 4 Feb 2026 23:54:40 -0600 Subject: [PATCH 35/45] working --- raphtory/src/algorithms/pathing/reweighting.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raphtory/src/algorithms/pathing/reweighting.rs b/raphtory/src/algorithms/pathing/reweighting.rs index fbabcdaa6f..5d047a4f95 100644 --- a/raphtory/src/algorithms/pathing/reweighting.rs +++ b/raphtory/src/algorithms/pathing/reweighting.rs @@ -23,7 +23,7 @@ use std::{ }; use super::super::pathing::{bellman_ford::bellman_ford_single_source_shortest_paths_algorithm, dijkstra::dijkstra_single_source_shortest_paths_algorithm, get_prop_val, to_prop}; -pub fn get_johnson_reweighting_function<'a, G: StaticGraphViewOps, F: Fn(&EdgeView) -> Option>( +pub fn get_johnson_reweighting_function<'a, G: StaticGraphViewOps>( g: &G, weight: Option<&'a str>, direction: Direction, From 182eba3476f9d8340098dde1824ecef0a5148aec Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Thu, 5 Feb 2026 01:32:10 -0600 Subject: [PATCH 36/45] working --- raphtory/src/algorithms/diameter/diameter.rs | 56 ++--- .../src/algorithms/pathing/bellman_ford.rs | 213 +++++++++++++++++- raphtory/src/algorithms/pathing/dijkstra.rs | 2 +- .../src/algorithms/pathing/reweighting.rs | 4 - 4 files changed, 232 insertions(+), 43 deletions(-) diff --git a/raphtory/src/algorithms/diameter/diameter.rs b/raphtory/src/algorithms/diameter/diameter.rs index d3e166a7da..dc251f1859 100644 --- a/raphtory/src/algorithms/diameter/diameter.rs +++ b/raphtory/src/algorithms/diameter/diameter.rs @@ -21,39 +21,25 @@ use raphtory_api::core::{ use std::{ collections::{HashMap}, }; -use super::super::pathing::{bellman_ford::bellman_ford_single_source_shortest_paths_algorithm, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; +use super::super::pathing::{reweighting::get_johnson_reweighting_function, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; -// pub fn diameter_approximation( -// g: &G, -// weight: Option<&str>, -// direction: Direction, -// ) { -// let dist_val = to_prop(g, weight, 0.0)?; -// let weight_fn = |edge: &EdgeView| -> Option { -// let edge_val = match weight{ -// None => Some(Prop::U8(1)), -// Some(weight) => match edge.properties().get(weight) { -// Some(prop) => Some(prop), -// _ => None -// } -// }; -// edge_val -// }; -// let source_node = if let Some(source_node) = g.nodes().iter().next() { -// source_node -// } else { -// return;; -// }; -// let nonnegative_weight_fn = |edge: &EdgeView| -> Option { -// let edge_val = match weight{ -// None => Some(Prop::U8(1)), -// Some(weight) => match edge.properties().get(weight) { -// Some(prop) => Some(prop), -// _ => None -// } -// }; -// edge_val -// }; - -// let result = bellman_ford_single_source_shortest_paths_algorithm(g, source_node, None, direction, dist_val, max_val, weight_fn)?; -// } \ No newline at end of file +pub fn diameter_approximation( + g: &G, + weight: Option<&str>, + direction: Direction, + s: usize +) -> Result { + let n_nodes = g.count_nodes(); + let mut idx_to_vid = vec![VID::default(); n_nodes]; + for node in g.nodes().iter() { + idx_to_vid[node.node.index()] = node.node; + } + let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; + let cost_val = to_prop(g, weight, 0.0)?; + let max_val = to_prop(g, weight, f64::MAX)?; + let max_vertex = g.nodes().par_iter().map(|node| { + let mut max_distance = 0.0; + let mut max_idx = node.node.index(); + let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; + }).max(); +} \ No newline at end of file diff --git a/raphtory/src/algorithms/pathing/bellman_ford.rs b/raphtory/src/algorithms/pathing/bellman_ford.rs index 4a78c88c45..8fd3d7298d 100644 --- a/raphtory/src/algorithms/pathing/bellman_ford.rs +++ b/raphtory/src/algorithms/pathing/bellman_ford.rs @@ -23,7 +23,7 @@ use std::hash::Hash; use std::{ collections::{HashMap}, }; -use super::to_prop; +use super::{to_prop, get_prop_val}; /// Finds the shortest paths from a single source to multiple targets in a graph. @@ -117,6 +117,7 @@ pub(crate) fn bellman_ford_single_source_shortest_paths_algorithm Result<(Vec, Vec), GraphError> { + let max_bound = get_prop_val(max_val.dtype(), f64::MAX)?; let mut dummy_node = false; // creates a dummy node if source node is none let source_node_vid = if let Some(source) = source { @@ -178,7 +179,7 @@ pub(crate) fn bellman_ford_single_source_shortest_paths_algorithm)>) -> Graph { + let graph = Graph::new(); + for (t, src, dst, props) in edges { + graph.add_edge(t, src, dst, props, None).unwrap(); + } + graph + } + + fn graph_with_negative_weights() -> Graph { + load_graph(vec![ + (0, "A", "B", vec![("weight", 4.0f32)]), + (1, "A", "C", vec![("weight", 2.0f32)]), + (2, "B", "C", vec![("weight", -3.0f32)]), + (3, "C", "D", vec![("weight", 2.0f32)]), + (4, "B", "D", vec![("weight", 5.0f32)]), + ]) + } + + #[test] + fn test_bellman_ford_with_virtual_source() { + // Test with source = None and max_val = 0 (same as dist_val) + // This simulates adding a virtual source node with zero-weight edges to all nodes + // Used in Johnson's algorithm to compute potential function h(v) + let graph = graph_with_negative_weights(); + + let dist_val = Prop::F32(0.0); + let max_val = Prop::F32(0.0); + + let weight_fn = |edge: &EdgeView| -> Option { + edge.properties().get("weight") + }; + + let result = bellman_ford_single_source_shortest_paths_algorithm( + &graph, + None::, + Direction::OUT, + dist_val, + max_val, + weight_fn, + ); + + assert!(result.is_ok(), "Bellman-Ford with virtual source should succeed"); + + let (distances, predecessors) = result.unwrap(); + + // All nodes should have distances computed from virtual source + assert_eq!(distances.len(), graph.count_nodes()); + assert_eq!(predecessors.len(), graph.count_nodes()); + + // All distances should be non-infinite (reachable from virtual source) + for (i, dist) in distances.iter().enumerate() { + let dist_f64 = dist.as_f64().unwrap(); + assert!( + dist_f64.is_finite(), + "Node index {} should have finite distance, got: {}", + i, + dist_f64 + ); + } + + // Check that reweighted edges would be non-negative + // w'(u,v) = w(u,v) + h(u) - h(v) where h = distances from virtual source + for edge in graph.edges() { + let u_idx = edge.src().node.index(); + let v_idx = edge.dst().node.index(); + + let h_u = distances[u_idx].as_f64().unwrap(); + let h_v = distances[v_idx].as_f64().unwrap(); + let w_uv = edge.properties().get("weight").unwrap().as_f64().unwrap(); + + let reweighted = w_uv + h_u - h_v; + + assert!( + reweighted >= -1e-10, + "Reweighted edge {} -> {} should be non-negative: {} + {} - {} = {}", + edge.src().name(), + edge.dst().name(), + w_uv, + h_u, + h_v, + reweighted + ); + } + } + + #[test] + fn test_bellman_ford_virtual_source_with_positive_weights() { + let graph = load_graph(vec![ + (0, "A", "B", vec![("weight", 1.0f32)]), + (1, "B", "C", vec![("weight", 2.0f32)]), + (2, "A", "C", vec![("weight", 4.0f32)]), + ]); + + let dist_val = Prop::F32(0.0); + let max_val = Prop::F32(0.0); + + let weight_fn = |edge: &EdgeView| -> Option { + edge.properties().get("weight") + }; + + let result = bellman_ford_single_source_shortest_paths_algorithm( + &graph, + None::, + Direction::OUT, + dist_val, + max_val, + weight_fn, + ); + + assert!(result.is_ok()); + + let (distances, _) = result.unwrap(); + + // With all positive weights and virtual source at 0, + // all distances should be <= 0 (since we're finding minimum distances) + for dist in distances.iter() { + let dist_f64 = dist.as_f64().unwrap(); + assert!(dist_f64 <= 1e-10, "Distance should be <= 0, got: {}", dist_f64); + } + } + + #[test] + fn test_bellman_ford_virtual_source_detects_negative_cycle() { + // Create a graph with a negative cycle + let graph = load_graph(vec![ + (0, "A", "B", vec![("weight", 1.0f32)]), + (1, "B", "C", vec![("weight", -2.0f32)]), + (2, "C", "A", vec![("weight", -1.0f32)]), + ]); + + let dist_val = Prop::F32(0.0); + let max_val = Prop::F32(0.0); + + let weight_fn = |edge: &EdgeView| -> Option { + edge.properties().get("weight") + }; + + let result = bellman_ford_single_source_shortest_paths_algorithm( + &graph, + None::, + Direction::OUT, + dist_val, + max_val, + weight_fn, + ); + + assert!(result.is_err(), "Should detect negative cycle"); + + if let Err(GraphError::InvalidProperty { reason }) = result { + assert!(reason.contains("Negative cycle")); + } else { + panic!("Expected InvalidProperty error with negative cycle message"); + } + } + + #[test] + fn test_bellman_ford_virtual_source_with_integer_weights() { + let edges = vec![ + (0, 1, 2, vec![("weight", 4i64)]), + (1, 1, 3, vec![("weight", 2i64)]), + (2, 2, 3, vec![("weight", -3i64)]), + (3, 3, 4, vec![("weight", 2i64)]), + ]; + + let graph = Graph::new(); + for (t, src, dst, props) in edges { + graph.add_edge(t, src, dst, props, None).unwrap(); + } + + let dist_val = Prop::I64(0); + let max_val = Prop::I64(0); + + let weight_fn = |edge: &EdgeView| -> Option { + edge.properties().get("weight") + }; + + let result = bellman_ford_single_source_shortest_paths_algorithm( + &graph, + None::, + Direction::OUT, + dist_val, + max_val, + weight_fn, + ); + + assert!(result.is_ok()); + + let (distances, _) = result.unwrap(); + + // Check all distances are finite + for dist in distances.iter() { + let dist_f64 = dist.as_f64().unwrap(); + assert!( + dist_f64.abs() < i64::MAX as f64 / 2.0, + "Distance should be finite, got: {}", + dist_f64 + ); + } + } +} diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index 9428c0254e..776ae4e75b 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -23,7 +23,7 @@ use std::{ cmp::Ordering, collections::{BinaryHeap, HashMap, HashSet}, }; -use super::to_prop; +use super::{to_prop, get_prop_val}; /// A state in the Dijkstra algorithm with a cost and a node name. #[derive(PartialEq)] diff --git a/raphtory/src/algorithms/pathing/reweighting.rs b/raphtory/src/algorithms/pathing/reweighting.rs index 5d047a4f95..23fc121ea8 100644 --- a/raphtory/src/algorithms/pathing/reweighting.rs +++ b/raphtory/src/algorithms/pathing/reweighting.rs @@ -175,10 +175,6 @@ mod tests { // Test with Direction::IN let reweight_fn_in = get_johnson_reweighting_function(&graph, Some("weight"), Direction::IN); assert!(reweight_fn_in.is_ok()); - - // Test with Direction::BOTH - let reweight_fn_both = get_johnson_reweighting_function(&graph, Some("weight"), Direction::BOTH); - assert!(reweight_fn_both.is_ok()); } #[test] From bf9d4a4e8b3a86790eab89028c91bc4868baf7f3 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Thu, 5 Feb 2026 14:19:36 -0600 Subject: [PATCH 37/45] workging --- raphtory/src/algorithms/pathing/dijkstra.rs | 96 +++++++++++++++++---- 1 file changed, 80 insertions(+), 16 deletions(-) diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index 776ae4e75b..35b9fcf9c4 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -23,7 +23,73 @@ use std::{ cmp::Ordering, collections::{BinaryHeap, HashMap, HashSet}, }; -use super::{to_prop, get_prop_val}; +use super::to_prop; + +pub trait GraphMap { + fn new(capacity: usize, default: T) -> Self; + + fn get_item(&self, vid: VID) -> T; + + fn set_item(&mut self, vid: VID, value: T); +} + +impl GraphMap for Vec { + fn new(capacity: usize, default: T) -> Self { + vec![default; capacity] + } + + fn get_item(&self, vid: VID) -> T { + self[vid.index()].clone() + } + + fn set_item(&mut self, vid: VID, value: T) { + self[vid.index()] = value; + } +} + +impl GraphMap for HashMap { + fn new(capacity: usize, default: T) -> Self { + HashMap::with_capacity(capacity) + } + + fn get_item(&self, vid: VID) -> T { + self.get(&vid).cloned().unwrap() + } + + fn set_item(&mut self, vid: VID, value: T) { + self.insert(vid, value); + } +} + +pub trait GraphSet { + fn new(capacity: usize) -> Self; + fn mark_visited(&mut self, vid: VID); + fn is_visited(&self, vid: VID) -> bool; +} + +impl GraphSet for Vec { + fn new(capacity: usize) -> Self { + vec![false; capacity] + } + fn mark_visited(&mut self, vid: VID) { + self[vid.index()] = true; + } + fn is_visited(&self, vid: VID) -> bool { + self[vid.index()] + } +} + +impl GraphSet for HashSet { + fn new(capacity: usize) -> Self { + HashSet::with_capacity(capacity) + } + fn mark_visited(&mut self, vid: VID) { + self.insert(vid); + } + fn is_visited(&self, vid: VID) -> bool { + self.contains(&vid) + } +} /// A state in the Dijkstra algorithm with a cost and a node name. #[derive(PartialEq)] @@ -81,7 +147,7 @@ pub fn dijkstra_single_source_shortest_paths, Vec, Vec>(g, source, direction, n_nodes - 1, cost_val, max_val, weight_fn)?; let mut paths: HashMap)> = HashMap::new(); for target in targets.into_iter() { let target_ref = target.as_node_ref(); @@ -124,7 +190,7 @@ pub fn dijkstra_single_source_shortest_paths) -> Option>( +pub(crate) fn dijkstra_single_source_shortest_paths_algorithm) -> Option,D: GraphMap, P: GraphMap, V: GraphSet>( g: &G, source: T, direction: Direction, @@ -132,7 +198,7 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm Result<(Vec, Vec), GraphError> { +) -> Result<(D, P), GraphError> { let source_ref = source.as_node_ref(); let source_node = match g.node(source_ref) { Some(src) => src, @@ -152,14 +218,13 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm = vec![max_val.clone(); n_nodes]; - dist[source_node.node.index()] = cost_val.clone(); - let mut predecessor: Vec = vec![VID(usize::MAX); n_nodes]; + let mut dist = D::new(n_nodes, max_val.clone()); + dist.set_item(source_node.node, cost_val); + let mut predecessor = P::new(n_nodes, VID(usize::MAX)); for node in g.nodes() { - predecessor[node.node.index()] = node.node; + predecessor.set_item(node.node, node.node); } - let mut visited: Vec = vec![false; n_nodes]; + let mut visited = V::new(n_nodes); let mut visited_count = 0; while let Some(State { @@ -171,10 +236,10 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm Date: Thu, 5 Feb 2026 14:42:07 -0600 Subject: [PATCH 38/45] workging --- raphtory/src/algorithms/pathing/dijkstra.rs | 30 ++++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index 35b9fcf9c4..257f2bdf98 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -26,7 +26,7 @@ use std::{ use super::to_prop; pub trait GraphMap { - fn new(capacity: usize, default: T) -> Self; + fn new(n_nodes: usize, s: usize, default: T) -> Self; fn get_item(&self, vid: VID) -> T; @@ -34,8 +34,8 @@ pub trait GraphMap { } impl GraphMap for Vec { - fn new(capacity: usize, default: T) -> Self { - vec![default; capacity] + fn new(n_nodes: usize, s: usize, default: T) -> Self { + vec![default; n_nodes] } fn get_item(&self, vid: VID) -> T { @@ -48,8 +48,8 @@ impl GraphMap for Vec { } impl GraphMap for HashMap { - fn new(capacity: usize, default: T) -> Self { - HashMap::with_capacity(capacity) + fn new(n_nodes: usize, s: usize, default: T) -> Self { + HashMap::with_capacity(s) } fn get_item(&self, vid: VID) -> T { @@ -62,14 +62,14 @@ impl GraphMap for HashMap { } pub trait GraphSet { - fn new(capacity: usize) -> Self; + fn new(n_nodes: usize, s: usize) -> Self; fn mark_visited(&mut self, vid: VID); fn is_visited(&self, vid: VID) -> bool; } impl GraphSet for Vec { - fn new(capacity: usize) -> Self { - vec![false; capacity] + fn new(n_nodes: usize, s: usize) -> Self { + vec![false; n_nodes] } fn mark_visited(&mut self, vid: VID) { self[vid.index()] = true; @@ -80,8 +80,8 @@ impl GraphSet for Vec { } impl GraphSet for HashSet { - fn new(capacity: usize) -> Self { - HashSet::with_capacity(capacity) + fn new(n_nodes: usize, s: usize) -> Self { + HashSet::with_capacity(s) } fn mark_visited(&mut self, vid: VID) { self.insert(vid); @@ -218,13 +218,11 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm Date: Thu, 5 Feb 2026 14:59:00 -0600 Subject: [PATCH 39/45] workging --- raphtory/src/algorithms/diameter/diameter.rs | 40 ++++---- raphtory/src/algorithms/pathing/dijkstra.rs | 98 ++++---------------- 2 files changed, 38 insertions(+), 100 deletions(-) diff --git a/raphtory/src/algorithms/diameter/diameter.rs b/raphtory/src/algorithms/diameter/diameter.rs index dc251f1859..0e72bcc347 100644 --- a/raphtory/src/algorithms/diameter/diameter.rs +++ b/raphtory/src/algorithms/diameter/diameter.rs @@ -23,23 +23,23 @@ use std::{ }; use super::super::pathing::{reweighting::get_johnson_reweighting_function, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; -pub fn diameter_approximation( - g: &G, - weight: Option<&str>, - direction: Direction, - s: usize -) -> Result { - let n_nodes = g.count_nodes(); - let mut idx_to_vid = vec![VID::default(); n_nodes]; - for node in g.nodes().iter() { - idx_to_vid[node.node.index()] = node.node; - } - let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; - let cost_val = to_prop(g, weight, 0.0)?; - let max_val = to_prop(g, weight, f64::MAX)?; - let max_vertex = g.nodes().par_iter().map(|node| { - let mut max_distance = 0.0; - let mut max_idx = node.node.index(); - let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; - }).max(); -} \ No newline at end of file +// pub fn diameter_approximation( +// g: &G, +// weight: Option<&str>, +// direction: Direction, +// s: usize +// ) -> Result { +// let n_nodes = g.count_nodes(); +// let mut idx_to_vid = vec![VID::default(); n_nodes]; +// for node in g.nodes().iter() { +// idx_to_vid[node.node.index()] = node.node; +// } +// let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; +// let cost_val = to_prop(g, weight, 0.0)?; +// let max_val = to_prop(g, weight, f64::MAX)?; +// let max_vertex = g.nodes().par_iter().map(|node| { +// let mut max_distance = 0.0; +// let mut max_idx = node.node.index(); +// let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; +// }).max(); +// } \ No newline at end of file diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index 257f2bdf98..776ae4e75b 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -23,73 +23,7 @@ use std::{ cmp::Ordering, collections::{BinaryHeap, HashMap, HashSet}, }; -use super::to_prop; - -pub trait GraphMap { - fn new(n_nodes: usize, s: usize, default: T) -> Self; - - fn get_item(&self, vid: VID) -> T; - - fn set_item(&mut self, vid: VID, value: T); -} - -impl GraphMap for Vec { - fn new(n_nodes: usize, s: usize, default: T) -> Self { - vec![default; n_nodes] - } - - fn get_item(&self, vid: VID) -> T { - self[vid.index()].clone() - } - - fn set_item(&mut self, vid: VID, value: T) { - self[vid.index()] = value; - } -} - -impl GraphMap for HashMap { - fn new(n_nodes: usize, s: usize, default: T) -> Self { - HashMap::with_capacity(s) - } - - fn get_item(&self, vid: VID) -> T { - self.get(&vid).cloned().unwrap() - } - - fn set_item(&mut self, vid: VID, value: T) { - self.insert(vid, value); - } -} - -pub trait GraphSet { - fn new(n_nodes: usize, s: usize) -> Self; - fn mark_visited(&mut self, vid: VID); - fn is_visited(&self, vid: VID) -> bool; -} - -impl GraphSet for Vec { - fn new(n_nodes: usize, s: usize) -> Self { - vec![false; n_nodes] - } - fn mark_visited(&mut self, vid: VID) { - self[vid.index()] = true; - } - fn is_visited(&self, vid: VID) -> bool { - self[vid.index()] - } -} - -impl GraphSet for HashSet { - fn new(n_nodes: usize, s: usize) -> Self { - HashSet::with_capacity(s) - } - fn mark_visited(&mut self, vid: VID) { - self.insert(vid); - } - fn is_visited(&self, vid: VID) -> bool { - self.contains(&vid) - } -} +use super::{to_prop, get_prop_val}; /// A state in the Dijkstra algorithm with a cost and a node name. #[derive(PartialEq)] @@ -147,7 +81,7 @@ pub fn dijkstra_single_source_shortest_paths, Vec, Vec>(g, source, direction, n_nodes - 1, cost_val, max_val, weight_fn)?; + let (distances, predecessor) = dijkstra_single_source_shortest_paths_algorithm(g, source, direction, n_nodes - 1, cost_val, max_val, weight_fn)?; let mut paths: HashMap)> = HashMap::new(); for target in targets.into_iter() { let target_ref = target.as_node_ref(); @@ -190,7 +124,7 @@ pub fn dijkstra_single_source_shortest_paths) -> Option,D: GraphMap, P: GraphMap, V: GraphSet>( +pub(crate) fn dijkstra_single_source_shortest_paths_algorithm) -> Option>( g: &G, source: T, direction: Direction, @@ -198,7 +132,7 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm Result<(D, P), GraphError> { +) -> Result<(Vec, Vec), GraphError> { let source_ref = source.as_node_ref(); let source_node = match g.node(source_ref) { Some(src) => src, @@ -218,11 +152,14 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm = vec![max_val.clone(); n_nodes]; + dist[source_node.node.index()] = cost_val.clone(); + let mut predecessor: Vec = vec![VID(usize::MAX); n_nodes]; + for node in g.nodes() { + predecessor[node.node.index()] = node.node; + } + let mut visited: Vec = vec![false; n_nodes]; let mut visited_count = 0; while let Some(State { @@ -234,10 +171,10 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm Date: Thu, 5 Feb 2026 15:05:05 -0600 Subject: [PATCH 40/45] Revert "workging" This reverts commit 31b17057665652944a4353569916943a2b2624c6. --- raphtory/src/algorithms/diameter/diameter.rs | 40 ++++---- raphtory/src/algorithms/pathing/dijkstra.rs | 98 ++++++++++++++++---- 2 files changed, 100 insertions(+), 38 deletions(-) diff --git a/raphtory/src/algorithms/diameter/diameter.rs b/raphtory/src/algorithms/diameter/diameter.rs index 0e72bcc347..dc251f1859 100644 --- a/raphtory/src/algorithms/diameter/diameter.rs +++ b/raphtory/src/algorithms/diameter/diameter.rs @@ -23,23 +23,23 @@ use std::{ }; use super::super::pathing::{reweighting::get_johnson_reweighting_function, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; -// pub fn diameter_approximation( -// g: &G, -// weight: Option<&str>, -// direction: Direction, -// s: usize -// ) -> Result { -// let n_nodes = g.count_nodes(); -// let mut idx_to_vid = vec![VID::default(); n_nodes]; -// for node in g.nodes().iter() { -// idx_to_vid[node.node.index()] = node.node; -// } -// let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; -// let cost_val = to_prop(g, weight, 0.0)?; -// let max_val = to_prop(g, weight, f64::MAX)?; -// let max_vertex = g.nodes().par_iter().map(|node| { -// let mut max_distance = 0.0; -// let mut max_idx = node.node.index(); -// let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; -// }).max(); -// } \ No newline at end of file +pub fn diameter_approximation( + g: &G, + weight: Option<&str>, + direction: Direction, + s: usize +) -> Result { + let n_nodes = g.count_nodes(); + let mut idx_to_vid = vec![VID::default(); n_nodes]; + for node in g.nodes().iter() { + idx_to_vid[node.node.index()] = node.node; + } + let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; + let cost_val = to_prop(g, weight, 0.0)?; + let max_val = to_prop(g, weight, f64::MAX)?; + let max_vertex = g.nodes().par_iter().map(|node| { + let mut max_distance = 0.0; + let mut max_idx = node.node.index(); + let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; + }).max(); +} \ No newline at end of file diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index 776ae4e75b..257f2bdf98 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -23,7 +23,73 @@ use std::{ cmp::Ordering, collections::{BinaryHeap, HashMap, HashSet}, }; -use super::{to_prop, get_prop_val}; +use super::to_prop; + +pub trait GraphMap { + fn new(n_nodes: usize, s: usize, default: T) -> Self; + + fn get_item(&self, vid: VID) -> T; + + fn set_item(&mut self, vid: VID, value: T); +} + +impl GraphMap for Vec { + fn new(n_nodes: usize, s: usize, default: T) -> Self { + vec![default; n_nodes] + } + + fn get_item(&self, vid: VID) -> T { + self[vid.index()].clone() + } + + fn set_item(&mut self, vid: VID, value: T) { + self[vid.index()] = value; + } +} + +impl GraphMap for HashMap { + fn new(n_nodes: usize, s: usize, default: T) -> Self { + HashMap::with_capacity(s) + } + + fn get_item(&self, vid: VID) -> T { + self.get(&vid).cloned().unwrap() + } + + fn set_item(&mut self, vid: VID, value: T) { + self.insert(vid, value); + } +} + +pub trait GraphSet { + fn new(n_nodes: usize, s: usize) -> Self; + fn mark_visited(&mut self, vid: VID); + fn is_visited(&self, vid: VID) -> bool; +} + +impl GraphSet for Vec { + fn new(n_nodes: usize, s: usize) -> Self { + vec![false; n_nodes] + } + fn mark_visited(&mut self, vid: VID) { + self[vid.index()] = true; + } + fn is_visited(&self, vid: VID) -> bool { + self[vid.index()] + } +} + +impl GraphSet for HashSet { + fn new(n_nodes: usize, s: usize) -> Self { + HashSet::with_capacity(s) + } + fn mark_visited(&mut self, vid: VID) { + self.insert(vid); + } + fn is_visited(&self, vid: VID) -> bool { + self.contains(&vid) + } +} /// A state in the Dijkstra algorithm with a cost and a node name. #[derive(PartialEq)] @@ -81,7 +147,7 @@ pub fn dijkstra_single_source_shortest_paths, Vec, Vec>(g, source, direction, n_nodes - 1, cost_val, max_val, weight_fn)?; let mut paths: HashMap)> = HashMap::new(); for target in targets.into_iter() { let target_ref = target.as_node_ref(); @@ -124,7 +190,7 @@ pub fn dijkstra_single_source_shortest_paths) -> Option>( +pub(crate) fn dijkstra_single_source_shortest_paths_algorithm) -> Option,D: GraphMap, P: GraphMap, V: GraphSet>( g: &G, source: T, direction: Direction, @@ -132,7 +198,7 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm Result<(Vec, Vec), GraphError> { +) -> Result<(D, P), GraphError> { let source_ref = source.as_node_ref(); let source_node = match g.node(source_ref) { Some(src) => src, @@ -152,14 +218,11 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm = vec![max_val.clone(); n_nodes]; - dist[source_node.node.index()] = cost_val.clone(); - let mut predecessor: Vec = vec![VID(usize::MAX); n_nodes]; - for node in g.nodes() { - predecessor[node.node.index()] = node.node; - } - let mut visited: Vec = vec![false; n_nodes]; + let s = n_nodes.min(k + 1); + let mut dist = D::new(n_nodes, s, max_val.clone()); + dist.set_item(source_node.node, cost_val); + let mut predecessor = P::new(n_nodes, s, VID(usize::MAX)); + let mut visited = V::new(n_nodes, s); let mut visited_count = 0; while let Some(State { @@ -171,10 +234,10 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm Date: Thu, 5 Feb 2026 15:09:12 -0600 Subject: [PATCH 41/45] working --- raphtory/src/algorithms/pathing/dijkstra.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index 257f2bdf98..8102df9864 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -222,6 +222,7 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm Date: Thu, 5 Feb 2026 15:13:10 -0600 Subject: [PATCH 42/45] working --- raphtory/src/algorithms/diameter/diameter.rs | 40 ++++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/raphtory/src/algorithms/diameter/diameter.rs b/raphtory/src/algorithms/diameter/diameter.rs index dc251f1859..0e72bcc347 100644 --- a/raphtory/src/algorithms/diameter/diameter.rs +++ b/raphtory/src/algorithms/diameter/diameter.rs @@ -23,23 +23,23 @@ use std::{ }; use super::super::pathing::{reweighting::get_johnson_reweighting_function, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; -pub fn diameter_approximation( - g: &G, - weight: Option<&str>, - direction: Direction, - s: usize -) -> Result { - let n_nodes = g.count_nodes(); - let mut idx_to_vid = vec![VID::default(); n_nodes]; - for node in g.nodes().iter() { - idx_to_vid[node.node.index()] = node.node; - } - let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; - let cost_val = to_prop(g, weight, 0.0)?; - let max_val = to_prop(g, weight, f64::MAX)?; - let max_vertex = g.nodes().par_iter().map(|node| { - let mut max_distance = 0.0; - let mut max_idx = node.node.index(); - let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; - }).max(); -} \ No newline at end of file +// pub fn diameter_approximation( +// g: &G, +// weight: Option<&str>, +// direction: Direction, +// s: usize +// ) -> Result { +// let n_nodes = g.count_nodes(); +// let mut idx_to_vid = vec![VID::default(); n_nodes]; +// for node in g.nodes().iter() { +// idx_to_vid[node.node.index()] = node.node; +// } +// let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; +// let cost_val = to_prop(g, weight, 0.0)?; +// let max_val = to_prop(g, weight, f64::MAX)?; +// let max_vertex = g.nodes().par_iter().map(|node| { +// let mut max_distance = 0.0; +// let mut max_idx = node.node.index(); +// let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; +// }).max(); +// } \ No newline at end of file From b20746a8ef6a3f31636618c41f56e4dfa7584c86 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Thu, 5 Feb 2026 16:21:54 -0600 Subject: [PATCH 43/45] working --- raphtory/src/algorithms/diameter/diameter.rs | 40 ++++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/raphtory/src/algorithms/diameter/diameter.rs b/raphtory/src/algorithms/diameter/diameter.rs index 0e72bcc347..dc251f1859 100644 --- a/raphtory/src/algorithms/diameter/diameter.rs +++ b/raphtory/src/algorithms/diameter/diameter.rs @@ -23,23 +23,23 @@ use std::{ }; use super::super::pathing::{reweighting::get_johnson_reweighting_function, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; -// pub fn diameter_approximation( -// g: &G, -// weight: Option<&str>, -// direction: Direction, -// s: usize -// ) -> Result { -// let n_nodes = g.count_nodes(); -// let mut idx_to_vid = vec![VID::default(); n_nodes]; -// for node in g.nodes().iter() { -// idx_to_vid[node.node.index()] = node.node; -// } -// let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; -// let cost_val = to_prop(g, weight, 0.0)?; -// let max_val = to_prop(g, weight, f64::MAX)?; -// let max_vertex = g.nodes().par_iter().map(|node| { -// let mut max_distance = 0.0; -// let mut max_idx = node.node.index(); -// let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; -// }).max(); -// } \ No newline at end of file +pub fn diameter_approximation( + g: &G, + weight: Option<&str>, + direction: Direction, + s: usize +) -> Result { + let n_nodes = g.count_nodes(); + let mut idx_to_vid = vec![VID::default(); n_nodes]; + for node in g.nodes().iter() { + idx_to_vid[node.node.index()] = node.node; + } + let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; + let cost_val = to_prop(g, weight, 0.0)?; + let max_val = to_prop(g, weight, f64::MAX)?; + let max_vertex = g.nodes().par_iter().map(|node| { + let mut max_distance = 0.0; + let mut max_idx = node.node.index(); + let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; + }).max(); +} \ No newline at end of file From 10a23f042ae216aa365c4cd53bdd8f3a8ab463f6 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Thu, 5 Feb 2026 17:10:14 -0600 Subject: [PATCH 44/45] working --- raphtory/src/algorithms/diameter/diameter.rs | 40 ++++++++++---------- raphtory/src/algorithms/pathing/dijkstra.rs | 31 ++++++++------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/raphtory/src/algorithms/diameter/diameter.rs b/raphtory/src/algorithms/diameter/diameter.rs index dc251f1859..0e72bcc347 100644 --- a/raphtory/src/algorithms/diameter/diameter.rs +++ b/raphtory/src/algorithms/diameter/diameter.rs @@ -23,23 +23,23 @@ use std::{ }; use super::super::pathing::{reweighting::get_johnson_reweighting_function, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; -pub fn diameter_approximation( - g: &G, - weight: Option<&str>, - direction: Direction, - s: usize -) -> Result { - let n_nodes = g.count_nodes(); - let mut idx_to_vid = vec![VID::default(); n_nodes]; - for node in g.nodes().iter() { - idx_to_vid[node.node.index()] = node.node; - } - let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; - let cost_val = to_prop(g, weight, 0.0)?; - let max_val = to_prop(g, weight, f64::MAX)?; - let max_vertex = g.nodes().par_iter().map(|node| { - let mut max_distance = 0.0; - let mut max_idx = node.node.index(); - let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; - }).max(); -} \ No newline at end of file +// pub fn diameter_approximation( +// g: &G, +// weight: Option<&str>, +// direction: Direction, +// s: usize +// ) -> Result { +// let n_nodes = g.count_nodes(); +// let mut idx_to_vid = vec![VID::default(); n_nodes]; +// for node in g.nodes().iter() { +// idx_to_vid[node.node.index()] = node.node; +// } +// let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; +// let cost_val = to_prop(g, weight, 0.0)?; +// let max_val = to_prop(g, weight, f64::MAX)?; +// let max_vertex = g.nodes().par_iter().map(|node| { +// let mut max_distance = 0.0; +// let mut max_idx = node.node.index(); +// let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; +// }).max(); +// } \ No newline at end of file diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index 8102df9864..2a038357b9 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -25,6 +25,8 @@ use std::{ }; use super::to_prop; +const NO_VID: VID = VID(usize::MAX); + pub trait GraphMap { fn new(n_nodes: usize, s: usize, default: T) -> Self; @@ -34,7 +36,7 @@ pub trait GraphMap { } impl GraphMap for Vec { - fn new(n_nodes: usize, s: usize, default: T) -> Self { + fn new(n_nodes: usize, capacity: usize, default: T) -> Self { vec![default; n_nodes] } @@ -48,8 +50,8 @@ impl GraphMap for Vec { } impl GraphMap for HashMap { - fn new(n_nodes: usize, s: usize, default: T) -> Self { - HashMap::with_capacity(s) + fn new(n_nodes: usize, capacity: usize, default: T) -> Self { + HashMap::with_capacity(capacity) } fn get_item(&self, vid: VID) -> T { @@ -62,13 +64,13 @@ impl GraphMap for HashMap { } pub trait GraphSet { - fn new(n_nodes: usize, s: usize) -> Self; + fn new(n_nodes: usize, capacity: usize) -> Self; fn mark_visited(&mut self, vid: VID); fn is_visited(&self, vid: VID) -> bool; } impl GraphSet for Vec { - fn new(n_nodes: usize, s: usize) -> Self { + fn new(n_nodes: usize, capacity: usize) -> Self { vec![false; n_nodes] } fn mark_visited(&mut self, vid: VID) { @@ -80,8 +82,8 @@ impl GraphSet for Vec { } impl GraphSet for HashSet { - fn new(n_nodes: usize, s: usize) -> Self { - HashSet::with_capacity(s) + fn new(n_nodes: usize, capacity: usize) -> Self { + HashSet::with_capacity(capacity) } fn mark_visited(&mut self, vid: VID) { self.insert(vid); @@ -147,7 +149,7 @@ pub fn dijkstra_single_source_shortest_paths, Vec, Vec>(g, source, direction, n_nodes - 1, cost_val, max_val, weight_fn)?; + let (distances, predecessor, _) = dijkstra_single_source_shortest_paths_algorithm::, Vec, Vec>(g, source, direction, n_nodes - 1, cost_val, max_val, weight_fn)?; let mut paths: HashMap)> = HashMap::new(); for target in targets.into_iter() { let target_ref = target.as_node_ref(); @@ -198,7 +200,7 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm Result<(D, P), GraphError> { +) -> Result<(D, P, Vec), GraphError> { let source_ref = source.as_node_ref(); let source_node = match g.node(source_ref) { Some(src) => src, @@ -218,13 +220,13 @@ pub(crate) fn dijkstra_single_source_shortest_paths_algorithm Date: Thu, 5 Feb 2026 18:07:44 -0600 Subject: [PATCH 45/45] working --- raphtory/src/algorithms/diameter/diameter.rs | 53 ++++++++++++-------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/raphtory/src/algorithms/diameter/diameter.rs b/raphtory/src/algorithms/diameter/diameter.rs index 0e72bcc347..d16fb8fd7e 100644 --- a/raphtory/src/algorithms/diameter/diameter.rs +++ b/raphtory/src/algorithms/diameter/diameter.rs @@ -1,3 +1,4 @@ +use crate::db::api::state::ops::node; use crate::db::graph::edge::EdgeView; use crate::db::graph::node::NodeView; use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; @@ -18,28 +19,40 @@ use raphtory_api::core::{ }, Direction, }; +use rayon::iter::ParallelIterator; +use std::collections::HashSet; +use std::hash::Hash; use std::{ collections::{HashMap}, }; +use rayon::prelude::*; use super::super::pathing::{reweighting::get_johnson_reweighting_function, dijkstra::dijkstra_single_source_shortest_paths_algorithm, to_prop}; -// pub fn diameter_approximation( -// g: &G, -// weight: Option<&str>, -// direction: Direction, -// s: usize -// ) -> Result { -// let n_nodes = g.count_nodes(); -// let mut idx_to_vid = vec![VID::default(); n_nodes]; -// for node in g.nodes().iter() { -// idx_to_vid[node.node.index()] = node.node; -// } -// let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; -// let cost_val = to_prop(g, weight, 0.0)?; -// let max_val = to_prop(g, weight, f64::MAX)?; -// let max_vertex = g.nodes().par_iter().map(|node| { -// let mut max_distance = 0.0; -// let mut max_idx = node.node.index(); -// let (distances, _) = dijkstra_single_source_shortest_paths_algorithm(g, node, direction, s, cost_val, max_val, weight_fn)?; -// }).max(); -// } \ No newline at end of file +pub fn diameter_approximation( + g: &G, + weight: Option<&str>, + direction: Direction, + s: usize +) -> Result { + let n_nodes = g.nodes().len(); + let weight_fn = get_johnson_reweighting_function(g, weight, direction)?; + let cost_val = to_prop(g, weight, 0.0)?; + let max_val = to_prop(g, weight, f64::MAX)?; + let nodes_of_s_tree_with_max_depth = g.nodes().par_iter().map(|node| { + k_ordered_paths_and_max_depth(g, direction, node.node, s, cost_val, max_val, weight_fn) + }).max_by(|a, b| a.1.partial_cmp(&b.1).unwrap()).map(|(k_ordered, _)| k_ordered).unwrap(); + let max_depth = nodes_of_s_tree_with_max_depth.par_iter().map(|node_vid| { + k_ordered_paths_and_max_depth(g, direction, *node_vid, n_nodes - 1, cost_val, max_val, weight_fn) + }).max_by(|a, b| a.1.partial_cmp(&b.1).unwrap()).map(|(_, dist)| dist).unwrap(); + + +} + +fn k_ordered_paths_and_max_depth(g: &G, direction: Direction, node_vid: VID, s: usize, cost_val: Prop, max_val: Prop, weight_fn: impl Fn(&EdgeView) -> Option) -> (Vec, f64) { + let node = g.node(&node_vid).unwrap(); + let (distances, _, k_ordered) = dijkstra_single_source_shortest_paths_algorithm::, HashMap, HashSet>(g, node, direction, s, cost_val, max_val, weight_fn).unwrap(); + let max_vid = k_ordered[k_ordered.len() - 1]; + let max_distance_val = distances.get(&max_vid).unwrap(); + (k_ordered, max_distance_val.as_f64().unwrap()) +} +