diff --git a/.gitignore b/.gitignore index 6e09a4a58b..0a6250c513 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ retworkx/*pyd **/*.so retworkx-core/Cargo.lock **/.DS_Store +venv/ +requirements.txt diff --git a/rustworkx/__init__.py b/rustworkx/__init__.py index be2419eea2..30b60d6bcb 100644 --- a/rustworkx/__init__.py +++ b/rustworkx/__init__.py @@ -1184,6 +1184,58 @@ def closeness_centrality(graph, wf_improved=True): raise TypeError("Invalid input type %s for graph" % type(graph)) +@_rustworkx_dispatch +def newman_weighted_closeness_centrality(graph, weight_fn, wf_improved=True, default_weight=0.0): + r"""Compute the weighted closeness centrality of each node in the graph. + + The weighted closeness centrality is an extension of the standard closeness + centrality measure where edge weights represent connection strength rather + than distance. To properly compute shortest paths, weights are inverted + so that stronger connections correspond to shorter effective distances. + The algorithm follows the method described by Newman (2001) in analyzing + weighted graphs.[Newman] + + The edges originally represent connection strength between nodes. + The idea is that if two nodes have a strong connection, the computed + distance between them should be small (shorter), and vice versa. + Note that this assume that the graph is modelling a measure of + connection strength (e.g. trust, collaboration, or similarity). + If the graph is not modelling a measure of connection strength, + the function `weight_fn` should invert the weights before calling this + function, if not it is considered as a logical error. + + In the case of a graphs with more than one connected component there is + an alternative improved formula that calculates the closeness centrality + as "a ratio of the fraction of actors in the group who are reachable, to + the average distance".[WF] + You can enable this by setting `wf_improved` to `true`. + + :param PyGraph graph: The input graph. Can either be a + :class:`~rustworkx.PyGraph` or :class:`~rustworkx.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`` + :param bool wf_improved: This is optional; the default is True. If True, + scale by the fraction of nodes reachable. + :param float default_weight: If ``weight_fn`` is not set the default weight + value to use for the weight of all edges + + :returns: A dictionary mapping each node index to its closeness centrality. + :rtype: CentralityMapping + + .. [Newman]: Newman, M. E. J. (2001). Scientific collaboration networks. + II. Shortest paths, weighted networks, and centrality. + Physical Review E, 64(1), 016132. + + .. [WF]: Wasserman, S., & Faust, K. (1994). Social Network Analysis: + Methods and Applications (Structural Analysis in the Social Sciences). + Cambridge: Cambridge University Press. + + """ + raise TypeError("Invalid input type %s for graph" % type(graph)) + + @_rustworkx_dispatch def degree_centrality(graph): r"""Compute the degree centrality of each node in a graph object. diff --git a/src/centrality.rs b/src/centrality.rs index 563239f5fc..22353c0f99 100644 --- a/src/centrality.rs +++ b/src/centrality.rs @@ -360,6 +360,136 @@ pub fn digraph_closeness_centrality( } } +/// Compute the weighted closeness centrality of each node in the graph. +/// +/// The weighted closeness centrality is an extension of the standard closeness +/// centrality measure where edge weights represent connection strength rather +/// than distance. To properly compute shortest paths, weights are inverted +/// so that stronger connections correspond to shorter effective distances. +/// The algorithm follows the method described by Newman (2001) in analyzing +/// weighted graphs.[Newman] +/// +/// The edges originally represent connection strength between nodes. +/// The idea is that if two nodes have a strong connection, the computed +/// distance between them should be small (shorter), and vice versa. +/// Note that this assume that the graph is modelling a measure of +/// connection strength (e.g. trust, collaboration, or similarity). +/// If the graph is not modelling a measure of connection strength, +/// the function `weight_fn` should invert the weights before calling this +/// function, if not it is considered as a logical error. +/// +/// In the case of a graphs with more than one connected component there is +/// an alternative improved formula that calculates the closeness centrality +/// as "a ratio of the fraction of actors in the group who are reachable, to +/// the average distance".[WF] +/// You can enable this by setting `wf_improved` to `true`. +/// +/// :param PyGraph graph: The input graph. Can either be a +/// :class:`~rustworkx.PyGraph` or :class:`~rustworkx.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`` +/// :param bool wf_improved: This is optional; the default is True. If True, +/// scale by the fraction of nodes reachable. +/// :param float default_weight: If ``weight_fn`` is not set the default weight +/// value to use for the weight of all edges +/// +/// :returns: A dictionary mapping each node index to its closeness centrality. +/// :rtype: CentralityMapping +#[pyfunction(signature = (graph, weight_fn, wf_improved=true, default_weight = 0.0))] +pub fn graph_newman_weighted_closeness_centrality( + py: Python, + graph: &graph::PyGraph, + weight_fn: PyObject, + wf_improved: bool, + default_weight: f64, +) -> PyResult { + let mut edge_weights = vec![default_weight; graph.graph.edge_bound()]; + let cost_fn = CostFn::from(weight_fn); + for edge in graph.graph.edge_indices() { + edge_weights[edge.index()] = cost_fn.call(py, graph.graph.edge_weight(edge).unwrap())?; + } + + let closeness = + centrality::newman_weighted_closeness_centrality(&graph.graph, wf_improved, |e| { + edge_weights[e.id().index()] + }); + + Ok(CentralityMapping { + centralities: closeness + .into_iter() + .enumerate() + .filter_map(|(i, v)| v.map(|x| (i, x))) + .collect(), + }) +} + +/// Compute the weighted closeness centrality of each node in the graph. +/// +/// The weighted closeness centrality is an extension of the standard closeness +/// centrality measure where edge weights represent connection strength rather +/// than distance. To properly compute shortest paths, weights are inverted +/// so that stronger connections correspond to shorter effective distances. +/// The algorithm follows the method described by Newman (2001) in analyzing +/// weighted graphs.[Newman] +/// +/// The edges originally represent connection strength between nodes. +/// The idea is that if two nodes have a strong connection, the computed +/// distance between them should be small (shorter), and vice versa. +/// Note that this assume that the graph is modelling a measure of +/// connection strength (e.g. trust, collaboration, or similarity). +/// If the graph is not modelling a measure of connection strength, +/// the function `weight_fn` should invert the weights before calling this +/// function, if not it is considered as a logical error. +/// +/// In the case of a graphs with more than one connected component there is +/// an alternative improved formula that calculates the closeness centrality +/// as "a ratio of the fraction of actors in the group who are reachable, to +/// the average distance".[WF] +/// You can enable this by setting `wf_improved` to `true`. +/// +/// :param PyDiGraph graph: The input graph. Can either be a +/// :class:`~rustworkx.PyGraph` or :class:`~rustworkx.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`` +/// :param bool wf_improved: This is optional; the default is True. If True, +/// scale by the fraction of nodes reachable. +/// :param float default_weight: If ``weight_fn`` is not set the default weight +/// value to use for the weight of all edges +/// +/// :returns: A dictionary mapping each node index to its closeness centrality. +/// :rtype: CentralityMapping +#[pyfunction(signature = (graph, weight_fn, wf_improved=true, default_weight = 0.0))] +pub fn digraph_newman_weighted_closeness_centrality( + py: Python, + graph: &digraph::PyDiGraph, + weight_fn: PyObject, + wf_improved: bool, + default_weight: f64, +) -> PyResult { + let mut edge_weights = vec![default_weight; graph.graph.edge_bound()]; + let cost_fn = CostFn::from(weight_fn); + for edge in graph.graph.edge_indices() { + edge_weights[edge.index()] = cost_fn.call(py, graph.graph.edge_weight(edge).unwrap())?; + } + + let closeness = + centrality::newman_weighted_closeness_centrality(&graph.graph, wf_improved, |e| { + edge_weights[e.id().index()] + }); + + Ok(CentralityMapping { + centralities: closeness + .into_iter() + .enumerate() + .filter_map(|(i, v)| v.map(|x| (i, x))) + .collect(), + }) +} + /// Compute the edge betweenness centrality of all edges in a :class:`~PyGraph`. /// /// Edge betweenness centrality of an edge :math:`e` is the sum of the diff --git a/src/lib.rs b/src/lib.rs index d58bc038ce..915238bf50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -530,6 +530,8 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { 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_newman_weighted_closeness_centrality))?; + m.add_wrapped(wrap_pyfunction!(digraph_newman_weighted_closeness_centrality))?; m.add_wrapped(wrap_pyfunction!(graph_edge_betweenness_centrality))?; m.add_wrapped(wrap_pyfunction!(digraph_edge_betweenness_centrality))?; m.add_wrapped(wrap_pyfunction!(graph_eigenvector_centrality))?;