Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of closeness_centrality #593

Merged
merged 11 commits into from
Mar 10, 2023
3 changes: 3 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Centrality
:toctree: apiref

retworkx.betweenness_centrality
retworkx.closeness_centrality

.. _traversal:

Expand Down Expand Up @@ -289,6 +290,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_closeness_centrality
retworkx.digraph_unweighted_average_shortest_path_length
retworkx.digraph_bfs_search
retworkx.digraph_dijkstra_search
Expand Down Expand Up @@ -336,6 +338,7 @@ typed API based on the data type.
retworkx.graph_spring_layout
retworkx.graph_num_shortest_paths_unweighted
retworkx.graph_betweenness_centrality
retworkx.graph_closeness_centrality
retworkx.graph_unweighted_average_shortest_path_length
retworkx.graph_bfs_search
retworkx.graph_dijkstra_search
Expand Down
76 changes: 70 additions & 6 deletions retworkx-core/src/centrality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,11 @@ use std::collections::VecDeque;
use std::sync::RwLock;

use hashbrown::HashMap;
use petgraph::algo::dijkstra;
use petgraph::graph::NodeIndex;
use petgraph::visit::{
GraphBase,
GraphProp, // allows is_directed
IntoNeighborsDirected,
IntoNodeIdentifiers,
NodeCount,
NodeIndexable,
GraphBase, GraphProp, IntoEdges, IntoEdgesDirected, IntoNeighborsDirected, IntoNodeIdentifiers,
NodeCount, NodeIndexable, Reversed, Visitable,
};
use rayon::prelude::*;

Expand Down Expand Up @@ -297,3 +294,70 @@ where
sigma,
}
}

/// Compute the closeness centrality of all nodes in a graph.
georgios-ts marked this conversation as resolved.
Show resolved Hide resolved
///
/// Arguments:
///
/// * `graph` - The graph object to run the algorithm on
/// * `wf_improved` - If True, scale by the fraction of nodes reachable.
///
/// # Example
/// ```rust
/// use retworkx_core::petgraph;
/// use retworkx_core::centrality::closeness_centrality;
///
/// // Calculate the betweeness centrality of Graph
georgios-ts marked this conversation as resolved.
Show resolved Hide resolved
/// let g = petgraph::graph::UnGraph::<i32, ()>::from_edges(&[
/// (0, 4), (1, 2), (2, 3), (3, 4), (1, 4)
/// ]);
/// let output = closeness_centrality(&g, true);
/// assert_eq!(
/// vec![Some(1./2.), Some(2./3.), Some(4./7.), Some(2./3.), Some(4./5.)],
/// output
/// );
///
/// // Calculate the betweeness centrality of DiGraph
georgios-ts marked this conversation as resolved.
Show resolved Hide resolved
/// let dg = petgraph::graph::DiGraph::<i32, ()>::from_edges(&[
/// (0, 4), (1, 2), (2, 3), (3, 4), (1, 4)
/// ]);
/// let output = closeness_centrality(&dg, true);
/// assert_eq!(
/// vec![Some(0.), Some(0.), Some(1./4.), Some(1./3.), Some(4./5.)],
/// output
/// );
/// ```
pub fn closeness_centrality<G>(graph: G, wf_improved: bool) -> Vec<Option<f64>>
where
G: NodeIndexable
+ IntoNodeIdentifiers
+ GraphBase<NodeId = NodeIndex>
+ IntoEdges
+ Visitable
+ NodeCount
+ IntoEdgesDirected,
{
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
let max_index = graph.node_bound();
let mut betweenness: Vec<Option<f64>> = vec![None; max_index];
georgios-ts marked this conversation as resolved.
Show resolved Hide resolved
for node_s in graph.node_identifiers() {
let is = graph.to_index(node_s);
let map = dijkstra(Reversed(&graph), node_s, None, |_| 1);
let mut reachable_nodes_count = 0;
let mut dists_sum = 0;
for (_, &value) in map.iter() {
reachable_nodes_count += 1;
dists_sum += value;
}
georgios-ts marked this conversation as resolved.
Show resolved Hide resolved
if reachable_nodes_count == 1 {
betweenness[is] = Some(0.0);
continue;
}
betweenness[is] = Some((reachable_nodes_count - 1) as f64 / dists_sum as f64);
if wf_improved {
let node_count = graph.node_count();
betweenness[is] = betweenness[is]
.map(|c| c * (reachable_nodes_count - 1) as f64 / (node_count - 1) as f64);
}
}
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
betweenness
}
112 changes: 112 additions & 0 deletions retworkx-core/tests/centrality.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use petgraph::visit::Reversed;
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
use petgraph::Graph;
use retworkx_core::centrality::closeness_centrality;
use retworkx_core::petgraph::graph::{DiGraph, UnGraph};

#[test]
fn test_simple() {
let g = UnGraph::<i32, ()>::from_edges(&[(1, 2), (2, 3), (3, 4), (1, 4)]);
let c = closeness_centrality(&g, true);
assert_eq!(
vec![
Some(0.0),
Some(0.5625),
Some(0.5625),
Some(0.5625),
Some(0.5625)
],
c
);
}

#[test]
fn test_wf_improved() {
let g = UnGraph::<i32, ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (4, 5), (5, 6)]);
let c = closeness_centrality(&g, true);
assert_eq!(
vec![
Some(1. / 4.),
Some(3. / 8.),
Some(3. / 8.),
Some(1. / 4.),
Some(2. / 9.),
Some(1. / 3.),
Some(2. / 9.)
],
c
);
let cwf = closeness_centrality(&g, false);
assert_eq!(
vec![
Some(1. / 2.),
Some(3. / 4.),
Some(3. / 4.),
Some(1. / 2.),
Some(2. / 3.),
Some(1.),
Some(2. / 3.)
],
cwf
);
}

#[test]
fn test_digraph() {
let g = DiGraph::<i32, ()>::from_edges(&[(0, 1), (1, 2)]);
let c = closeness_centrality(&g, true);
assert_eq!(vec![Some(0.), Some(1. / 2.), Some(2. / 3.)], c);

let cr = closeness_centrality(Reversed(&g), true);
assert_eq!(vec![Some(2. / 3.), Some(1. / 2.), Some(0.)], cr);
}

#[test]
fn test_k5() {
let g = 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 c = closeness_centrality(&g, true);
assert_eq!(
vec![Some(1.0), Some(1.0), Some(1.0), Some(1.0), Some(1.0)],
c
);
}

#[test]
fn test_path() {
let g = UnGraph::<i32, ()>::from_edges(&[(0, 1), (1, 2)]);
let c = closeness_centrality(&g, true);
assert_eq!(vec![Some(2. / 3.), Some(1.), Some(2. / 3.)], c);
}

#[test]
fn test_weighted_closeness() {
georgios-ts marked this conversation as resolved.
Show resolved Hide resolved
let mut g = Graph::new();
let s = g.add_node(0);
let u = g.add_node(0);
let x = g.add_node(0);
let v = g.add_node(0);
let y = g.add_node(0);
g.add_edge(s, u, 10.);
g.add_edge(s, x, 5.);
g.add_edge(u, v, 1.);
g.add_edge(u, x, 2.);
g.add_edge(v, y, 1.);
g.add_edge(x, u, 3.);
g.add_edge(x, v, 5.);
g.add_edge(x, y, 2.);
g.add_edge(y, s, 7.);
g.add_edge(y, v, 6.);
let c = closeness_centrality(&g, true);
println!("{:?}", c);
georgios-ts marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(0, 0)
georgios-ts marked this conversation as resolved.
Show resolved Hide resolved
}
23 changes: 23 additions & 0 deletions retworkx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,29 @@ def _graph_betweenness_centrality(graph, normalized=True, endpoints=False, paral
)


@functools.singledispatch
def closeness_centrality(graph, wf_improved=True):
r"""Returns the closeness centrality of each node in the graph.

:param PyDiGraph graph: The input graph
georgios-ts marked this conversation as resolved.
Show resolved Hide resolved
:param bool wf_improved: If True, scale by the fraction of nodes reachable.

:returns: A dictionary mapping each node index to its closeness centrality.
:rtype: dict
"""
raise TypeError("Invalid input type %s for graph" % type(graph))


@closeness_centrality.register(PyDiGraph)
def _digraph_closeness_centrality(graph, wf_improved=True):
return digraph_closeness_centrality(graph, wf_improved=wf_improved)


@closeness_centrality.register(PyGraph)
def _graph_closeness_centrality(graph, wf_improved=True):
return graph_closeness_centrality(graph, wf_improved=wf_improved)


@functools.singledispatch
def vf2_mapping(
first,
Expand Down
45 changes: 45 additions & 0 deletions src/centrality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,48 @@ pub fn digraph_betweenness_centrality(
.collect(),
}
}

/// Compute the closeness centrality of all nodes in a PyGraph.
///
/// :param PyGraph graph: The input graph
/// :param bool normalized: Whether to normalize the betweenness scores by the number of distinct
/// paths between all pairs of nodes.
/// :param bool endpoints: Whether to include the endpoints of paths in pathlengths used to
/// compute the betweenness.
/// :param int parallel_threshold: The number of nodes to calculate the
/// the betweenness centrality in parallel at if the number of nodes in
/// the graph is less than this value it will run in a single thread. The
/// default value is 50
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
///
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
/// betweenness score for each node.
/// :rtype: CentralityMapping
#[pyfunction(wf_improved = "true")]
#[pyo3(text_signature = "(graph, /, wf_improved=True)")]
pub fn graph_closeness_centrality(graph: &graph::PyGraph, wf_improved: bool) -> CentralityMapping {
let betweenness = centrality::closeness_centrality(&graph.graph, wf_improved);
CentralityMapping {
centralities: betweenness
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
.into_iter()
.enumerate()
.filter_map(|(i, v)| v.map(|x| (i, x)))
.collect(),
}
}

/// Compute the closeness centrality of all nodes in a PyDiGraph.
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
#[pyfunction(wf_improved = "true")]
#[pyo3(text_signature = "(graph, /, wf_improved=True)")]
pub fn digraph_closeness_centrality(
graph: &digraph::PyDiGraph,
wf_improved: bool,
) -> CentralityMapping {
let betweenness = centrality::closeness_centrality(&graph.graph, wf_improved);
CentralityMapping {
centralities: betweenness
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
.into_iter()
.enumerate()
.filter_map(|(i, v)| v.map(|x| (i, x)))
.collect(),
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(graph_all_pairs_dijkstra_shortest_paths))?;
m.add_wrapped(wrap_pyfunction!(graph_betweenness_centrality))?;
m.add_wrapped(wrap_pyfunction!(digraph_betweenness_centrality))?;
m.add_wrapped(wrap_pyfunction!(graph_closeness_centrality))?;
m.add_wrapped(wrap_pyfunction!(digraph_closeness_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))?;
Expand Down
20 changes: 20 additions & 0 deletions tests/digraph/test_centrality.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ def test_betweenness_centrality_unnormalized_parallel(self):
expected = {0: 0.0, 1: 2.0, 2: 2.0, 3: 0.0}
self.assertEqual(expected, betweenness)

def test_closeness_centrality(self):
betweenness = retworkx.digraph_closeness_centrality(self.graph)
expected = {0: 0.0, 1: 1.0 / 3.0, 2: 4.0 / 9.0, 3: 0.5}
self.assertEqual(expected, betweenness)
mtreinish marked this conversation as resolved.
Show resolved Hide resolved

def test_closeness_centrality_wf_improved(self):
betweenness = retworkx.digraph_closeness_centrality(self.graph, wf_improved=False)
expected = {0: 0.0, 1: 1.0, 2: 2.0 / 3.0, 3: 0.5}
self.assertEqual(expected, betweenness)
mtreinish marked this conversation as resolved.
Show resolved Hide resolved


class TestCentralityDiGraphDeletedNode(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -128,3 +138,13 @@ def test_betweenness_centrality_unnormalized(self):
)
expected = {0: 0.0, 1: 2.0, 2: 2.0, 4: 0.0}
self.assertEqual(expected, betweenness)

def test_closeness_centrality(self):
betweenness = retworkx.digraph_closeness_centrality(self.graph)
expected = {0: 0.0, 1: 1.0 / 3.0, 2: 4.0 / 9.0, 4: 0.5}
self.assertEqual(expected, betweenness)
mtreinish marked this conversation as resolved.
Show resolved Hide resolved

def test_closeness_centrality_wf_improved(self):
betweenness = retworkx.digraph_closeness_centrality(self.graph, wf_improved=False)
expected = {0: 0.0, 1: 1.0, 2: 2.0 / 3.0, 4: 0.5}
self.assertEqual(expected, betweenness)
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
20 changes: 20 additions & 0 deletions tests/graph/test_centrality.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ def test_betweenness_centrality_unnormalized(self):
expected = {0: 0.0, 1: 2.0, 2: 2.0, 3: 0.0}
self.assertEqual(expected, betweenness)

def test_closeness_centrality(self):
betweenness = retworkx.graph_closeness_centrality(self.graph)
expected = {0: 0.5, 1: 0.75, 2: 0.75, 3: 0.5}
self.assertEqual(expected, betweenness)
mtreinish marked this conversation as resolved.
Show resolved Hide resolved

def test_closeness_centrality_wf_improved(self):
betweenness = retworkx.graph_closeness_centrality(self.graph, wf_improved=False)
expected = {0: 0.5, 1: 0.75, 2: 0.75, 3: 0.5}
self.assertEqual(expected, betweenness)
mtreinish marked this conversation as resolved.
Show resolved Hide resolved


class TestCentralityGraphDeletedNode(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -99,3 +109,13 @@ def test_betweenness_centrality_unnormalized(self):
)
expected = {0: 0.0, 1: 2.0, 2: 2.0, 4: 0.0}
self.assertEqual(expected, betweenness)

def test_closeness_centrality(self):
betweenness = retworkx.graph_closeness_centrality(self.graph)
expected = {0: 0.5, 1: 0.75, 2: 0.75, 4: 0.5}
self.assertEqual(expected, betweenness)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
betweenness = retworkx.graph_closeness_centrality(self.graph)
expected = {0: 0.5, 1: 0.75, 2: 0.75, 4: 0.5}
self.assertEqual(expected, betweenness)
closeness = retworkx.graph_closeness_centrality(self.graph)
expected = {0: 0.5, 1: 0.75, 2: 0.75, 4: 0.5}
self.assertEqual(expected, closeness)


def test_closeness_centrality_wf_improved(self):
betweenness = retworkx.graph_closeness_centrality(self.graph, wf_improved=False)
expected = {0: 0.5, 1: 0.75, 2: 0.75, 4: 0.5}
self.assertEqual(expected, betweenness)
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions tests/test_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ def test_betweenness_centrality(self):
res = retworkx.betweenness_centrality(self.graph)
self.assertIsInstance(res, retworkx.CentralityMapping)

def test_closeness_centrality(self):
res = retworkx.closeness_centrality(self.graph)
self.assertIsInstance(res, retworkx.CentralityMapping)


class TestDispatchPyDiGraph(TestDispatchPyGraph):

Expand Down