From bee67a9a9e4e1d85de6d668e675e8f663849d960 Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Sun, 8 Mar 2026 20:43:01 +0100 Subject: [PATCH 01/11] init --- rustworkx-core/src/centrality.rs | 526 +++++++++++++++++++++++++++++++ rustworkx/__init__.py | 86 +++++ rustworkx/__init__.pyi | 21 ++ rustworkx/rustworkx.pyi | 32 ++ src/centrality.rs | 271 ++++++++++++++++ src/lib.rs | 6 + tests/digraph/test_centrality.py | 165 ++++++++++ tests/graph/test_centrality.py | 202 ++++++++++++ 8 files changed, 1309 insertions(+) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 431e0627f9..961d32376c 100755 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -14,6 +14,8 @@ use std::collections::VecDeque; use std::hash::Hash; use std::sync::RwLock; +use hashbrown::HashSet; + use hashbrown::HashMap; use petgraph::algo::dijkstra; use petgraph::visit::{ @@ -1739,3 +1741,527 @@ mod test_newman_weighted_closeness_centrality { test_case(1); // parallel } } + +/// Compute the group degree centrality of a set of nodes. +/// +/// Group degree centrality measures the fraction of non-group nodes that are +/// connected to at least one member of the group. It is defined as: +/// +/// C_D(S) = |N(S) \ S| / (|V| - |S|) +/// +/// where N(S) is the union of neighborhoods of all nodes in S. +/// +/// Based on: Everett, M. G., & Borgatti, S. P. (1999). +/// The centrality of groups and classes. +/// Journal of Mathematical Sociology, 23(3), 181-201. +/// +/// Arguments: +/// +/// * `graph` - The graph object to run the algorithm on +/// * `group` - A slice of node indices representing the group +/// * `direction` - Optional direction for directed graphs: +/// - `None` uses outgoing edges (default) +/// - `Some(Incoming)` counts nodes with edges into the group +/// - `Some(Outgoing)` counts nodes reachable from the group +/// +/// # Example +/// ```rust +/// use rustworkx_core::petgraph; +/// use rustworkx_core::centrality::group_degree_centrality; +/// +/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// (0, 1), (1, 2), (2, 3), (3, 4) +/// ]); +/// let output = group_degree_centrality(&g, &[0, 1], None); +/// // Nodes 0,1 are the group. Neighbors of {0,1} outside the group = {2}. +/// // So centrality = 1 / (5 - 2) = 1/3. +/// assert!((output - 1.0 / 3.0).abs() < 1e-10); +/// ``` +pub fn group_degree_centrality( + graph: G, + group: &[usize], + direction: Option, +) -> f64 +where + G: NodeIndexable + + IntoNodeIdentifiers + + IntoNeighbors + + IntoNeighborsDirected + + NodeCount + + GraphProp, + G::NodeId: Eq + Hash, +{ + let node_count = graph.node_count(); + let group_size = group.len(); + if group_size >= node_count { + return 0.0; + } + + let group_set: HashSet = group.iter().copied().collect(); + let mut reached: HashSet = HashSet::new(); + + for &node_idx in group { + let node_id = graph.from_index(node_idx); + let neighbors: Box> = match direction { + Some(dir) => Box::new(graph.neighbors_directed(node_id, dir)), + None => Box::new(graph.neighbors(node_id)), + }; + for neighbor in neighbors { + let neighbor_idx = graph.to_index(neighbor); + if !group_set.contains(&neighbor_idx) { + reached.insert(neighbor_idx); + } + } + } + + reached.len() as f64 / (node_count - group_size) as f64 +} + +/// Compute the group closeness centrality of a set of nodes. +/// +/// Group closeness centrality measures how close a group of nodes is to +/// all non-group nodes. It is defined as: +/// +/// C_close(S) = |V \ S| / sum_{v in V\S} d(S, v) +/// +/// where d(S, v) = min_{u in S} d(u, v) is the minimum distance from any +/// group member to node v. +/// +/// Based on: Everett, M. G., & Borgatti, S. P. (1999). +/// The centrality of groups and classes. +/// Journal of Mathematical Sociology, 23(3), 181-201. +/// +/// Arguments: +/// +/// * `graph` - The graph object to run the algorithm on +/// * `group` - A slice of node indices representing the group +/// +/// # Example +/// ```rust +/// use rustworkx_core::petgraph; +/// use rustworkx_core::centrality::group_closeness_centrality; +/// +/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// (0, 1), (1, 2), (2, 3), (3, 4) +/// ]); +/// let output = group_closeness_centrality(&g, &[0, 1]); +/// // Group = {0, 1}. Non-group = {2, 3, 4}. +/// // d({0,1}, 2) = 1, d({0,1}, 3) = 2, d({0,1}, 4) = 3. Sum = 6. +/// // Closeness = 3 / 6 = 0.5 +/// assert!((output - 0.5).abs() < 1e-10); +/// ``` +pub fn group_closeness_centrality(graph: G, group: &[usize]) -> f64 +where + G: NodeIndexable + + IntoNodeIdentifiers + + GraphBase + + IntoEdges + + IntoEdgesDirected + + Visitable + + NodeCount, + G::NodeId: Eq + Hash, + G::EdgeId: Eq + Hash, +{ + let node_count = graph.node_count(); + let group_size = group.len(); + if group_size >= node_count { + return 0.0; + } + + let group_set: HashSet = group.iter().copied().collect(); + + // Multi-source BFS on Reversed graph (incoming edges), matching the + // convention used by NX and by per-node closeness_centrality: d(S,v) + // is the distance from v to the nearest group member. + let reversed = Reversed(&graph); + let max_index = graph.node_bound(); + let mut distance: Vec> = vec![None; max_index]; + let mut queue: VecDeque = VecDeque::new(); + + for &node_idx in group { + let node_id = graph.from_index(node_idx); + distance[node_idx] = Some(0); + queue.push_back(node_id); + } + + while let Some(v) = queue.pop_front() { + let v_idx = graph.to_index(v); + let dist_v = distance[v_idx].unwrap(); + for edge in reversed.edges(v) { + let w = edge.target(); + let w_idx = graph.to_index(w); + if distance[w_idx].is_none() { + distance[w_idx] = Some(dist_v + 1); + queue.push_back(w); + } + } + } + + let mut dist_sum: usize = 0; + for node in graph.node_identifiers() { + let idx = graph.to_index(node); + if group_set.contains(&idx) { + continue; + } + if let Some(d) = distance[idx] { + dist_sum += d; + } + } + + if dist_sum == 0 { + return 0.0; + } + + (node_count - group_size) as f64 / dist_sum as f64 +} + +/// Compute the group betweenness centrality of a set of nodes. +/// +/// Group betweenness centrality measures the fraction of shortest paths +/// between non-group node pairs that pass through at least one group member. +/// It is defined as: +/// +/// C_B(S) = sum_{s,t in V\S} sigma(s,t|S) / sigma(s,t) +/// +/// where sigma(s,t) is the number of shortest paths from s to t, and +/// sigma(s,t|S) is the number of those paths passing through at least +/// one node in S. +/// +/// Based on: Everett, M. G., & Borgatti, S. P. (1999). +/// The centrality of groups and classes. +/// Journal of Mathematical Sociology, 23(3), 181-201. +/// +/// Arguments: +/// +/// * `graph` - The graph object to run the algorithm on +/// * `group` - A slice of node indices representing the group +/// * `normalized` - Whether to normalize the result +/// +/// # Example +/// ```rust +/// use rustworkx_core::petgraph; +/// use rustworkx_core::centrality::group_betweenness_centrality; +/// +/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// (0, 1), (1, 2), (2, 3), (3, 4) +/// ]); +/// let output = group_betweenness_centrality(&g, &[2], true); +/// // Node 2 is on every shortest path between {0,1} and {3,4}. +/// assert!(output > 0.0); +/// ``` +pub fn group_betweenness_centrality( + graph: G, + group: &[usize], + normalized: bool, +) -> f64 +where + G: NodeIndexable + + IntoNodeIdentifiers + + IntoNeighborsDirected + + NodeCount + + GraphProp + + GraphBase, + G::NodeId: Eq + Hash, +{ + let node_count = graph.node_count(); + let group_size = group.len(); + + if group_size == 0 || node_count <= 1 { + return 0.0; + } + + let group_set: HashSet = group.iter().copied().collect(); + let max_index = graph.node_bound(); + + // For each non-group source, run BFS on the full graph and on the graph + // with group nodes removed. The difference in path counts gives us the + // fraction of shortest paths passing through the group. + let mut group_betweenness: f64 = 0.0; + + let node_ids: Vec = graph.node_identifiers().collect(); + + for &source_id in &node_ids { + let source_idx = graph.to_index(source_id); + if group_set.contains(&source_idx) { + continue; + } + + // BFS on full graph from source + let mut sigma_full = vec![0.0_f64; max_index]; + let mut dist_full: Vec> = vec![None; max_index]; + let mut queue: VecDeque = VecDeque::new(); + + sigma_full[source_idx] = 1.0; + dist_full[source_idx] = Some(0); + queue.push_back(source_id); + + while let Some(v) = queue.pop_front() { + let v_idx = graph.to_index(v); + let dist_v = dist_full[v_idx].unwrap(); + for w in graph.neighbors(v) { + let w_idx = graph.to_index(w); + if dist_full[w_idx].is_none() { + dist_full[w_idx] = Some(dist_v + 1); + queue.push_back(w); + } + if dist_full[w_idx] == Some(dist_v + 1) { + sigma_full[w_idx] += sigma_full[v_idx]; + } + } + } + + // BFS on graph with group nodes removed + let mut sigma_no_group = vec![0.0_f64; max_index]; + let mut dist_no_group: Vec> = vec![None; max_index]; + let mut queue2: VecDeque = VecDeque::new(); + + sigma_no_group[source_idx] = 1.0; + dist_no_group[source_idx] = Some(0); + queue2.push_back(source_id); + + while let Some(v) = queue2.pop_front() { + let v_idx = graph.to_index(v); + let dist_v = dist_no_group[v_idx].unwrap(); + for w in graph.neighbors(v) { + let w_idx = graph.to_index(w); + if group_set.contains(&w_idx) { + continue; + } + if dist_no_group[w_idx].is_none() { + dist_no_group[w_idx] = Some(dist_v + 1); + queue2.push_back(w); + } + if dist_no_group[w_idx] == Some(dist_v + 1) { + sigma_no_group[w_idx] += sigma_no_group[v_idx]; + } + } + } + + // For each non-group target, accumulate the fraction of shortest paths + // that pass through at least one group member. + for &target_id in &node_ids { + let target_idx = graph.to_index(target_id); + if target_idx == source_idx || group_set.contains(&target_idx) { + continue; + } + if sigma_full[target_idx] == 0.0 { + continue; + } + + // Paths through group = total - paths avoiding group, + // but only if the shortest path length is the same. If it differs, + // none of the shortest paths avoid the group. + let paths_avoiding = if dist_no_group[target_idx] == dist_full[target_idx] { + sigma_no_group[target_idx] + } else { + 0.0 + }; + + let fraction_through_group = + (sigma_full[target_idx] - paths_avoiding) / sigma_full[target_idx]; + group_betweenness += fraction_through_group; + } + } + + if !graph.is_directed() { + group_betweenness /= 2.0; + } + + if normalized { + let non_group = node_count - group_size; + if non_group > 1 { + let norm = if graph.is_directed() { + (non_group * (non_group - 1)) as f64 + } else { + ((non_group * (non_group - 1)) / 2) as f64 + }; + group_betweenness /= norm; + } + } + + group_betweenness +} + +#[cfg(test)] +mod test_group_degree_centrality { + use crate::centrality::group_degree_centrality; + use crate::petgraph; + + #[test] + fn test_undirected_path() { + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + ]); + let result = group_degree_centrality(&g, &[0, 1], None); + // Neighbors of {0,1} outside group = {2}. Centrality = 1/3. + assert!((result - 1.0 / 3.0).abs() < 1e-10); + } + + #[test] + fn test_undirected_complete() { + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ + (0, 1), + (0, 2), + (0, 3), + (1, 2), + (1, 3), + (2, 3), + ]); + let result = group_degree_centrality(&g, &[0], None); + assert!((result - 1.0).abs() < 1e-10); + } + + #[test] + fn test_directed_out() { + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ + (0, 1), + (1, 2), + (2, 3), + ]); + let result = + group_degree_centrality(&g, &[0, 1], Some(petgraph::Direction::Outgoing)); + // Out-neighbors of {0,1} outside group = {2}. Centrality = 1/2. + assert!((result - 0.5).abs() < 1e-10); + } + + #[test] + fn test_directed_in() { + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ + (0, 1), + (1, 2), + (2, 3), + ]); + let result = + group_degree_centrality(&g, &[2, 3], Some(petgraph::Direction::Incoming)); + // In-neighbors of {2,3} outside group = {1}. Centrality = 1/2. + assert!((result - 0.5).abs() < 1e-10); + } +} + +#[cfg(test)] +mod test_group_closeness_centrality { + use crate::centrality::group_closeness_centrality; + use crate::petgraph; + + macro_rules! assert_almost_equal { + ($x:expr, $y:expr, $d:expr) => { + if ($x - $y).abs() >= $d { + panic!("{} != {} within delta of {}", $x, $y, $d); + } + }; + } + + #[test] + fn test_undirected_path() { + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + ]); + let result = group_closeness_centrality(&g, &[0, 1]); + // Non-group = {2,3,4}. d(S,2)=1, d(S,3)=2, d(S,4)=3. Sum=6. + // Closeness = 3/6 = 0.5 + assert_almost_equal!(result, 0.5, 1e-10); + } + + #[test] + fn test_undirected_center_node() { + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ + (0, 2), + (1, 2), + (2, 3), + (2, 4), + ]); + let result = group_closeness_centrality(&g, &[2]); + // Non-group = {0,1,3,4}. All at distance 1. Sum=4. + // Closeness = 4/4 = 1.0 + assert_almost_equal!(result, 1.0, 1e-10); + } + + #[test] + fn test_disconnected() { + // Two disconnected components + let mut g = petgraph::graph::UnGraph::<(), ()>::new_undirected(); + g.add_node(()); + g.add_node(()); + g.add_node(()); + g.add_edge( + petgraph::graph::NodeIndex::new(0), + petgraph::graph::NodeIndex::new(1), + (), + ); + // Node 2 is disconnected + let result = group_closeness_centrality(&g, &[0]); + // |V-S|=2, only node 1 reachable at distance 1. Node 2 unreachable. + // dist_sum=1. closeness = 2/1 = 2.0 + assert_almost_equal!(result, 2.0, 1e-10); + } +} + +#[cfg(test)] +mod test_group_betweenness_centrality { + use crate::centrality::group_betweenness_centrality; + use crate::petgraph; + + macro_rules! assert_almost_equal { + ($x:expr, $y:expr, $d:expr) => { + if ($x - $y).abs() >= $d { + panic!("{} != {} within delta of {}", $x, $y, $d); + } + }; + } + + #[test] + fn test_undirected_path_center() { + // Path: 0-1-2-3-4 + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + ]); + // Group = {2}. Node 2 is on all shortest paths between {0,1} and {3,4}. + let result = group_betweenness_centrality(&g, &[2], false); + // Pairs through node 2: (0,3), (0,4), (1,3), (1,4) = 4 paths + assert_almost_equal!(result, 4.0, 1e-10); + } + + #[test] + fn test_undirected_path_center_normalized() { + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + ]); + let result = group_betweenness_centrality(&g, &[2], true); + // Non-group size = 4. Normalization = C(4,2) = 6. + // Normalized = 4/6 = 2/3 + assert_almost_equal!(result, 2.0 / 3.0, 1e-10); + } + + #[test] + fn test_empty_group() { + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (1, 2)]); + let result = group_betweenness_centrality(&g, &[], false); + assert_almost_equal!(result, 0.0, 1e-10); + } + + #[test] + fn test_single_node_group() { + // Star graph: center=0, leaves=1,2,3,4 + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + ]); + let result = group_betweenness_centrality(&g, &[0], false); + // Node 0 is on all 6 shortest paths between leaf pairs + assert_almost_equal!(result, 6.0, 1e-10); + } +} diff --git a/rustworkx/__init__.py b/rustworkx/__init__.py index de8600326c..35f91d3fae 100644 --- a/rustworkx/__init__.py +++ b/rustworkx/__init__.py @@ -1268,6 +1268,92 @@ def degree_centrality(graph): raise TypeError(f"Invalid Input Type {type(graph)} for graph") +@_rustworkx_dispatch +def group_degree_centrality(graph, group): + r"""Compute the group degree centrality of a set of nodes. + + Group degree centrality measures the fraction of non-group nodes that are + connected to at least one member of the group. It is defined as: + + .. math:: + + C_D(S) = \frac{|N(S) \setminus S|}{|V| - |S|} + + where :math:`N(S)` is the union of neighborhoods of all nodes in :math:`S`. + + Based on: Everett, M. G., & Borgatti, S. P. (1999). + The centrality of groups and classes. + Journal of Mathematical Sociology, 23(3), 181-201. + + :param graph: The input graph. Can either be a + :class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph`. + :param list group: A list of node indices representing the group. + + :returns: The group degree centrality as a float. + :rtype: float + """ + raise TypeError(f"Invalid Input Type {type(graph)} for graph") + + +@_rustworkx_dispatch +def group_closeness_centrality(graph, group): + r"""Compute the group closeness centrality of a set of nodes. + + Group closeness centrality measures how close a group of nodes is to all + non-group nodes. It is defined as: + + .. math:: + + C_{close}(S) = \frac{|V \setminus S|}{\sum_{v \in V \setminus S} d(S, v)} + + where :math:`d(S, v) = \min_{u \in S} d(u, v)` is the minimum distance + from any group member to node :math:`v`. + + Based on: Everett, M. G., & Borgatti, S. P. (1999). + The centrality of groups and classes. + Journal of Mathematical Sociology, 23(3), 181-201. + + :param graph: The input graph. Can either be a + :class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph`. + :param list group: A list of node indices representing the group. + + :returns: The group closeness centrality as a float. + :rtype: float + """ + raise TypeError(f"Invalid Input Type {type(graph)} for graph") + + +@_rustworkx_dispatch +def group_betweenness_centrality(graph, group, normalized=True): + r"""Compute the group betweenness centrality of a set of nodes. + + Group betweenness centrality measures the fraction of shortest paths + between non-group node pairs that pass through at least one group member. + It is defined as: + + .. math:: + + C_B(S) = \sum_{s,t \in V \setminus S} \frac{\sigma(s, t|S)}{\sigma(s, t)} + + where :math:`\sigma(s,t)` is the number of shortest paths from :math:`s` + to :math:`t`, and :math:`\sigma(s,t|S)` is the number of those paths + passing through at least one node in :math:`S`. + + Based on: Everett, M. G., & Borgatti, S. P. (1999). + The centrality of groups and classes. + Journal of Mathematical Sociology, 23(3), 181-201. + + :param graph: The input graph. Can either be a + :class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph`. + :param list group: A list of node indices representing the group. + :param bool normalized: Whether to normalize the result. Defaults to True. + + :returns: The group betweenness centrality as a float. + :rtype: float + """ + raise TypeError(f"Invalid Input Type {type(graph)} for graph") + + @_rustworkx_dispatch def edge_betweenness_centrality(graph, normalized=True, parallel_threshold=50): r"""Compute the edge betweenness centrality of all edges in a graph. diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index ff7b360031..2ffeafdf62 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -68,6 +68,14 @@ from .rustworkx import digraph_degree_centrality as digraph_degree_centrality from .rustworkx import graph_degree_centrality as graph_degree_centrality from .rustworkx import in_degree_centrality as in_degree_centrality from .rustworkx import out_degree_centrality as out_degree_centrality +from .rustworkx import graph_group_degree_centrality as graph_group_degree_centrality +from .rustworkx import digraph_group_degree_centrality as digraph_group_degree_centrality +from .rustworkx import graph_group_closeness_centrality as graph_group_closeness_centrality +from .rustworkx import digraph_group_closeness_centrality as digraph_group_closeness_centrality +from .rustworkx import graph_group_betweenness_centrality as graph_group_betweenness_centrality +from .rustworkx import ( + digraph_group_betweenness_centrality as digraph_group_betweenness_centrality, +) from .rustworkx import graph_greedy_color as graph_greedy_color from .rustworkx import graph_greedy_edge_color as graph_greedy_edge_color from .rustworkx import graph_is_bipartite as graph_is_bipartite @@ -548,6 +556,19 @@ def newman_weighted_closeness_centrality( def degree_centrality( graph: PyGraph[_S, _T] | PyDiGraph[_S, _T], ) -> CentralityMapping: ... +def group_degree_centrality( + graph: PyGraph[_S, _T] | PyDiGraph[_S, _T], + group: list[int], +) -> float: ... +def group_closeness_centrality( + graph: PyGraph[_S, _T] | PyDiGraph[_S, _T], + group: list[int], +) -> float: ... +def group_betweenness_centrality( + graph: PyGraph[_S, _T] | PyDiGraph[_S, _T], + group: list[int], + normalized: bool = ..., +) -> float: ... def edge_betweenness_centrality( graph: PyGraph[_S, _T] | PyDiGraph[_S, _T], normalized: bool = ..., diff --git a/rustworkx/rustworkx.pyi b/rustworkx/rustworkx.pyi index c0c3d1c941..c920649ef0 100644 --- a/rustworkx/rustworkx.pyi +++ b/rustworkx/rustworkx.pyi @@ -189,6 +189,38 @@ def graph_degree_centrality( graph: PyGraph[_S, _T], /, ) -> CentralityMapping: ... +def graph_group_degree_centrality( + graph: PyGraph[_S, _T], + group: list[int], + /, +) -> float: ... +def digraph_group_degree_centrality( + graph: PyDiGraph[_S, _T], + group: list[int], + /, +) -> float: ... +def graph_group_closeness_centrality( + graph: PyGraph[_S, _T], + group: list[int], + /, +) -> float: ... +def digraph_group_closeness_centrality( + graph: PyDiGraph[_S, _T], + group: list[int], + /, +) -> float: ... +def graph_group_betweenness_centrality( + graph: PyGraph[_S, _T], + group: list[int], + /, + normalized: bool = ..., +) -> float: ... +def digraph_group_betweenness_centrality( + graph: PyDiGraph[_S, _T], + group: list[int], + /, + normalized: bool = ..., +) -> float: ... def digraph_katz_centrality( graph: PyDiGraph[_S, _T], /, diff --git a/src/centrality.rs b/src/centrality.rs index 5433a28e4e..ab3f6a69d4 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -1098,3 +1098,274 @@ pub fn digraph_katz_centrality( ))), } } + +/// Compute the group degree centrality of a set of nodes in a +/// :class:`~rustworkx.PyGraph`. +/// +/// Group degree centrality measures the fraction of non-group nodes that are +/// connected to at least one member of the group. It is defined as: +/// +/// .. math:: +/// +/// C_D(S) = \frac{|N(S) \setminus S|}{|V| - |S|} +/// +/// where :math:`N(S)` is the union of neighborhoods of all nodes in :math:`S`. +/// +/// Based on: Everett, M. G., & Borgatti, S. P. (1999). +/// The centrality of groups and classes. +/// Journal of Mathematical Sociology, 23(3), 181-201. +/// +/// :param PyGraph graph: The input graph +/// :param list group: A list of node indices representing the group +/// +/// :returns: The group degree centrality as a float +/// :rtype: float +/// +/// :raises PyValueError: If any node index in the group is not in the graph +#[pyfunction(signature = (graph, group))] +#[pyo3(text_signature = "(graph, group, /)")] +pub fn graph_group_degree_centrality( + graph: &graph::PyGraph, + group: Vec, +) -> PyResult { + for &idx in &group { + if !graph.graph.contains_node(NodeIndex::new(idx)) { + return Err(PyValueError::new_err(format!( + "Node index {idx} is not in the graph" + ))); + } + } + Ok(centrality::group_degree_centrality( + &graph.graph, + &group, + None, + )) +} + +/// Compute the group degree centrality of a set of nodes in a +/// :class:`~rustworkx.PyDiGraph`. +/// +/// Group degree centrality measures the fraction of non-group nodes that are +/// connected to at least one member of the group. For directed graphs, this +/// uses outgoing edges by default. +/// +/// .. math:: +/// +/// C_D(S) = \frac{|N(S) \setminus S|}{|V| - |S|} +/// +/// where :math:`N(S)` is the union of neighborhoods of all nodes in :math:`S`. +/// +/// Based on: Everett, M. G., & Borgatti, S. P. (1999). +/// The centrality of groups and classes. +/// Journal of Mathematical Sociology, 23(3), 181-201. +/// +/// :param PyDiGraph graph: The input graph +/// :param list group: A list of node indices representing the group +/// +/// :returns: The group degree centrality as a float +/// :rtype: float +/// +/// :raises PyValueError: If any node index in the group is not in the graph +#[pyfunction(signature = (graph, group))] +#[pyo3(text_signature = "(graph, group, /)")] +pub fn digraph_group_degree_centrality( + graph: &digraph::PyDiGraph, + group: Vec, +) -> PyResult { + for &idx in &group { + if !graph.graph.contains_node(NodeIndex::new(idx)) { + return Err(PyValueError::new_err(format!( + "Node index {idx} is not in the graph" + ))); + } + } + Ok(centrality::group_degree_centrality( + &graph.graph, + &group, + None, + )) +} + +/// Compute the group closeness centrality of a set of nodes in a +/// :class:`~rustworkx.PyGraph`. +/// +/// Group closeness centrality measures how close a group of nodes is to all +/// non-group nodes. It is defined as: +/// +/// .. math:: +/// +/// C_{close}(S) = \frac{|V \setminus S|}{\sum_{v \in V \setminus S} d(S, v)} +/// +/// where :math:`d(S, v) = \min_{u \in S} d(u, v)` is the minimum distance +/// from any group member to node :math:`v`. +/// +/// Based on: Everett, M. G., & Borgatti, S. P. (1999). +/// The centrality of groups and classes. +/// Journal of Mathematical Sociology, 23(3), 181-201. +/// +/// :param PyGraph graph: The input graph +/// :param list group: A list of node indices representing the group +/// +/// :returns: The group closeness centrality as a float +/// :rtype: float +/// +/// :raises PyValueError: If any node index in the group is not in the graph +#[pyfunction(signature = (graph, group))] +#[pyo3(text_signature = "(graph, group, /)")] +pub fn graph_group_closeness_centrality( + graph: &graph::PyGraph, + group: Vec, +) -> PyResult { + for &idx in &group { + if !graph.graph.contains_node(NodeIndex::new(idx)) { + return Err(PyValueError::new_err(format!( + "Node index {idx} is not in the graph" + ))); + } + } + Ok(centrality::group_closeness_centrality( + &graph.graph, + &group, + )) +} + +/// Compute the group closeness centrality of a set of nodes in a +/// :class:`~rustworkx.PyDiGraph`. +/// +/// Group closeness centrality measures how close a group of nodes is to all +/// non-group nodes. It is defined as: +/// +/// .. math:: +/// +/// C_{close}(S) = \frac{|V \setminus S|}{\sum_{v \in V \setminus S} d(S, v)} +/// +/// where :math:`d(S, v) = \min_{u \in S} d(u, v)` is the minimum distance +/// from any group member to node :math:`v`. +/// +/// Based on: Everett, M. G., & Borgatti, S. P. (1999). +/// The centrality of groups and classes. +/// Journal of Mathematical Sociology, 23(3), 181-201. +/// +/// :param PyDiGraph graph: The input graph +/// :param list group: A list of node indices representing the group +/// +/// :returns: The group closeness centrality as a float +/// :rtype: float +/// +/// :raises PyValueError: If any node index in the group is not in the graph +#[pyfunction(signature = (graph, group))] +#[pyo3(text_signature = "(graph, group, /)")] +pub fn digraph_group_closeness_centrality( + graph: &digraph::PyDiGraph, + group: Vec, +) -> PyResult { + for &idx in &group { + if !graph.graph.contains_node(NodeIndex::new(idx)) { + return Err(PyValueError::new_err(format!( + "Node index {idx} is not in the graph" + ))); + } + } + Ok(centrality::group_closeness_centrality( + &graph.graph, + &group, + )) +} + +/// Compute the group betweenness centrality of a set of nodes in a +/// :class:`~rustworkx.PyGraph`. +/// +/// Group betweenness centrality measures the fraction of shortest paths +/// between non-group node pairs that pass through at least one group member. +/// It is defined as: +/// +/// .. math:: +/// +/// C_B(S) = \sum_{s,t \in V \setminus S} \frac{\sigma(s, t|S)}{\sigma(s, t)} +/// +/// where :math:`\sigma(s,t)` is the number of shortest paths from :math:`s` +/// to :math:`t`, and :math:`\sigma(s,t|S)` is the number of those paths +/// passing through at least one node in :math:`S`. +/// +/// Based on: Everett, M. G., & Borgatti, S. P. (1999). +/// The centrality of groups and classes. +/// Journal of Mathematical Sociology, 23(3), 181-201. +/// +/// :param PyGraph graph: The input graph +/// :param list group: A list of node indices representing the group +/// :param bool normalized: Whether to normalize the result. If True, +/// the result is divided by the number of non-group node pairs. +/// +/// :returns: The group betweenness centrality as a float +/// :rtype: float +/// +/// :raises PyValueError: If any node index in the group is not in the graph +#[pyfunction(signature = (graph, group, normalized=true))] +#[pyo3(text_signature = "(graph, group, /, normalized=True)")] +pub fn graph_group_betweenness_centrality( + graph: &graph::PyGraph, + group: Vec, + normalized: bool, +) -> PyResult { + for &idx in &group { + if !graph.graph.contains_node(NodeIndex::new(idx)) { + return Err(PyValueError::new_err(format!( + "Node index {idx} is not in the graph" + ))); + } + } + Ok(centrality::group_betweenness_centrality( + &graph.graph, + &group, + normalized, + )) +} + +/// Compute the group betweenness centrality of a set of nodes in a +/// :class:`~rustworkx.PyDiGraph`. +/// +/// Group betweenness centrality measures the fraction of shortest paths +/// between non-group node pairs that pass through at least one group member. +/// It is defined as: +/// +/// .. math:: +/// +/// C_B(S) = \sum_{s,t \in V \setminus S} \frac{\sigma(s, t|S)}{\sigma(s, t)} +/// +/// where :math:`\sigma(s,t)` is the number of shortest paths from :math:`s` +/// to :math:`t`, and :math:`\sigma(s,t|S)` is the number of those paths +/// passing through at least one node in :math:`S`. +/// +/// Based on: Everett, M. G., & Borgatti, S. P. (1999). +/// The centrality of groups and classes. +/// Journal of Mathematical Sociology, 23(3), 181-201. +/// +/// :param PyDiGraph graph: The input graph +/// :param list group: A list of node indices representing the group +/// :param bool normalized: Whether to normalize the result. If True, +/// the result is divided by the number of non-group node pairs. +/// +/// :returns: The group betweenness centrality as a float +/// :rtype: float +/// +/// :raises PyValueError: If any node index in the group is not in the graph +#[pyfunction(signature = (graph, group, normalized=true))] +#[pyo3(text_signature = "(graph, group, /, normalized=True)")] +pub fn digraph_group_betweenness_centrality( + graph: &digraph::PyDiGraph, + group: Vec, + normalized: bool, +) -> PyResult { + for &idx in &group { + if !graph.graph.contains_node(NodeIndex::new(idx)) { + return Err(PyValueError::new_err(format!( + "Node index {idx} is not in the graph" + ))); + } + } + Ok(centrality::group_betweenness_centrality( + &graph.graph, + &group, + normalized, + )) +} diff --git a/src/lib.rs b/src/lib.rs index 06fd5ce674..a7482d39c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -593,6 +593,12 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(digraph_degree_centrality))?; m.add_wrapped(wrap_pyfunction!(in_degree_centrality))?; m.add_wrapped(wrap_pyfunction!(out_degree_centrality))?; + m.add_wrapped(wrap_pyfunction!(graph_group_degree_centrality))?; + m.add_wrapped(wrap_pyfunction!(digraph_group_degree_centrality))?; + m.add_wrapped(wrap_pyfunction!(graph_group_closeness_centrality))?; + m.add_wrapped(wrap_pyfunction!(digraph_group_closeness_centrality))?; + m.add_wrapped(wrap_pyfunction!(graph_group_betweenness_centrality))?; + m.add_wrapped(wrap_pyfunction!(digraph_group_betweenness_centrality))?; m.add_wrapped(wrap_pyfunction!(graph_astar_shortest_path))?; m.add_wrapped(wrap_pyfunction!(digraph_astar_shortest_path))?; m.add_wrapped(wrap_pyfunction!(graph_greedy_color))?; diff --git a/tests/digraph/test_centrality.py b/tests/digraph/test_centrality.py index 4467bc8071..e56bb91b5a 100644 --- a/tests/digraph/test_centrality.py +++ b/tests/digraph/test_centrality.py @@ -352,3 +352,168 @@ def test_out_degree_centrality_directed_path(self): } for k, v in centrality.items(): self.assertAlmostEqual(v, expected[k]) + + +class TestGroupDegreeCentralityDiGraph(unittest.TestCase): + def test_directed_path(self): + # 0->1->2->3: out-neighbors of {0,1} outside group = {2}. = 1/2 + graph = rustworkx.generators.directed_path_graph(4) + result = rustworkx.digraph_group_degree_centrality(graph, [0, 1]) + self.assertAlmostEqual(result, 0.5) + + def test_dispatch(self): + graph = rustworkx.generators.directed_path_graph(4) + result = rustworkx.group_degree_centrality(graph, [0, 1]) + self.assertAlmostEqual(result, 0.5) + + def test_invalid_node(self): + graph = rustworkx.generators.directed_path_graph(3) + with self.assertRaises(ValueError): + rustworkx.digraph_group_degree_centrality(graph, [10]) + + +class TestGroupClosenessCentralityDiGraph(unittest.TestCase): + def test_directed_path(self): + # 0->1->2->3: uses reversed edges (incoming closeness). + # group={3}: reversed BFS from 3 follows 3<-2<-1<-0. + # Distances to non-group: {2:1, 1:2, 0:3}. Sum=6, |V-S|=3. + # Closeness = 3/6 = 0.5 + graph = rustworkx.generators.directed_path_graph(4) + result = rustworkx.digraph_group_closeness_centrality(graph, [3]) + self.assertAlmostEqual(result, 0.5) + + def test_dispatch(self): + graph = rustworkx.generators.directed_path_graph(4) + result = rustworkx.group_closeness_centrality(graph, [3]) + self.assertAlmostEqual(result, 0.5) + + def test_invalid_node(self): + graph = rustworkx.generators.directed_path_graph(3) + with self.assertRaises(ValueError): + rustworkx.digraph_group_closeness_centrality(graph, [10]) + + +class TestGroupBetweennessCentralityDiGraph(unittest.TestCase): + def test_directed_path(self): + graph = rustworkx.generators.directed_path_graph(5) + result = rustworkx.digraph_group_betweenness_centrality( + graph, [2], normalized=False + ) + self.assertAlmostEqual(result, 4.0) + + def test_directed_path_normalized(self): + graph = rustworkx.generators.directed_path_graph(5) + result = rustworkx.digraph_group_betweenness_centrality( + graph, [2], normalized=True + ) + self.assertAlmostEqual(result, 4.0 / 12.0) + + def test_empty_group(self): + graph = rustworkx.generators.directed_path_graph(3) + result = rustworkx.digraph_group_betweenness_centrality( + graph, [], normalized=False + ) + self.assertAlmostEqual(result, 0.0) + + def test_dispatch(self): + graph = rustworkx.generators.directed_path_graph(5) + result = rustworkx.group_betweenness_centrality(graph, [2]) + self.assertAlmostEqual(result, 4.0 / 12.0) + + def test_invalid_node(self): + graph = rustworkx.generators.directed_path_graph(3) + with self.assertRaises(ValueError): + rustworkx.digraph_group_betweenness_centrality(graph, [10]) + + +class TestGroupCentralityNetworkXComparisonDiGraph(unittest.TestCase): + """Cross-validate group centrality results against NetworkX.""" + + def _build_graphs(self, nx_graph): + rx_graph = rustworkx.PyDiGraph() + node_map = {} + for node in nx_graph.nodes(): + node_map[node] = rx_graph.add_node(node) + for u, v in nx_graph.edges(): + rx_graph.add_edge(node_map[u], node_map[v], None) + return rx_graph, node_map + + def test_degree_directed_path(self): + g_nx = nx.path_graph(6, create_using=nx.DiGraph) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {2}, {0, 1}, {1, 3}, {0, 2, 4}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_degree_centrality(g_nx, group_nodes) + result = rustworkx.digraph_group_degree_centrality(g_rx, rx_group) + self.assertAlmostEqual(result, expected, places=10) + + def test_degree_directed_cycle(self): + g_nx = nx.cycle_graph(6, create_using=nx.DiGraph) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {0, 3}, {0, 2, 4}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_degree_centrality(g_nx, group_nodes) + result = rustworkx.digraph_group_degree_centrality(g_rx, rx_group) + self.assertAlmostEqual(result, expected, places=10) + + def test_closeness_directed_path(self): + g_nx = nx.path_graph(6, create_using=nx.DiGraph) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {2}, {0, 1}, {1, 3}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_closeness_centrality(g_nx, group_nodes) + result = rustworkx.digraph_group_closeness_centrality(g_rx, rx_group) + self.assertAlmostEqual(result, expected, places=10) + + def test_closeness_directed_cycle(self): + g_nx = nx.cycle_graph(6, create_using=nx.DiGraph) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {0, 3}, {0, 2, 4}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_closeness_centrality(g_nx, group_nodes) + result = rustworkx.digraph_group_closeness_centrality(g_rx, rx_group) + self.assertAlmostEqual(result, expected, places=10) + + def test_betweenness_bidirectional_path(self): + g_nx = nx.path_graph(6).to_directed() + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{2}, {1, 4}, {0, 5}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + result = rustworkx.digraph_group_betweenness_centrality( + g_rx, rx_group, normalized=True + ) + self.assertAlmostEqual(result, expected, places=10) + + def test_betweenness_directed_star(self): + g_nx = nx.star_graph(4).to_directed() + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {1, 2}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + result = rustworkx.digraph_group_betweenness_centrality( + g_rx, rx_group, normalized=True + ) + self.assertAlmostEqual(result, expected, places=10) + + def test_betweenness_directed_cycle(self): + g_nx = nx.cycle_graph(6, create_using=nx.DiGraph) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {0, 3}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + result = rustworkx.digraph_group_betweenness_centrality( + g_rx, rx_group, normalized=True + ) + self.assertAlmostEqual(result, expected, places=10) + + def test_betweenness_complete_digraph(self): + g_nx = nx.complete_graph(5, create_using=nx.DiGraph) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {0, 1}, {0, 2, 4}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + result = rustworkx.digraph_group_betweenness_centrality( + g_rx, rx_group, normalized=True + ) + self.assertAlmostEqual(result, expected, places=10) diff --git a/tests/graph/test_centrality.py b/tests/graph/test_centrality.py index ddda74ebe9..e741369c11 100644 --- a/tests/graph/test_centrality.py +++ b/tests/graph/test_centrality.py @@ -309,3 +309,205 @@ def test_degree_centrality_multigraph(self): 2: 0.5, # Node C has 1 edge } self.assertEqual(expected, dict(centrality)) + + +class TestGroupDegreeCentralityGraph(unittest.TestCase): + def test_path_graph(self): + graph = rustworkx.generators.path_graph(5) + result = rustworkx.graph_group_degree_centrality(graph, [0, 1]) + self.assertAlmostEqual(result, 1.0 / 3.0) + + def test_complete_graph(self): + graph = rustworkx.generators.complete_graph(4) + result = rustworkx.graph_group_degree_centrality(graph, [0]) + self.assertAlmostEqual(result, 1.0) + + def test_single_node_group(self): + graph = rustworkx.generators.path_graph(3) + result = rustworkx.graph_group_degree_centrality(graph, [1]) + self.assertAlmostEqual(result, 1.0) + + def test_dispatch(self): + graph = rustworkx.generators.path_graph(5) + result = rustworkx.group_degree_centrality(graph, [0, 1]) + self.assertAlmostEqual(result, 1.0 / 3.0) + + def test_invalid_node(self): + graph = rustworkx.generators.path_graph(3) + with self.assertRaises(ValueError): + rustworkx.graph_group_degree_centrality(graph, [10]) + + +class TestGroupClosenessCentralityGraph(unittest.TestCase): + def test_path_graph(self): + graph = rustworkx.generators.path_graph(5) + result = rustworkx.graph_group_closeness_centrality(graph, [0, 1]) + self.assertAlmostEqual(result, 0.5) + + def test_star_center(self): + graph = rustworkx.PyGraph() + center = graph.add_node("center") + for _ in range(4): + leaf = graph.add_node("leaf") + graph.add_edge(center, leaf, None) + result = rustworkx.graph_group_closeness_centrality(graph, [center]) + self.assertAlmostEqual(result, 1.0) + + def test_dispatch(self): + graph = rustworkx.generators.path_graph(5) + result = rustworkx.group_closeness_centrality(graph, [0, 1]) + self.assertAlmostEqual(result, 0.5) + + def test_invalid_node(self): + graph = rustworkx.generators.path_graph(3) + with self.assertRaises(ValueError): + rustworkx.graph_group_closeness_centrality(graph, [10]) + + +class TestGroupBetweennessCentralityGraph(unittest.TestCase): + def test_path_center(self): + graph = rustworkx.generators.path_graph(5) + result = rustworkx.graph_group_betweenness_centrality(graph, [2], normalized=False) + self.assertAlmostEqual(result, 4.0) + + def test_path_center_normalized(self): + graph = rustworkx.generators.path_graph(5) + result = rustworkx.graph_group_betweenness_centrality(graph, [2], normalized=True) + self.assertAlmostEqual(result, 2.0 / 3.0) + + def test_star_center(self): + graph = rustworkx.PyGraph() + center = graph.add_node("center") + for _ in range(4): + leaf = graph.add_node("leaf") + graph.add_edge(center, leaf, None) + result = rustworkx.graph_group_betweenness_centrality( + graph, [center], normalized=False + ) + self.assertAlmostEqual(result, 6.0) + + def test_empty_group(self): + graph = rustworkx.generators.path_graph(3) + result = rustworkx.graph_group_betweenness_centrality(graph, [], normalized=False) + self.assertAlmostEqual(result, 0.0) + + def test_dispatch(self): + graph = rustworkx.generators.path_graph(5) + result = rustworkx.group_betweenness_centrality(graph, [2]) + self.assertAlmostEqual(result, 2.0 / 3.0) + + def test_invalid_node(self): + graph = rustworkx.generators.path_graph(3) + with self.assertRaises(ValueError): + rustworkx.graph_group_betweenness_centrality(graph, [10]) + + +class TestGroupCentralityNetworkXComparisonGraph(unittest.TestCase): + """Cross-validate group centrality results against NetworkX.""" + + def _build_graphs(self, nx_graph): + rx_graph = rustworkx.PyGraph() + node_map = {} + for node in nx_graph.nodes(): + node_map[node] = rx_graph.add_node(node) + for u, v in nx_graph.edges(): + rx_graph.add_edge(node_map[u], node_map[v], None) + return rx_graph, node_map + + def test_degree_path_graph(self): + g_nx = nx.path_graph(5) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {2}, {0, 1}, {1, 3}, {0, 2, 4}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_degree_centrality(g_nx, group_nodes) + result = rustworkx.graph_group_degree_centrality(g_rx, rx_group) + self.assertAlmostEqual(result, expected, places=10) + + def test_degree_complete_graph(self): + g_nx = nx.complete_graph(6) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {0, 1}, {0, 2, 4}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_degree_centrality(g_nx, group_nodes) + result = rustworkx.graph_group_degree_centrality(g_rx, rx_group) + self.assertAlmostEqual(result, expected, places=10) + + def test_degree_cycle_graph(self): + g_nx = nx.cycle_graph(8) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {0, 4}, {0, 2, 4, 6}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_degree_centrality(g_nx, group_nodes) + result = rustworkx.graph_group_degree_centrality(g_rx, rx_group) + self.assertAlmostEqual(result, expected, places=10) + + def test_closeness_path_graph(self): + g_nx = nx.path_graph(5) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {2}, {0, 1}, {1, 3}, {0, 2, 4}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_closeness_centrality(g_nx, group_nodes) + result = rustworkx.graph_group_closeness_centrality(g_rx, rx_group) + self.assertAlmostEqual(result, expected, places=10) + + def test_closeness_complete_graph(self): + g_nx = nx.complete_graph(6) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {0, 1}, {0, 2, 4}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_closeness_centrality(g_nx, group_nodes) + result = rustworkx.graph_group_closeness_centrality(g_rx, rx_group) + self.assertAlmostEqual(result, expected, places=10) + + def test_closeness_cycle_graph(self): + g_nx = nx.cycle_graph(8) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {0, 4}, {0, 2, 4, 6}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_closeness_centrality(g_nx, group_nodes) + result = rustworkx.graph_group_closeness_centrality(g_rx, rx_group) + self.assertAlmostEqual(result, expected, places=10) + + def test_betweenness_path_graph(self): + g_nx = nx.path_graph(5) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{2}, {1, 3}, {0, 4}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + result = rustworkx.graph_group_betweenness_centrality( + g_rx, rx_group, normalized=True + ) + self.assertAlmostEqual(result, expected, places=10) + + def test_betweenness_complete_graph(self): + g_nx = nx.complete_graph(6) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {0, 1}, {0, 2, 4}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + result = rustworkx.graph_group_betweenness_centrality( + g_rx, rx_group, normalized=True + ) + self.assertAlmostEqual(result, expected, places=10) + + def test_betweenness_star_graph(self): + g_nx = nx.star_graph(4) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{0}, {1}, {0, 1}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + result = rustworkx.graph_group_betweenness_centrality( + g_rx, rx_group, normalized=True + ) + self.assertAlmostEqual(result, expected, places=10) + + def test_betweenness_barbell_graph(self): + g_nx = nx.barbell_graph(4, 1) + g_rx, nmap = self._build_graphs(g_nx) + for group_nodes in [{4}, {3, 4, 5}, {0, 8}]: + rx_group = [nmap[n] for n in group_nodes] + expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + result = rustworkx.graph_group_betweenness_centrality( + g_rx, rx_group, normalized=True + ) + self.assertAlmostEqual(result, expected, places=10) From 79b2e2af8e72b86af0b08968fb06842d994d5dd5 Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Thu, 12 Mar 2026 02:23:36 +0100 Subject: [PATCH 02/11] remove redundant traits --- rustworkx-core/src/centrality.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 961d32376c..b7f6bf3c63 100755 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -355,7 +355,6 @@ pub fn degree_centrality(graph: G, direction: Option) -> where G: NodeIndexable + IntoNodeIdentifiers - + IntoNeighbors + IntoNeighborsDirected + NodeCount + GraphProp, @@ -1785,7 +1784,6 @@ pub fn group_degree_centrality( where G: NodeIndexable + IntoNodeIdentifiers - + IntoNeighbors + IntoNeighborsDirected + NodeCount + GraphProp, From e6b8dcbc3b904e955f3931d9ac97cdc1a153c495 Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Thu, 12 Mar 2026 02:41:57 +0100 Subject: [PATCH 03/11] remove redundant traits --- rustworkx-core/src/centrality.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index b7f6bf3c63..42b24e53a0 100755 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -1855,10 +1855,9 @@ where + GraphBase + IntoEdges + IntoEdgesDirected - + Visitable + NodeCount, G::NodeId: Eq + Hash, - G::EdgeId: Eq + Hash, + G::EdgeId: Eq, { let node_count = graph.node_count(); let group_size = group.len(); From 3310b1ee8a434dcc4693cc047250533cf98bb3c1 Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Thu, 12 Mar 2026 02:54:14 +0100 Subject: [PATCH 04/11] no dyn --- rustworkx-core/src/centrality.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 42b24e53a0..66d7946061 100755 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -1800,15 +1800,23 @@ where for &node_idx in group { let node_id = graph.from_index(node_idx); - let neighbors: Box> = match direction { - Some(dir) => Box::new(graph.neighbors_directed(node_id, dir)), - None => Box::new(graph.neighbors(node_id)), - }; - for neighbor in neighbors { + let mut process_neighbor = |neighbor: G::NodeId| { let neighbor_idx = graph.to_index(neighbor); if !group_set.contains(&neighbor_idx) { reached.insert(neighbor_idx); } + }; + match direction { + Some(dir) => { + for neighbor in graph.neighbors_directed(node_id, dir) { + process_neighbor(neighbor); + } + } + None => { + for neighbor in graph.neighbors(node_id) { + process_neighbor(neighbor); + } + } } } From f70d003273fcdee11833af4e86e142f776e5ede9 Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Thu, 12 Mar 2026 03:09:07 +0100 Subject: [PATCH 05/11] add tests --- rustworkx-core/src/centrality.rs | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 66d7946061..1d82696867 100755 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -1833,6 +1833,10 @@ where /// where d(S, v) = min_{u in S} d(u, v) is the minimum distance from any /// group member to node v. /// +/// Note: For disconnected graphs, unreachable nodes do not contribute to +/// the distance sum but are still counted in |V \ S|. This can produce +/// values greater than 1.0, matching the convention used by NetworkX. +/// /// Based on: Everett, M. G., & Borgatti, S. P. (1999). /// The centrality of groups and classes. /// Journal of Mathematical Sociology, 23(3), 181-201. @@ -2269,4 +2273,64 @@ mod test_group_betweenness_centrality { // Node 0 is on all 6 shortest paths between leaf pairs assert_almost_equal!(result, 6.0, 1e-10); } + + #[test] + fn test_directed_path() { + // Directed path: 0->1->2->3->4 + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + ]); + // Group = {2}. Directed shortest paths through node 2: + // (0,3), (0,4), (1,3), (1,4) = 4 pairs + let result = group_betweenness_centrality(&g, &[2], false); + assert_almost_equal!(result, 4.0, 1e-10); + } + + #[test] + fn test_directed_path_normalized() { + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + ]); + let result = group_betweenness_centrality(&g, &[2], true); + // Non-group = 4 nodes. Directed normalization = 4 * 3 = 12. + // Normalized = 4 / 12 = 1/3 + assert_almost_equal!(result, 1.0 / 3.0, 1e-10); + } + + #[test] + fn test_directed_star() { + // Directed star: 0->1, 0->2, 0->3, 0->4 + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + ]); + // Group = {0}. No directed shortest paths between leaf pairs + // pass through 0 (leaves are unreachable from each other). + let result = group_betweenness_centrality(&g, &[0], false); + assert_almost_equal!(result, 0.0, 1e-10); + } + + #[test] + fn test_directed_bidirectional_star() { + // Bidirectional star: edges in both directions between center and leaves + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ + (0, 1), (1, 0), + (0, 2), (2, 0), + (0, 3), (3, 0), + (0, 4), (4, 0), + ]); + // Group = {0}. All 12 directed shortest paths between leaf pairs + // go through node 0 (e.g. 1->0->2, 2->0->1, etc.). + // 4 leaves, 4*3 = 12 ordered pairs. + let result = group_betweenness_centrality(&g, &[0], false); + assert_almost_equal!(result, 12.0, 1e-10); + } } From 8bad7c80a40a51d4ed646c1a11552a9b94767738 Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Thu, 12 Mar 2026 03:11:05 +0100 Subject: [PATCH 06/11] fmt --- rustworkx-core/src/centrality.rs | 118 +++++++------------------------ src/centrality.rs | 15 +--- 2 files changed, 28 insertions(+), 105 deletions(-) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 1d82696867..2e874c2fa8 100755 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -353,11 +353,7 @@ fn accumulate_edges( /// ``` pub fn degree_centrality(graph: G, direction: Option) -> Vec where - G: NodeIndexable - + IntoNodeIdentifiers - + IntoNeighborsDirected - + NodeCount - + GraphProp, + G: NodeIndexable + IntoNodeIdentifiers + IntoNeighborsDirected + NodeCount + GraphProp, G::NodeId: Eq, { let node_count = graph.node_count() as f64; @@ -1782,11 +1778,7 @@ pub fn group_degree_centrality( direction: Option, ) -> f64 where - G: NodeIndexable - + IntoNodeIdentifiers - + IntoNeighborsDirected - + NodeCount - + GraphProp, + G: NodeIndexable + IntoNodeIdentifiers + IntoNeighborsDirected + NodeCount + GraphProp, G::NodeId: Eq + Hash, { let node_count = graph.node_count(); @@ -1862,12 +1854,7 @@ where /// ``` pub fn group_closeness_centrality(graph: G, group: &[usize]) -> f64 where - G: NodeIndexable - + IntoNodeIdentifiers - + GraphBase - + IntoEdges - + IntoEdgesDirected - + NodeCount, + G: NodeIndexable + IntoNodeIdentifiers + GraphBase + IntoEdges + IntoEdgesDirected + NodeCount, G::NodeId: Eq + Hash, G::EdgeId: Eq, { @@ -1958,11 +1945,7 @@ where /// // Node 2 is on every shortest path between {0,1} and {3,4}. /// assert!(output > 0.0); /// ``` -pub fn group_betweenness_centrality( - graph: G, - group: &[usize], - normalized: bool, -) -> f64 +pub fn group_betweenness_centrality(graph: G, group: &[usize], normalized: bool) -> f64 where G: NodeIndexable + IntoNodeIdentifiers @@ -2098,12 +2081,7 @@ mod test_group_degree_centrality { #[test] fn test_undirected_path() { - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - ]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); let result = group_degree_centrality(&g, &[0, 1], None); // Neighbors of {0,1} outside group = {2}. Centrality = 1/3. assert!((result - 1.0 / 3.0).abs() < 1e-10); @@ -2125,26 +2103,16 @@ mod test_group_degree_centrality { #[test] fn test_directed_out() { - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ - (0, 1), - (1, 2), - (2, 3), - ]); - let result = - group_degree_centrality(&g, &[0, 1], Some(petgraph::Direction::Outgoing)); + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3)]); + let result = group_degree_centrality(&g, &[0, 1], Some(petgraph::Direction::Outgoing)); // Out-neighbors of {0,1} outside group = {2}. Centrality = 1/2. assert!((result - 0.5).abs() < 1e-10); } #[test] fn test_directed_in() { - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ - (0, 1), - (1, 2), - (2, 3), - ]); - let result = - group_degree_centrality(&g, &[2, 3], Some(petgraph::Direction::Incoming)); + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3)]); + let result = group_degree_centrality(&g, &[2, 3], Some(petgraph::Direction::Incoming)); // In-neighbors of {2,3} outside group = {1}. Centrality = 1/2. assert!((result - 0.5).abs() < 1e-10); } @@ -2165,12 +2133,7 @@ mod test_group_closeness_centrality { #[test] fn test_undirected_path() { - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - ]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); let result = group_closeness_centrality(&g, &[0, 1]); // Non-group = {2,3,4}. d(S,2)=1, d(S,3)=2, d(S,4)=3. Sum=6. // Closeness = 3/6 = 0.5 @@ -2179,12 +2142,7 @@ mod test_group_closeness_centrality { #[test] fn test_undirected_center_node() { - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ - (0, 2), - (1, 2), - (2, 3), - (2, 4), - ]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 2), (1, 2), (2, 3), (2, 4)]); let result = group_closeness_centrality(&g, &[2]); // Non-group = {0,1,3,4}. All at distance 1. Sum=4. // Closeness = 4/4 = 1.0 @@ -2227,12 +2185,7 @@ mod test_group_betweenness_centrality { #[test] fn test_undirected_path_center() { // Path: 0-1-2-3-4 - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - ]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); // Group = {2}. Node 2 is on all shortest paths between {0,1} and {3,4}. let result = group_betweenness_centrality(&g, &[2], false); // Pairs through node 2: (0,3), (0,4), (1,3), (1,4) = 4 paths @@ -2241,12 +2194,7 @@ mod test_group_betweenness_centrality { #[test] fn test_undirected_path_center_normalized() { - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - ]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); let result = group_betweenness_centrality(&g, &[2], true); // Non-group size = 4. Normalization = C(4,2) = 6. // Normalized = 4/6 = 2/3 @@ -2263,12 +2211,7 @@ mod test_group_betweenness_centrality { #[test] fn test_single_node_group() { // Star graph: center=0, leaves=1,2,3,4 - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ - (0, 1), - (0, 2), - (0, 3), - (0, 4), - ]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)]); let result = group_betweenness_centrality(&g, &[0], false); // Node 0 is on all 6 shortest paths between leaf pairs assert_almost_equal!(result, 6.0, 1e-10); @@ -2277,12 +2220,7 @@ mod test_group_betweenness_centrality { #[test] fn test_directed_path() { // Directed path: 0->1->2->3->4 - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - ]); + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); // Group = {2}. Directed shortest paths through node 2: // (0,3), (0,4), (1,3), (1,4) = 4 pairs let result = group_betweenness_centrality(&g, &[2], false); @@ -2291,12 +2229,7 @@ mod test_group_betweenness_centrality { #[test] fn test_directed_path_normalized() { - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - ]); + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); let result = group_betweenness_centrality(&g, &[2], true); // Non-group = 4 nodes. Directed normalization = 4 * 3 = 12. // Normalized = 4 / 12 = 1/3 @@ -2306,12 +2239,7 @@ mod test_group_betweenness_centrality { #[test] fn test_directed_star() { // Directed star: 0->1, 0->2, 0->3, 0->4 - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ - (0, 1), - (0, 2), - (0, 3), - (0, 4), - ]); + let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)]); // Group = {0}. No directed shortest paths between leaf pairs // pass through 0 (leaves are unreachable from each other). let result = group_betweenness_centrality(&g, &[0], false); @@ -2322,10 +2250,14 @@ mod test_group_betweenness_centrality { fn test_directed_bidirectional_star() { // Bidirectional star: edges in both directions between center and leaves let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ - (0, 1), (1, 0), - (0, 2), (2, 0), - (0, 3), (3, 0), - (0, 4), (4, 0), + (0, 1), + (1, 0), + (0, 2), + (2, 0), + (0, 3), + (3, 0), + (0, 4), + (4, 0), ]); // Group = {0}. All 12 directed shortest paths between leaf pairs // go through node 0 (e.g. 1->0->2, 2->0->1, etc.). diff --git a/src/centrality.rs b/src/centrality.rs index ab3f6a69d4..4cd8df546c 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -1124,10 +1124,7 @@ pub fn digraph_katz_centrality( /// :raises PyValueError: If any node index in the group is not in the graph #[pyfunction(signature = (graph, group))] #[pyo3(text_signature = "(graph, group, /)")] -pub fn graph_group_degree_centrality( - graph: &graph::PyGraph, - group: Vec, -) -> PyResult { +pub fn graph_group_degree_centrality(graph: &graph::PyGraph, group: Vec) -> PyResult { for &idx in &group { if !graph.graph.contains_node(NodeIndex::new(idx)) { return Err(PyValueError::new_err(format!( @@ -1223,10 +1220,7 @@ pub fn graph_group_closeness_centrality( ))); } } - Ok(centrality::group_closeness_centrality( - &graph.graph, - &group, - )) + Ok(centrality::group_closeness_centrality(&graph.graph, &group)) } /// Compute the group closeness centrality of a set of nodes in a @@ -1266,10 +1260,7 @@ pub fn digraph_group_closeness_centrality( ))); } } - Ok(centrality::group_closeness_centrality( - &graph.graph, - &group, - )) + Ok(centrality::group_closeness_centrality(&graph.graph, &group)) } /// Compute the group betweenness centrality of a set of nodes in a From fa6306dedc4f4779fd359060ea9487245634aa33 Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Mon, 16 Mar 2026 20:46:31 +0300 Subject: [PATCH 07/11] dont pass refrence --- rustworkx-core/src/centrality.rs | 54 ++++++++++++++++---------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 2e874c2fa8..054fa71c6a 100755 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -55,7 +55,7 @@ use rayon_cond::CondIterator; /// use rustworkx_core::petgraph; /// use rustworkx_core::centrality::betweenness_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// let g = petgraph::graph::UnGraph::::from_edges([ /// (0, 4), (1, 2), (2, 3), (3, 4), (1, 4) /// ]); /// // Calculate the betweenness centrality @@ -161,7 +161,7 @@ where /// use rustworkx_core::petgraph; /// use rustworkx_core::centrality::edge_betweenness_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// let g = petgraph::graph::UnGraph::::from_edges([ /// (0, 4), (1, 2), (1, 3), (2, 3), (3, 4), (1, 4) /// ]); /// @@ -340,13 +340,13 @@ fn accumulate_edges( /// use rustworkx_core::centrality::degree_centrality; /// /// // Undirected graph example -/// let graph = UnGraph::::from_edges(&[ +/// let graph = UnGraph::::from_edges([ /// (0, 1), (1, 2), (2, 3), (3, 0) /// ]); /// let centrality = degree_centrality(&graph, None); /// /// // Directed graph example -/// let digraph = DiGraph::::from_edges(&[ +/// let digraph = DiGraph::::from_edges([ /// (0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3) /// ]); /// let centrality = degree_centrality(&digraph, None); @@ -728,7 +728,7 @@ mod test_betweenness_centrality { /// use rustworkx_core::petgraph::visit::{IntoEdges, IntoNodeIdentifiers}; /// use rustworkx_core::centrality::eigenvector_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// let g = petgraph::graph::UnGraph::::from_edges([ /// (0, 1), (1, 2) /// ]); /// // Calculate the eigenvector centrality @@ -818,7 +818,7 @@ where /// use rustworkx_core::petgraph::visit::{IntoEdges, IntoNodeIdentifiers}; /// use rustworkx_core::centrality::katz_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// let g = petgraph::graph::UnGraph::::from_edges([ /// (0, 1), (1, 2) /// ]); /// // Calculate the eigenvector centrality @@ -1139,7 +1139,7 @@ mod test_katz_centrality { /// use rustworkx_core::centrality::closeness_centrality; /// /// // Calculate the closeness centrality of Graph -/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// let g = petgraph::graph::UnGraph::::from_edges([ /// (0, 4), (1, 2), (2, 3), (3, 4), (1, 4) /// ]); /// let output = closeness_centrality(&g, true, 200); @@ -1149,7 +1149,7 @@ mod test_katz_centrality { /// ); /// /// // Calculate the closeness centrality of DiGraph -/// let dg = petgraph::graph::DiGraph::::from_edges(&[ +/// let dg = petgraph::graph::DiGraph::::from_edges([ /// (0, 4), (1, 2), (2, 3), (3, 4), (1, 4) /// ]); /// let output = closeness_centrality(&dg, true, 200); @@ -1276,14 +1276,14 @@ where /// use crate::rustworkx_core::petgraph::visit::EdgeRef; /// /// // Calculate the closeness centrality of Graph -/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// let g = petgraph::graph::UnGraph::::from_edges([ /// (0, 1, 0.7), (1, 2, 0.2), (2, 3, 0.5), /// ]); /// let output = newman_weighted_closeness_centrality(&g, false, |x| *x.weight(), 200); /// assert!(output[1] > output[3]); /// /// // Calculate the closeness centrality of DiGraph -/// let g = petgraph::graph::DiGraph::::from_edges(&[ +/// let g = petgraph::graph::DiGraph::::from_edges([ /// (0, 1, 0.7), (1, 2, 0.2), (2, 3, 0.5), /// ]); /// let output = newman_weighted_closeness_centrality(&g, false, |x| *x.weight(), 200); @@ -1764,7 +1764,7 @@ mod test_newman_weighted_closeness_centrality { /// use rustworkx_core::petgraph; /// use rustworkx_core::centrality::group_degree_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// let g = petgraph::graph::UnGraph::::from_edges([ /// (0, 1), (1, 2), (2, 3), (3, 4) /// ]); /// let output = group_degree_centrality(&g, &[0, 1], None); @@ -1843,7 +1843,7 @@ where /// use rustworkx_core::petgraph; /// use rustworkx_core::centrality::group_closeness_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// let g = petgraph::graph::UnGraph::::from_edges([ /// (0, 1), (1, 2), (2, 3), (3, 4) /// ]); /// let output = group_closeness_centrality(&g, &[0, 1]); @@ -1938,7 +1938,7 @@ where /// use rustworkx_core::petgraph; /// use rustworkx_core::centrality::group_betweenness_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// let g = petgraph::graph::UnGraph::::from_edges([ /// (0, 1), (1, 2), (2, 3), (3, 4) /// ]); /// let output = group_betweenness_centrality(&g, &[2], true); @@ -2081,7 +2081,7 @@ mod test_group_degree_centrality { #[test] fn test_undirected_path() { - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges([(0, 1), (1, 2), (2, 3), (3, 4)]); let result = group_degree_centrality(&g, &[0, 1], None); // Neighbors of {0,1} outside group = {2}. Centrality = 1/3. assert!((result - 1.0 / 3.0).abs() < 1e-10); @@ -2089,7 +2089,7 @@ mod test_group_degree_centrality { #[test] fn test_undirected_complete() { - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[ + let g = petgraph::graph::UnGraph::<(), ()>::from_edges([ (0, 1), (0, 2), (0, 3), @@ -2103,7 +2103,7 @@ mod test_group_degree_centrality { #[test] fn test_directed_out() { - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3)]); + let g = petgraph::graph::DiGraph::<(), ()>::from_edges([(0, 1), (1, 2), (2, 3)]); let result = group_degree_centrality(&g, &[0, 1], Some(petgraph::Direction::Outgoing)); // Out-neighbors of {0,1} outside group = {2}. Centrality = 1/2. assert!((result - 0.5).abs() < 1e-10); @@ -2111,7 +2111,7 @@ mod test_group_degree_centrality { #[test] fn test_directed_in() { - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3)]); + let g = petgraph::graph::DiGraph::<(), ()>::from_edges([(0, 1), (1, 2), (2, 3)]); let result = group_degree_centrality(&g, &[2, 3], Some(petgraph::Direction::Incoming)); // In-neighbors of {2,3} outside group = {1}. Centrality = 1/2. assert!((result - 0.5).abs() < 1e-10); @@ -2133,7 +2133,7 @@ mod test_group_closeness_centrality { #[test] fn test_undirected_path() { - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges([(0, 1), (1, 2), (2, 3), (3, 4)]); let result = group_closeness_centrality(&g, &[0, 1]); // Non-group = {2,3,4}. d(S,2)=1, d(S,3)=2, d(S,4)=3. Sum=6. // Closeness = 3/6 = 0.5 @@ -2142,7 +2142,7 @@ mod test_group_closeness_centrality { #[test] fn test_undirected_center_node() { - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 2), (1, 2), (2, 3), (2, 4)]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges([(0, 2), (1, 2), (2, 3), (2, 4)]); let result = group_closeness_centrality(&g, &[2]); // Non-group = {0,1,3,4}. All at distance 1. Sum=4. // Closeness = 4/4 = 1.0 @@ -2185,7 +2185,7 @@ mod test_group_betweenness_centrality { #[test] fn test_undirected_path_center() { // Path: 0-1-2-3-4 - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges([(0, 1), (1, 2), (2, 3), (3, 4)]); // Group = {2}. Node 2 is on all shortest paths between {0,1} and {3,4}. let result = group_betweenness_centrality(&g, &[2], false); // Pairs through node 2: (0,3), (0,4), (1,3), (1,4) = 4 paths @@ -2194,7 +2194,7 @@ mod test_group_betweenness_centrality { #[test] fn test_undirected_path_center_normalized() { - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges([(0, 1), (1, 2), (2, 3), (3, 4)]); let result = group_betweenness_centrality(&g, &[2], true); // Non-group size = 4. Normalization = C(4,2) = 6. // Normalized = 4/6 = 2/3 @@ -2203,7 +2203,7 @@ mod test_group_betweenness_centrality { #[test] fn test_empty_group() { - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (1, 2)]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges([(0, 1), (1, 2)]); let result = group_betweenness_centrality(&g, &[], false); assert_almost_equal!(result, 0.0, 1e-10); } @@ -2211,7 +2211,7 @@ mod test_group_betweenness_centrality { #[test] fn test_single_node_group() { // Star graph: center=0, leaves=1,2,3,4 - let g = petgraph::graph::UnGraph::<(), ()>::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)]); + let g = petgraph::graph::UnGraph::<(), ()>::from_edges([(0, 1), (0, 2), (0, 3), (0, 4)]); let result = group_betweenness_centrality(&g, &[0], false); // Node 0 is on all 6 shortest paths between leaf pairs assert_almost_equal!(result, 6.0, 1e-10); @@ -2220,7 +2220,7 @@ mod test_group_betweenness_centrality { #[test] fn test_directed_path() { // Directed path: 0->1->2->3->4 - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); + let g = petgraph::graph::DiGraph::<(), ()>::from_edges([(0, 1), (1, 2), (2, 3), (3, 4)]); // Group = {2}. Directed shortest paths through node 2: // (0,3), (0,4), (1,3), (1,4) = 4 pairs let result = group_betweenness_centrality(&g, &[2], false); @@ -2229,7 +2229,7 @@ mod test_group_betweenness_centrality { #[test] fn test_directed_path_normalized() { - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)]); + let g = petgraph::graph::DiGraph::<(), ()>::from_edges([(0, 1), (1, 2), (2, 3), (3, 4)]); let result = group_betweenness_centrality(&g, &[2], true); // Non-group = 4 nodes. Directed normalization = 4 * 3 = 12. // Normalized = 4 / 12 = 1/3 @@ -2239,7 +2239,7 @@ mod test_group_betweenness_centrality { #[test] fn test_directed_star() { // Directed star: 0->1, 0->2, 0->3, 0->4 - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)]); + let g = petgraph::graph::DiGraph::<(), ()>::from_edges([(0, 1), (0, 2), (0, 3), (0, 4)]); // Group = {0}. No directed shortest paths between leaf pairs // pass through 0 (leaves are unreachable from each other). let result = group_betweenness_centrality(&g, &[0], false); @@ -2249,7 +2249,7 @@ mod test_group_betweenness_centrality { #[test] fn test_directed_bidirectional_star() { // Bidirectional star: edges in both directions between center and leaves - let g = petgraph::graph::DiGraph::<(), ()>::from_edges(&[ + let g = petgraph::graph::DiGraph::<(), ()>::from_edges([ (0, 1), (1, 0), (0, 2), From a6cf1bb2cdfc959741b138d3d8bba2df93222838 Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Mon, 16 Mar 2026 20:55:50 +0300 Subject: [PATCH 08/11] fix the tests --- tests/digraph/test_centrality.py | 154 +++++++++++++++---------- tests/graph/test_centrality.py | 186 +++++++++++++++++++------------ 2 files changed, 209 insertions(+), 131 deletions(-) diff --git a/tests/digraph/test_centrality.py b/tests/digraph/test_centrality.py index e56bb91b5a..944ed12665 100644 --- a/tests/digraph/test_centrality.py +++ b/tests/digraph/test_centrality.py @@ -426,94 +426,128 @@ def test_invalid_node(self): rustworkx.digraph_group_betweenness_centrality(graph, [10]) -class TestGroupCentralityNetworkXComparisonDiGraph(unittest.TestCase): - """Cross-validate group centrality results against NetworkX.""" - - def _build_graphs(self, nx_graph): - rx_graph = rustworkx.PyDiGraph() - node_map = {} - for node in nx_graph.nodes(): - node_map[node] = rx_graph.add_node(node) - for u, v in nx_graph.edges(): - rx_graph.add_edge(node_map[u], node_map[v], None) - return rx_graph, node_map +class TestGroupCentralityExpectedValuesDiGraph(unittest.TestCase): + """Test group centrality against known expected values. + + Expected values were obtained with NetworkX 3.6.1: + import networkx as nx + nx.group_degree_centrality(g, group) + nx.group_closeness_centrality(g, group) + nx.group_betweenness_centrality(g, [group])[0] # normalized=True + """ def test_degree_directed_path(self): - g_nx = nx.path_graph(6, create_using=nx.DiGraph) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {2}, {0, 1}, {1, 3}, {0, 2, 4}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_degree_centrality(g_nx, group_nodes) - result = rustworkx.digraph_group_degree_centrality(g_rx, rx_group) + # obtained with: g = nx.path_graph(6, create_using=nx.DiGraph) + graph = rustworkx.generators.directed_path_graph(6) + cases = { + (0,): 0.2, + (2,): 0.2, + (0, 1): 0.25, + (1, 3): 0.5, + (0, 2, 4): 1.0, + } + for group, expected in cases.items(): + result = rustworkx.digraph_group_degree_centrality(graph, list(group)) self.assertAlmostEqual(result, expected, places=10) def test_degree_directed_cycle(self): - g_nx = nx.cycle_graph(6, create_using=nx.DiGraph) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {0, 3}, {0, 2, 4}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_degree_centrality(g_nx, group_nodes) - result = rustworkx.digraph_group_degree_centrality(g_rx, rx_group) + # obtained with: g = nx.cycle_graph(6, create_using=nx.DiGraph) + graph = rustworkx.generators.directed_cycle_graph(6) + cases = { + (0,): 0.2, + (0, 3): 0.5, + (0, 2, 4): 1.0, + } + for group, expected in cases.items(): + result = rustworkx.digraph_group_degree_centrality(graph, list(group)) self.assertAlmostEqual(result, expected, places=10) def test_closeness_directed_path(self): - g_nx = nx.path_graph(6, create_using=nx.DiGraph) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {2}, {0, 1}, {1, 3}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_closeness_centrality(g_nx, group_nodes) - result = rustworkx.digraph_group_closeness_centrality(g_rx, rx_group) + # obtained with: g = nx.path_graph(6, create_using=nx.DiGraph) + # NX uses incoming closeness (reversed BFS); our implementation matches. + graph = rustworkx.generators.directed_path_graph(6) + cases = { + (0,): 0.0, + (2,): 5 / 3, + (0, 1): 0.0, + (1, 3): 2.0, + } + for group, expected in cases.items(): + result = rustworkx.digraph_group_closeness_centrality(graph, list(group)) self.assertAlmostEqual(result, expected, places=10) def test_closeness_directed_cycle(self): - g_nx = nx.cycle_graph(6, create_using=nx.DiGraph) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {0, 3}, {0, 2, 4}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_closeness_centrality(g_nx, group_nodes) - result = rustworkx.digraph_group_closeness_centrality(g_rx, rx_group) + # obtained with: g = nx.cycle_graph(6, create_using=nx.DiGraph) + graph = rustworkx.generators.directed_cycle_graph(6) + cases = { + (0,): 1 / 3, + (0, 3): 2 / 3, + (0, 2, 4): 1.0, + } + for group, expected in cases.items(): + result = rustworkx.digraph_group_closeness_centrality(graph, list(group)) self.assertAlmostEqual(result, expected, places=10) def test_betweenness_bidirectional_path(self): - g_nx = nx.path_graph(6).to_directed() - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{2}, {1, 4}, {0, 5}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + # obtained with: g = nx.path_graph(6).to_directed() + graph = rustworkx.PyDiGraph() + for _ in range(6): + graph.add_node(None) + for i in range(5): + graph.add_edge(i, i + 1, None) + graph.add_edge(i + 1, i, None) + cases = { + (2,): 0.6, + (1, 4): 5 / 6, + (0, 5): 0.0, + } + for group, expected in cases.items(): result = rustworkx.digraph_group_betweenness_centrality( - g_rx, rx_group, normalized=True + graph, list(group), normalized=True ) self.assertAlmostEqual(result, expected, places=10) - def test_betweenness_directed_star(self): - g_nx = nx.star_graph(4).to_directed() - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {1, 2}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + def test_betweenness_bidirectional_star(self): + # obtained with: g = nx.star_graph(4).to_directed() + graph = rustworkx.PyDiGraph() + for _ in range(5): + graph.add_node(None) + for i in range(1, 5): + graph.add_edge(0, i, None) + graph.add_edge(i, 0, None) + cases = { + (0,): 1.0, + (1, 2): 0.0, + } + for group, expected in cases.items(): result = rustworkx.digraph_group_betweenness_centrality( - g_rx, rx_group, normalized=True + graph, list(group), normalized=True ) self.assertAlmostEqual(result, expected, places=10) def test_betweenness_directed_cycle(self): - g_nx = nx.cycle_graph(6, create_using=nx.DiGraph) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {0, 3}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + # obtained with: g = nx.cycle_graph(6, create_using=nx.DiGraph) + graph = rustworkx.generators.directed_cycle_graph(6) + cases = { + (0,): 0.5, + (0, 3): 5 / 6, + } + for group, expected in cases.items(): result = rustworkx.digraph_group_betweenness_centrality( - g_rx, rx_group, normalized=True + graph, list(group), normalized=True ) self.assertAlmostEqual(result, expected, places=10) def test_betweenness_complete_digraph(self): - g_nx = nx.complete_graph(5, create_using=nx.DiGraph) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {0, 1}, {0, 2, 4}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + # obtained with: g = nx.complete_graph(5, create_using=nx.DiGraph) + graph = rustworkx.generators.directed_complete_graph(5) + cases = { + (0,): 0.0, + (0, 1): 0.0, + (0, 2, 4): 0.0, + } + for group, expected in cases.items(): result = rustworkx.digraph_group_betweenness_centrality( - g_rx, rx_group, normalized=True + graph, list(group), normalized=True ) self.assertAlmostEqual(result, expected, places=10) diff --git a/tests/graph/test_centrality.py b/tests/graph/test_centrality.py index e741369c11..a9db5d2c44 100644 --- a/tests/graph/test_centrality.py +++ b/tests/graph/test_centrality.py @@ -402,112 +402,156 @@ def test_invalid_node(self): rustworkx.graph_group_betweenness_centrality(graph, [10]) -class TestGroupCentralityNetworkXComparisonGraph(unittest.TestCase): - """Cross-validate group centrality results against NetworkX.""" - - def _build_graphs(self, nx_graph): - rx_graph = rustworkx.PyGraph() - node_map = {} - for node in nx_graph.nodes(): - node_map[node] = rx_graph.add_node(node) - for u, v in nx_graph.edges(): - rx_graph.add_edge(node_map[u], node_map[v], None) - return rx_graph, node_map +class TestGroupCentralityExpectedValuesGraph(unittest.TestCase): + """Test group centrality against known expected values. + + Expected values were obtained with NetworkX 3.6.1: + import networkx as nx + nx.group_degree_centrality(g, group) + nx.group_closeness_centrality(g, group) + nx.group_betweenness_centrality(g, [group])[0] # normalized=True + """ def test_degree_path_graph(self): - g_nx = nx.path_graph(5) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {2}, {0, 1}, {1, 3}, {0, 2, 4}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_degree_centrality(g_nx, group_nodes) - result = rustworkx.graph_group_degree_centrality(g_rx, rx_group) + # obtained with: g = nx.path_graph(5) + graph = rustworkx.generators.path_graph(5) + cases = { + (0,): 0.25, + (2,): 0.5, + (0, 1): 1 / 3, + (1, 3): 1.0, + (0, 2, 4): 1.0, + } + for group, expected in cases.items(): + result = rustworkx.graph_group_degree_centrality(graph, list(group)) self.assertAlmostEqual(result, expected, places=10) def test_degree_complete_graph(self): - g_nx = nx.complete_graph(6) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {0, 1}, {0, 2, 4}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_degree_centrality(g_nx, group_nodes) - result = rustworkx.graph_group_degree_centrality(g_rx, rx_group) + # obtained with: g = nx.complete_graph(6) + graph = rustworkx.generators.complete_graph(6) + cases = { + (0,): 1.0, + (0, 1): 1.0, + (0, 2, 4): 1.0, + } + for group, expected in cases.items(): + result = rustworkx.graph_group_degree_centrality(graph, list(group)) self.assertAlmostEqual(result, expected, places=10) def test_degree_cycle_graph(self): - g_nx = nx.cycle_graph(8) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {0, 4}, {0, 2, 4, 6}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_degree_centrality(g_nx, group_nodes) - result = rustworkx.graph_group_degree_centrality(g_rx, rx_group) + # obtained with: g = nx.cycle_graph(8) + graph = rustworkx.generators.cycle_graph(8) + cases = { + (0,): 2 / 7, + (0, 4): 2 / 3, + (0, 2, 4, 6): 1.0, + } + for group, expected in cases.items(): + result = rustworkx.graph_group_degree_centrality(graph, list(group)) self.assertAlmostEqual(result, expected, places=10) def test_closeness_path_graph(self): - g_nx = nx.path_graph(5) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {2}, {0, 1}, {1, 3}, {0, 2, 4}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_closeness_centrality(g_nx, group_nodes) - result = rustworkx.graph_group_closeness_centrality(g_rx, rx_group) + # obtained with: g = nx.path_graph(5) + graph = rustworkx.generators.path_graph(5) + cases = { + (0,): 0.4, + (2,): 2 / 3, + (0, 1): 0.5, + (1, 3): 1.0, + (0, 2, 4): 1.0, + } + for group, expected in cases.items(): + result = rustworkx.graph_group_closeness_centrality(graph, list(group)) self.assertAlmostEqual(result, expected, places=10) def test_closeness_complete_graph(self): - g_nx = nx.complete_graph(6) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {0, 1}, {0, 2, 4}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_closeness_centrality(g_nx, group_nodes) - result = rustworkx.graph_group_closeness_centrality(g_rx, rx_group) + # obtained with: g = nx.complete_graph(6) + graph = rustworkx.generators.complete_graph(6) + cases = { + (0,): 1.0, + (0, 1): 1.0, + (0, 2, 4): 1.0, + } + for group, expected in cases.items(): + result = rustworkx.graph_group_closeness_centrality(graph, list(group)) self.assertAlmostEqual(result, expected, places=10) def test_closeness_cycle_graph(self): - g_nx = nx.cycle_graph(8) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {0, 4}, {0, 2, 4, 6}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_closeness_centrality(g_nx, group_nodes) - result = rustworkx.graph_group_closeness_centrality(g_rx, rx_group) + # obtained with: g = nx.cycle_graph(8) + graph = rustworkx.generators.cycle_graph(8) + cases = { + (0,): 0.4375, + (0, 4): 0.75, + (0, 2, 4, 6): 1.0, + } + for group, expected in cases.items(): + result = rustworkx.graph_group_closeness_centrality(graph, list(group)) self.assertAlmostEqual(result, expected, places=10) def test_betweenness_path_graph(self): - g_nx = nx.path_graph(5) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{2}, {1, 3}, {0, 4}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + # obtained with: g = nx.path_graph(5) + graph = rustworkx.generators.path_graph(5) + cases = { + (2,): 2 / 3, + (1, 3): 1.0, + (0, 4): 0.0, + } + for group, expected in cases.items(): result = rustworkx.graph_group_betweenness_centrality( - g_rx, rx_group, normalized=True + graph, list(group), normalized=True ) self.assertAlmostEqual(result, expected, places=10) def test_betweenness_complete_graph(self): - g_nx = nx.complete_graph(6) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {0, 1}, {0, 2, 4}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + # obtained with: g = nx.complete_graph(6) + graph = rustworkx.generators.complete_graph(6) + cases = { + (0,): 0.0, + (0, 1): 0.0, + (0, 2, 4): 0.0, + } + for group, expected in cases.items(): result = rustworkx.graph_group_betweenness_centrality( - g_rx, rx_group, normalized=True + graph, list(group), normalized=True ) self.assertAlmostEqual(result, expected, places=10) def test_betweenness_star_graph(self): - g_nx = nx.star_graph(4) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{0}, {1}, {0, 1}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + # obtained with: g = nx.star_graph(4) + graph = rustworkx.generators.star_graph(5) + cases = { + (0,): 1.0, + (1,): 0.0, + (0, 1): 1.0, + } + for group, expected in cases.items(): result = rustworkx.graph_group_betweenness_centrality( - g_rx, rx_group, normalized=True + graph, list(group), normalized=True ) self.assertAlmostEqual(result, expected, places=10) def test_betweenness_barbell_graph(self): - g_nx = nx.barbell_graph(4, 1) - g_rx, nmap = self._build_graphs(g_nx) - for group_nodes in [{4}, {3, 4, 5}, {0, 8}]: - rx_group = [nmap[n] for n in group_nodes] - expected = nx.group_betweenness_centrality(g_nx, [group_nodes])[0] + # obtained with: g = nx.barbell_graph(4, 1) + # barbell(4,1): two K4 cliques connected by a path of length 1 + # nodes 0-3 = left clique, 4 = bridge, 5-8 = right clique + graph = rustworkx.PyGraph() + for _ in range(9): + graph.add_node(None) + for i in range(4): + for j in range(i + 1, 4): + graph.add_edge(i, j, None) + for i in range(5, 9): + for j in range(i + 1, 9): + graph.add_edge(i, j, None) + graph.add_edge(3, 4, None) + graph.add_edge(4, 5, None) + cases = { + (4,): 4 / 7, + (3, 4, 5): 0.6, + (0, 8): 0.0, + } + for group, expected in cases.items(): result = rustworkx.graph_group_betweenness_centrality( - g_rx, rx_group, normalized=True + graph, list(group), normalized=True ) self.assertAlmostEqual(result, expected, places=10) From 7904d2041c4f041e1809db47f5eefc44956e7819 Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Mon, 16 Mar 2026 20:57:17 +0300 Subject: [PATCH 09/11] undo changes unrelated to PR --- rustworkx-core/src/centrality.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 054fa71c6a..7f9dafd64c 100755 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -55,7 +55,7 @@ use rayon_cond::CondIterator; /// use rustworkx_core::petgraph; /// use rustworkx_core::centrality::betweenness_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges([ +/// let g = petgraph::graph::UnGraph::::from_edges(&[ /// (0, 4), (1, 2), (2, 3), (3, 4), (1, 4) /// ]); /// // Calculate the betweenness centrality @@ -161,7 +161,7 @@ where /// use rustworkx_core::petgraph; /// use rustworkx_core::centrality::edge_betweenness_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges([ +/// let g = petgraph::graph::UnGraph::::from_edges(&[ /// (0, 4), (1, 2), (1, 3), (2, 3), (3, 4), (1, 4) /// ]); /// @@ -340,20 +340,25 @@ fn accumulate_edges( /// use rustworkx_core::centrality::degree_centrality; /// /// // Undirected graph example -/// let graph = UnGraph::::from_edges([ +/// let graph = UnGraph::::from_edges(&[ /// (0, 1), (1, 2), (2, 3), (3, 0) /// ]); /// let centrality = degree_centrality(&graph, None); /// /// // Directed graph example -/// let digraph = DiGraph::::from_edges([ +/// let digraph = DiGraph::::from_edges(&[ /// (0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3) /// ]); /// let centrality = degree_centrality(&digraph, None); /// ``` pub fn degree_centrality(graph: G, direction: Option) -> Vec where - G: NodeIndexable + IntoNodeIdentifiers + IntoNeighborsDirected + NodeCount + GraphProp, + G: NodeIndexable + + IntoNodeIdentifiers + + IntoNeighbors + + IntoNeighborsDirected + + NodeCount + + GraphProp, G::NodeId: Eq, { let node_count = graph.node_count() as f64; @@ -728,7 +733,7 @@ mod test_betweenness_centrality { /// use rustworkx_core::petgraph::visit::{IntoEdges, IntoNodeIdentifiers}; /// use rustworkx_core::centrality::eigenvector_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges([ +/// let g = petgraph::graph::UnGraph::::from_edges(&[ /// (0, 1), (1, 2) /// ]); /// // Calculate the eigenvector centrality @@ -818,7 +823,7 @@ where /// use rustworkx_core::petgraph::visit::{IntoEdges, IntoNodeIdentifiers}; /// use rustworkx_core::centrality::katz_centrality; /// -/// let g = petgraph::graph::UnGraph::::from_edges([ +/// let g = petgraph::graph::UnGraph::::from_edges(&[ /// (0, 1), (1, 2) /// ]); /// // Calculate the eigenvector centrality @@ -1139,7 +1144,7 @@ mod test_katz_centrality { /// use rustworkx_core::centrality::closeness_centrality; /// /// // Calculate the closeness centrality of Graph -/// let g = petgraph::graph::UnGraph::::from_edges([ +/// let g = petgraph::graph::UnGraph::::from_edges(&[ /// (0, 4), (1, 2), (2, 3), (3, 4), (1, 4) /// ]); /// let output = closeness_centrality(&g, true, 200); @@ -1149,7 +1154,7 @@ mod test_katz_centrality { /// ); /// /// // Calculate the closeness centrality of DiGraph -/// let dg = petgraph::graph::DiGraph::::from_edges([ +/// let dg = petgraph::graph::DiGraph::::from_edges(&[ /// (0, 4), (1, 2), (2, 3), (3, 4), (1, 4) /// ]); /// let output = closeness_centrality(&dg, true, 200); @@ -1276,14 +1281,14 @@ where /// use crate::rustworkx_core::petgraph::visit::EdgeRef; /// /// // Calculate the closeness centrality of Graph -/// let g = petgraph::graph::UnGraph::::from_edges([ +/// let g = petgraph::graph::UnGraph::::from_edges(&[ /// (0, 1, 0.7), (1, 2, 0.2), (2, 3, 0.5), /// ]); /// let output = newman_weighted_closeness_centrality(&g, false, |x| *x.weight(), 200); /// assert!(output[1] > output[3]); /// /// // Calculate the closeness centrality of DiGraph -/// let g = petgraph::graph::DiGraph::::from_edges([ +/// let g = petgraph::graph::DiGraph::::from_edges(&[ /// (0, 1, 0.7), (1, 2, 0.2), (2, 3, 0.5), /// ]); /// let output = newman_weighted_closeness_centrality(&g, false, |x| *x.weight(), 200); From cc14f428c9b747e03bc36d57d4b7be37e46e4770 Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Fri, 3 Apr 2026 21:33:15 +0200 Subject: [PATCH 10/11] nox lint --- tests/digraph/test_centrality.py | 12 +++--------- tests/graph/test_centrality.py | 4 +--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/digraph/test_centrality.py b/tests/digraph/test_centrality.py index 944ed12665..f9e47e6e2e 100644 --- a/tests/digraph/test_centrality.py +++ b/tests/digraph/test_centrality.py @@ -396,23 +396,17 @@ def test_invalid_node(self): class TestGroupBetweennessCentralityDiGraph(unittest.TestCase): def test_directed_path(self): graph = rustworkx.generators.directed_path_graph(5) - result = rustworkx.digraph_group_betweenness_centrality( - graph, [2], normalized=False - ) + result = rustworkx.digraph_group_betweenness_centrality(graph, [2], normalized=False) self.assertAlmostEqual(result, 4.0) def test_directed_path_normalized(self): graph = rustworkx.generators.directed_path_graph(5) - result = rustworkx.digraph_group_betweenness_centrality( - graph, [2], normalized=True - ) + result = rustworkx.digraph_group_betweenness_centrality(graph, [2], normalized=True) self.assertAlmostEqual(result, 4.0 / 12.0) def test_empty_group(self): graph = rustworkx.generators.directed_path_graph(3) - result = rustworkx.digraph_group_betweenness_centrality( - graph, [], normalized=False - ) + result = rustworkx.digraph_group_betweenness_centrality(graph, [], normalized=False) self.assertAlmostEqual(result, 0.0) def test_dispatch(self): diff --git a/tests/graph/test_centrality.py b/tests/graph/test_centrality.py index a9db5d2c44..0587f81d78 100644 --- a/tests/graph/test_centrality.py +++ b/tests/graph/test_centrality.py @@ -381,9 +381,7 @@ def test_star_center(self): for _ in range(4): leaf = graph.add_node("leaf") graph.add_edge(center, leaf, None) - result = rustworkx.graph_group_betweenness_centrality( - graph, [center], normalized=False - ) + result = rustworkx.graph_group_betweenness_centrality(graph, [center], normalized=False) self.assertAlmostEqual(result, 6.0) def test_empty_group(self): From 6aeab80682d3264c6ed80bef0fcfbf9ad49412cf Mon Sep 17 00:00:00 2001 From: selmanozleyen Date: Fri, 3 Apr 2026 21:38:16 +0200 Subject: [PATCH 11/11] release note --- .../notes/add-group-centrality-5634f19af475fa05.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 releasenotes/notes/add-group-centrality-5634f19af475fa05.yaml diff --git a/releasenotes/notes/add-group-centrality-5634f19af475fa05.yaml b/releasenotes/notes/add-group-centrality-5634f19af475fa05.yaml new file mode 100644 index 0000000000..8f3fc85da1 --- /dev/null +++ b/releasenotes/notes/add-group-centrality-5634f19af475fa05.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added new functions for computing group centrality measures: + :func:`.group_degree_centrality`, :func:`.group_closeness_centrality`, + and :func:`.group_betweenness_centrality`. These compute the centrality + of a group of nodes as defined by Everett & Borgatti (1999). Each function + works with both :class:`.PyGraph` and :class:`.PyDiGraph` objects. + The corresponding generic Rust implementations are also available in + ``rustworkx_core::centrality``. \ No newline at end of file