|
"""Graph diameter, radius, eccentricity and other properties.""" |
|
|
|
import networkx as nx |
|
from networkx.utils import not_implemented_for |
|
|
|
__all__ = [ |
|
"eccentricity", |
|
"diameter", |
|
"radius", |
|
"periphery", |
|
"center", |
|
"barycenter", |
|
"resistance_distance", |
|
"kemeny_constant", |
|
"effective_graph_resistance", |
|
] |
|
|
|
|
|
def _extrema_bounding(G, compute="diameter", weight=None): |
|
"""Compute requested extreme distance metric of undirected graph G |
|
|
|
Computation is based on smart lower and upper bounds, and in practice |
|
linear in the number of nodes, rather than quadratic (except for some |
|
border cases such as complete graphs or circle shaped graphs). |
|
|
|
Parameters |
|
---------- |
|
G : NetworkX graph |
|
An undirected graph |
|
|
|
compute : string denoting the requesting metric |
|
"diameter" for the maximal eccentricity value, |
|
"radius" for the minimal eccentricity value, |
|
"periphery" for the set of nodes with eccentricity equal to the diameter, |
|
"center" for the set of nodes with eccentricity equal to the radius, |
|
"eccentricities" for the maximum distance from each node to all other nodes in G |
|
|
|
weight : string, function, or None |
|
If this is a string, then edge weights will be accessed via the |
|
edge attribute with this key (that is, the weight of the edge |
|
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no |
|
such edge attribute exists, the weight of the edge is assumed to |
|
be one. |
|
|
|
If this is a function, the weight of an edge is the value |
|
returned by the function. The function must accept exactly three |
|
positional arguments: the two endpoints of an edge and the |
|
dictionary of edge attributes for that edge. The function must |
|
return a number. |
|
|
|
If this is None, every edge has weight/distance/cost 1. |
|
|
|
Weights stored as floating point values can lead to small round-off |
|
errors in distances. Use integer weights to avoid this. |
|
|
|
Weights should be positive, since they are distances. |
|
|
|
Returns |
|
------- |
|
value : value of the requested metric |
|
int for "diameter" and "radius" or |
|
list of nodes for "center" and "periphery" or |
|
dictionary of eccentricity values keyed by node for "eccentricities" |
|
|
|
Raises |
|
------ |
|
NetworkXError |
|
If the graph consists of multiple components |
|
ValueError |
|
If `compute` is not one of "diameter", "radius", "periphery", "center", or "eccentricities". |
|
|
|
Notes |
|
----- |
|
This algorithm was proposed in [1]_ and discussed further in [2]_ and [3]_. |
|
|
|
References |
|
---------- |
|
.. [1] F. W. Takes, W. A. Kosters, |
|
"Determining the diameter of small world networks." |
|
Proceedings of the 20th ACM international conference on Information and knowledge management, 2011 |
|
https://dl.acm.org/doi/abs/10.1145/2063576.2063748 |
|
.. [2] F. W. Takes, W. A. Kosters, |
|
"Computing the Eccentricity Distribution of Large Graphs." |
|
Algorithms, 2013 |
|
https://www.mdpi.com/1999-4893/6/1/100 |
|
.. [3] M. Borassi, P. Crescenzi, M. Habib, W. A. Kosters, A. Marino, F. W. Takes, |
|
"Fast diameter and radius BFS-based computation in (weakly connected) real-world graphs: With an application to the six degrees of separation games. " |
|
Theoretical Computer Science, 2015 |
|
https://www.sciencedirect.com/science/article/pii/S0304397515001644 |
|
""" |
|
|
|
degrees = dict(G.degree()) |
|
minlowernode = max(degrees, key=degrees.get) |
|
N = len(degrees) |
|
|
|
high = False |
|
|
|
ecc_lower = dict.fromkeys(G, 0) |
|
ecc_upper = dict.fromkeys(G, N) |
|
candidates = set(G) |
|
|
|
|
|
minlower = N |
|
maxlower = 0 |
|
minupper = N |
|
maxupper = 0 |
|
|
|
|
|
while candidates: |
|
if high: |
|
current = maxuppernode |
|
else: |
|
current = minlowernode |
|
high = not high |
|
|
|
|
|
dist = nx.shortest_path_length(G, source=current, weight=weight) |
|
|
|
if len(dist) != N: |
|
msg = "Cannot compute metric because graph is not connected." |
|
raise nx.NetworkXError(msg) |
|
current_ecc = max(dist.values()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
maxuppernode = None |
|
minlowernode = None |
|
|
|
|
|
for i in candidates: |
|
|
|
d = dist[i] |
|
ecc_lower[i] = low = max(ecc_lower[i], max(d, (current_ecc - d))) |
|
ecc_upper[i] = upp = min(ecc_upper[i], current_ecc + d) |
|
|
|
|
|
minlower = min(ecc_lower[i], minlower) |
|
maxlower = max(ecc_lower[i], maxlower) |
|
minupper = min(ecc_upper[i], minupper) |
|
maxupper = max(ecc_upper[i], maxupper) |
|
|
|
|
|
if compute == "diameter": |
|
ruled_out = { |
|
i |
|
for i in candidates |
|
if ecc_upper[i] <= maxlower and 2 * ecc_lower[i] >= maxupper |
|
} |
|
elif compute == "radius": |
|
ruled_out = { |
|
i |
|
for i in candidates |
|
if ecc_lower[i] >= minupper and ecc_upper[i] + 1 <= 2 * minlower |
|
} |
|
elif compute == "periphery": |
|
ruled_out = { |
|
i |
|
for i in candidates |
|
if ecc_upper[i] < maxlower |
|
and (maxlower == maxupper or ecc_lower[i] > maxupper) |
|
} |
|
elif compute == "center": |
|
ruled_out = { |
|
i |
|
for i in candidates |
|
if ecc_lower[i] > minupper |
|
and (minlower == minupper or ecc_upper[i] + 1 < 2 * minlower) |
|
} |
|
elif compute == "eccentricities": |
|
ruled_out = set() |
|
else: |
|
msg = "compute must be one of 'diameter', 'radius', 'periphery', 'center', 'eccentricities'" |
|
raise ValueError(msg) |
|
|
|
ruled_out.update(i for i in candidates if ecc_lower[i] == ecc_upper[i]) |
|
candidates -= ruled_out |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for i in candidates: |
|
if ( |
|
minlowernode is None |
|
or ( |
|
ecc_lower[i] == ecc_lower[minlowernode] |
|
and degrees[i] > degrees[minlowernode] |
|
) |
|
or (ecc_lower[i] < ecc_lower[minlowernode]) |
|
): |
|
minlowernode = i |
|
|
|
if ( |
|
maxuppernode is None |
|
or ( |
|
ecc_upper[i] == ecc_upper[maxuppernode] |
|
and degrees[i] > degrees[maxuppernode] |
|
) |
|
or (ecc_upper[i] > ecc_upper[maxuppernode]) |
|
): |
|
maxuppernode = i |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if compute == "diameter": |
|
return maxlower |
|
if compute == "radius": |
|
return minupper |
|
if compute == "periphery": |
|
p = [v for v in G if ecc_lower[v] == maxlower] |
|
return p |
|
if compute == "center": |
|
c = [v for v in G if ecc_upper[v] == minupper] |
|
return c |
|
if compute == "eccentricities": |
|
return ecc_lower |
|
return None |
|
|
|
|
|
@nx._dispatchable(edge_attrs="weight") |
|
def eccentricity(G, v=None, sp=None, weight=None): |
|
"""Returns the eccentricity of nodes in G. |
|
|
|
The eccentricity of a node v is the maximum distance from v to |
|
all other nodes in G. |
|
|
|
Parameters |
|
---------- |
|
G : NetworkX graph |
|
A graph |
|
|
|
v : node, optional |
|
Return value of specified node |
|
|
|
sp : dict of dicts, optional |
|
All pairs shortest path lengths as a dictionary of dictionaries |
|
|
|
weight : string, function, or None (default=None) |
|
If this is a string, then edge weights will be accessed via the |
|
edge attribute with this key (that is, the weight of the edge |
|
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no |
|
such edge attribute exists, the weight of the edge is assumed to |
|
be one. |
|
|
|
If this is a function, the weight of an edge is the value |
|
returned by the function. The function must accept exactly three |
|
positional arguments: the two endpoints of an edge and the |
|
dictionary of edge attributes for that edge. The function must |
|
return a number. |
|
|
|
If this is None, every edge has weight/distance/cost 1. |
|
|
|
Weights stored as floating point values can lead to small round-off |
|
errors in distances. Use integer weights to avoid this. |
|
|
|
Weights should be positive, since they are distances. |
|
|
|
Returns |
|
------- |
|
ecc : dictionary |
|
A dictionary of eccentricity values keyed by node. |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) |
|
>>> dict(nx.eccentricity(G)) |
|
{1: 2, 2: 3, 3: 2, 4: 2, 5: 3} |
|
|
|
>>> dict(nx.eccentricity(G, v=[1, 5])) # This returns the eccentricity of node 1 & 5 |
|
{1: 2, 5: 3} |
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
order = G.order() |
|
e = {} |
|
for n in G.nbunch_iter(v): |
|
if sp is None: |
|
length = nx.shortest_path_length(G, source=n, weight=weight) |
|
|
|
L = len(length) |
|
else: |
|
try: |
|
length = sp[n] |
|
L = len(length) |
|
except TypeError as err: |
|
raise nx.NetworkXError('Format of "sp" is invalid.') from err |
|
if L != order: |
|
if G.is_directed(): |
|
msg = ( |
|
"Found infinite path length because the digraph is not" |
|
" strongly connected" |
|
) |
|
else: |
|
msg = "Found infinite path length because the graph is not" " connected" |
|
raise nx.NetworkXError(msg) |
|
|
|
e[n] = max(length.values()) |
|
|
|
if v in G: |
|
return e[v] |
|
return e |
|
|
|
|
|
@nx._dispatchable(edge_attrs="weight") |
|
def diameter(G, e=None, usebounds=False, weight=None): |
|
"""Returns the diameter of the graph G. |
|
|
|
The diameter is the maximum eccentricity. |
|
|
|
Parameters |
|
---------- |
|
G : NetworkX graph |
|
A graph |
|
|
|
e : eccentricity dictionary, optional |
|
A precomputed dictionary of eccentricities. |
|
|
|
weight : string, function, or None |
|
If this is a string, then edge weights will be accessed via the |
|
edge attribute with this key (that is, the weight of the edge |
|
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no |
|
such edge attribute exists, the weight of the edge is assumed to |
|
be one. |
|
|
|
If this is a function, the weight of an edge is the value |
|
returned by the function. The function must accept exactly three |
|
positional arguments: the two endpoints of an edge and the |
|
dictionary of edge attributes for that edge. The function must |
|
return a number. |
|
|
|
If this is None, every edge has weight/distance/cost 1. |
|
|
|
Weights stored as floating point values can lead to small round-off |
|
errors in distances. Use integer weights to avoid this. |
|
|
|
Weights should be positive, since they are distances. |
|
|
|
Returns |
|
------- |
|
d : integer |
|
Diameter of graph |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) |
|
>>> nx.diameter(G) |
|
3 |
|
|
|
See Also |
|
-------- |
|
eccentricity |
|
""" |
|
if usebounds is True and e is None and not G.is_directed(): |
|
return _extrema_bounding(G, compute="diameter", weight=weight) |
|
if e is None: |
|
e = eccentricity(G, weight=weight) |
|
return max(e.values()) |
|
|
|
|
|
@nx._dispatchable(edge_attrs="weight") |
|
def periphery(G, e=None, usebounds=False, weight=None): |
|
"""Returns the periphery of the graph G. |
|
|
|
The periphery is the set of nodes with eccentricity equal to the diameter. |
|
|
|
Parameters |
|
---------- |
|
G : NetworkX graph |
|
A graph |
|
|
|
e : eccentricity dictionary, optional |
|
A precomputed dictionary of eccentricities. |
|
|
|
weight : string, function, or None |
|
If this is a string, then edge weights will be accessed via the |
|
edge attribute with this key (that is, the weight of the edge |
|
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no |
|
such edge attribute exists, the weight of the edge is assumed to |
|
be one. |
|
|
|
If this is a function, the weight of an edge is the value |
|
returned by the function. The function must accept exactly three |
|
positional arguments: the two endpoints of an edge and the |
|
dictionary of edge attributes for that edge. The function must |
|
return a number. |
|
|
|
If this is None, every edge has weight/distance/cost 1. |
|
|
|
Weights stored as floating point values can lead to small round-off |
|
errors in distances. Use integer weights to avoid this. |
|
|
|
Weights should be positive, since they are distances. |
|
|
|
Returns |
|
------- |
|
p : list |
|
List of nodes in periphery |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) |
|
>>> nx.periphery(G) |
|
[2, 5] |
|
|
|
See Also |
|
-------- |
|
barycenter |
|
center |
|
""" |
|
if usebounds is True and e is None and not G.is_directed(): |
|
return _extrema_bounding(G, compute="periphery", weight=weight) |
|
if e is None: |
|
e = eccentricity(G, weight=weight) |
|
diameter = max(e.values()) |
|
p = [v for v in e if e[v] == diameter] |
|
return p |
|
|
|
|
|
@nx._dispatchable(edge_attrs="weight") |
|
def radius(G, e=None, usebounds=False, weight=None): |
|
"""Returns the radius of the graph G. |
|
|
|
The radius is the minimum eccentricity. |
|
|
|
Parameters |
|
---------- |
|
G : NetworkX graph |
|
A graph |
|
|
|
e : eccentricity dictionary, optional |
|
A precomputed dictionary of eccentricities. |
|
|
|
weight : string, function, or None |
|
If this is a string, then edge weights will be accessed via the |
|
edge attribute with this key (that is, the weight of the edge |
|
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no |
|
such edge attribute exists, the weight of the edge is assumed to |
|
be one. |
|
|
|
If this is a function, the weight of an edge is the value |
|
returned by the function. The function must accept exactly three |
|
positional arguments: the two endpoints of an edge and the |
|
dictionary of edge attributes for that edge. The function must |
|
return a number. |
|
|
|
If this is None, every edge has weight/distance/cost 1. |
|
|
|
Weights stored as floating point values can lead to small round-off |
|
errors in distances. Use integer weights to avoid this. |
|
|
|
Weights should be positive, since they are distances. |
|
|
|
Returns |
|
------- |
|
r : integer |
|
Radius of graph |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) |
|
>>> nx.radius(G) |
|
2 |
|
|
|
""" |
|
if usebounds is True and e is None and not G.is_directed(): |
|
return _extrema_bounding(G, compute="radius", weight=weight) |
|
if e is None: |
|
e = eccentricity(G, weight=weight) |
|
return min(e.values()) |
|
|
|
|
|
@nx._dispatchable(edge_attrs="weight") |
|
def center(G, e=None, usebounds=False, weight=None): |
|
"""Returns the center of the graph G. |
|
|
|
The center is the set of nodes with eccentricity equal to radius. |
|
|
|
Parameters |
|
---------- |
|
G : NetworkX graph |
|
A graph |
|
|
|
e : eccentricity dictionary, optional |
|
A precomputed dictionary of eccentricities. |
|
|
|
weight : string, function, or None |
|
If this is a string, then edge weights will be accessed via the |
|
edge attribute with this key (that is, the weight of the edge |
|
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no |
|
such edge attribute exists, the weight of the edge is assumed to |
|
be one. |
|
|
|
If this is a function, the weight of an edge is the value |
|
returned by the function. The function must accept exactly three |
|
positional arguments: the two endpoints of an edge and the |
|
dictionary of edge attributes for that edge. The function must |
|
return a number. |
|
|
|
If this is None, every edge has weight/distance/cost 1. |
|
|
|
Weights stored as floating point values can lead to small round-off |
|
errors in distances. Use integer weights to avoid this. |
|
|
|
Weights should be positive, since they are distances. |
|
|
|
Returns |
|
------- |
|
c : list |
|
List of nodes in center |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) |
|
>>> list(nx.center(G)) |
|
[1, 3, 4] |
|
|
|
See Also |
|
-------- |
|
barycenter |
|
periphery |
|
""" |
|
if usebounds is True and e is None and not G.is_directed(): |
|
return _extrema_bounding(G, compute="center", weight=weight) |
|
if e is None: |
|
e = eccentricity(G, weight=weight) |
|
radius = min(e.values()) |
|
p = [v for v in e if e[v] == radius] |
|
return p |
|
|
|
|
|
@nx._dispatchable(edge_attrs="weight", mutates_input={"attr": 2}) |
|
def barycenter(G, weight=None, attr=None, sp=None): |
|
r"""Calculate barycenter of a connected graph, optionally with edge weights. |
|
|
|
The :dfn:`barycenter` a |
|
:func:`connected <networkx.algorithms.components.is_connected>` graph |
|
:math:`G` is the subgraph induced by the set of its nodes :math:`v` |
|
minimizing the objective function |
|
|
|
.. math:: |
|
|
|
\sum_{u \in V(G)} d_G(u, v), |
|
|
|
where :math:`d_G` is the (possibly weighted) :func:`path length |
|
<networkx.algorithms.shortest_paths.generic.shortest_path_length>`. |
|
The barycenter is also called the :dfn:`median`. See [West01]_, p. 78. |
|
|
|
Parameters |
|
---------- |
|
G : :class:`networkx.Graph` |
|
The connected graph :math:`G`. |
|
weight : :class:`str`, optional |
|
Passed through to |
|
:func:`~networkx.algorithms.shortest_paths.generic.shortest_path_length`. |
|
attr : :class:`str`, optional |
|
If given, write the value of the objective function to each node's |
|
`attr` attribute. Otherwise do not store the value. |
|
sp : dict of dicts, optional |
|
All pairs shortest path lengths as a dictionary of dictionaries |
|
|
|
Returns |
|
------- |
|
list |
|
Nodes of `G` that induce the barycenter of `G`. |
|
|
|
Raises |
|
------ |
|
NetworkXNoPath |
|
If `G` is disconnected. `G` may appear disconnected to |
|
:func:`barycenter` if `sp` is given but is missing shortest path |
|
lengths for any pairs. |
|
ValueError |
|
If `sp` and `weight` are both given. |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) |
|
>>> nx.barycenter(G) |
|
[1, 3, 4] |
|
|
|
See Also |
|
-------- |
|
center |
|
periphery |
|
""" |
|
if sp is None: |
|
sp = nx.shortest_path_length(G, weight=weight) |
|
else: |
|
sp = sp.items() |
|
if weight is not None: |
|
raise ValueError("Cannot use both sp, weight arguments together") |
|
smallest, barycenter_vertices, n = float("inf"), [], len(G) |
|
for v, dists in sp: |
|
if len(dists) < n: |
|
raise nx.NetworkXNoPath( |
|
f"Input graph {G} is disconnected, so every induced subgraph " |
|
"has infinite barycentricity." |
|
) |
|
barycentricity = sum(dists.values()) |
|
if attr is not None: |
|
G.nodes[v][attr] = barycentricity |
|
if barycentricity < smallest: |
|
smallest = barycentricity |
|
barycenter_vertices = [v] |
|
elif barycentricity == smallest: |
|
barycenter_vertices.append(v) |
|
if attr is not None: |
|
nx._clear_cache(G) |
|
return barycenter_vertices |
|
|
|
|
|
@not_implemented_for("directed") |
|
@nx._dispatchable(edge_attrs="weight") |
|
def resistance_distance(G, nodeA=None, nodeB=None, weight=None, invert_weight=True): |
|
"""Returns the resistance distance between pairs of nodes in graph G. |
|
|
|
The resistance distance between two nodes of a graph is akin to treating |
|
the graph as a grid of resistors with a resistance equal to the provided |
|
weight [1]_, [2]_. |
|
|
|
If weight is not provided, then a weight of 1 is used for all edges. |
|
|
|
If two nodes are the same, the resistance distance is zero. |
|
|
|
Parameters |
|
---------- |
|
G : NetworkX graph |
|
A graph |
|
|
|
nodeA : node or None, optional (default=None) |
|
A node within graph G. |
|
If None, compute resistance distance using all nodes as source nodes. |
|
|
|
nodeB : node or None, optional (default=None) |
|
A node within graph G. |
|
If None, compute resistance distance using all nodes as target nodes. |
|
|
|
weight : string or None, optional (default=None) |
|
The edge data key used to compute the resistance distance. |
|
If None, then each edge has weight 1. |
|
|
|
invert_weight : boolean (default=True) |
|
Proper calculation of resistance distance requires building the |
|
Laplacian matrix with the reciprocal of the weight. Not required |
|
if the weight is already inverted. Weight cannot be zero. |
|
|
|
Returns |
|
------- |
|
rd : dict or float |
|
If `nodeA` and `nodeB` are given, resistance distance between `nodeA` |
|
and `nodeB`. If `nodeA` or `nodeB` is unspecified (the default), a |
|
dictionary of nodes with resistance distances as the value. |
|
|
|
Raises |
|
------ |
|
NetworkXNotImplemented |
|
If `G` is a directed graph. |
|
|
|
NetworkXError |
|
If `G` is not connected, or contains no nodes, |
|
or `nodeA` is not in `G` or `nodeB` is not in `G`. |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) |
|
>>> round(nx.resistance_distance(G, 1, 3), 10) |
|
0.625 |
|
|
|
Notes |
|
----- |
|
The implementation is based on Theorem A in [2]_. Self-loops are ignored. |
|
Multi-edges are contracted in one edge with weight equal to the harmonic sum of the weights. |
|
|
|
References |
|
---------- |
|
.. [1] Wikipedia |
|
"Resistance distance." |
|
https://en.wikipedia.org/wiki/Resistance_distance |
|
.. [2] D. J. Klein and M. Randic. |
|
Resistance distance. |
|
J. of Math. Chem. 12:81-95, 1993. |
|
""" |
|
import numpy as np |
|
|
|
if len(G) == 0: |
|
raise nx.NetworkXError("Graph G must contain at least one node.") |
|
if not nx.is_connected(G): |
|
raise nx.NetworkXError("Graph G must be strongly connected.") |
|
if nodeA is not None and nodeA not in G: |
|
raise nx.NetworkXError("Node A is not in graph G.") |
|
if nodeB is not None and nodeB not in G: |
|
raise nx.NetworkXError("Node B is not in graph G.") |
|
|
|
G = G.copy() |
|
node_list = list(G) |
|
|
|
|
|
if invert_weight and weight is not None: |
|
if G.is_multigraph(): |
|
for u, v, k, d in G.edges(keys=True, data=True): |
|
d[weight] = 1 / d[weight] |
|
else: |
|
for u, v, d in G.edges(data=True): |
|
d[weight] = 1 / d[weight] |
|
|
|
|
|
|
|
L = nx.laplacian_matrix(G, weight=weight).todense() |
|
Linv = np.linalg.pinv(L, hermitian=True) |
|
|
|
|
|
if nodeA is not None and nodeB is not None: |
|
i = node_list.index(nodeA) |
|
j = node_list.index(nodeB) |
|
return Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i) |
|
|
|
elif nodeA is not None: |
|
i = node_list.index(nodeA) |
|
d = {} |
|
for n in G: |
|
j = node_list.index(n) |
|
d[n] = Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i) |
|
return d |
|
|
|
elif nodeB is not None: |
|
j = node_list.index(nodeB) |
|
d = {} |
|
for n in G: |
|
i = node_list.index(n) |
|
d[n] = Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i) |
|
return d |
|
|
|
else: |
|
d = {} |
|
for n in G: |
|
i = node_list.index(n) |
|
d[n] = {} |
|
for n2 in G: |
|
j = node_list.index(n2) |
|
d[n][n2] = ( |
|
Linv.item(i, i) |
|
+ Linv.item(j, j) |
|
- Linv.item(i, j) |
|
- Linv.item(j, i) |
|
) |
|
return d |
|
|
|
|
|
@not_implemented_for("directed") |
|
@nx._dispatchable(edge_attrs="weight") |
|
def effective_graph_resistance(G, weight=None, invert_weight=True): |
|
"""Returns the Effective graph resistance of G. |
|
|
|
Also known as the Kirchhoff index. |
|
|
|
The effective graph resistance is defined as the sum |
|
of the resistance distance of every node pair in G [1]_. |
|
|
|
If weight is not provided, then a weight of 1 is used for all edges. |
|
|
|
The effective graph resistance of a disconnected graph is infinite. |
|
|
|
Parameters |
|
---------- |
|
G : NetworkX graph |
|
A graph |
|
|
|
weight : string or None, optional (default=None) |
|
The edge data key used to compute the effective graph resistance. |
|
If None, then each edge has weight 1. |
|
|
|
invert_weight : boolean (default=True) |
|
Proper calculation of resistance distance requires building the |
|
Laplacian matrix with the reciprocal of the weight. Not required |
|
if the weight is already inverted. Weight cannot be zero. |
|
|
|
Returns |
|
------- |
|
RG : float |
|
The effective graph resistance of `G`. |
|
|
|
Raises |
|
------ |
|
NetworkXNotImplemented |
|
If `G` is a directed graph. |
|
|
|
NetworkXError |
|
If `G` does not contain any nodes. |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) |
|
>>> round(nx.effective_graph_resistance(G), 10) |
|
10.25 |
|
|
|
Notes |
|
----- |
|
The implementation is based on Theorem 2.2 in [2]_. Self-loops are ignored. |
|
Multi-edges are contracted in one edge with weight equal to the harmonic sum of the weights. |
|
|
|
References |
|
---------- |
|
.. [1] Wolfram |
|
"Kirchhoff Index." |
|
https://mathworld.wolfram.com/KirchhoffIndex.html |
|
.. [2] W. Ellens, F. M. Spieksma, P. Van Mieghem, A. Jamakovic, R. E. Kooij. |
|
Effective graph resistance. |
|
Lin. Alg. Appl. 435:2491-2506, 2011. |
|
""" |
|
import numpy as np |
|
|
|
if len(G) == 0: |
|
raise nx.NetworkXError("Graph G must contain at least one node.") |
|
|
|
|
|
if not nx.is_connected(G): |
|
return float("inf") |
|
|
|
|
|
G = G.copy() |
|
if invert_weight and weight is not None: |
|
if G.is_multigraph(): |
|
for u, v, k, d in G.edges(keys=True, data=True): |
|
d[weight] = 1 / d[weight] |
|
else: |
|
for u, v, d in G.edges(data=True): |
|
d[weight] = 1 / d[weight] |
|
|
|
|
|
mu = np.sort(nx.laplacian_spectrum(G, weight=weight)) |
|
|
|
|
|
|
|
return float(np.sum(1 / mu[1:]) * G.number_of_nodes()) |
|
|
|
|
|
@nx.utils.not_implemented_for("directed") |
|
@nx._dispatchable(edge_attrs="weight") |
|
def kemeny_constant(G, *, weight=None): |
|
"""Returns the Kemeny constant of the given graph. |
|
|
|
The *Kemeny constant* (or Kemeny's constant) of a graph `G` |
|
can be computed by regarding the graph as a Markov chain. |
|
The Kemeny constant is then the expected number of time steps |
|
to transition from a starting state i to a random destination state |
|
sampled from the Markov chain's stationary distribution. |
|
The Kemeny constant is independent of the chosen initial state [1]_. |
|
|
|
The Kemeny constant measures the time needed for spreading |
|
across a graph. Low values indicate a closely connected graph |
|
whereas high values indicate a spread-out graph. |
|
|
|
If weight is not provided, then a weight of 1 is used for all edges. |
|
|
|
Since `G` represents a Markov chain, the weights must be positive. |
|
|
|
Parameters |
|
---------- |
|
G : NetworkX graph |
|
|
|
weight : string or None, optional (default=None) |
|
The edge data key used to compute the Kemeny constant. |
|
If None, then each edge has weight 1. |
|
|
|
Returns |
|
------- |
|
float |
|
The Kemeny constant of the graph `G`. |
|
|
|
Raises |
|
------ |
|
NetworkXNotImplemented |
|
If the graph `G` is directed. |
|
|
|
NetworkXError |
|
If the graph `G` is not connected, or contains no nodes, |
|
or has edges with negative weights. |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.complete_graph(5) |
|
>>> round(nx.kemeny_constant(G), 10) |
|
3.2 |
|
|
|
Notes |
|
----- |
|
The implementation is based on equation (3.3) in [2]_. |
|
Self-loops are allowed and indicate a Markov chain where |
|
the state can remain the same. Multi-edges are contracted |
|
in one edge with weight equal to the sum of the weights. |
|
|
|
References |
|
---------- |
|
.. [1] Wikipedia |
|
"Kemeny's constant." |
|
https://en.wikipedia.org/wiki/Kemeny%27s_constant |
|
.. [2] Lovász L. |
|
Random walks on graphs: A survey. |
|
Paul Erdös is Eighty, vol. 2, Bolyai Society, |
|
Mathematical Studies, Keszthely, Hungary (1993), pp. 1-46 |
|
""" |
|
import numpy as np |
|
import scipy as sp |
|
|
|
if len(G) == 0: |
|
raise nx.NetworkXError("Graph G must contain at least one node.") |
|
if not nx.is_connected(G): |
|
raise nx.NetworkXError("Graph G must be connected.") |
|
if nx.is_negatively_weighted(G, weight=weight): |
|
raise nx.NetworkXError("The weights of graph G must be nonnegative.") |
|
|
|
|
|
A = nx.adjacency_matrix(G, weight=weight) |
|
n, m = A.shape |
|
diags = A.sum(axis=1) |
|
with np.errstate(divide="ignore"): |
|
diags_sqrt = 1.0 / np.sqrt(diags) |
|
diags_sqrt[np.isinf(diags_sqrt)] = 0 |
|
DH = sp.sparse.csr_array(sp.sparse.spdiags(diags_sqrt, 0, m, n, format="csr")) |
|
H = DH @ (A @ DH) |
|
|
|
|
|
eig = np.sort(sp.linalg.eigvalsh(H.todense())) |
|
|
|
|
|
return float(np.sum(1 / (1 - eig[:-1]))) |
|
|