From 5668e3bbb2c7870485ab3a454bbb869596c41375 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:34:47 -0400 Subject: [PATCH 01/11] Comment space for the implementation --- src/centrality.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/centrality.rs b/src/centrality.rs index ca055cec3..a36b30dc6 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -164,6 +164,22 @@ pub fn digraph_betweenness_centrality( .collect(), } } +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// /// Compute the closeness centrality of each node in a :class:`~.PyGraph` object. /// From aec9aa111242f4d9396edc61a9616ba6f19a5d7c Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:54:25 -0400 Subject: [PATCH 02/11] Just reference for Ons --- rustworkx-core/src/centrality.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 09e21affa..dc1af39be 100644 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -335,6 +335,11 @@ fn accumulate_edges( } } } +// Here for reference for ONS +// +// +// +// struct ShortestPathData where From d249a1de55ac267dc7070643d8889b4cfe4015ad Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sat, 26 Oct 2024 18:55:41 -0400 Subject: [PATCH 03/11] Does not compile: some issues with calculating 'weights' --- rustworkx-core/src/centrality.rs | 30 ++++++++++++++++++ src/centrality.rs | 53 ++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index dc1af39be..3323f07d4 100644 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -340,6 +340,34 @@ fn accumulate_edges( // // // +pub fn degree_centrality( + graph: G, + kind: &str, + weighted: bool, + mut weight_fn: F, +) -> Result>, E> +where + G: IntoNodeIdentifiers + NodeCount + IntoNeighborsDirected + NodeIndexable + IntoEdges, + F: FnMut(G::EdgeRef) -> Result, +{ + let node_count = graph.node_count(); + let mut centrality = vec![None; graph.node_bound()]; + for node in graph.node_identifiers() { + let degree = match kind { + "in" => graph.neighbors_directed(node, petgraph::Direction::Incoming).count(), + "out" => graph.neighbors_directed(node, petgraph::Direction::Outgoing).count(), + _ => graph.neighbors(node).count(), + }; + let degree = if weighted { + graph.edges(node) + .try_fold(0.0, |acc, edge| weight_fn(edge).map(|w| acc + w))? + } else { + degree as f64 + }; + centrality[graph.to_index(node)] = Some(degree / (node_count - 1) as f64); + } + Ok(centrality) +} struct ShortestPathData where @@ -1077,3 +1105,5 @@ where } closeness } + + diff --git a/src/centrality.rs b/src/centrality.rs index a36b30dc6..3834a98da 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -164,22 +164,42 @@ pub fn digraph_betweenness_centrality( .collect(), } } -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// + +/// Compute the degree centrality for nodes in a PyDiGraph. +/// +/// Degree centrality assigns an importance score based simply on the number of edges held by each node. +/// +/// :param PyDiGraph graph: The input graph +/// :param str kind: The type of degree to compute. Can be "in", "out", or "total" +/// :param bool weighted: If True, compute weighted degree centrality +/// +/// :returns: a read-only dict-like object whose keys are the node indices and values are the +/// centrality score for each node. +/// :rtype: CentralityMapping +#[pyfunction(signature = (graph, kind="total", weighted=false))] +#[pyo3(text_signature = "(graph, /, kind='total', weighted=False)")] +pub fn degree_centrality_py( + py: Python, + graph: &digraph::PyDiGraph, + kind: &str, + weighted: bool, +) -> PyResult { + let weight_fn = |edge: digraph::PyDiGraph::EdgeRef<'_, PyObject>| -> PyResult { + let weight = edge.weight(); + weight.extract::(py).unwrap_or(1.0) + }; + + let centrality = centrality::degree_centrality(&graph.graph, kind, weighted, weight_fn) + .map_err(|e| PyErr::new::(format!("{:?}", e)))?; + + Ok(CentralityMapping { + centralities: centrality + .into_iter() + .enumerate() + .filter_map(|(i, v)| v.map(|x| (i, x))) + .collect(), + }) +} /// Compute the closeness centrality of each node in a :class:`~.PyGraph` object. /// @@ -839,3 +859,4 @@ pub fn digraph_katz_centrality( ))), } } + From 917147cbe0560718de6d9ee917b91b95e67b4fe1 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sat, 26 Oct 2024 21:49:26 -0400 Subject: [PATCH 04/11] Passes testcases --- rustworkx-core/src/centrality.rs | 32 +++++++----------------- rustworkx/__init__.pyi | 2 ++ src/centrality.rs | 25 ++++++------------- src/lib.rs | 1 + tests/graph/test_centrality.py | 43 ++++++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 41 deletions(-) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 3323f07d4..5b60b22bd 100644 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -340,33 +340,19 @@ fn accumulate_edges( // // // -pub fn degree_centrality( - graph: G, - kind: &str, - weighted: bool, - mut weight_fn: F, -) -> Result>, E> +pub fn degree_centrality(graph: G) -> Vec where - G: IntoNodeIdentifiers + NodeCount + IntoNeighborsDirected + NodeIndexable + IntoEdges, - F: FnMut(G::EdgeRef) -> Result, + G: IntoNodeIdentifiers + NodeCount + IntoNeighbors + NodeIndexable, { - let node_count = graph.node_count(); - let mut centrality = vec![None; graph.node_bound()]; + let node_count = graph.node_count() as f64; + let mut centrality = vec![0.0; graph.node_bound()]; + for node in graph.node_identifiers() { - let degree = match kind { - "in" => graph.neighbors_directed(node, petgraph::Direction::Incoming).count(), - "out" => graph.neighbors_directed(node, petgraph::Direction::Outgoing).count(), - _ => graph.neighbors(node).count(), - }; - let degree = if weighted { - graph.edges(node) - .try_fold(0.0, |acc, edge| weight_fn(edge).map(|w| acc + w))? - } else { - degree as f64 - }; - centrality[graph.to_index(node)] = Some(degree / (node_count - 1) as f64); + let degree = graph.neighbors(node).count() as f64; + centrality[graph.to_index(node)] = degree / (node_count - 1.0); } - Ok(centrality) + + centrality } struct ShortestPathData diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index 63f5f4be4..9832c0980 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -50,6 +50,7 @@ from .rustworkx import digraph_closeness_centrality as digraph_closeness_central from .rustworkx import graph_closeness_centrality as graph_closeness_centrality from .rustworkx import digraph_katz_centrality as digraph_katz_centrality from .rustworkx import graph_katz_centrality as graph_katz_centrality +from .rustworkx import degree_centrality as degree_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 @@ -264,6 +265,7 @@ from .rustworkx import AllPairsMultiplePathMapping as AllPairsMultiplePathMappin from .rustworkx import PyGraph as PyGraph from .rustworkx import PyDiGraph as PyDiGraph + _S = TypeVar("_S") _T = TypeVar("_T") _BFSVisitor = TypeVar("_BFSVisitor", bound=visit.BFSVisitor) diff --git a/src/centrality.rs b/src/centrality.rs index 3834a98da..61939bfee 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -165,38 +165,27 @@ pub fn digraph_betweenness_centrality( } } -/// Compute the degree centrality for nodes in a PyDiGraph. +/// Compute the degree centrality for nodes in a PyGraph. /// /// Degree centrality assigns an importance score based simply on the number of edges held by each node. /// -/// :param PyDiGraph graph: The input graph -/// :param str kind: The type of degree to compute. Can be "in", "out", or "total" -/// :param bool weighted: If True, compute weighted degree centrality +/// :param PyGraph graph: The input graph /// /// :returns: a read-only dict-like object whose keys are the node indices and values are the /// centrality score for each node. /// :rtype: CentralityMapping -#[pyfunction(signature = (graph, kind="total", weighted=false))] -#[pyo3(text_signature = "(graph, /, kind='total', weighted=False)")] +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] pub fn degree_centrality_py( - py: Python, - graph: &digraph::PyDiGraph, - kind: &str, - weighted: bool, + graph: &graph::PyGraph, ) -> PyResult { - let weight_fn = |edge: digraph::PyDiGraph::EdgeRef<'_, PyObject>| -> PyResult { - let weight = edge.weight(); - weight.extract::(py).unwrap_or(1.0) - }; - - let centrality = centrality::degree_centrality(&graph.graph, kind, weighted, weight_fn) - .map_err(|e| PyErr::new::(format!("{:?}", e)))?; + let centrality = centrality::degree_centrality(&graph.graph); Ok(CentralityMapping { centralities: centrality .into_iter() .enumerate() - .filter_map(|(i, v)| v.map(|x| (i, x))) + .map(|(i, v)| (i, v)) .collect(), }) } diff --git a/src/lib.rs b/src/lib.rs index 79f183462..ba9c016d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -533,6 +533,7 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(digraph_eigenvector_centrality))?; m.add_wrapped(wrap_pyfunction!(graph_katz_centrality))?; m.add_wrapped(wrap_pyfunction!(digraph_katz_centrality))?; + m.add_wrapped(wrap_pyfunction!(degree_centrality_py))?; 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/graph/test_centrality.py b/tests/graph/test_centrality.py index 12ed67457..17065f8b3 100644 --- a/tests/graph/test_centrality.py +++ b/tests/graph/test_centrality.py @@ -230,3 +230,46 @@ def test_custom_graph_unnormalized(self): expected = {0: 9, 1: 9, 2: 12, 3: 15, 4: 11, 5: 14, 6: 10, 7: 13, 8: 9, 9: 9} for k, v in centrality.items(): self.assertAlmostEqual(v, expected[k]) + + +class TestDegreeCentrality(unittest.TestCase): + def setUp(self): + self.graph = rustworkx.PyGraph() + self.a = self.graph.add_node("A") + self.b = self.graph.add_node("B") + self.c = self.graph.add_node("C") + self.d = self.graph.add_node("D") + edge_list = [ + (self.a, self.b, 1), + (self.b, self.c, 1), + (self.c, self.d, 1), + ] + self.graph.add_edges_from(edge_list) + + def test_degree_centrality(self): + centrality = rustworkx.degree_centrality_py(self.graph) + expected = { + 0: 1/3, # Node A has 1 edge, normalized by (n-1) = 3 + 1: 2/3, # Node B has 2 edges + 2: 2/3, # Node C has 2 edges + 3: 1/3, # Node D has 1 edge + } + self.assertEqual(expected, centrality) + + def test_degree_centrality_complete_graph(self): + graph = rustworkx.generators.complete_graph(5) + centrality = rustworkx.degree_centrality_py(graph) + expected = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0} + self.assertEqual(expected, centrality) + + def test_degree_centrality_star_graph(self): + graph = rustworkx.generators.star_graph(5) + centrality = rustworkx.degree_centrality_py(graph) + expected = {0: 1.0, 1: 0.25, 2: 0.25, 3: 0.25, 4: 0.25} + self.assertEqual(expected, centrality) + + def test_degree_centrality_empty_graph(self): + graph = rustworkx.PyGraph() + centrality = rustworkx.degree_centrality_py(graph) + expected = {} + self.assertEqual(expected, centrality) From 7b6837e6e4c852684ba136300097219d741ea50f Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sat, 26 Oct 2024 21:55:35 -0400 Subject: [PATCH 05/11] Formatting --- rustworkx-core/src/centrality.rs | 14 ++++++-------- src/centrality.rs | 5 +---- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 5b60b22bd..7fa69573b 100644 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -336,22 +336,22 @@ fn accumulate_edges( } } // Here for reference for ONS -// -// -// -// +// +// +// +// pub fn degree_centrality(graph: G) -> Vec where G: IntoNodeIdentifiers + NodeCount + IntoNeighbors + NodeIndexable, { let node_count = graph.node_count() as f64; let mut centrality = vec![0.0; graph.node_bound()]; - + for node in graph.node_identifiers() { let degree = graph.neighbors(node).count() as f64; centrality[graph.to_index(node)] = degree / (node_count - 1.0); } - + centrality } @@ -1091,5 +1091,3 @@ where } closeness } - - diff --git a/src/centrality.rs b/src/centrality.rs index 61939bfee..c847ed039 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -176,9 +176,7 @@ pub fn digraph_betweenness_centrality( /// :rtype: CentralityMapping #[pyfunction] #[pyo3(text_signature = "(graph, /)")] -pub fn degree_centrality_py( - graph: &graph::PyGraph, -) -> PyResult { +pub fn degree_centrality_py(graph: &graph::PyGraph) -> PyResult { let centrality = centrality::degree_centrality(&graph.graph); Ok(CentralityMapping { @@ -848,4 +846,3 @@ pub fn digraph_katz_centrality( ))), } } - From d3f6042b45621805651600fe953b9f672f8c91ee Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sat, 26 Oct 2024 21:56:28 -0400 Subject: [PATCH 06/11] python formatting --- rustworkx/__init__.pyi | 1 - tests/graph/test_centrality.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index 9832c0980..36d840ed4 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -265,7 +265,6 @@ from .rustworkx import AllPairsMultiplePathMapping as AllPairsMultiplePathMappin from .rustworkx import PyGraph as PyGraph from .rustworkx import PyDiGraph as PyDiGraph - _S = TypeVar("_S") _T = TypeVar("_T") _BFSVisitor = TypeVar("_BFSVisitor", bound=visit.BFSVisitor) diff --git a/tests/graph/test_centrality.py b/tests/graph/test_centrality.py index 17065f8b3..81b1a3d4f 100644 --- a/tests/graph/test_centrality.py +++ b/tests/graph/test_centrality.py @@ -249,10 +249,10 @@ def setUp(self): def test_degree_centrality(self): centrality = rustworkx.degree_centrality_py(self.graph) expected = { - 0: 1/3, # Node A has 1 edge, normalized by (n-1) = 3 - 1: 2/3, # Node B has 2 edges - 2: 2/3, # Node C has 2 edges - 3: 1/3, # Node D has 1 edge + 0: 1 / 3, # Node A has 1 edge, normalized by (n-1) = 3 + 1: 2 / 3, # Node B has 2 edges + 2: 2 / 3, # Node C has 2 edges + 3: 1 / 3, # Node D has 1 edge } self.assertEqual(expected, centrality) From 4da4860ff1cb7d5711700ab2c798193f99ab5901 Mon Sep 17 00:00:00 2001 From: ons Date: Sun, 27 Oct 2024 22:29:39 -0400 Subject: [PATCH 07/11] Added MD Documenation and CLI usage instructions --- degree_centrality_implementation_rustworkx.md | 147 ++++++++++++++++++ rustworkx-core/src/centrality.rs | 36 ++++- 2 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 degree_centrality_implementation_rustworkx.md diff --git a/degree_centrality_implementation_rustworkx.md b/degree_centrality_implementation_rustworkx.md new file mode 100644 index 000000000..02dd7e6cf --- /dev/null +++ b/degree_centrality_implementation_rustworkx.md @@ -0,0 +1,147 @@ + +`rustworkx` is a high-performance graph algorithms library that combines the accessibility of Python with the performance of Rust. This implementation enhances the library's centrality module by adding degree centrality calculation capabilities. + +### Degree Centrality Implementation + +Degree centrality measures the connectivity of each node within a graph, signifying its influence or importance. This implementation provides an unweighted version, assigning equal weight to each edge, resulting in a simpler measure of influence in unweighted networks. + +This implementation: + +- Is integrated within the centrality module in `rustworkx-core`. +- Supports unweighted, and undirected graphs. +- Returns normalized centrality scores ranging from 0 to 1. +- Is compatible with `rustworkx`’s existing petgraph-based architecture. + +### Contributing to `rustworkx` + +Following `rustworkx`'s contribution structure, we implemented the following: + +1. **Core Implementation** + - Added the pure Rust function in `rustworkx-core/src/centrality.rs`. + - Implemented core logic based on existing centrality measures. +2. **Python Binding** + - Created a wrapper in `src/centrality.rs`. + - Function signature: `degree_centrality_py(graph: PyGraph) -> CentralityMapping`. +3. **Testing** + - Added tests in `tests/graph/test_centrality.py`. + - Covered basic graphs, complete graphs, star graphs, and edge cases. + +### Pure Rust Function: `degree_centrality` + +The Rust core function, `degree_centrality`, is designed to calculate the centrality score for each node based on its connections. The function is structured as follows: + +```rust +pub fn degree_centrality(graph: G) -> Vec +where + G: IntoNodeIdentifiers + NodeCount + IntoNeighbors + NodeIndexable, +{ + let node_count = graph.node_count() as f64; + let mut centrality = vec![0.0; graph.node_bound()]; + + for node in graph.node_identifiers() { + let degree = graph.neighbors(node).count() as f64; + centrality[graph.to_index(node)] = degree / (node_count - 1.0); + } + + centrality +} +``` + +1. **Trait Compatibility**: + - The function is generic over `G`, allowing it to work with any graph that implements the necessary traits from `petgraph` (such as `IntoNodeIdentifiers`, `NodeCount`, `IntoNeighbors`, and `NodeIndexable`). This setup enables seamless support for different graph types within the `rustworkx` ecosystem. + +2. **Node Count and Initialization**: + - The total number of nodes in the graph is obtained as `node_count`, and a `centrality` vector is initialized to the size of the number of nodes, with each index eventually storing the centrality score for each node. + +3. **Degree Calculation and Normalization**: + - For each node, the degree is determined by counting the neighbors (`graph.neighbors(node).count()`), giving the number of edges connected to the node. + - This degree is normalized by dividing by the maximum possible connections in the graph, `node_count - 1.0`. This normalization step provides a relative measure of connectivity for each node. + +4. **Returning Centrality Scores**: + - Finally, the function returns the `centrality` vector, which contains floating-point values representing the degree centrality score for each node. + +#### Example Usage: + +```rust +use rustworkx_core::petgraph::graph::UnGraph; +use rustworkx_core::centrality::degree_centrality; + +// Create an undirected graph with 4 nodes and 4 edges forming a square +let graph = UnGraph::::from_edges(&[ + (0, 1), (1, 2), (2, 3), (3, 0) +]); + +// Calculate degree centrality +let centrality = degree_centrality(&graph); + +// Expected output: +// centrality[0] = 0.67 (node 0 has 2 connections, normalized) +// centrality[1] = 0.67 (node 1 has 2 connections, normalized) +// centrality[2] = 0.67 (node 2 has 2 connections, normalized) +// centrality[3] = 0.67 (node 3 has 2 connections, normalized) + +assert_eq!(centrality, vec![0.67, 0.67, 0.67, 0.67]); +``` + +### Python Wrapper Function: `degree_centrality_py` + +The `degree_centrality_py` function is a Python wrapper implemented with PyO3 to expose the Rust function in a Python-friendly format. + +```rust +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn degree_centrality_py( + graph: &graph::PyGraph, +) -> PyResult { + let centrality = centrality::degree_centrality(&graph.graph); + + Ok(CentralityMapping { + centralities: centrality + .into_iter() + .enumerate() + .map(|(i, v)| (i, v)) + .collect(), + }) +} +``` + +1. **Input Handling**: Accepts a `PyGraph` object from Python, referencing the internal Rust graph to pass directly to the Rust `degree_centrality` function. + +2. **Result Mapping**: Converts the returned centrality vector into a Python-friendly `CentralityMapping` (a dictionary-like structure), where each node index maps to its centrality score. + +3. **Return**: Outputs the node centrality scores as a normalized dictionary for easy access in Python. + +**Usage Example**: + +```python +import rustworkx as rx + +# Create a sample graph +graph = rx.PyGraph() +graph.add_nodes_from([0, 1, 2, 3]) +graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) + +# Calculate degree centrality +degree_centrality = rx.degree_centrality(graph) +print(degree_centrality) # Output: {0: 0.67, 1: 0.67, 2: 0.67, 3: 0.67} +``` + +### Testing + +Testing is conducted in `tests/graph/test_centrality.py`, covering: + +- **Basic Graphs**: Verifies individual node centrality. +- **Complete Graphs**: Confirms all nodes have a centrality score of 1.0. +- **Star Graphs**: Validates centrality for hub-and-spoke structures. +- **Empty Graphs**: Ensures correct handling of graphs without nodes. + +### Development History + +The implementation progressed through four major commits: + +| Commit | Description | Key Changes | +| ------- | ---------------------- | --------------------------------------- | +| d249a1d | Initial Implementation | Basic structure & compilation issues | +| 917147c | Test Case Resolution | Fixed weight calculations, passed tests | +| 7b6837e | Code Formatting | Enhanced code readability | +| d3f6042 | Python Interface | Improved Bindings | diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 7fa69573b..aa1ea6983 100644 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -335,11 +335,37 @@ fn accumulate_edges( } } } -// Here for reference for ONS -// -// -// -// +/// Compute the degree centrality of all nodes in a graph. +/// +/// The algorithm used in this function is based on: +/// +/// Degree Centrality, Betweenness Centrality, and Closeness Centrality in Social Network +/// Atlantis Press - Junlong Zhang, Yu Luo +/// https://doi.org/10.2991/msam-17.2017.68 +/// +/// Example usage of `degree_centrality` function +/// ``` +/// use rustworkx_core::petgraph::graph::UnGraph; +/// use rustworkx_core::centrality::degree_centrality; +/// +/// // Create an undirected graph with 4 nodes and 4 edges forming a square +/// let graph = UnGraph::::from_edges(&[ +/// (0, 1), (1, 2), (2, 3), (3, 0) +/// ]); +/// +/// // Calculate degree centrality +/// let centrality = degree_centrality(&graph); +/// +/// // Expected output: +/// // centrality[0] = 0.67 (node 0 has 2 connections, normalized) +/// // centrality[1] = 0.67 (node 1 has 2 connections, normalized) +/// // centrality[2] = 0.67 (node 2 has 2 connections, normalized) +/// // centrality[3] = 0.67 (node 3 has 2 connections, normalized) +/// +/// assert_eq!(centrality, vec![0.67, 0.67, 0.67, 0.67]); +/// ``` + + pub fn degree_centrality(graph: G) -> Vec where G: IntoNodeIdentifiers + NodeCount + IntoNeighbors + NodeIndexable, From a7bf9bc1f05706f2c000eb995392263badb78b60 Mon Sep 17 00:00:00 2001 From: onsali <64367557+onsali@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:23:36 -0400 Subject: [PATCH 08/11] Update degree_centrality_implementation_rustworkx.md --- degree_centrality_implementation_rustworkx.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/degree_centrality_implementation_rustworkx.md b/degree_centrality_implementation_rustworkx.md index 02dd7e6cf..6cbee7c34 100644 --- a/degree_centrality_implementation_rustworkx.md +++ b/degree_centrality_implementation_rustworkx.md @@ -3,6 +3,8 @@ ### Degree Centrality Implementation +![Centralities](https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Wp-01.png/450px-Wp-01.png) + Degree centrality measures the connectivity of each node within a graph, signifying its influence or importance. This implementation provides an unweighted version, assigning equal weight to each edge, resulting in a simpler measure of influence in unweighted networks. This implementation: From a1d990afe08f36ad404a8442d4b695275f3cbb99 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sun, 3 Nov 2024 11:37:53 -0500 Subject: [PATCH 09/11] Fixed the doctest --- .gitignore | 2 ++ rustworkx-core/src/centrality.rs | 13 +++++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 6afef31f5..7bfd5a884 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ retworkx/*pyd *.jpg **/*.so retworkx-core/Cargo.lock +.DS_Store +**/.DS_Store diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index aa1ea6983..74bca55ba 100644 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -348,7 +348,7 @@ fn accumulate_edges( /// use rustworkx_core::petgraph::graph::UnGraph; /// use rustworkx_core::centrality::degree_centrality; /// -/// // Create an undirected graph with 4 nodes and 4 edges forming a square +/// // Create an undirected graph with 4 nodes and 4 edges /// let graph = UnGraph::::from_edges(&[ /// (0, 1), (1, 2), (2, 3), (3, 0) /// ]); @@ -356,13 +356,10 @@ fn accumulate_edges( /// // Calculate degree centrality /// let centrality = degree_centrality(&graph); /// -/// // Expected output: -/// // centrality[0] = 0.67 (node 0 has 2 connections, normalized) -/// // centrality[1] = 0.67 (node 1 has 2 connections, normalized) -/// // centrality[2] = 0.67 (node 2 has 2 connections, normalized) -/// // centrality[3] = 0.67 (node 3 has 2 connections, normalized) -/// -/// assert_eq!(centrality, vec![0.67, 0.67, 0.67, 0.67]); +/// // Each node has 2 connections out of 3 possible (n-1 where n=4) +/// for value in centrality { +/// assert!((value - 2.0/3.0).abs() < 1e-10); +/// } /// ``` From dac92afdf5c0438c931dc5cd0d67adf06b39bff6 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sun, 3 Nov 2024 11:41:35 -0500 Subject: [PATCH 10/11] Ran FMT --- rustworkx-core/src/centrality.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index 74bca55ba..d09e302b7 100644 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -340,8 +340,8 @@ fn accumulate_edges( /// The algorithm used in this function is based on: /// /// Degree Centrality, Betweenness Centrality, and Closeness Centrality in Social Network -/// Atlantis Press - Junlong Zhang, Yu Luo -/// https://doi.org/10.2991/msam-17.2017.68 +/// Atlantis Press - Junlong Zhang, Yu Luo +/// https://doi.org/10.2991/msam-17.2017.68 /// /// Example usage of `degree_centrality` function /// ``` @@ -362,7 +362,6 @@ fn accumulate_edges( /// } /// ``` - pub fn degree_centrality(graph: G) -> Vec where G: IntoNodeIdentifiers + NodeCount + IntoNeighbors + NodeIndexable, From 608ee9cb3efbce07a85cb80ab40b43df368021d5 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Mon, 11 Nov 2024 09:35:16 -0500 Subject: [PATCH 11/11] Fixed changes --- .gitignore | 1 - degree_centrality_implementation_rustworkx.md | 149 ------------------ rustworkx-core/src/centrality.rs | 26 ++- rustworkx/__init__.pyi | 3 +- src/centrality.rs | 18 ++- src/lib.rs | 3 +- tests/graph/test_centrality.py | 54 ++++++- 7 files changed, 94 insertions(+), 160 deletions(-) delete mode 100644 degree_centrality_implementation_rustworkx.md diff --git a/.gitignore b/.gitignore index 7bfd5a884..6e09a4a58 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,4 @@ retworkx/*pyd *.jpg **/*.so retworkx-core/Cargo.lock -.DS_Store **/.DS_Store diff --git a/degree_centrality_implementation_rustworkx.md b/degree_centrality_implementation_rustworkx.md deleted file mode 100644 index 6cbee7c34..000000000 --- a/degree_centrality_implementation_rustworkx.md +++ /dev/null @@ -1,149 +0,0 @@ - -`rustworkx` is a high-performance graph algorithms library that combines the accessibility of Python with the performance of Rust. This implementation enhances the library's centrality module by adding degree centrality calculation capabilities. - -### Degree Centrality Implementation - -![Centralities](https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Wp-01.png/450px-Wp-01.png) - -Degree centrality measures the connectivity of each node within a graph, signifying its influence or importance. This implementation provides an unweighted version, assigning equal weight to each edge, resulting in a simpler measure of influence in unweighted networks. - -This implementation: - -- Is integrated within the centrality module in `rustworkx-core`. -- Supports unweighted, and undirected graphs. -- Returns normalized centrality scores ranging from 0 to 1. -- Is compatible with `rustworkx`’s existing petgraph-based architecture. - -### Contributing to `rustworkx` - -Following `rustworkx`'s contribution structure, we implemented the following: - -1. **Core Implementation** - - Added the pure Rust function in `rustworkx-core/src/centrality.rs`. - - Implemented core logic based on existing centrality measures. -2. **Python Binding** - - Created a wrapper in `src/centrality.rs`. - - Function signature: `degree_centrality_py(graph: PyGraph) -> CentralityMapping`. -3. **Testing** - - Added tests in `tests/graph/test_centrality.py`. - - Covered basic graphs, complete graphs, star graphs, and edge cases. - -### Pure Rust Function: `degree_centrality` - -The Rust core function, `degree_centrality`, is designed to calculate the centrality score for each node based on its connections. The function is structured as follows: - -```rust -pub fn degree_centrality(graph: G) -> Vec -where - G: IntoNodeIdentifiers + NodeCount + IntoNeighbors + NodeIndexable, -{ - let node_count = graph.node_count() as f64; - let mut centrality = vec![0.0; graph.node_bound()]; - - for node in graph.node_identifiers() { - let degree = graph.neighbors(node).count() as f64; - centrality[graph.to_index(node)] = degree / (node_count - 1.0); - } - - centrality -} -``` - -1. **Trait Compatibility**: - - The function is generic over `G`, allowing it to work with any graph that implements the necessary traits from `petgraph` (such as `IntoNodeIdentifiers`, `NodeCount`, `IntoNeighbors`, and `NodeIndexable`). This setup enables seamless support for different graph types within the `rustworkx` ecosystem. - -2. **Node Count and Initialization**: - - The total number of nodes in the graph is obtained as `node_count`, and a `centrality` vector is initialized to the size of the number of nodes, with each index eventually storing the centrality score for each node. - -3. **Degree Calculation and Normalization**: - - For each node, the degree is determined by counting the neighbors (`graph.neighbors(node).count()`), giving the number of edges connected to the node. - - This degree is normalized by dividing by the maximum possible connections in the graph, `node_count - 1.0`. This normalization step provides a relative measure of connectivity for each node. - -4. **Returning Centrality Scores**: - - Finally, the function returns the `centrality` vector, which contains floating-point values representing the degree centrality score for each node. - -#### Example Usage: - -```rust -use rustworkx_core::petgraph::graph::UnGraph; -use rustworkx_core::centrality::degree_centrality; - -// Create an undirected graph with 4 nodes and 4 edges forming a square -let graph = UnGraph::::from_edges(&[ - (0, 1), (1, 2), (2, 3), (3, 0) -]); - -// Calculate degree centrality -let centrality = degree_centrality(&graph); - -// Expected output: -// centrality[0] = 0.67 (node 0 has 2 connections, normalized) -// centrality[1] = 0.67 (node 1 has 2 connections, normalized) -// centrality[2] = 0.67 (node 2 has 2 connections, normalized) -// centrality[3] = 0.67 (node 3 has 2 connections, normalized) - -assert_eq!(centrality, vec![0.67, 0.67, 0.67, 0.67]); -``` - -### Python Wrapper Function: `degree_centrality_py` - -The `degree_centrality_py` function is a Python wrapper implemented with PyO3 to expose the Rust function in a Python-friendly format. - -```rust -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -pub fn degree_centrality_py( - graph: &graph::PyGraph, -) -> PyResult { - let centrality = centrality::degree_centrality(&graph.graph); - - Ok(CentralityMapping { - centralities: centrality - .into_iter() - .enumerate() - .map(|(i, v)| (i, v)) - .collect(), - }) -} -``` - -1. **Input Handling**: Accepts a `PyGraph` object from Python, referencing the internal Rust graph to pass directly to the Rust `degree_centrality` function. - -2. **Result Mapping**: Converts the returned centrality vector into a Python-friendly `CentralityMapping` (a dictionary-like structure), where each node index maps to its centrality score. - -3. **Return**: Outputs the node centrality scores as a normalized dictionary for easy access in Python. - -**Usage Example**: - -```python -import rustworkx as rx - -# Create a sample graph -graph = rx.PyGraph() -graph.add_nodes_from([0, 1, 2, 3]) -graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) - -# Calculate degree centrality -degree_centrality = rx.degree_centrality(graph) -print(degree_centrality) # Output: {0: 0.67, 1: 0.67, 2: 0.67, 3: 0.67} -``` - -### Testing - -Testing is conducted in `tests/graph/test_centrality.py`, covering: - -- **Basic Graphs**: Verifies individual node centrality. -- **Complete Graphs**: Confirms all nodes have a centrality score of 1.0. -- **Star Graphs**: Validates centrality for hub-and-spoke structures. -- **Empty Graphs**: Ensures correct handling of graphs without nodes. - -### Development History - -The implementation progressed through four major commits: - -| Commit | Description | Key Changes | -| ------- | ---------------------- | --------------------------------------- | -| d249a1d | Initial Implementation | Basic structure & compilation issues | -| 917147c | Test Case Resolution | Fixed weight calculations, passed tests | -| 7b6837e | Code Formatting | Enhanced code readability | -| d3f6042 | Python Interface | Improved Bindings | diff --git a/rustworkx-core/src/centrality.rs b/rustworkx-core/src/centrality.rs index d09e302b7..2579c6a72 100644 --- a/rustworkx-core/src/centrality.rs +++ b/rustworkx-core/src/centrality.rs @@ -32,6 +32,7 @@ use petgraph::visit::{ Reversed, Visitable, }; +use petgraph::Direction; use rayon_cond::CondIterator; /// Compute the betweenness centrality of all nodes in a graph. @@ -362,7 +363,7 @@ fn accumulate_edges( /// } /// ``` -pub fn degree_centrality(graph: G) -> Vec +pub fn graph_degree_centrality(graph: G) -> Vec where G: IntoNodeIdentifiers + NodeCount + IntoNeighbors + NodeIndexable, { @@ -377,6 +378,29 @@ where centrality } +pub fn digraph_degree_centrality(graph: G, direction: Option) -> Vec +where + G: IntoNodeIdentifiers + NodeCount + IntoNeighborsDirected + NodeIndexable, +{ + let node_count = graph.node_count() as f64; + let mut centrality = vec![0.0; graph.node_bound()]; + + for node in graph.node_identifiers() { + let degree = match direction { + Some(Direction::Incoming) => { + graph.neighbors_directed(node, Direction::Incoming).count() as f64 + } + Some(Direction::Outgoing) => { + graph.neighbors_directed(node, Direction::Outgoing).count() as f64 + } + _ => graph.neighbors(node).count() as f64, + }; + centrality[graph.to_index(node)] = degree / (node_count - 1.0); + } + + centrality +} + struct ShortestPathData where G: GraphBase, diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index 36d840ed4..7a5ada3c3 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -50,7 +50,8 @@ from .rustworkx import digraph_closeness_centrality as digraph_closeness_central from .rustworkx import graph_closeness_centrality as graph_closeness_centrality from .rustworkx import digraph_katz_centrality as digraph_katz_centrality from .rustworkx import graph_katz_centrality as graph_katz_centrality -from .rustworkx import degree_centrality as degree_centrality +from .rustworkx import graph_degree_centrality as graph_degree_centrality +from .rustworkx import digraph_degree_centrality as digraph_degree_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 diff --git a/src/centrality.rs b/src/centrality.rs index c847ed039..000682fac 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -176,8 +176,22 @@ pub fn digraph_betweenness_centrality( /// :rtype: CentralityMapping #[pyfunction] #[pyo3(text_signature = "(graph, /)")] -pub fn degree_centrality_py(graph: &graph::PyGraph) -> PyResult { - let centrality = centrality::degree_centrality(&graph.graph); +pub fn graph_degree_centrality(graph: &graph::PyGraph) -> PyResult { + let centrality = centrality::graph_degree_centrality(&graph.graph); + + Ok(CentralityMapping { + centralities: centrality + .into_iter() + .enumerate() + .map(|(i, v)| (i, v)) + .collect(), + }) +} + +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn digraph_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult { + let centrality = centrality::digraph_degree_centrality(&graph.graph, None); Ok(CentralityMapping { centralities: centrality diff --git a/src/lib.rs b/src/lib.rs index ba9c016d3..c4938e949 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -533,7 +533,8 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(digraph_eigenvector_centrality))?; m.add_wrapped(wrap_pyfunction!(graph_katz_centrality))?; m.add_wrapped(wrap_pyfunction!(digraph_katz_centrality))?; - m.add_wrapped(wrap_pyfunction!(degree_centrality_py))?; + m.add_wrapped(wrap_pyfunction!(graph_degree_centrality))?; + m.add_wrapped(wrap_pyfunction!(digraph_degree_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/graph/test_centrality.py b/tests/graph/test_centrality.py index 81b1a3d4f..f8418e626 100644 --- a/tests/graph/test_centrality.py +++ b/tests/graph/test_centrality.py @@ -232,7 +232,7 @@ def test_custom_graph_unnormalized(self): self.assertAlmostEqual(v, expected[k]) -class TestDegreeCentrality(unittest.TestCase): +class TestGraphDegreeCentrality(unittest.TestCase): def setUp(self): self.graph = rustworkx.PyGraph() self.a = self.graph.add_node("A") @@ -247,7 +247,7 @@ def setUp(self): self.graph.add_edges_from(edge_list) def test_degree_centrality(self): - centrality = rustworkx.degree_centrality_py(self.graph) + centrality = rustworkx.graph_degree_centrality(self.graph) expected = { 0: 1 / 3, # Node A has 1 edge, normalized by (n-1) = 3 1: 2 / 3, # Node B has 2 edges @@ -258,18 +258,62 @@ def test_degree_centrality(self): def test_degree_centrality_complete_graph(self): graph = rustworkx.generators.complete_graph(5) - centrality = rustworkx.degree_centrality_py(graph) + centrality = rustworkx.graph_degree_centrality(graph) expected = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0} self.assertEqual(expected, centrality) def test_degree_centrality_star_graph(self): graph = rustworkx.generators.star_graph(5) - centrality = rustworkx.degree_centrality_py(graph) + centrality = rustworkx.graph_degree_centrality(graph) expected = {0: 1.0, 1: 0.25, 2: 0.25, 3: 0.25, 4: 0.25} self.assertEqual(expected, centrality) def test_degree_centrality_empty_graph(self): graph = rustworkx.PyGraph() - centrality = rustworkx.degree_centrality_py(graph) + centrality = rustworkx.graph_degree_centrality(graph) expected = {} self.assertEqual(expected, centrality) + +class TestDiGraphDegreeCentrality(unittest.TestCase): + def setUp(self): + self.digraph = rustworkx.PyDiGraph() + self.a = self.digraph.add_node("A") + self.b = self.digraph.add_node("B") + self.c = self.digraph.add_node("C") + self.d = self.digraph.add_node("D") + edge_list = [ + (self.a, self.b, 1), + (self.b, self.c, 1), + (self.c, self.d, 1), + ] + self.digraph.add_edges_from(edge_list) + + def test_digraph_degree_centrality(self): + centrality = rustworkx.digraph_degree_centrality(self.digraph) + expected = { + 0: 1 / 3, # Node A has 1 outgoing edge + 1: 2 / 3, # Node B has 1 incoming and 1 outgoing edge + 2: 2 / 3, # Node C has 1 incoming and 1 outgoing edge + 3: 1 / 3, # Node D has 1 incoming edge + } + self.assertEqual(expected, centrality) + + def test_digraph_degree_centrality_with_direction(self): + centrality_incoming = rustworkx.digraph_degree_centrality(self.digraph, direction=rustworkx.Direction.Incoming) + expected_incoming = { + 0: 0.0, # Node A has 0 incoming edges + 1: 1 / 3, # Node B has 1 incoming edge + 2: 1 / 3, # Node C has 1 incoming edge + 3: 1 / 3, # Node D has 1 incoming edge + } + self.assertEqual(expected_incoming, centrality_incoming) + + centrality_outgoing = rustworkx.digraph_degree_centrality(self.digraph, direction=rustworkx.Direction.Outgoing) + expected_outgoing = { + 0: 1 / 3, # Node A has 1 outgoing edge + 1: 1 / 3, # Node B has 1 outgoing edge + 2: 1 / 3, # Node C has 1 outgoing edge + 3: 0.0, # Node D has 0 outgoing edges + } + self.assertEqual(expected_outgoing, centrality_outgoing) +