From 806a5d81628b68361b2539c264ac2de952436bab Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 28 Jan 2026 19:59:15 -0600 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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 {