Skip to content

Commit

Permalink
Add Eigenvector centrality function
Browse files Browse the repository at this point in the history
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 Qiskit#441
  • Loading branch information
mtreinish committed Jun 4, 2022
1 parent a10e453 commit 3b68fc4
Show file tree
Hide file tree
Showing 7 changed files with 444 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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.
201 changes: 201 additions & 0 deletions retworkx-core/src/centrality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
/// <https://doi.org/10.1086/228631>
///
/// 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<f64>` 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::<i32, ()>::from_edges(&[
/// (0, 1), (1, 2)
/// ]);
/// // Calculate the betweeness centrality
/// let output: Result<Option<HashMap<petgraph::graph::NodeIndex, f64>>> =
/// eigenvector_centrality(&g, |_| {Ok(1.)}, None, None);
/// ```
pub fn eigenvector_centrality<G, F, E>(
graph: G,
mut weight_fn: F,
max_iter: Option<usize>,
tol: Option<f64>,
) -> Result<Option<HashMap<G::NodeId, f64>>, E>
where
G: NodeIndexable
+ IntoNodeIdentifiers
+ IntoNeighbors
+ IntoEdges
+ NodeCount
+ GraphProp
+ GraphBase<NodeId = NodeIndex>,
F: FnMut(G::EdgeRef) -> Result<f64, E>,
{
let tol: f64 = tol.unwrap_or(1e-6);
let max_iter = max_iter.unwrap_or(50);
let n_start: HashMap<G::NodeId, f64> = graph.node_identifiers().map(|n| (n, 1.)).collect();
let n_start_sum: f64 = n_start.len() as f64;
let mut x: HashMap<G::NodeId, f64> =
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<G::EdgeRef> = 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::<f64>().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::<f64>()
< 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::<i32, ()>::from_edges(&[(0, 1), (1, 2)]);
let output: Result<Option<HashMap<petgraph::graph::NodeIndex, f64>>> =
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::<i32, ()>::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<Option<HashMap<petgraph::graph::NodeIndex, f64>>> =
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<f64> = 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::<i32, ()>::from_edges(&[(0, 1), (1, 2)]);
let output: Result<Option<HashMap<petgraph::graph::NodeIndex, f64>>> =
eigenvector_centrality(&g, |_| Ok(1.), None, None);
let result = output.unwrap().unwrap();
let expected_values: Vec<f64> = 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::<i32, ()>::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<Option<HashMap<petgraph::graph::NodeIndex, f64>>> =
eigenvector_centrality(&g, |_| Ok(2.), None, None);
let result = output.unwrap().unwrap();
let expected_values: Vec<f64> = 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);
}
}
}
57 changes: 57 additions & 0 deletions retworkx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://doi.org/10.1086/228631>
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.
: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,
Expand Down
Loading

0 comments on commit 3b68fc4

Please sign in to comment.