From 3b68fc4b60f901928729d9a4294de4d96a2966e3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 3 Jun 2022 16:30:03 -0400 Subject: [PATCH 01/12] Add Eigenvector centrality function This commit adds a new function eigenvector_centrality() to compute the eigenvector centrality of a graph. It uses the same power function approach that the NetworkX function eigenvector_centrality() [1] function uses. This is for two reasons, the first is that a more traditional eigenvector linear algebra/BLAS function would either require us to link against BLAS at build time (which is a big change in the build system and a large requirement) or to call out to numpy via python both of which seemed less than ideal. The second reason was to make handling holes in node indices bit easier. Using this approach also enabled us to put the implementation in retworkx-core so it can be reused with any petgraph graph. Part of #441 --- ...genvector-centrality-e8ca30e31738a666.yaml | 9 + retworkx-core/src/centrality.rs | 201 ++++++++++++++++++ retworkx/__init__.py | 57 +++++ src/centrality.rs | 129 ++++++++++- src/lib.rs | 5 + tests/digraph/test_centrality.py | 22 ++ tests/graph/test_centrality.py | 22 ++ 7 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-eigenvector-centrality-e8ca30e31738a666.yaml diff --git a/releasenotes/notes/add-eigenvector-centrality-e8ca30e31738a666.yaml b/releasenotes/notes/add-eigenvector-centrality-e8ca30e31738a666.yaml new file mode 100644 index 000000000..48f6fdc4b --- /dev/null +++ b/releasenotes/notes/add-eigenvector-centrality-e8ca30e31738a666.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added a new function, :func:`~.eigenvector_centrality()` which is used to + compute the eigenvector centrality for all nodes in a given graph. + - | + Added a new function to retworkx-core ``eigenvector_centrality`` which is + used to compute the eigenvector centrality for all nodes in a given graph. + diff --git a/retworkx-core/src/centrality.rs b/retworkx-core/src/centrality.rs index 3474be99a..dac5c287c 100644 --- a/retworkx-core/src/centrality.rs +++ b/retworkx-core/src/centrality.rs @@ -16,8 +16,11 @@ use std::sync::RwLock; use hashbrown::HashMap; use petgraph::graph::NodeIndex; use petgraph::visit::{ + EdgeRef, GraphBase, GraphProp, // allows is_directed + IntoEdges, + IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCount, @@ -297,3 +300,201 @@ where sigma, } } + +/// Compute the eigenvector centrality of a graph +/// +/// For details on the eigenvector centrality refer to: +/// +/// Phillip Bonacich. “Power and Centrality: A Family of Measures.” +/// American Journal of Sociology 92(5):1170–1182, 1986 +/// +/// +/// This function uses a power iteration method to compute the eigenvector +/// and convergence is not guaranteed. The function will stop when `max_iter` +/// iterations is reached or when the computed vector between two iterations +/// is smaller than the error tolerance multiplied by the number of nodes. +/// The implementation of this algorithm is based on the NetworkX +/// [`eigenvector_centrality()`](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.centrality.eigenvector_centrality.html) +/// function. +/// +/// In the case of multigraphs the weights of any parallel edges will be +/// summed when computing the eigenvector centrality. +/// +/// Arguments: +/// +/// * `graph` - The graph object to run the algorithm on +/// * `weight_fn` - An input callable that will be pased the `EdgeRef` for +/// an edge in the graph and is expected to return a `Result` of +/// the weight of that edge. +/// * `max_iter` - The maximum number of iterations in the power method. If +/// set to `None` a default value of 100 is used. +/// * `tol` - The error tolerance used when checking for convergence in the +/// power method. If set to `None` a dfault value of 1e-6 is used. +/// +/// # Example +/// ```rust +/// use hashbrown::HashMap; +/// +/// use retworkx_core::Result; +/// use retworkx_core::petgraph; +/// use retworkx_core::petgraph::visit::{IntoEdges, IntoNodeIdentifiers}; +/// use retworkx_core::centrality::eigenvector_centrality; +/// +/// let g = petgraph::graph::UnGraph::::from_edges(&[ +/// (0, 1), (1, 2) +/// ]); +/// // Calculate the betweeness centrality +/// let output: Result>> = +/// eigenvector_centrality(&g, |_| {Ok(1.)}, None, None); +/// ``` +pub fn eigenvector_centrality( + graph: G, + mut weight_fn: F, + max_iter: Option, + tol: Option, +) -> Result>, E> +where + G: NodeIndexable + + IntoNodeIdentifiers + + IntoNeighbors + + IntoEdges + + NodeCount + + GraphProp + + GraphBase, + F: FnMut(G::EdgeRef) -> Result, +{ + let tol: f64 = tol.unwrap_or(1e-6); + let max_iter = max_iter.unwrap_or(50); + let n_start: HashMap = graph.node_identifiers().map(|n| (n, 1.)).collect(); + let n_start_sum: f64 = n_start.len() as f64; + let mut x: HashMap = + n_start.iter().map(|(k, v)| (*k, v / n_start_sum)).collect(); + let node_count = graph.node_count(); + for _ in 0..max_iter { + let x_last = x.clone(); + for node in x_last.keys() { + for neighbor in graph.neighbors(*node) { + let w_vec: Vec = graph + .edges(*node) + .filter(|edge| edge.target() == neighbor) + .collect(); + let mut w = 0.; + for edge in w_vec { + w += weight_fn(edge)?; + } + *x.get_mut(&neighbor).unwrap() += x_last[node] * w; + } + } + let mut norm: f64 = x.values().map(|val| val.powi(2)).sum::().sqrt(); + if norm == 0. { + norm = 1.; + } + x = x.iter().map(|(k, v)| (*k, v / norm)).collect(); + if x.keys() + .map(|node| (x[node] - x_last[node]).abs()) + .sum::() + < node_count as f64 * tol + { + return Ok(Some(x)); + } + } + Ok(None) +} + +#[cfg(test)] +mod test_eigenvector_centrality { + + use hashbrown::HashMap; + + use crate::centrality::eigenvector_centrality; + use crate::petgraph; + use crate::Result; + + macro_rules! assert_almost_equal { + ($x:expr, $y:expr, $d:expr) => { + if !($x - $y < $d || $y - $x < $d) { + panic!("{} != {} within delta of {}", $x, $y, $d); + } + }; + } + #[test] + fn test_no_convergence() { + let g = petgraph::graph::UnGraph::::from_edges(&[(0, 1), (1, 2)]); + let output: Result>> = + eigenvector_centrality(&g, |_| Ok(1.), Some(0), None); + let result = output.unwrap(); + assert_eq!(None, result); + } + + #[test] + fn test_undirected_complete_graph() { + let g = petgraph::graph::UnGraph::::from_edges([ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 4), + (3, 4), + ]); + let output: Result>> = + eigenvector_centrality(&g, |_| Ok(1.), None, None); + let result = output.unwrap().unwrap(); + let expected_value: f64 = (1_f64 / 5_f64).sqrt(); + let expected_values: Vec = vec![expected_value; 5]; + for i in 0..5 { + let index = petgraph::graph::NodeIndex::new(i); + assert_almost_equal!(expected_values[i], result[&index], 1e-4); + } + } + + #[test] + fn test_undirected_path_graph() { + let g = petgraph::graph::UnGraph::::from_edges(&[(0, 1), (1, 2)]); + let output: Result>> = + eigenvector_centrality(&g, |_| Ok(1.), None, None); + let result = output.unwrap().unwrap(); + let expected_values: Vec = vec![0.5, 0.7071, 0.5]; + for i in 0..3 { + let index = petgraph::graph::NodeIndex::new(i); + assert_almost_equal!(expected_values[i], result[&index], 1e-4); + } + } + + #[test] + fn test_directed_graph() { + let g = petgraph::graph::DiGraph::::from_edges([ + (0, 1), + (0, 2), + (1, 3), + (2, 1), + (2, 4), + (3, 1), + (3, 4), + (3, 5), + (4, 5), + (4, 6), + (4, 7), + (5, 7), + (6, 0), + (6, 4), + (6, 7), + (7, 5), + (7, 6), + ]); + let output: Result>> = + eigenvector_centrality(&g, |_| Ok(2.), None, None); + let result = output.unwrap().unwrap(); + let expected_values: Vec = vec![ + 0.25368793, 0.19576478, 0.32817092, 0.40430835, 0.48199885, 0.15724483, 0.51346196, + 0.32475403, + ]; + for i in 0..8 { + let index = petgraph::graph::NodeIndex::new(i); + assert_almost_equal!(expected_values[i], result[&index], 1e-4); + } + } +} diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 209fafbea..b76a0247c 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -1592,6 +1592,63 @@ def _graph_betweenness_centrality(graph, normalized=True, endpoints=False, paral ) +@functools.singledispatch +def eigenvector_centrality(graph, weight_fn=None, default_weight=1.0, max_iter=100, tol=1e-6): + """Compute the eigenvector centrality of a :class:`~PyGraph`. + + For details on the eigenvector centrality refer to: + + Phillip Bonacich. “Power and Centrality: A Family of Measures.” + American Journal of Sociology 92(5):1170–1182, 1986 + + + This function uses a power iteration method to compute the eigenvector + and convergence is not guaranteed. The function will stop when `max_iter` + iterations is reached or when the computed vector between two iterations + is smaller than the error tolerance multiplied by the number of nodes. + The implementation of this algorithm is based on the NetworkX + `eigenvector_centrality() `__ + function. + + In the case of multigraphs the weights of any parallel edges will be + summed when computing the eigenvector centrality. + + :param PyDigraph graph: The graph object to run the algorithm on + :param weight_fn: An optional input callable that will be pased the edge's + payload object and is expected to return a `float` weight for that edge. + If this is not specified ``default_weight`` will be used as the weight + for every edge in ``graph`` + :param float default_weight: If ``weight_fn`` is not set the default weight + value to use for the weight of all edges + :param int max_iter: The maximum number of iterations in the power method. If + not specified a default value of 100 is used. + :param float tol: The error tolerance used when checking for convergence in the + power method. If this is not specified default value of 1e-6 is used. + + :returns: a read-only dict-like object whose keys are the node indices and values are the + centrality score for that node. + :rtype: CentralityMapping + """ + + +@eigenvector_centrality.register(PyDiGraph) +def _digraph_eigenvector_centrality( + graph, weight_fn=None, default_weight=1.0, max_iter=100, tol=1e-6 +): + return digraph_eigenvector_centrality( + graph, weight_fn=weight_fn, default_weight=default_weight, max_iter=max_iter, tol=tol + ) + + +@eigenvector_centrality.register(PyGraph) +def _graph_eigenvector_centrality( + graph, weight_fn=None, default_weight=1.0, max_iter=100, tol=1e-6 +): + return graph_eigenvector_centrality( + graph, weight_fn=weight_fn, default_weight=default_weight, max_iter=max_iter, tol=tol + ) + + @functools.singledispatch def vf2_mapping( first, diff --git a/src/centrality.rs b/src/centrality.rs index df87c7f8a..494247d7e 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -10,10 +10,13 @@ // License for the specific language governing permissions and limitations // under the License. -use crate::iterators::CentralityMapping; +use std::convert::TryFrom; use crate::digraph; use crate::graph; +use crate::iterators::CentralityMapping; +use crate::CostFn; +use crate::FailedToConverge; use pyo3::prelude::*; @@ -132,3 +135,127 @@ pub fn digraph_betweenness_centrality( .collect(), } } + +/// Compute the eigenvector centrality of a :class:`~PyGraph`. +/// +/// For details on the eigenvector centrality refer to: +/// +/// Phillip Bonacich. “Power and Centrality: A Family of Measures.” +/// American Journal of Sociology 92(5):1170–1182, 1986 +/// +/// +/// This function uses a power iteration method to compute the eigenvector +/// and convergence is not guaranteed. The function will stop when `max_iter` +/// iterations is reached or when the computed vector between two iterations +/// is smaller than the error tolerance multiplied by the number of nodes. +/// The implementation of this algorithm is based on the NetworkX +/// `eigenvector_centrality() `__ +/// function. +/// +/// In the case of multigraphs the weights of any parallel edges will be +/// summed when computing the eigenvector centrality. +/// +/// :param PyDigraph graph: The graph object to run the algorithm on +/// :param weight_fn: An optional input callable that will be pased the edge's +/// payload object and is expected to return a `float` weight for that edge. +/// If this is not specified ``default_weight`` will be used as the weight +/// for every edge in ``graph`` +/// :param float default_weight: If ``weight_fn`` is not set the default weight +/// value to use for the weight of all edges +/// :param int max_iter: The maximum number of iterations in the power method. If +/// not specified a default value of 100 is used. +/// :param float tol: The error tolerance used when checking for convergence in the +/// power method. If this is not specified default value of 1e-6 is used. +/// +/// :returns: a read-only dict-like object whose keys are the node indices and values are the +/// centrality score for that node. +/// :rtype: CentralityMapping +#[pyfunction(default_weight = "1.0", max_iter = "100", tol = "1e-6")] +#[pyo3(text_signature = "(graph, /, weight_fn=None, default_weight=1.0, max_iter=100, tol=1e-6)")] +pub fn graph_eigenvector_centrality( + py: Python, + graph: &graph::PyGraph, + weight_fn: Option, + default_weight: f64, + max_iter: usize, + tol: f64, +) -> PyResult { + let cost_fn = CostFn::try_from((weight_fn, default_weight))?; + let ev_centrality = centrality::eigenvector_centrality( + &graph.graph, + |e| cost_fn.call(py, e.weight()), + Some(max_iter), + Some(tol), + )?; + match ev_centrality { + Some(centrality) => Ok(CentralityMapping { + centralities: centrality.iter().map(|(k, v)| (k.index(), *v)).collect(), + }), + None => Err(FailedToConverge::new_err(format!( + "Function failed to converge on a solution in {} iterations", + max_iter + ))), + } +} + +/// Compute the eigenvector centrality of a :class:`~PyDiGraph`. +/// +/// For details on the eigenvector centrality refer to: +/// +/// Phillip Bonacich. “Power and Centrality: A Family of Measures.” +/// American Journal of Sociology 92(5):1170–1182, 1986 +/// +/// +/// This function uses a power iteration method to compute the eigenvector +/// and convergence is not guaranteed. The function will stop when `max_iter` +/// iterations is reached or when the computed vector between two iterations +/// is smaller than the error tolerance multiplied by the number of nodes. +/// The implementation of this algorithm is based on the NetworkX +/// `eigenvector_centrality() `__ +/// function. +/// +/// In the case of multigraphs the weights of any parallel edges will be +/// summed when computing the eigenvector centrality. +/// +/// :param PyDigraph graph: The graph object to run the algorithm on +/// :param weight_fn: An optional input callable that will be pased the edge's +/// payload object and is expected to return a `float` weight for that edge. +/// If this is not specified ``default_weight`` will be used as the weight +/// for every edge in ``graph`` +/// :param float default_weight: If ``weight_fn`` is not set the default weight +/// value to use for the weight of all edges +/// :param int max_iter: The maximum number of iterations in the power method. If +/// not specified a default value of 100 is used. +/// :param float tol: The error tolerance used when checking for convergence in the +/// power method. If this is not specified default value of 1e-6 is used. +/// +/// :returns: a read-only dict-like object whose keys are the node indices and values are the +/// centrality score for that node. +/// :rtype: CentralityMapping +#[pyfunction(default_weight = "1.0", max_iter = "100", tol = "1e-6")] +#[pyo3(text_signature = "(graph, /, weight_fn=None, default_weight=1.0, max_iter=100, tol=1e-6)")] +pub fn digraph_eigenvector_centrality( + py: Python, + graph: &digraph::PyDiGraph, + weight_fn: Option, + default_weight: f64, + max_iter: usize, + tol: f64, +) -> PyResult { + let cost_fn = CostFn::try_from((weight_fn, default_weight))?; + let ev_centrality = centrality::eigenvector_centrality( + &graph.graph, + |e| cost_fn.call(py, e.weight()), + Some(max_iter), + Some(tol), + )?; + match ev_centrality { + Some(centrality) => Ok(CentralityMapping { + centralities: centrality.iter().map(|(k, v)| (k.index(), *v)).collect(), + }), + None => Err(FailedToConverge::new_err(format!( + "Function failed to converge on a solution in {} iterations", + max_iter + ))), + } +} diff --git a/src/lib.rs b/src/lib.rs index 94586bc45..de051ed7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -316,6 +316,8 @@ import_exception!(retworkx.visit, PruneSearch); import_exception!(retworkx.visit, StopSearch); // Negative Cycle found on shortest-path algorithm create_exception!(retworkx, NegativeCycle, PyException); +// Failed to Converge on a solution +create_exception!(retworkx, FailedToConverge, PyException); #[pymodule] fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { @@ -328,6 +330,7 @@ fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add("NoPathFound", py.get_type::())?; m.add("NullGraph", py.get_type::())?; m.add("NegativeCycle", py.get_type::())?; + m.add("FailedToConverge", py.get_type::())?; m.add_wrapped(wrap_pyfunction!(bfs_successors))?; m.add_wrapped(wrap_pyfunction!(graph_bfs_search))?; m.add_wrapped(wrap_pyfunction!(digraph_bfs_search))?; @@ -400,6 +403,8 @@ fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { ))?; m.add_wrapped(wrap_pyfunction!(graph_betweenness_centrality))?; m.add_wrapped(wrap_pyfunction!(digraph_betweenness_centrality))?; + m.add_wrapped(wrap_pyfunction!(graph_eigenvector_centrality))?; + m.add_wrapped(wrap_pyfunction!(digraph_eigenvector_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 da0540e0d..674571273 100644 --- a/tests/digraph/test_centrality.py +++ b/tests/digraph/test_centrality.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import math import unittest import retworkx @@ -128,3 +129,24 @@ def test_betweenness_centrality_unnormalized(self): ) expected = {0: 0.0, 1: 2.0, 2: 2.0, 4: 0.0} self.assertEqual(expected, betweenness) + + +class TestEigenvectorCentrality(unittest.TestCase): + def test_complete_graph(self): + graph = retworkx.generators.directed_mesh_graph(5) + centrality = retworkx.eigenvector_centrality(graph) + expected_value = math.sqrt(1.0 / 5.0) + for value in centrality.values(): + self.assertAlmostEqual(value, expected_value) + + def test_path_graph(self): + graph = retworkx.generators.directed_path_graph(3, bidirectional=True) + centrality = retworkx.eigenvector_centrality(graph) + expected = [0.5, 0.7071, 0.5] + for k, v in centrality.items(): + self.assertAlmostEqual(v, expected[k], 4) + + def test_no_convergence(self): + graph = retworkx.PyDiGraph() + with self.assertRaises(retworkx.FailedToConverge): + retworkx.eigenvector_centrality(graph, max_iter=0) diff --git a/tests/graph/test_centrality.py b/tests/graph/test_centrality.py index 5c382361e..d7a51e9dd 100644 --- a/tests/graph/test_centrality.py +++ b/tests/graph/test_centrality.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import math import unittest import retworkx @@ -99,3 +100,24 @@ def test_betweenness_centrality_unnormalized(self): ) expected = {0: 0.0, 1: 2.0, 2: 2.0, 4: 0.0} self.assertEqual(expected, betweenness) + + +class TestEigenvectorCentrality(unittest.TestCase): + def test_complete_graph(self): + graph = retworkx.generators.mesh_graph(5) + centrality = retworkx.eigenvector_centrality(graph) + expected_value = math.sqrt(1.0 / 5.0) + for value in centrality.values(): + self.assertAlmostEqual(value, expected_value) + + def test_path_graph(self): + graph = retworkx.generators.path_graph(3) + centrality = retworkx.eigenvector_centrality(graph) + expected = [0.5, 0.7071, 0.5] + for k, v in centrality.items(): + self.assertAlmostEqual(v, expected[k], 4) + + def test_no_convergence(self): + graph = retworkx.PyGraph() + with self.assertRaises(retworkx.FailedToConverge): + retworkx.eigenvector_centrality(graph, max_iter=0) From 54a9ed26448c5dfd182b58ca9e23744e47aaf57c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Jun 2022 16:04:12 -0400 Subject: [PATCH 02/12] Fix issues with docstrings Co-authored-by: georgios-ts <45130028+georgios-ts@users.noreply.github.com> --- retworkx-core/src/centrality.rs | 15 +++++---------- retworkx/__init__.py | 7 ++++--- src/centrality.rs | 8 ++++---- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/retworkx-core/src/centrality.rs b/retworkx-core/src/centrality.rs index dac5c287c..c433ddc28 100644 --- a/retworkx-core/src/centrality.rs +++ b/retworkx-core/src/centrality.rs @@ -323,13 +323,13 @@ where /// Arguments: /// /// * `graph` - The graph object to run the algorithm on -/// * `weight_fn` - An input callable that will be pased the `EdgeRef` for +/// * `weight_fn` - An input callable that will be passed the `EdgeRef` for /// an edge in the graph and is expected to return a `Result` of /// the weight of that edge. /// * `max_iter` - The maximum number of iterations in the power method. If /// set to `None` a default value of 100 is used. /// * `tol` - The error tolerance used when checking for convergence in the -/// power method. If set to `None` a dfault value of 1e-6 is used. +/// power method. If set to `None` a default value of 1e-6 is used. /// /// # Example /// ```rust @@ -343,7 +343,7 @@ where /// let g = petgraph::graph::UnGraph::::from_edges(&[ /// (0, 1), (1, 2) /// ]); -/// // Calculate the betweeness centrality +/// // Calculate the eigenvector centrality /// let output: Result>> = /// eigenvector_centrality(&g, |_| {Ok(1.)}, None, None); /// ``` @@ -354,13 +354,8 @@ pub fn eigenvector_centrality( tol: Option, ) -> Result>, E> where - G: NodeIndexable - + IntoNodeIdentifiers - + IntoNeighbors - + IntoEdges - + NodeCount - + GraphProp - + GraphBase, + G: NodeIndexable + IntoNodeIdentifiers + IntoNeighbors + IntoEdges + NodeCount, + G::NodeId: Eq + std::hash::Hash, F: FnMut(G::EdgeRef) -> Result, { let tol: f64 = tol.unwrap_or(1e-6); diff --git a/retworkx/__init__.py b/retworkx/__init__.py index b76a0247c..1be867d87 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -1594,7 +1594,7 @@ def _graph_betweenness_centrality(graph, normalized=True, endpoints=False, paral @functools.singledispatch def eigenvector_centrality(graph, weight_fn=None, default_weight=1.0, max_iter=100, tol=1e-6): - """Compute the eigenvector centrality of a :class:`~PyGraph`. + """Compute the eigenvector centrality of a graph. For details on the eigenvector centrality refer to: @@ -1613,8 +1613,9 @@ def eigenvector_centrality(graph, weight_fn=None, default_weight=1.0, max_iter=1 In the case of multigraphs the weights of any parallel edges will be summed when computing the eigenvector centrality. - :param PyDigraph graph: The graph object to run the algorithm on - :param weight_fn: An optional input callable that will be pased the edge's + :param graph: Graph to be used. Can either be a + :class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph`. + :param weight_fn: An optional input callable that will be passed the edge's payload object and is expected to return a `float` weight for that edge. If this is not specified ``default_weight`` will be used as the weight for every edge in ``graph`` diff --git a/src/centrality.rs b/src/centrality.rs index 494247d7e..36df0b441 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -155,8 +155,8 @@ pub fn digraph_betweenness_centrality( /// In the case of multigraphs the weights of any parallel edges will be /// summed when computing the eigenvector centrality. /// -/// :param PyDigraph graph: The graph object to run the algorithm on -/// :param weight_fn: An optional input callable that will be pased the edge's +/// :param PyGraph graph: The graph object to run the algorithm on +/// :param weight_fn: An optional input callable that will be passed the edge's /// payload object and is expected to return a `float` weight for that edge. /// If this is not specified ``default_weight`` will be used as the weight /// for every edge in ``graph`` @@ -217,8 +217,8 @@ pub fn graph_eigenvector_centrality( /// In the case of multigraphs the weights of any parallel edges will be /// summed when computing the eigenvector centrality. /// -/// :param PyDigraph graph: The graph object to run the algorithm on -/// :param weight_fn: An optional input callable that will be pased the edge's +/// :param PyDiGraph graph: The graph object to run the algorithm on +/// :param weight_fn: An optional input callable that will be passed the edge's /// payload object and is expected to return a `float` weight for that edge. /// If this is not specified ``default_weight`` will be used as the weight /// for every edge in ``graph`` From 81e4e2ff904e1b81356866c56b2d7b21b4707abe Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Jun 2022 16:05:22 -0400 Subject: [PATCH 03/12] Correct default value of max_iter Co-authored-by: georgios-ts <45130028+georgios-ts@users.noreply.github.com> --- retworkx-core/src/centrality.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retworkx-core/src/centrality.rs b/retworkx-core/src/centrality.rs index c433ddc28..c0bd28795 100644 --- a/retworkx-core/src/centrality.rs +++ b/retworkx-core/src/centrality.rs @@ -359,7 +359,7 @@ where F: FnMut(G::EdgeRef) -> Result, { let tol: f64 = tol.unwrap_or(1e-6); - let max_iter = max_iter.unwrap_or(50); + let max_iter = max_iter.unwrap_or(100); let n_start: HashMap = graph.node_identifiers().map(|n| (n, 1.)).collect(); let n_start_sum: f64 = n_start.len() as f64; let mut x: HashMap = From e91093966892934df01638c5eed62876bbb80619 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Jun 2022 16:24:37 -0400 Subject: [PATCH 04/12] Apply suggestions from code review Co-authored-by: georgios-ts <45130028+georgios-ts@users.noreply.github.com> --- retworkx-core/src/centrality.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/retworkx-core/src/centrality.rs b/retworkx-core/src/centrality.rs index c0bd28795..55ee2a271 100644 --- a/retworkx-core/src/centrality.rs +++ b/retworkx-core/src/centrality.rs @@ -360,10 +360,7 @@ where { let tol: f64 = tol.unwrap_or(1e-6); let max_iter = max_iter.unwrap_or(100); - let n_start: HashMap = graph.node_identifiers().map(|n| (n, 1.)).collect(); - let n_start_sum: f64 = n_start.len() as f64; - let mut x: HashMap = - n_start.iter().map(|(k, v)| (*k, v / n_start_sum)).collect(); + let mut x: HashMap = graph.node_identifiers().map(|n| (n, 1.)).collect(); let node_count = graph.node_count(); for _ in 0..max_iter { let x_last = x.clone(); @@ -382,7 +379,7 @@ where } let mut norm: f64 = x.values().map(|val| val.powi(2)).sum::().sqrt(); if norm == 0. { - norm = 1.; + return Ok(None); } x = x.iter().map(|(k, v)| (*k, v / norm)).collect(); if x.keys() From 7798859c368b9d175aca37ba5bf32158e8f115a3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Jun 2022 16:27:24 -0400 Subject: [PATCH 05/12] Fix compiler warning --- retworkx-core/src/centrality.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retworkx-core/src/centrality.rs b/retworkx-core/src/centrality.rs index 55ee2a271..41537d5b5 100644 --- a/retworkx-core/src/centrality.rs +++ b/retworkx-core/src/centrality.rs @@ -377,7 +377,7 @@ where *x.get_mut(&neighbor).unwrap() += x_last[node] * w; } } - let mut norm: f64 = x.values().map(|val| val.powi(2)).sum::().sqrt(); + let norm: f64 = x.values().map(|val| val.powi(2)).sum::().sqrt(); if norm == 0. { return Ok(None); } From 09595a14fa9e5ef5526faea5e936fcf976cb6428 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Jun 2022 18:40:56 -0400 Subject: [PATCH 06/12] Switch to a Vec instead of HashMap to track centrality --- retworkx-core/src/centrality.rs | 19 ++++++++++--------- src/centrality.rs | 26 +++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/retworkx-core/src/centrality.rs b/retworkx-core/src/centrality.rs index 41537d5b5..1505b69c0 100644 --- a/retworkx-core/src/centrality.rs +++ b/retworkx-core/src/centrality.rs @@ -352,7 +352,7 @@ pub fn eigenvector_centrality( mut weight_fn: F, max_iter: Option, tol: Option, -) -> Result>, E> +) -> Result>, E> where G: NodeIndexable + IntoNodeIdentifiers + IntoNeighbors + IntoEdges + NodeCount, G::NodeId: Eq + std::hash::Hash, @@ -360,29 +360,30 @@ where { let tol: f64 = tol.unwrap_or(1e-6); let max_iter = max_iter.unwrap_or(100); - let mut x: HashMap = graph.node_identifiers().map(|n| (n, 1.)).collect(); + let mut x: Vec = vec![1.; graph.node_bound()]; let node_count = graph.node_count(); for _ in 0..max_iter { let x_last = x.clone(); - for node in x_last.keys() { - for neighbor in graph.neighbors(*node) { + for node_index in graph.node_identifiers() { + let node = graph.to_index(node_index); + for neighbor in graph.neighbors(node_index) { let w_vec: Vec = graph - .edges(*node) + .edges(node_index) .filter(|edge| edge.target() == neighbor) .collect(); let mut w = 0.; for edge in w_vec { w += weight_fn(edge)?; } - *x.get_mut(&neighbor).unwrap() += x_last[node] * w; + x[graph.to_index(neighbor)] += x_last[node] * w; } } - let norm: f64 = x.values().map(|val| val.powi(2)).sum::().sqrt(); + let norm: f64 = x.iter().map(|val| val.powi(2)).sum::().sqrt(); if norm == 0. { return Ok(None); } - x = x.iter().map(|(k, v)| (*k, v / norm)).collect(); - if x.keys() + x = x.iter().map(|v| v / norm).collect(); + if (0..x.len()) .map(|node| (x[node] - x_last[node]).abs()) .sum::() < node_count as f64 * tol diff --git a/src/centrality.rs b/src/centrality.rs index 36df0b441..07cd77a63 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -18,8 +18,8 @@ use crate::iterators::CentralityMapping; use crate::CostFn; use crate::FailedToConverge; +use petgraph::graph::NodeIndex; use pyo3::prelude::*; - use retworkx_core::centrality; /// Compute the betweenness centrality of all nodes in a PyGraph. @@ -189,7 +189,17 @@ pub fn graph_eigenvector_centrality( )?; match ev_centrality { Some(centrality) => Ok(CentralityMapping { - centralities: centrality.iter().map(|(k, v)| (k.index(), *v)).collect(), + centralities: centrality + .iter() + .enumerate() + .filter_map(|(k, v)| { + if graph.graph.node_weight(NodeIndex::new(k)).is_some() { + Some((k, *v)) + } else { + None + } + }) + .collect(), }), None => Err(FailedToConverge::new_err(format!( "Function failed to converge on a solution in {} iterations", @@ -251,7 +261,17 @@ pub fn digraph_eigenvector_centrality( )?; match ev_centrality { Some(centrality) => Ok(CentralityMapping { - centralities: centrality.iter().map(|(k, v)| (k.index(), *v)).collect(), + centralities: centrality + .iter() + .enumerate() + .filter_map(|(k, v)| { + if graph.graph.node_weight(NodeIndex::new(k)).is_some() { + Some((k, *v)) + } else { + None + } + }) + .collect(), }), None => Err(FailedToConverge::new_err(format!( "Function failed to converge on a solution in {} iterations", From 97fcc616d6cc309c2a790f82a24bf890f79fb28b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Jun 2022 18:58:02 -0400 Subject: [PATCH 07/12] Update retworkx-core tests too --- retworkx-core/src/centrality.rs | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/retworkx-core/src/centrality.rs b/retworkx-core/src/centrality.rs index 1505b69c0..ad349a42c 100644 --- a/retworkx-core/src/centrality.rs +++ b/retworkx-core/src/centrality.rs @@ -333,8 +333,6 @@ where /// /// # Example /// ```rust -/// use hashbrown::HashMap; -/// /// use retworkx_core::Result; /// use retworkx_core::petgraph; /// use retworkx_core::petgraph::visit::{IntoEdges, IntoNodeIdentifiers}; @@ -344,8 +342,7 @@ where /// (0, 1), (1, 2) /// ]); /// // Calculate the eigenvector centrality -/// let output: Result>> = -/// eigenvector_centrality(&g, |_| {Ok(1.)}, None, None); +/// let output: Result>> = eigenvector_centrality(&g, |_| {Ok(1.)}, None, None); /// ``` pub fn eigenvector_centrality( graph: G, @@ -397,8 +394,6 @@ where #[cfg(test)] mod test_eigenvector_centrality { - use hashbrown::HashMap; - use crate::centrality::eigenvector_centrality; use crate::petgraph; use crate::Result; @@ -413,7 +408,7 @@ mod test_eigenvector_centrality { #[test] fn test_no_convergence() { let g = petgraph::graph::UnGraph::::from_edges(&[(0, 1), (1, 2)]); - let output: Result>> = + let output: Result>> = eigenvector_centrality(&g, |_| Ok(1.), Some(0), None); let result = output.unwrap(); assert_eq!(None, result); @@ -433,27 +428,23 @@ mod test_eigenvector_centrality { (2, 4), (3, 4), ]); - let output: Result>> = - eigenvector_centrality(&g, |_| Ok(1.), None, None); + let output: Result>> = eigenvector_centrality(&g, |_| Ok(1.), None, None); let result = output.unwrap().unwrap(); let expected_value: f64 = (1_f64 / 5_f64).sqrt(); let expected_values: Vec = vec![expected_value; 5]; for i in 0..5 { - let index = petgraph::graph::NodeIndex::new(i); - assert_almost_equal!(expected_values[i], result[&index], 1e-4); + assert_almost_equal!(expected_values[i], result[i], 1e-4); } } #[test] fn test_undirected_path_graph() { let g = petgraph::graph::UnGraph::::from_edges(&[(0, 1), (1, 2)]); - let output: Result>> = - eigenvector_centrality(&g, |_| Ok(1.), None, None); + let output: Result>> = eigenvector_centrality(&g, |_| Ok(1.), None, None); let result = output.unwrap().unwrap(); let expected_values: Vec = vec![0.5, 0.7071, 0.5]; for i in 0..3 { - let index = petgraph::graph::NodeIndex::new(i); - assert_almost_equal!(expected_values[i], result[&index], 1e-4); + assert_almost_equal!(expected_values[i], result[i], 1e-4); } } @@ -478,16 +469,14 @@ mod test_eigenvector_centrality { (7, 5), (7, 6), ]); - let output: Result>> = - eigenvector_centrality(&g, |_| Ok(2.), None, None); + let output: Result>> = eigenvector_centrality(&g, |_| Ok(2.), None, None); let result = output.unwrap().unwrap(); let expected_values: Vec = vec![ 0.25368793, 0.19576478, 0.32817092, 0.40430835, 0.48199885, 0.15724483, 0.51346196, 0.32475403, ]; for i in 0..8 { - let index = petgraph::graph::NodeIndex::new(i); - assert_almost_equal!(expected_values[i], result[&index], 1e-4); + assert_almost_equal!(expected_values[i], result[i], 1e-4); } } } From e03856b1f6ee4430d49f45e915e1683074d54884 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 9 Jun 2022 10:13:46 -0400 Subject: [PATCH 08/12] Fix handling of parallel edges This commit fixes an issue where we were overcounting parallel edges in the graph. There was a bug in the logic that was calculating the combined weight of parallel edges incorrectly. Co-authored-by: georgios-ts <45130028+georgios-ts@users.noreply.github.com> --- retworkx-core/src/centrality.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/retworkx-core/src/centrality.rs b/retworkx-core/src/centrality.rs index ad349a42c..d0cc7d51f 100644 --- a/retworkx-core/src/centrality.rs +++ b/retworkx-core/src/centrality.rs @@ -363,15 +363,9 @@ where let x_last = x.clone(); for node_index in graph.node_identifiers() { let node = graph.to_index(node_index); - for neighbor in graph.neighbors(node_index) { - let w_vec: Vec = graph - .edges(node_index) - .filter(|edge| edge.target() == neighbor) - .collect(); - let mut w = 0.; - for edge in w_vec { - w += weight_fn(edge)?; - } + for edge in graph.edges(node_index) { + let w = weight_fn(edge)?; + let neighbor = edge.target(); x[graph.to_index(neighbor)] += x_last[node] * w; } } From 3204e09eb55509860ead214a116755ec86148637 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 9 Jun 2022 12:04:38 -0400 Subject: [PATCH 09/12] Precompute weight edges from python interface This commit changes the callback from the retworkx functions for determining edge weight to precompute them to avoid requiring a python context in the callback. This make a minor improvement in runtime since we only callback to python once at the very beginning instead of in the middle of the loop. --- src/centrality.rs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/centrality.rs b/src/centrality.rs index 07cd77a63..0cc098e3b 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -19,6 +19,8 @@ use crate::CostFn; use crate::FailedToConverge; use petgraph::graph::NodeIndex; +use petgraph::visit::EdgeIndexable; +use petgraph::visit::EdgeRef; use pyo3::prelude::*; use retworkx_core::centrality; @@ -180,10 +182,17 @@ pub fn graph_eigenvector_centrality( max_iter: usize, tol: f64, ) -> PyResult { - let cost_fn = CostFn::try_from((weight_fn, default_weight))?; + let mut edge_weights = vec![default_weight; graph.graph.edge_bound()]; + if weight_fn.is_some() { + let cost_fn = CostFn::try_from((weight_fn, default_weight))?; + for edge in graph.graph.edge_indices() { + edge_weights[edge.index()] = + cost_fn.call(py, graph.graph.edge_weight(edge).unwrap())?; + } + } let ev_centrality = centrality::eigenvector_centrality( &graph.graph, - |e| cost_fn.call(py, e.weight()), + |e| -> PyResult { Ok(edge_weights[e.id().index()]) }, Some(max_iter), Some(tol), )?; @@ -252,13 +261,21 @@ pub fn digraph_eigenvector_centrality( max_iter: usize, tol: f64, ) -> PyResult { - let cost_fn = CostFn::try_from((weight_fn, default_weight))?; + let mut edge_weights = vec![default_weight; graph.graph.edge_bound()]; + if weight_fn.is_some() { + let cost_fn = CostFn::try_from((weight_fn, default_weight))?; + for edge in graph.graph.edge_indices() { + edge_weights[edge.index()] = + cost_fn.call(py, graph.graph.edge_weight(edge).unwrap())?; + } + } let ev_centrality = centrality::eigenvector_centrality( &graph.graph, - |e| cost_fn.call(py, e.weight()), + |e| -> PyResult { Ok(edge_weights[e.id().index()]) }, Some(max_iter), Some(tol), )?; + match ev_centrality { Some(centrality) => Ok(CentralityMapping { centralities: centrality From 2f279bd3b14aff22d5dd4c2951ef914adf1193e1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 10 Jun 2022 15:18:39 -0400 Subject: [PATCH 10/12] Apply suggestions from code review Co-authored-by: georgios-ts <45130028+georgios-ts@users.noreply.github.com> --- retworkx-core/src/centrality.rs | 4 +++- src/centrality.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/retworkx-core/src/centrality.rs b/retworkx-core/src/centrality.rs index d0cc7d51f..b3e51c76b 100644 --- a/retworkx-core/src/centrality.rs +++ b/retworkx-core/src/centrality.rs @@ -373,7 +373,9 @@ where if norm == 0. { return Ok(None); } - x = x.iter().map(|v| v / norm).collect(); + for v in x.iter_mut() { + *v /= norm; + } if (0..x.len()) .map(|node| (x[node] - x_last[node]).abs()) .sum::() diff --git a/src/centrality.rs b/src/centrality.rs index 0cc098e3b..0fb372a2b 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -282,7 +282,7 @@ pub fn digraph_eigenvector_centrality( .iter() .enumerate() .filter_map(|(k, v)| { - if graph.graph.node_weight(NodeIndex::new(k)).is_some() { + if graph.graph.contains_node(NodeIndex::new(k)) { Some((k, *v)) } else { None From be031be67cdaaf48ba65bc2f815874ad34c53725 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 10 Jun 2022 15:18:48 -0400 Subject: [PATCH 11/12] Update src/centrality.rs Co-authored-by: georgios-ts <45130028+georgios-ts@users.noreply.github.com> --- src/centrality.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/centrality.rs b/src/centrality.rs index 0fb372a2b..f8825944c 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -202,7 +202,7 @@ pub fn graph_eigenvector_centrality( .iter() .enumerate() .filter_map(|(k, v)| { - if graph.graph.node_weight(NodeIndex::new(k)).is_some() { + if graph.graph.contains_node(NodeIndex::new(k)) { Some((k, *v)) } else { None From 9a6ea1a97b870c0f3564d46df7588dfec34b9537 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 10 Jun 2022 17:04:41 -0400 Subject: [PATCH 12/12] Add docs to toc --- docs/source/api.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/api.rst b/docs/source/api.rst index ab105d197..6dc8117e8 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -54,6 +54,7 @@ Centrality :toctree: apiref retworkx.betweenness_centrality + retworkx.eigenvector_centrality .. _traversal: @@ -311,6 +312,7 @@ the functions from the explicitly typed based on the data type. retworkx.digraph_spring_layout retworkx.digraph_num_shortest_paths_unweighted retworkx.digraph_betweenness_centrality + retworkx.digraph_eigenvector_centrality retworkx.digraph_unweighted_average_shortest_path_length retworkx.digraph_bfs_search retworkx.digraph_dijkstra_search @@ -363,6 +365,7 @@ typed API based on the data type. retworkx.graph_spring_layout retworkx.graph_num_shortest_paths_unweighted retworkx.graph_betweenness_centrality + retworkx.graph_eigenvector_centrality retworkx.graph_unweighted_average_shortest_path_length retworkx.graph_bfs_search retworkx.graph_dijkstra_search