Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +4 -0
- .venv/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so +3 -0
- .venv/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so +3 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/asteroidal.py +171 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/boundary.py +168 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/bridges.py +205 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/chains.py +172 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/chordal.py +443 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/clique.py +755 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/cluster.py +609 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/communicability_alg.py +163 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/core.py +649 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/covering.py +142 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/cuts.py +398 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/cycles.py +1230 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/d_separation.py +722 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/dag.py +1418 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/distance_measures.py +1022 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/distance_regular.py +238 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/dominance.py +135 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/efficiency_measures.py +167 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/euler.py +470 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/flow/__init__.py +11 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/flow/boykovkolmogorov.py +370 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/flow/capacityscaling.py +407 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/flow/dinitz_alg.py +238 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/flow/gomory_hu.py +178 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/flow/maxflow.py +607 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/flow/networksimplex.py +666 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/flow/preflowpush.py +425 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/graph_hashing.py +328 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/graphical.py +483 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/hierarchy.py +57 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/hybrid.py +196 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/isolate.py +107 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/link_prediction.py +687 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/lowest_common_ancestors.py +269 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/matching.py +1152 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/moral.py +59 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/non_randomness.py +98 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/planar_drawing.py +464 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/polynomials.py +306 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/reciprocity.py +98 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/regular.py +215 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/richclub.py +138 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/similarity.py +1780 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/simple_paths.py +950 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/smallworld.py +404 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/smetric.py +30 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/sparsifiers.py +296 -0
.gitattributes
CHANGED
|
@@ -301,3 +301,7 @@ tuning-competition-baseline/.venv/lib/python3.11/site-packages/nvidia/cudnn/lib/
|
|
| 301 |
.venv/lib/python3.11/site-packages/torchaudio/lib/pybind11_prefixctc.so filter=lfs diff=lfs merge=lfs -text
|
| 302 |
.venv/lib/python3.11/site-packages/torchaudio/lib/libctc_prefix_decoder.so filter=lfs diff=lfs merge=lfs -text
|
| 303 |
.venv/lib/python3.11/site-packages/torchaudio/lib/libtorchaudio_sox.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
.venv/lib/python3.11/site-packages/torchaudio/lib/pybind11_prefixctc.so filter=lfs diff=lfs merge=lfs -text
|
| 302 |
.venv/lib/python3.11/site-packages/torchaudio/lib/libctc_prefix_decoder.so filter=lfs diff=lfs merge=lfs -text
|
| 303 |
.venv/lib/python3.11/site-packages/torchaudio/lib/libtorchaudio_sox.so filter=lfs diff=lfs merge=lfs -text
|
| 304 |
+
.venv/lib/python3.11/site-packages/torchaudio/lib/_torchaudio_sox.so filter=lfs diff=lfs merge=lfs -text
|
| 305 |
+
.venv/lib/python3.11/site-packages/torchaudio/transforms/__pycache__/_transforms.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 306 |
+
.venv/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 307 |
+
.venv/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
.venv/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e423efe7a4ff703ed5e70eff519697c0cf641e674f29d2446c4af895f0a4f1be
|
| 3 |
+
size 145401
|
.venv/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1cc372f73a5562f6a0a364bfd1c42234e42e67403f814a83aab901136cce3a29
|
| 3 |
+
size 149024
|
.venv/lib/python3.11/site-packages/networkx/algorithms/asteroidal.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Algorithms for asteroidal triples and asteroidal numbers in graphs.
|
| 3 |
+
|
| 4 |
+
An asteroidal triple in a graph G is a set of three non-adjacent vertices
|
| 5 |
+
u, v and w such that there exist a path between any two of them that avoids
|
| 6 |
+
closed neighborhood of the third. More formally, v_j, v_k belongs to the same
|
| 7 |
+
connected component of G - N[v_i], where N[v_i] denotes the closed neighborhood
|
| 8 |
+
of v_i. A graph which does not contain any asteroidal triples is called
|
| 9 |
+
an AT-free graph. The class of AT-free graphs is a graph class for which
|
| 10 |
+
many NP-complete problems are solvable in polynomial time. Amongst them,
|
| 11 |
+
independent set and coloring.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import networkx as nx
|
| 15 |
+
from networkx.utils import not_implemented_for
|
| 16 |
+
|
| 17 |
+
__all__ = ["is_at_free", "find_asteroidal_triple"]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@not_implemented_for("directed")
|
| 21 |
+
@not_implemented_for("multigraph")
|
| 22 |
+
@nx._dispatchable
|
| 23 |
+
def find_asteroidal_triple(G):
|
| 24 |
+
r"""Find an asteroidal triple in the given graph.
|
| 25 |
+
|
| 26 |
+
An asteroidal triple is a triple of non-adjacent vertices such that
|
| 27 |
+
there exists a path between any two of them which avoids the closed
|
| 28 |
+
neighborhood of the third. It checks all independent triples of vertices
|
| 29 |
+
and whether they are an asteroidal triple or not. This is done with the
|
| 30 |
+
help of a data structure called a component structure.
|
| 31 |
+
A component structure encodes information about which vertices belongs to
|
| 32 |
+
the same connected component when the closed neighborhood of a given vertex
|
| 33 |
+
is removed from the graph. The algorithm used to check is the trivial
|
| 34 |
+
one, outlined in [1]_, which has a runtime of
|
| 35 |
+
:math:`O(|V||\overline{E} + |V||E|)`, where the second term is the
|
| 36 |
+
creation of the component structure.
|
| 37 |
+
|
| 38 |
+
Parameters
|
| 39 |
+
----------
|
| 40 |
+
G : NetworkX Graph
|
| 41 |
+
The graph to check whether is AT-free or not
|
| 42 |
+
|
| 43 |
+
Returns
|
| 44 |
+
-------
|
| 45 |
+
list or None
|
| 46 |
+
An asteroidal triple is returned as a list of nodes. If no asteroidal
|
| 47 |
+
triple exists, i.e. the graph is AT-free, then None is returned.
|
| 48 |
+
The returned value depends on the certificate parameter. The default
|
| 49 |
+
option is a bool which is True if the graph is AT-free, i.e. the
|
| 50 |
+
given graph contains no asteroidal triples, and False otherwise, i.e.
|
| 51 |
+
if the graph contains at least one asteroidal triple.
|
| 52 |
+
|
| 53 |
+
Notes
|
| 54 |
+
-----
|
| 55 |
+
The component structure and the algorithm is described in [1]_. The current
|
| 56 |
+
implementation implements the trivial algorithm for simple graphs.
|
| 57 |
+
|
| 58 |
+
References
|
| 59 |
+
----------
|
| 60 |
+
.. [1] Ekkehard Köhler,
|
| 61 |
+
"Recognizing Graphs without asteroidal triples",
|
| 62 |
+
Journal of Discrete Algorithms 2, pages 439-452, 2004.
|
| 63 |
+
https://www.sciencedirect.com/science/article/pii/S157086670400019X
|
| 64 |
+
"""
|
| 65 |
+
V = set(G.nodes)
|
| 66 |
+
|
| 67 |
+
if len(V) < 6:
|
| 68 |
+
# An asteroidal triple cannot exist in a graph with 5 or less vertices.
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
component_structure = create_component_structure(G)
|
| 72 |
+
E_complement = set(nx.complement(G).edges)
|
| 73 |
+
|
| 74 |
+
for e in E_complement:
|
| 75 |
+
u = e[0]
|
| 76 |
+
v = e[1]
|
| 77 |
+
u_neighborhood = set(G[u]).union([u])
|
| 78 |
+
v_neighborhood = set(G[v]).union([v])
|
| 79 |
+
union_of_neighborhoods = u_neighborhood.union(v_neighborhood)
|
| 80 |
+
for w in V - union_of_neighborhoods:
|
| 81 |
+
# Check for each pair of vertices whether they belong to the
|
| 82 |
+
# same connected component when the closed neighborhood of the
|
| 83 |
+
# third is removed.
|
| 84 |
+
if (
|
| 85 |
+
component_structure[u][v] == component_structure[u][w]
|
| 86 |
+
and component_structure[v][u] == component_structure[v][w]
|
| 87 |
+
and component_structure[w][u] == component_structure[w][v]
|
| 88 |
+
):
|
| 89 |
+
return [u, v, w]
|
| 90 |
+
return None
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@not_implemented_for("directed")
|
| 94 |
+
@not_implemented_for("multigraph")
|
| 95 |
+
@nx._dispatchable
|
| 96 |
+
def is_at_free(G):
|
| 97 |
+
"""Check if a graph is AT-free.
|
| 98 |
+
|
| 99 |
+
The method uses the `find_asteroidal_triple` method to recognize
|
| 100 |
+
an AT-free graph. If no asteroidal triple is found the graph is
|
| 101 |
+
AT-free and True is returned. If at least one asteroidal triple is
|
| 102 |
+
found the graph is not AT-free and False is returned.
|
| 103 |
+
|
| 104 |
+
Parameters
|
| 105 |
+
----------
|
| 106 |
+
G : NetworkX Graph
|
| 107 |
+
The graph to check whether is AT-free or not.
|
| 108 |
+
|
| 109 |
+
Returns
|
| 110 |
+
-------
|
| 111 |
+
bool
|
| 112 |
+
True if G is AT-free and False otherwise.
|
| 113 |
+
|
| 114 |
+
Examples
|
| 115 |
+
--------
|
| 116 |
+
>>> G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)])
|
| 117 |
+
>>> nx.is_at_free(G)
|
| 118 |
+
True
|
| 119 |
+
|
| 120 |
+
>>> G = nx.cycle_graph(6)
|
| 121 |
+
>>> nx.is_at_free(G)
|
| 122 |
+
False
|
| 123 |
+
"""
|
| 124 |
+
return find_asteroidal_triple(G) is None
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@not_implemented_for("directed")
|
| 128 |
+
@not_implemented_for("multigraph")
|
| 129 |
+
@nx._dispatchable
|
| 130 |
+
def create_component_structure(G):
|
| 131 |
+
r"""Create component structure for G.
|
| 132 |
+
|
| 133 |
+
A *component structure* is an `nxn` array, denoted `c`, where `n` is
|
| 134 |
+
the number of vertices, where each row and column corresponds to a vertex.
|
| 135 |
+
|
| 136 |
+
.. math::
|
| 137 |
+
c_{uv} = \begin{cases} 0, if v \in N[u] \\
|
| 138 |
+
k, if v \in component k of G \setminus N[u] \end{cases}
|
| 139 |
+
|
| 140 |
+
Where `k` is an arbitrary label for each component. The structure is used
|
| 141 |
+
to simplify the detection of asteroidal triples.
|
| 142 |
+
|
| 143 |
+
Parameters
|
| 144 |
+
----------
|
| 145 |
+
G : NetworkX Graph
|
| 146 |
+
Undirected, simple graph.
|
| 147 |
+
|
| 148 |
+
Returns
|
| 149 |
+
-------
|
| 150 |
+
component_structure : dictionary
|
| 151 |
+
A dictionary of dictionaries, keyed by pairs of vertices.
|
| 152 |
+
|
| 153 |
+
"""
|
| 154 |
+
V = set(G.nodes)
|
| 155 |
+
component_structure = {}
|
| 156 |
+
for v in V:
|
| 157 |
+
label = 0
|
| 158 |
+
closed_neighborhood = set(G[v]).union({v})
|
| 159 |
+
row_dict = {}
|
| 160 |
+
for u in closed_neighborhood:
|
| 161 |
+
row_dict[u] = 0
|
| 162 |
+
|
| 163 |
+
G_reduced = G.subgraph(set(G.nodes) - closed_neighborhood)
|
| 164 |
+
for cc in nx.connected_components(G_reduced):
|
| 165 |
+
label += 1
|
| 166 |
+
for u in cc:
|
| 167 |
+
row_dict[u] = label
|
| 168 |
+
|
| 169 |
+
component_structure[v] = row_dict
|
| 170 |
+
|
| 171 |
+
return component_structure
|
.venv/lib/python3.11/site-packages/networkx/algorithms/boundary.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Routines to find the boundary of a set of nodes.
|
| 2 |
+
|
| 3 |
+
An edge boundary is a set of edges, each of which has exactly one
|
| 4 |
+
endpoint in a given set of nodes (or, in the case of directed graphs,
|
| 5 |
+
the set of edges whose source node is in the set).
|
| 6 |
+
|
| 7 |
+
A node boundary of a set *S* of nodes is the set of (out-)neighbors of
|
| 8 |
+
nodes in *S* that are outside *S*.
|
| 9 |
+
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from itertools import chain
|
| 13 |
+
|
| 14 |
+
import networkx as nx
|
| 15 |
+
|
| 16 |
+
__all__ = ["edge_boundary", "node_boundary"]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@nx._dispatchable(edge_attrs={"data": "default"}, preserve_edge_attrs="data")
|
| 20 |
+
def edge_boundary(G, nbunch1, nbunch2=None, data=False, keys=False, default=None):
|
| 21 |
+
"""Returns the edge boundary of `nbunch1`.
|
| 22 |
+
|
| 23 |
+
The *edge boundary* of a set *S* with respect to a set *T* is the
|
| 24 |
+
set of edges (*u*, *v*) such that *u* is in *S* and *v* is in *T*.
|
| 25 |
+
If *T* is not specified, it is assumed to be the set of all nodes
|
| 26 |
+
not in *S*.
|
| 27 |
+
|
| 28 |
+
Parameters
|
| 29 |
+
----------
|
| 30 |
+
G : NetworkX graph
|
| 31 |
+
|
| 32 |
+
nbunch1 : iterable
|
| 33 |
+
Iterable of nodes in the graph representing the set of nodes
|
| 34 |
+
whose edge boundary will be returned. (This is the set *S* from
|
| 35 |
+
the definition above.)
|
| 36 |
+
|
| 37 |
+
nbunch2 : iterable
|
| 38 |
+
Iterable of nodes representing the target (or "exterior") set of
|
| 39 |
+
nodes. (This is the set *T* from the definition above.) If not
|
| 40 |
+
specified, this is assumed to be the set of all nodes in `G`
|
| 41 |
+
not in `nbunch1`.
|
| 42 |
+
|
| 43 |
+
keys : bool
|
| 44 |
+
This parameter has the same meaning as in
|
| 45 |
+
:meth:`MultiGraph.edges`.
|
| 46 |
+
|
| 47 |
+
data : bool or object
|
| 48 |
+
This parameter has the same meaning as in
|
| 49 |
+
:meth:`MultiGraph.edges`.
|
| 50 |
+
|
| 51 |
+
default : object
|
| 52 |
+
This parameter has the same meaning as in
|
| 53 |
+
:meth:`MultiGraph.edges`.
|
| 54 |
+
|
| 55 |
+
Returns
|
| 56 |
+
-------
|
| 57 |
+
iterator
|
| 58 |
+
An iterator over the edges in the boundary of `nbunch1` with
|
| 59 |
+
respect to `nbunch2`. If `keys`, `data`, or `default`
|
| 60 |
+
are specified and `G` is a multigraph, then edges are returned
|
| 61 |
+
with keys and/or data, as in :meth:`MultiGraph.edges`.
|
| 62 |
+
|
| 63 |
+
Examples
|
| 64 |
+
--------
|
| 65 |
+
>>> G = nx.wheel_graph(6)
|
| 66 |
+
|
| 67 |
+
When nbunch2=None:
|
| 68 |
+
|
| 69 |
+
>>> list(nx.edge_boundary(G, (1, 3)))
|
| 70 |
+
[(1, 0), (1, 2), (1, 5), (3, 0), (3, 2), (3, 4)]
|
| 71 |
+
|
| 72 |
+
When nbunch2 is given:
|
| 73 |
+
|
| 74 |
+
>>> list(nx.edge_boundary(G, (1, 3), (2, 0)))
|
| 75 |
+
[(1, 0), (1, 2), (3, 0), (3, 2)]
|
| 76 |
+
|
| 77 |
+
Notes
|
| 78 |
+
-----
|
| 79 |
+
Any element of `nbunch` that is not in the graph `G` will be
|
| 80 |
+
ignored.
|
| 81 |
+
|
| 82 |
+
`nbunch1` and `nbunch2` are usually meant to be disjoint, but in
|
| 83 |
+
the interest of speed and generality, that is not required here.
|
| 84 |
+
|
| 85 |
+
"""
|
| 86 |
+
nset1 = {n for n in nbunch1 if n in G}
|
| 87 |
+
# Here we create an iterator over edges incident to nodes in the set
|
| 88 |
+
# `nset1`. The `Graph.edges()` method does not provide a guarantee
|
| 89 |
+
# on the orientation of the edges, so our algorithm below must
|
| 90 |
+
# handle the case in which exactly one orientation, either (u, v) or
|
| 91 |
+
# (v, u), appears in this iterable.
|
| 92 |
+
if G.is_multigraph():
|
| 93 |
+
edges = G.edges(nset1, data=data, keys=keys, default=default)
|
| 94 |
+
else:
|
| 95 |
+
edges = G.edges(nset1, data=data, default=default)
|
| 96 |
+
# If `nbunch2` is not provided, then it is assumed to be the set
|
| 97 |
+
# complement of `nbunch1`. For the sake of efficiency, this is
|
| 98 |
+
# implemented by using the `not in` operator, instead of by creating
|
| 99 |
+
# an additional set and using the `in` operator.
|
| 100 |
+
if nbunch2 is None:
|
| 101 |
+
return (e for e in edges if (e[0] in nset1) ^ (e[1] in nset1))
|
| 102 |
+
nset2 = set(nbunch2)
|
| 103 |
+
return (
|
| 104 |
+
e
|
| 105 |
+
for e in edges
|
| 106 |
+
if (e[0] in nset1 and e[1] in nset2) or (e[1] in nset1 and e[0] in nset2)
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@nx._dispatchable
|
| 111 |
+
def node_boundary(G, nbunch1, nbunch2=None):
|
| 112 |
+
"""Returns the node boundary of `nbunch1`.
|
| 113 |
+
|
| 114 |
+
The *node boundary* of a set *S* with respect to a set *T* is the
|
| 115 |
+
set of nodes *v* in *T* such that for some *u* in *S*, there is an
|
| 116 |
+
edge joining *u* to *v*. If *T* is not specified, it is assumed to
|
| 117 |
+
be the set of all nodes not in *S*.
|
| 118 |
+
|
| 119 |
+
Parameters
|
| 120 |
+
----------
|
| 121 |
+
G : NetworkX graph
|
| 122 |
+
|
| 123 |
+
nbunch1 : iterable
|
| 124 |
+
Iterable of nodes in the graph representing the set of nodes
|
| 125 |
+
whose node boundary will be returned. (This is the set *S* from
|
| 126 |
+
the definition above.)
|
| 127 |
+
|
| 128 |
+
nbunch2 : iterable
|
| 129 |
+
Iterable of nodes representing the target (or "exterior") set of
|
| 130 |
+
nodes. (This is the set *T* from the definition above.) If not
|
| 131 |
+
specified, this is assumed to be the set of all nodes in `G`
|
| 132 |
+
not in `nbunch1`.
|
| 133 |
+
|
| 134 |
+
Returns
|
| 135 |
+
-------
|
| 136 |
+
set
|
| 137 |
+
The node boundary of `nbunch1` with respect to `nbunch2`.
|
| 138 |
+
|
| 139 |
+
Examples
|
| 140 |
+
--------
|
| 141 |
+
>>> G = nx.wheel_graph(6)
|
| 142 |
+
|
| 143 |
+
When nbunch2=None:
|
| 144 |
+
|
| 145 |
+
>>> list(nx.node_boundary(G, (3, 4)))
|
| 146 |
+
[0, 2, 5]
|
| 147 |
+
|
| 148 |
+
When nbunch2 is given:
|
| 149 |
+
|
| 150 |
+
>>> list(nx.node_boundary(G, (3, 4), (0, 1, 5)))
|
| 151 |
+
[0, 5]
|
| 152 |
+
|
| 153 |
+
Notes
|
| 154 |
+
-----
|
| 155 |
+
Any element of `nbunch` that is not in the graph `G` will be
|
| 156 |
+
ignored.
|
| 157 |
+
|
| 158 |
+
`nbunch1` and `nbunch2` are usually meant to be disjoint, but in
|
| 159 |
+
the interest of speed and generality, that is not required here.
|
| 160 |
+
|
| 161 |
+
"""
|
| 162 |
+
nset1 = {n for n in nbunch1 if n in G}
|
| 163 |
+
bdy = set(chain.from_iterable(G[v] for v in nset1)) - nset1
|
| 164 |
+
# If `nbunch2` is not specified, it is assumed to be the set
|
| 165 |
+
# complement of `nbunch1`.
|
| 166 |
+
if nbunch2 is not None:
|
| 167 |
+
bdy &= set(nbunch2)
|
| 168 |
+
return bdy
|
.venv/lib/python3.11/site-packages/networkx/algorithms/bridges.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bridge-finding algorithms."""
|
| 2 |
+
|
| 3 |
+
from itertools import chain
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.utils import not_implemented_for
|
| 7 |
+
|
| 8 |
+
__all__ = ["bridges", "has_bridges", "local_bridges"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@not_implemented_for("directed")
|
| 12 |
+
@nx._dispatchable
|
| 13 |
+
def bridges(G, root=None):
|
| 14 |
+
"""Generate all bridges in a graph.
|
| 15 |
+
|
| 16 |
+
A *bridge* in a graph is an edge whose removal causes the number of
|
| 17 |
+
connected components of the graph to increase. Equivalently, a bridge is an
|
| 18 |
+
edge that does not belong to any cycle. Bridges are also known as cut-edges,
|
| 19 |
+
isthmuses, or cut arcs.
|
| 20 |
+
|
| 21 |
+
Parameters
|
| 22 |
+
----------
|
| 23 |
+
G : undirected graph
|
| 24 |
+
|
| 25 |
+
root : node (optional)
|
| 26 |
+
A node in the graph `G`. If specified, only the bridges in the
|
| 27 |
+
connected component containing this node will be returned.
|
| 28 |
+
|
| 29 |
+
Yields
|
| 30 |
+
------
|
| 31 |
+
e : edge
|
| 32 |
+
An edge in the graph whose removal disconnects the graph (or
|
| 33 |
+
causes the number of connected components to increase).
|
| 34 |
+
|
| 35 |
+
Raises
|
| 36 |
+
------
|
| 37 |
+
NodeNotFound
|
| 38 |
+
If `root` is not in the graph `G`.
|
| 39 |
+
|
| 40 |
+
NetworkXNotImplemented
|
| 41 |
+
If `G` is a directed graph.
|
| 42 |
+
|
| 43 |
+
Examples
|
| 44 |
+
--------
|
| 45 |
+
The barbell graph with parameter zero has a single bridge:
|
| 46 |
+
|
| 47 |
+
>>> G = nx.barbell_graph(10, 0)
|
| 48 |
+
>>> list(nx.bridges(G))
|
| 49 |
+
[(9, 10)]
|
| 50 |
+
|
| 51 |
+
Notes
|
| 52 |
+
-----
|
| 53 |
+
This is an implementation of the algorithm described in [1]_. An edge is a
|
| 54 |
+
bridge if and only if it is not contained in any chain. Chains are found
|
| 55 |
+
using the :func:`networkx.chain_decomposition` function.
|
| 56 |
+
|
| 57 |
+
The algorithm described in [1]_ requires a simple graph. If the provided
|
| 58 |
+
graph is a multigraph, we convert it to a simple graph and verify that any
|
| 59 |
+
bridges discovered by the chain decomposition algorithm are not multi-edges.
|
| 60 |
+
|
| 61 |
+
Ignoring polylogarithmic factors, the worst-case time complexity is the
|
| 62 |
+
same as the :func:`networkx.chain_decomposition` function,
|
| 63 |
+
$O(m + n)$, where $n$ is the number of nodes in the graph and $m$ is
|
| 64 |
+
the number of edges.
|
| 65 |
+
|
| 66 |
+
References
|
| 67 |
+
----------
|
| 68 |
+
.. [1] https://en.wikipedia.org/wiki/Bridge_%28graph_theory%29#Bridge-Finding_with_Chain_Decompositions
|
| 69 |
+
"""
|
| 70 |
+
multigraph = G.is_multigraph()
|
| 71 |
+
H = nx.Graph(G) if multigraph else G
|
| 72 |
+
chains = nx.chain_decomposition(H, root=root)
|
| 73 |
+
chain_edges = set(chain.from_iterable(chains))
|
| 74 |
+
if root is not None:
|
| 75 |
+
H = H.subgraph(nx.node_connected_component(H, root)).copy()
|
| 76 |
+
for u, v in H.edges():
|
| 77 |
+
if (u, v) not in chain_edges and (v, u) not in chain_edges:
|
| 78 |
+
if multigraph and len(G[u][v]) > 1:
|
| 79 |
+
continue
|
| 80 |
+
yield u, v
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@not_implemented_for("directed")
|
| 84 |
+
@nx._dispatchable
|
| 85 |
+
def has_bridges(G, root=None):
|
| 86 |
+
"""Decide whether a graph has any bridges.
|
| 87 |
+
|
| 88 |
+
A *bridge* in a graph is an edge whose removal causes the number of
|
| 89 |
+
connected components of the graph to increase.
|
| 90 |
+
|
| 91 |
+
Parameters
|
| 92 |
+
----------
|
| 93 |
+
G : undirected graph
|
| 94 |
+
|
| 95 |
+
root : node (optional)
|
| 96 |
+
A node in the graph `G`. If specified, only the bridges in the
|
| 97 |
+
connected component containing this node will be considered.
|
| 98 |
+
|
| 99 |
+
Returns
|
| 100 |
+
-------
|
| 101 |
+
bool
|
| 102 |
+
Whether the graph (or the connected component containing `root`)
|
| 103 |
+
has any bridges.
|
| 104 |
+
|
| 105 |
+
Raises
|
| 106 |
+
------
|
| 107 |
+
NodeNotFound
|
| 108 |
+
If `root` is not in the graph `G`.
|
| 109 |
+
|
| 110 |
+
NetworkXNotImplemented
|
| 111 |
+
If `G` is a directed graph.
|
| 112 |
+
|
| 113 |
+
Examples
|
| 114 |
+
--------
|
| 115 |
+
The barbell graph with parameter zero has a single bridge::
|
| 116 |
+
|
| 117 |
+
>>> G = nx.barbell_graph(10, 0)
|
| 118 |
+
>>> nx.has_bridges(G)
|
| 119 |
+
True
|
| 120 |
+
|
| 121 |
+
On the other hand, the cycle graph has no bridges::
|
| 122 |
+
|
| 123 |
+
>>> G = nx.cycle_graph(5)
|
| 124 |
+
>>> nx.has_bridges(G)
|
| 125 |
+
False
|
| 126 |
+
|
| 127 |
+
Notes
|
| 128 |
+
-----
|
| 129 |
+
This implementation uses the :func:`networkx.bridges` function, so
|
| 130 |
+
it shares its worst-case time complexity, $O(m + n)$, ignoring
|
| 131 |
+
polylogarithmic factors, where $n$ is the number of nodes in the
|
| 132 |
+
graph and $m$ is the number of edges.
|
| 133 |
+
|
| 134 |
+
"""
|
| 135 |
+
try:
|
| 136 |
+
next(bridges(G, root=root))
|
| 137 |
+
except StopIteration:
|
| 138 |
+
return False
|
| 139 |
+
else:
|
| 140 |
+
return True
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@not_implemented_for("multigraph")
|
| 144 |
+
@not_implemented_for("directed")
|
| 145 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 146 |
+
def local_bridges(G, with_span=True, weight=None):
|
| 147 |
+
"""Iterate over local bridges of `G` optionally computing the span
|
| 148 |
+
|
| 149 |
+
A *local bridge* is an edge whose endpoints have no common neighbors.
|
| 150 |
+
That is, the edge is not part of a triangle in the graph.
|
| 151 |
+
|
| 152 |
+
The *span* of a *local bridge* is the shortest path length between
|
| 153 |
+
the endpoints if the local bridge is removed.
|
| 154 |
+
|
| 155 |
+
Parameters
|
| 156 |
+
----------
|
| 157 |
+
G : undirected graph
|
| 158 |
+
|
| 159 |
+
with_span : bool
|
| 160 |
+
If True, yield a 3-tuple `(u, v, span)`
|
| 161 |
+
|
| 162 |
+
weight : function, string or None (default: None)
|
| 163 |
+
If function, used to compute edge weights for the span.
|
| 164 |
+
If string, the edge data attribute used in calculating span.
|
| 165 |
+
If None, all edges have weight 1.
|
| 166 |
+
|
| 167 |
+
Yields
|
| 168 |
+
------
|
| 169 |
+
e : edge
|
| 170 |
+
The local bridges as an edge 2-tuple of nodes `(u, v)` or
|
| 171 |
+
as a 3-tuple `(u, v, span)` when `with_span is True`.
|
| 172 |
+
|
| 173 |
+
Raises
|
| 174 |
+
------
|
| 175 |
+
NetworkXNotImplemented
|
| 176 |
+
If `G` is a directed graph or multigraph.
|
| 177 |
+
|
| 178 |
+
Examples
|
| 179 |
+
--------
|
| 180 |
+
A cycle graph has every edge a local bridge with span N-1.
|
| 181 |
+
|
| 182 |
+
>>> G = nx.cycle_graph(9)
|
| 183 |
+
>>> (0, 8, 8) in set(nx.local_bridges(G))
|
| 184 |
+
True
|
| 185 |
+
"""
|
| 186 |
+
if with_span is not True:
|
| 187 |
+
for u, v in G.edges:
|
| 188 |
+
if not (set(G[u]) & set(G[v])):
|
| 189 |
+
yield u, v
|
| 190 |
+
else:
|
| 191 |
+
wt = nx.weighted._weight_function(G, weight)
|
| 192 |
+
for u, v in G.edges:
|
| 193 |
+
if not (set(G[u]) & set(G[v])):
|
| 194 |
+
enodes = {u, v}
|
| 195 |
+
|
| 196 |
+
def hide_edge(n, nbr, d):
|
| 197 |
+
if n not in enodes or nbr not in enodes:
|
| 198 |
+
return wt(n, nbr, d)
|
| 199 |
+
return None
|
| 200 |
+
|
| 201 |
+
try:
|
| 202 |
+
span = nx.shortest_path_length(G, u, v, weight=hide_edge)
|
| 203 |
+
yield u, v, span
|
| 204 |
+
except nx.NetworkXNoPath:
|
| 205 |
+
yield u, v, float("inf")
|
.venv/lib/python3.11/site-packages/networkx/algorithms/chains.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions for finding chains in a graph."""
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from networkx.utils import not_implemented_for
|
| 5 |
+
|
| 6 |
+
__all__ = ["chain_decomposition"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@not_implemented_for("directed")
|
| 10 |
+
@not_implemented_for("multigraph")
|
| 11 |
+
@nx._dispatchable
|
| 12 |
+
def chain_decomposition(G, root=None):
|
| 13 |
+
"""Returns the chain decomposition of a graph.
|
| 14 |
+
|
| 15 |
+
The *chain decomposition* of a graph with respect a depth-first
|
| 16 |
+
search tree is a set of cycles or paths derived from the set of
|
| 17 |
+
fundamental cycles of the tree in the following manner. Consider
|
| 18 |
+
each fundamental cycle with respect to the given tree, represented
|
| 19 |
+
as a list of edges beginning with the nontree edge oriented away
|
| 20 |
+
from the root of the tree. For each fundamental cycle, if it
|
| 21 |
+
overlaps with any previous fundamental cycle, just take the initial
|
| 22 |
+
non-overlapping segment, which is a path instead of a cycle. Each
|
| 23 |
+
cycle or path is called a *chain*. For more information, see [1]_.
|
| 24 |
+
|
| 25 |
+
Parameters
|
| 26 |
+
----------
|
| 27 |
+
G : undirected graph
|
| 28 |
+
|
| 29 |
+
root : node (optional)
|
| 30 |
+
A node in the graph `G`. If specified, only the chain
|
| 31 |
+
decomposition for the connected component containing this node
|
| 32 |
+
will be returned. This node indicates the root of the depth-first
|
| 33 |
+
search tree.
|
| 34 |
+
|
| 35 |
+
Yields
|
| 36 |
+
------
|
| 37 |
+
chain : list
|
| 38 |
+
A list of edges representing a chain. There is no guarantee on
|
| 39 |
+
the orientation of the edges in each chain (for example, if a
|
| 40 |
+
chain includes the edge joining nodes 1 and 2, the chain may
|
| 41 |
+
include either (1, 2) or (2, 1)).
|
| 42 |
+
|
| 43 |
+
Raises
|
| 44 |
+
------
|
| 45 |
+
NodeNotFound
|
| 46 |
+
If `root` is not in the graph `G`.
|
| 47 |
+
|
| 48 |
+
Examples
|
| 49 |
+
--------
|
| 50 |
+
>>> G = nx.Graph([(0, 1), (1, 4), (3, 4), (3, 5), (4, 5)])
|
| 51 |
+
>>> list(nx.chain_decomposition(G))
|
| 52 |
+
[[(4, 5), (5, 3), (3, 4)]]
|
| 53 |
+
|
| 54 |
+
Notes
|
| 55 |
+
-----
|
| 56 |
+
The worst-case running time of this implementation is linear in the
|
| 57 |
+
number of nodes and number of edges [1]_.
|
| 58 |
+
|
| 59 |
+
References
|
| 60 |
+
----------
|
| 61 |
+
.. [1] Jens M. Schmidt (2013). "A simple test on 2-vertex-
|
| 62 |
+
and 2-edge-connectivity." *Information Processing Letters*,
|
| 63 |
+
113, 241–244. Elsevier. <https://doi.org/10.1016/j.ipl.2013.01.016>
|
| 64 |
+
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
def _dfs_cycle_forest(G, root=None):
|
| 68 |
+
"""Builds a directed graph composed of cycles from the given graph.
|
| 69 |
+
|
| 70 |
+
`G` is an undirected simple graph. `root` is a node in the graph
|
| 71 |
+
from which the depth-first search is started.
|
| 72 |
+
|
| 73 |
+
This function returns both the depth-first search cycle graph
|
| 74 |
+
(as a :class:`~networkx.DiGraph`) and the list of nodes in
|
| 75 |
+
depth-first preorder. The depth-first search cycle graph is a
|
| 76 |
+
directed graph whose edges are the edges of `G` oriented toward
|
| 77 |
+
the root if the edge is a tree edge and away from the root if
|
| 78 |
+
the edge is a non-tree edge. If `root` is not specified, this
|
| 79 |
+
performs a depth-first search on each connected component of `G`
|
| 80 |
+
and returns a directed forest instead.
|
| 81 |
+
|
| 82 |
+
If `root` is not in the graph, this raises :exc:`KeyError`.
|
| 83 |
+
|
| 84 |
+
"""
|
| 85 |
+
# Create a directed graph from the depth-first search tree with
|
| 86 |
+
# root node `root` in which tree edges are directed toward the
|
| 87 |
+
# root and nontree edges are directed away from the root. For
|
| 88 |
+
# each node with an incident nontree edge, this creates a
|
| 89 |
+
# directed cycle starting with the nontree edge and returning to
|
| 90 |
+
# that node.
|
| 91 |
+
#
|
| 92 |
+
# The `parent` node attribute stores the parent of each node in
|
| 93 |
+
# the DFS tree. The `nontree` edge attribute indicates whether
|
| 94 |
+
# the edge is a tree edge or a nontree edge.
|
| 95 |
+
#
|
| 96 |
+
# We also store the order of the nodes found in the depth-first
|
| 97 |
+
# search in the `nodes` list.
|
| 98 |
+
H = nx.DiGraph()
|
| 99 |
+
nodes = []
|
| 100 |
+
for u, v, d in nx.dfs_labeled_edges(G, source=root):
|
| 101 |
+
if d == "forward":
|
| 102 |
+
# `dfs_labeled_edges()` yields (root, root, 'forward')
|
| 103 |
+
# if it is beginning the search on a new connected
|
| 104 |
+
# component.
|
| 105 |
+
if u == v:
|
| 106 |
+
H.add_node(v, parent=None)
|
| 107 |
+
nodes.append(v)
|
| 108 |
+
else:
|
| 109 |
+
H.add_node(v, parent=u)
|
| 110 |
+
H.add_edge(v, u, nontree=False)
|
| 111 |
+
nodes.append(v)
|
| 112 |
+
# `dfs_labeled_edges` considers nontree edges in both
|
| 113 |
+
# orientations, so we need to not add the edge if it its
|
| 114 |
+
# other orientation has been added.
|
| 115 |
+
elif d == "nontree" and v not in H[u]:
|
| 116 |
+
H.add_edge(v, u, nontree=True)
|
| 117 |
+
else:
|
| 118 |
+
# Do nothing on 'reverse' edges; we only care about
|
| 119 |
+
# forward and nontree edges.
|
| 120 |
+
pass
|
| 121 |
+
return H, nodes
|
| 122 |
+
|
| 123 |
+
def _build_chain(G, u, v, visited):
|
| 124 |
+
"""Generate the chain starting from the given nontree edge.
|
| 125 |
+
|
| 126 |
+
`G` is a DFS cycle graph as constructed by
|
| 127 |
+
:func:`_dfs_cycle_graph`. The edge (`u`, `v`) is a nontree edge
|
| 128 |
+
that begins a chain. `visited` is a set representing the nodes
|
| 129 |
+
in `G` that have already been visited.
|
| 130 |
+
|
| 131 |
+
This function yields the edges in an initial segment of the
|
| 132 |
+
fundamental cycle of `G` starting with the nontree edge (`u`,
|
| 133 |
+
`v`) that includes all the edges up until the first node that
|
| 134 |
+
appears in `visited`. The tree edges are given by the 'parent'
|
| 135 |
+
node attribute. The `visited` set is updated to add each node in
|
| 136 |
+
an edge yielded by this function.
|
| 137 |
+
|
| 138 |
+
"""
|
| 139 |
+
while v not in visited:
|
| 140 |
+
yield u, v
|
| 141 |
+
visited.add(v)
|
| 142 |
+
u, v = v, G.nodes[v]["parent"]
|
| 143 |
+
yield u, v
|
| 144 |
+
|
| 145 |
+
# Check if the root is in the graph G. If not, raise NodeNotFound
|
| 146 |
+
if root is not None and root not in G:
|
| 147 |
+
raise nx.NodeNotFound(f"Root node {root} is not in graph")
|
| 148 |
+
|
| 149 |
+
# Create a directed version of H that has the DFS edges directed
|
| 150 |
+
# toward the root and the nontree edges directed away from the root
|
| 151 |
+
# (in each connected component).
|
| 152 |
+
H, nodes = _dfs_cycle_forest(G, root)
|
| 153 |
+
|
| 154 |
+
# Visit the nodes again in DFS order. For each node, and for each
|
| 155 |
+
# nontree edge leaving that node, compute the fundamental cycle for
|
| 156 |
+
# that nontree edge starting with that edge. If the fundamental
|
| 157 |
+
# cycle overlaps with any visited nodes, just take the prefix of the
|
| 158 |
+
# cycle up to the point of visited nodes.
|
| 159 |
+
#
|
| 160 |
+
# We repeat this process for each connected component (implicitly,
|
| 161 |
+
# since `nodes` already has a list of the nodes grouped by connected
|
| 162 |
+
# component).
|
| 163 |
+
visited = set()
|
| 164 |
+
for u in nodes:
|
| 165 |
+
visited.add(u)
|
| 166 |
+
# For each nontree edge going out of node u...
|
| 167 |
+
edges = ((u, v) for u, v, d in H.out_edges(u, data="nontree") if d)
|
| 168 |
+
for u, v in edges:
|
| 169 |
+
# Create the cycle or cycle prefix starting with the
|
| 170 |
+
# nontree edge.
|
| 171 |
+
chain = list(_build_chain(H, u, v, visited))
|
| 172 |
+
yield chain
|
.venv/lib/python3.11/site-packages/networkx/algorithms/chordal.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Algorithms for chordal graphs.
|
| 3 |
+
|
| 4 |
+
A graph is chordal if every cycle of length at least 4 has a chord
|
| 5 |
+
(an edge joining two nodes not adjacent in the cycle).
|
| 6 |
+
https://en.wikipedia.org/wiki/Chordal_graph
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import sys
|
| 10 |
+
|
| 11 |
+
import networkx as nx
|
| 12 |
+
from networkx.algorithms.components import connected_components
|
| 13 |
+
from networkx.utils import arbitrary_element, not_implemented_for
|
| 14 |
+
|
| 15 |
+
__all__ = [
|
| 16 |
+
"is_chordal",
|
| 17 |
+
"find_induced_nodes",
|
| 18 |
+
"chordal_graph_cliques",
|
| 19 |
+
"chordal_graph_treewidth",
|
| 20 |
+
"NetworkXTreewidthBoundExceeded",
|
| 21 |
+
"complete_to_chordal_graph",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class NetworkXTreewidthBoundExceeded(nx.NetworkXException):
|
| 26 |
+
"""Exception raised when a treewidth bound has been provided and it has
|
| 27 |
+
been exceeded"""
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@not_implemented_for("directed")
|
| 31 |
+
@not_implemented_for("multigraph")
|
| 32 |
+
@nx._dispatchable
|
| 33 |
+
def is_chordal(G):
|
| 34 |
+
"""Checks whether G is a chordal graph.
|
| 35 |
+
|
| 36 |
+
A graph is chordal if every cycle of length at least 4 has a chord
|
| 37 |
+
(an edge joining two nodes not adjacent in the cycle).
|
| 38 |
+
|
| 39 |
+
Parameters
|
| 40 |
+
----------
|
| 41 |
+
G : graph
|
| 42 |
+
A NetworkX graph.
|
| 43 |
+
|
| 44 |
+
Returns
|
| 45 |
+
-------
|
| 46 |
+
chordal : bool
|
| 47 |
+
True if G is a chordal graph and False otherwise.
|
| 48 |
+
|
| 49 |
+
Raises
|
| 50 |
+
------
|
| 51 |
+
NetworkXNotImplemented
|
| 52 |
+
The algorithm does not support DiGraph, MultiGraph and MultiDiGraph.
|
| 53 |
+
|
| 54 |
+
Examples
|
| 55 |
+
--------
|
| 56 |
+
>>> e = [
|
| 57 |
+
... (1, 2),
|
| 58 |
+
... (1, 3),
|
| 59 |
+
... (2, 3),
|
| 60 |
+
... (2, 4),
|
| 61 |
+
... (3, 4),
|
| 62 |
+
... (3, 5),
|
| 63 |
+
... (3, 6),
|
| 64 |
+
... (4, 5),
|
| 65 |
+
... (4, 6),
|
| 66 |
+
... (5, 6),
|
| 67 |
+
... ]
|
| 68 |
+
>>> G = nx.Graph(e)
|
| 69 |
+
>>> nx.is_chordal(G)
|
| 70 |
+
True
|
| 71 |
+
|
| 72 |
+
Notes
|
| 73 |
+
-----
|
| 74 |
+
The routine tries to go through every node following maximum cardinality
|
| 75 |
+
search. It returns False when it finds that the separator for any node
|
| 76 |
+
is not a clique. Based on the algorithms in [1]_.
|
| 77 |
+
|
| 78 |
+
Self loops are ignored.
|
| 79 |
+
|
| 80 |
+
References
|
| 81 |
+
----------
|
| 82 |
+
.. [1] R. E. Tarjan and M. Yannakakis, Simple linear-time algorithms
|
| 83 |
+
to test chordality of graphs, test acyclicity of hypergraphs, and
|
| 84 |
+
selectively reduce acyclic hypergraphs, SIAM J. Comput., 13 (1984),
|
| 85 |
+
pp. 566–579.
|
| 86 |
+
"""
|
| 87 |
+
if len(G.nodes) <= 3:
|
| 88 |
+
return True
|
| 89 |
+
return len(_find_chordality_breaker(G)) == 0
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@nx._dispatchable
|
| 93 |
+
def find_induced_nodes(G, s, t, treewidth_bound=sys.maxsize):
|
| 94 |
+
"""Returns the set of induced nodes in the path from s to t.
|
| 95 |
+
|
| 96 |
+
Parameters
|
| 97 |
+
----------
|
| 98 |
+
G : graph
|
| 99 |
+
A chordal NetworkX graph
|
| 100 |
+
s : node
|
| 101 |
+
Source node to look for induced nodes
|
| 102 |
+
t : node
|
| 103 |
+
Destination node to look for induced nodes
|
| 104 |
+
treewidth_bound: float
|
| 105 |
+
Maximum treewidth acceptable for the graph H. The search
|
| 106 |
+
for induced nodes will end as soon as the treewidth_bound is exceeded.
|
| 107 |
+
|
| 108 |
+
Returns
|
| 109 |
+
-------
|
| 110 |
+
induced_nodes : Set of nodes
|
| 111 |
+
The set of induced nodes in the path from s to t in G
|
| 112 |
+
|
| 113 |
+
Raises
|
| 114 |
+
------
|
| 115 |
+
NetworkXError
|
| 116 |
+
The algorithm does not support DiGraph, MultiGraph and MultiDiGraph.
|
| 117 |
+
If the input graph is an instance of one of these classes, a
|
| 118 |
+
:exc:`NetworkXError` is raised.
|
| 119 |
+
The algorithm can only be applied to chordal graphs. If the input
|
| 120 |
+
graph is found to be non-chordal, a :exc:`NetworkXError` is raised.
|
| 121 |
+
|
| 122 |
+
Examples
|
| 123 |
+
--------
|
| 124 |
+
>>> G = nx.Graph()
|
| 125 |
+
>>> G = nx.generators.classic.path_graph(10)
|
| 126 |
+
>>> induced_nodes = nx.find_induced_nodes(G, 1, 9, 2)
|
| 127 |
+
>>> sorted(induced_nodes)
|
| 128 |
+
[1, 2, 3, 4, 5, 6, 7, 8, 9]
|
| 129 |
+
|
| 130 |
+
Notes
|
| 131 |
+
-----
|
| 132 |
+
G must be a chordal graph and (s,t) an edge that is not in G.
|
| 133 |
+
|
| 134 |
+
If a treewidth_bound is provided, the search for induced nodes will end
|
| 135 |
+
as soon as the treewidth_bound is exceeded.
|
| 136 |
+
|
| 137 |
+
The algorithm is inspired by Algorithm 4 in [1]_.
|
| 138 |
+
A formal definition of induced node can also be found on that reference.
|
| 139 |
+
|
| 140 |
+
Self Loops are ignored
|
| 141 |
+
|
| 142 |
+
References
|
| 143 |
+
----------
|
| 144 |
+
.. [1] Learning Bounded Treewidth Bayesian Networks.
|
| 145 |
+
Gal Elidan, Stephen Gould; JMLR, 9(Dec):2699--2731, 2008.
|
| 146 |
+
http://jmlr.csail.mit.edu/papers/volume9/elidan08a/elidan08a.pdf
|
| 147 |
+
"""
|
| 148 |
+
if not is_chordal(G):
|
| 149 |
+
raise nx.NetworkXError("Input graph is not chordal.")
|
| 150 |
+
|
| 151 |
+
H = nx.Graph(G)
|
| 152 |
+
H.add_edge(s, t)
|
| 153 |
+
induced_nodes = set()
|
| 154 |
+
triplet = _find_chordality_breaker(H, s, treewidth_bound)
|
| 155 |
+
while triplet:
|
| 156 |
+
(u, v, w) = triplet
|
| 157 |
+
induced_nodes.update(triplet)
|
| 158 |
+
for n in triplet:
|
| 159 |
+
if n != s:
|
| 160 |
+
H.add_edge(s, n)
|
| 161 |
+
triplet = _find_chordality_breaker(H, s, treewidth_bound)
|
| 162 |
+
if induced_nodes:
|
| 163 |
+
# Add t and the second node in the induced path from s to t.
|
| 164 |
+
induced_nodes.add(t)
|
| 165 |
+
for u in G[s]:
|
| 166 |
+
if len(induced_nodes & set(G[u])) == 2:
|
| 167 |
+
induced_nodes.add(u)
|
| 168 |
+
break
|
| 169 |
+
return induced_nodes
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@nx._dispatchable
|
| 173 |
+
def chordal_graph_cliques(G):
|
| 174 |
+
"""Returns all maximal cliques of a chordal graph.
|
| 175 |
+
|
| 176 |
+
The algorithm breaks the graph in connected components and performs a
|
| 177 |
+
maximum cardinality search in each component to get the cliques.
|
| 178 |
+
|
| 179 |
+
Parameters
|
| 180 |
+
----------
|
| 181 |
+
G : graph
|
| 182 |
+
A NetworkX graph
|
| 183 |
+
|
| 184 |
+
Yields
|
| 185 |
+
------
|
| 186 |
+
frozenset of nodes
|
| 187 |
+
Maximal cliques, each of which is a frozenset of
|
| 188 |
+
nodes in `G`. The order of cliques is arbitrary.
|
| 189 |
+
|
| 190 |
+
Raises
|
| 191 |
+
------
|
| 192 |
+
NetworkXError
|
| 193 |
+
The algorithm does not support DiGraph, MultiGraph and MultiDiGraph.
|
| 194 |
+
The algorithm can only be applied to chordal graphs. If the input
|
| 195 |
+
graph is found to be non-chordal, a :exc:`NetworkXError` is raised.
|
| 196 |
+
|
| 197 |
+
Examples
|
| 198 |
+
--------
|
| 199 |
+
>>> e = [
|
| 200 |
+
... (1, 2),
|
| 201 |
+
... (1, 3),
|
| 202 |
+
... (2, 3),
|
| 203 |
+
... (2, 4),
|
| 204 |
+
... (3, 4),
|
| 205 |
+
... (3, 5),
|
| 206 |
+
... (3, 6),
|
| 207 |
+
... (4, 5),
|
| 208 |
+
... (4, 6),
|
| 209 |
+
... (5, 6),
|
| 210 |
+
... (7, 8),
|
| 211 |
+
... ]
|
| 212 |
+
>>> G = nx.Graph(e)
|
| 213 |
+
>>> G.add_node(9)
|
| 214 |
+
>>> cliques = [c for c in chordal_graph_cliques(G)]
|
| 215 |
+
>>> cliques[0]
|
| 216 |
+
frozenset({1, 2, 3})
|
| 217 |
+
"""
|
| 218 |
+
for C in (G.subgraph(c).copy() for c in connected_components(G)):
|
| 219 |
+
if C.number_of_nodes() == 1:
|
| 220 |
+
if nx.number_of_selfloops(C) > 0:
|
| 221 |
+
raise nx.NetworkXError("Input graph is not chordal.")
|
| 222 |
+
yield frozenset(C.nodes())
|
| 223 |
+
else:
|
| 224 |
+
unnumbered = set(C.nodes())
|
| 225 |
+
v = arbitrary_element(C)
|
| 226 |
+
unnumbered.remove(v)
|
| 227 |
+
numbered = {v}
|
| 228 |
+
clique_wanna_be = {v}
|
| 229 |
+
while unnumbered:
|
| 230 |
+
v = _max_cardinality_node(C, unnumbered, numbered)
|
| 231 |
+
unnumbered.remove(v)
|
| 232 |
+
numbered.add(v)
|
| 233 |
+
new_clique_wanna_be = set(C.neighbors(v)) & numbered
|
| 234 |
+
sg = C.subgraph(clique_wanna_be)
|
| 235 |
+
if _is_complete_graph(sg):
|
| 236 |
+
new_clique_wanna_be.add(v)
|
| 237 |
+
if not new_clique_wanna_be >= clique_wanna_be:
|
| 238 |
+
yield frozenset(clique_wanna_be)
|
| 239 |
+
clique_wanna_be = new_clique_wanna_be
|
| 240 |
+
else:
|
| 241 |
+
raise nx.NetworkXError("Input graph is not chordal.")
|
| 242 |
+
yield frozenset(clique_wanna_be)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
@nx._dispatchable
|
| 246 |
+
def chordal_graph_treewidth(G):
|
| 247 |
+
"""Returns the treewidth of the chordal graph G.
|
| 248 |
+
|
| 249 |
+
Parameters
|
| 250 |
+
----------
|
| 251 |
+
G : graph
|
| 252 |
+
A NetworkX graph
|
| 253 |
+
|
| 254 |
+
Returns
|
| 255 |
+
-------
|
| 256 |
+
treewidth : int
|
| 257 |
+
The size of the largest clique in the graph minus one.
|
| 258 |
+
|
| 259 |
+
Raises
|
| 260 |
+
------
|
| 261 |
+
NetworkXError
|
| 262 |
+
The algorithm does not support DiGraph, MultiGraph and MultiDiGraph.
|
| 263 |
+
The algorithm can only be applied to chordal graphs. If the input
|
| 264 |
+
graph is found to be non-chordal, a :exc:`NetworkXError` is raised.
|
| 265 |
+
|
| 266 |
+
Examples
|
| 267 |
+
--------
|
| 268 |
+
>>> e = [
|
| 269 |
+
... (1, 2),
|
| 270 |
+
... (1, 3),
|
| 271 |
+
... (2, 3),
|
| 272 |
+
... (2, 4),
|
| 273 |
+
... (3, 4),
|
| 274 |
+
... (3, 5),
|
| 275 |
+
... (3, 6),
|
| 276 |
+
... (4, 5),
|
| 277 |
+
... (4, 6),
|
| 278 |
+
... (5, 6),
|
| 279 |
+
... (7, 8),
|
| 280 |
+
... ]
|
| 281 |
+
>>> G = nx.Graph(e)
|
| 282 |
+
>>> G.add_node(9)
|
| 283 |
+
>>> nx.chordal_graph_treewidth(G)
|
| 284 |
+
3
|
| 285 |
+
|
| 286 |
+
References
|
| 287 |
+
----------
|
| 288 |
+
.. [1] https://en.wikipedia.org/wiki/Tree_decomposition#Treewidth
|
| 289 |
+
"""
|
| 290 |
+
if not is_chordal(G):
|
| 291 |
+
raise nx.NetworkXError("Input graph is not chordal.")
|
| 292 |
+
|
| 293 |
+
max_clique = -1
|
| 294 |
+
for clique in nx.chordal_graph_cliques(G):
|
| 295 |
+
max_clique = max(max_clique, len(clique))
|
| 296 |
+
return max_clique - 1
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _is_complete_graph(G):
|
| 300 |
+
"""Returns True if G is a complete graph."""
|
| 301 |
+
if nx.number_of_selfloops(G) > 0:
|
| 302 |
+
raise nx.NetworkXError("Self loop found in _is_complete_graph()")
|
| 303 |
+
n = G.number_of_nodes()
|
| 304 |
+
if n < 2:
|
| 305 |
+
return True
|
| 306 |
+
e = G.number_of_edges()
|
| 307 |
+
max_edges = (n * (n - 1)) / 2
|
| 308 |
+
return e == max_edges
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def _find_missing_edge(G):
|
| 312 |
+
"""Given a non-complete graph G, returns a missing edge."""
|
| 313 |
+
nodes = set(G)
|
| 314 |
+
for u in G:
|
| 315 |
+
missing = nodes - set(list(G[u].keys()) + [u])
|
| 316 |
+
if missing:
|
| 317 |
+
return (u, missing.pop())
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def _max_cardinality_node(G, choices, wanna_connect):
|
| 321 |
+
"""Returns a the node in choices that has more connections in G
|
| 322 |
+
to nodes in wanna_connect.
|
| 323 |
+
"""
|
| 324 |
+
max_number = -1
|
| 325 |
+
for x in choices:
|
| 326 |
+
number = len([y for y in G[x] if y in wanna_connect])
|
| 327 |
+
if number > max_number:
|
| 328 |
+
max_number = number
|
| 329 |
+
max_cardinality_node = x
|
| 330 |
+
return max_cardinality_node
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def _find_chordality_breaker(G, s=None, treewidth_bound=sys.maxsize):
|
| 334 |
+
"""Given a graph G, starts a max cardinality search
|
| 335 |
+
(starting from s if s is given and from an arbitrary node otherwise)
|
| 336 |
+
trying to find a non-chordal cycle.
|
| 337 |
+
|
| 338 |
+
If it does find one, it returns (u,v,w) where u,v,w are the three
|
| 339 |
+
nodes that together with s are involved in the cycle.
|
| 340 |
+
|
| 341 |
+
It ignores any self loops.
|
| 342 |
+
"""
|
| 343 |
+
if len(G) == 0:
|
| 344 |
+
raise nx.NetworkXPointlessConcept("Graph has no nodes.")
|
| 345 |
+
unnumbered = set(G)
|
| 346 |
+
if s is None:
|
| 347 |
+
s = arbitrary_element(G)
|
| 348 |
+
unnumbered.remove(s)
|
| 349 |
+
numbered = {s}
|
| 350 |
+
current_treewidth = -1
|
| 351 |
+
while unnumbered: # and current_treewidth <= treewidth_bound:
|
| 352 |
+
v = _max_cardinality_node(G, unnumbered, numbered)
|
| 353 |
+
unnumbered.remove(v)
|
| 354 |
+
numbered.add(v)
|
| 355 |
+
clique_wanna_be = set(G[v]) & numbered
|
| 356 |
+
sg = G.subgraph(clique_wanna_be)
|
| 357 |
+
if _is_complete_graph(sg):
|
| 358 |
+
# The graph seems to be chordal by now. We update the treewidth
|
| 359 |
+
current_treewidth = max(current_treewidth, len(clique_wanna_be))
|
| 360 |
+
if current_treewidth > treewidth_bound:
|
| 361 |
+
raise nx.NetworkXTreewidthBoundExceeded(
|
| 362 |
+
f"treewidth_bound exceeded: {current_treewidth}"
|
| 363 |
+
)
|
| 364 |
+
else:
|
| 365 |
+
# sg is not a clique,
|
| 366 |
+
# look for an edge that is not included in sg
|
| 367 |
+
(u, w) = _find_missing_edge(sg)
|
| 368 |
+
return (u, v, w)
|
| 369 |
+
return ()
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
@not_implemented_for("directed")
|
| 373 |
+
@nx._dispatchable(returns_graph=True)
|
| 374 |
+
def complete_to_chordal_graph(G):
|
| 375 |
+
"""Return a copy of G completed to a chordal graph
|
| 376 |
+
|
| 377 |
+
Adds edges to a copy of G to create a chordal graph. A graph G=(V,E) is
|
| 378 |
+
called chordal if for each cycle with length bigger than 3, there exist
|
| 379 |
+
two non-adjacent nodes connected by an edge (called a chord).
|
| 380 |
+
|
| 381 |
+
Parameters
|
| 382 |
+
----------
|
| 383 |
+
G : NetworkX graph
|
| 384 |
+
Undirected graph
|
| 385 |
+
|
| 386 |
+
Returns
|
| 387 |
+
-------
|
| 388 |
+
H : NetworkX graph
|
| 389 |
+
The chordal enhancement of G
|
| 390 |
+
alpha : Dictionary
|
| 391 |
+
The elimination ordering of nodes of G
|
| 392 |
+
|
| 393 |
+
Notes
|
| 394 |
+
-----
|
| 395 |
+
There are different approaches to calculate the chordal
|
| 396 |
+
enhancement of a graph. The algorithm used here is called
|
| 397 |
+
MCS-M and gives at least minimal (local) triangulation of graph. Note
|
| 398 |
+
that this triangulation is not necessarily a global minimum.
|
| 399 |
+
|
| 400 |
+
https://en.wikipedia.org/wiki/Chordal_graph
|
| 401 |
+
|
| 402 |
+
References
|
| 403 |
+
----------
|
| 404 |
+
.. [1] Berry, Anne & Blair, Jean & Heggernes, Pinar & Peyton, Barry. (2004)
|
| 405 |
+
Maximum Cardinality Search for Computing Minimal Triangulations of
|
| 406 |
+
Graphs. Algorithmica. 39. 287-298. 10.1007/s00453-004-1084-3.
|
| 407 |
+
|
| 408 |
+
Examples
|
| 409 |
+
--------
|
| 410 |
+
>>> from networkx.algorithms.chordal import complete_to_chordal_graph
|
| 411 |
+
>>> G = nx.wheel_graph(10)
|
| 412 |
+
>>> H, alpha = complete_to_chordal_graph(G)
|
| 413 |
+
"""
|
| 414 |
+
H = G.copy()
|
| 415 |
+
alpha = {node: 0 for node in H}
|
| 416 |
+
if nx.is_chordal(H):
|
| 417 |
+
return H, alpha
|
| 418 |
+
chords = set()
|
| 419 |
+
weight = {node: 0 for node in H.nodes()}
|
| 420 |
+
unnumbered_nodes = list(H.nodes())
|
| 421 |
+
for i in range(len(H.nodes()), 0, -1):
|
| 422 |
+
# get the node in unnumbered_nodes with the maximum weight
|
| 423 |
+
z = max(unnumbered_nodes, key=lambda node: weight[node])
|
| 424 |
+
unnumbered_nodes.remove(z)
|
| 425 |
+
alpha[z] = i
|
| 426 |
+
update_nodes = []
|
| 427 |
+
for y in unnumbered_nodes:
|
| 428 |
+
if G.has_edge(y, z):
|
| 429 |
+
update_nodes.append(y)
|
| 430 |
+
else:
|
| 431 |
+
# y_weight will be bigger than node weights between y and z
|
| 432 |
+
y_weight = weight[y]
|
| 433 |
+
lower_nodes = [
|
| 434 |
+
node for node in unnumbered_nodes if weight[node] < y_weight
|
| 435 |
+
]
|
| 436 |
+
if nx.has_path(H.subgraph(lower_nodes + [z, y]), y, z):
|
| 437 |
+
update_nodes.append(y)
|
| 438 |
+
chords.add((z, y))
|
| 439 |
+
# during calculation of paths the weights should not be updated
|
| 440 |
+
for node in update_nodes:
|
| 441 |
+
weight[node] += 1
|
| 442 |
+
H.add_edges_from(chords)
|
| 443 |
+
return H, alpha
|
.venv/lib/python3.11/site-packages/networkx/algorithms/clique.py
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions for finding and manipulating cliques.
|
| 2 |
+
|
| 3 |
+
Finding the largest clique in a graph is NP-complete problem, so most of
|
| 4 |
+
these algorithms have an exponential running time; for more information,
|
| 5 |
+
see the Wikipedia article on the clique problem [1]_.
|
| 6 |
+
|
| 7 |
+
.. [1] clique problem:: https://en.wikipedia.org/wiki/Clique_problem
|
| 8 |
+
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from collections import defaultdict, deque
|
| 12 |
+
from itertools import chain, combinations, islice
|
| 13 |
+
|
| 14 |
+
import networkx as nx
|
| 15 |
+
from networkx.utils import not_implemented_for
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
"find_cliques",
|
| 19 |
+
"find_cliques_recursive",
|
| 20 |
+
"make_max_clique_graph",
|
| 21 |
+
"make_clique_bipartite",
|
| 22 |
+
"node_clique_number",
|
| 23 |
+
"number_of_cliques",
|
| 24 |
+
"enumerate_all_cliques",
|
| 25 |
+
"max_weight_clique",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@not_implemented_for("directed")
|
| 30 |
+
@nx._dispatchable
|
| 31 |
+
def enumerate_all_cliques(G):
|
| 32 |
+
"""Returns all cliques in an undirected graph.
|
| 33 |
+
|
| 34 |
+
This function returns an iterator over cliques, each of which is a
|
| 35 |
+
list of nodes. The iteration is ordered by cardinality of the
|
| 36 |
+
cliques: first all cliques of size one, then all cliques of size
|
| 37 |
+
two, etc.
|
| 38 |
+
|
| 39 |
+
Parameters
|
| 40 |
+
----------
|
| 41 |
+
G : NetworkX graph
|
| 42 |
+
An undirected graph.
|
| 43 |
+
|
| 44 |
+
Returns
|
| 45 |
+
-------
|
| 46 |
+
iterator
|
| 47 |
+
An iterator over cliques, each of which is a list of nodes in
|
| 48 |
+
`G`. The cliques are ordered according to size.
|
| 49 |
+
|
| 50 |
+
Notes
|
| 51 |
+
-----
|
| 52 |
+
To obtain a list of all cliques, use
|
| 53 |
+
`list(enumerate_all_cliques(G))`. However, be aware that in the
|
| 54 |
+
worst-case, the length of this list can be exponential in the number
|
| 55 |
+
of nodes in the graph (for example, when the graph is the complete
|
| 56 |
+
graph). This function avoids storing all cliques in memory by only
|
| 57 |
+
keeping current candidate node lists in memory during its search.
|
| 58 |
+
|
| 59 |
+
The implementation is adapted from the algorithm by Zhang, et
|
| 60 |
+
al. (2005) [1]_ to output all cliques discovered.
|
| 61 |
+
|
| 62 |
+
This algorithm ignores self-loops and parallel edges, since cliques
|
| 63 |
+
are not conventionally defined with such edges.
|
| 64 |
+
|
| 65 |
+
References
|
| 66 |
+
----------
|
| 67 |
+
.. [1] Yun Zhang, Abu-Khzam, F.N., Baldwin, N.E., Chesler, E.J.,
|
| 68 |
+
Langston, M.A., Samatova, N.F.,
|
| 69 |
+
"Genome-Scale Computational Approaches to Memory-Intensive
|
| 70 |
+
Applications in Systems Biology".
|
| 71 |
+
*Supercomputing*, 2005. Proceedings of the ACM/IEEE SC 2005
|
| 72 |
+
Conference, pp. 12, 12--18 Nov. 2005.
|
| 73 |
+
<https://doi.org/10.1109/SC.2005.29>.
|
| 74 |
+
|
| 75 |
+
"""
|
| 76 |
+
index = {}
|
| 77 |
+
nbrs = {}
|
| 78 |
+
for u in G:
|
| 79 |
+
index[u] = len(index)
|
| 80 |
+
# Neighbors of u that appear after u in the iteration order of G.
|
| 81 |
+
nbrs[u] = {v for v in G[u] if v not in index}
|
| 82 |
+
|
| 83 |
+
queue = deque(([u], sorted(nbrs[u], key=index.__getitem__)) for u in G)
|
| 84 |
+
# Loop invariants:
|
| 85 |
+
# 1. len(base) is nondecreasing.
|
| 86 |
+
# 2. (base + cnbrs) is sorted with respect to the iteration order of G.
|
| 87 |
+
# 3. cnbrs is a set of common neighbors of nodes in base.
|
| 88 |
+
while queue:
|
| 89 |
+
base, cnbrs = map(list, queue.popleft())
|
| 90 |
+
yield base
|
| 91 |
+
for i, u in enumerate(cnbrs):
|
| 92 |
+
# Use generators to reduce memory consumption.
|
| 93 |
+
queue.append(
|
| 94 |
+
(
|
| 95 |
+
chain(base, [u]),
|
| 96 |
+
filter(nbrs[u].__contains__, islice(cnbrs, i + 1, None)),
|
| 97 |
+
)
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@not_implemented_for("directed")
|
| 102 |
+
@nx._dispatchable
|
| 103 |
+
def find_cliques(G, nodes=None):
|
| 104 |
+
"""Returns all maximal cliques in an undirected graph.
|
| 105 |
+
|
| 106 |
+
For each node *n*, a *maximal clique for n* is a largest complete
|
| 107 |
+
subgraph containing *n*. The largest maximal clique is sometimes
|
| 108 |
+
called the *maximum clique*.
|
| 109 |
+
|
| 110 |
+
This function returns an iterator over cliques, each of which is a
|
| 111 |
+
list of nodes. It is an iterative implementation, so should not
|
| 112 |
+
suffer from recursion depth issues.
|
| 113 |
+
|
| 114 |
+
This function accepts a list of `nodes` and only the maximal cliques
|
| 115 |
+
containing all of these `nodes` are returned. It can considerably speed up
|
| 116 |
+
the running time if some specific cliques are desired.
|
| 117 |
+
|
| 118 |
+
Parameters
|
| 119 |
+
----------
|
| 120 |
+
G : NetworkX graph
|
| 121 |
+
An undirected graph.
|
| 122 |
+
|
| 123 |
+
nodes : list, optional (default=None)
|
| 124 |
+
If provided, only yield *maximal cliques* containing all nodes in `nodes`.
|
| 125 |
+
If `nodes` isn't a clique itself, a ValueError is raised.
|
| 126 |
+
|
| 127 |
+
Returns
|
| 128 |
+
-------
|
| 129 |
+
iterator
|
| 130 |
+
An iterator over maximal cliques, each of which is a list of
|
| 131 |
+
nodes in `G`. If `nodes` is provided, only the maximal cliques
|
| 132 |
+
containing all the nodes in `nodes` are returned. The order of
|
| 133 |
+
cliques is arbitrary.
|
| 134 |
+
|
| 135 |
+
Raises
|
| 136 |
+
------
|
| 137 |
+
ValueError
|
| 138 |
+
If `nodes` is not a clique.
|
| 139 |
+
|
| 140 |
+
Examples
|
| 141 |
+
--------
|
| 142 |
+
>>> from pprint import pprint # For nice dict formatting
|
| 143 |
+
>>> G = nx.karate_club_graph()
|
| 144 |
+
>>> sum(1 for c in nx.find_cliques(G)) # The number of maximal cliques in G
|
| 145 |
+
36
|
| 146 |
+
>>> max(nx.find_cliques(G), key=len) # The largest maximal clique in G
|
| 147 |
+
[0, 1, 2, 3, 13]
|
| 148 |
+
|
| 149 |
+
The size of the largest maximal clique is known as the *clique number* of
|
| 150 |
+
the graph, which can be found directly with:
|
| 151 |
+
|
| 152 |
+
>>> max(len(c) for c in nx.find_cliques(G))
|
| 153 |
+
5
|
| 154 |
+
|
| 155 |
+
One can also compute the number of maximal cliques in `G` that contain a given
|
| 156 |
+
node. The following produces a dictionary keyed by node whose
|
| 157 |
+
values are the number of maximal cliques in `G` that contain the node:
|
| 158 |
+
|
| 159 |
+
>>> pprint({n: sum(1 for c in nx.find_cliques(G) if n in c) for n in G})
|
| 160 |
+
{0: 13,
|
| 161 |
+
1: 6,
|
| 162 |
+
2: 7,
|
| 163 |
+
3: 3,
|
| 164 |
+
4: 2,
|
| 165 |
+
5: 3,
|
| 166 |
+
6: 3,
|
| 167 |
+
7: 1,
|
| 168 |
+
8: 3,
|
| 169 |
+
9: 2,
|
| 170 |
+
10: 2,
|
| 171 |
+
11: 1,
|
| 172 |
+
12: 1,
|
| 173 |
+
13: 2,
|
| 174 |
+
14: 1,
|
| 175 |
+
15: 1,
|
| 176 |
+
16: 1,
|
| 177 |
+
17: 1,
|
| 178 |
+
18: 1,
|
| 179 |
+
19: 2,
|
| 180 |
+
20: 1,
|
| 181 |
+
21: 1,
|
| 182 |
+
22: 1,
|
| 183 |
+
23: 3,
|
| 184 |
+
24: 2,
|
| 185 |
+
25: 2,
|
| 186 |
+
26: 1,
|
| 187 |
+
27: 3,
|
| 188 |
+
28: 2,
|
| 189 |
+
29: 2,
|
| 190 |
+
30: 2,
|
| 191 |
+
31: 4,
|
| 192 |
+
32: 9,
|
| 193 |
+
33: 14}
|
| 194 |
+
|
| 195 |
+
Or, similarly, the maximal cliques in `G` that contain a given node.
|
| 196 |
+
For example, the 4 maximal cliques that contain node 31:
|
| 197 |
+
|
| 198 |
+
>>> [c for c in nx.find_cliques(G) if 31 in c]
|
| 199 |
+
[[0, 31], [33, 32, 31], [33, 28, 31], [24, 25, 31]]
|
| 200 |
+
|
| 201 |
+
See Also
|
| 202 |
+
--------
|
| 203 |
+
find_cliques_recursive
|
| 204 |
+
A recursive version of the same algorithm.
|
| 205 |
+
|
| 206 |
+
Notes
|
| 207 |
+
-----
|
| 208 |
+
To obtain a list of all maximal cliques, use
|
| 209 |
+
`list(find_cliques(G))`. However, be aware that in the worst-case,
|
| 210 |
+
the length of this list can be exponential in the number of nodes in
|
| 211 |
+
the graph. This function avoids storing all cliques in memory by
|
| 212 |
+
only keeping current candidate node lists in memory during its search.
|
| 213 |
+
|
| 214 |
+
This implementation is based on the algorithm published by Bron and
|
| 215 |
+
Kerbosch (1973) [1]_, as adapted by Tomita, Tanaka and Takahashi
|
| 216 |
+
(2006) [2]_ and discussed in Cazals and Karande (2008) [3]_. It
|
| 217 |
+
essentially unrolls the recursion used in the references to avoid
|
| 218 |
+
issues of recursion stack depth (for a recursive implementation, see
|
| 219 |
+
:func:`find_cliques_recursive`).
|
| 220 |
+
|
| 221 |
+
This algorithm ignores self-loops and parallel edges, since cliques
|
| 222 |
+
are not conventionally defined with such edges.
|
| 223 |
+
|
| 224 |
+
References
|
| 225 |
+
----------
|
| 226 |
+
.. [1] Bron, C. and Kerbosch, J.
|
| 227 |
+
"Algorithm 457: finding all cliques of an undirected graph".
|
| 228 |
+
*Communications of the ACM* 16, 9 (Sep. 1973), 575--577.
|
| 229 |
+
<http://portal.acm.org/citation.cfm?doid=362342.362367>
|
| 230 |
+
|
| 231 |
+
.. [2] Etsuji Tomita, Akira Tanaka, Haruhisa Takahashi,
|
| 232 |
+
"The worst-case time complexity for generating all maximal
|
| 233 |
+
cliques and computational experiments",
|
| 234 |
+
*Theoretical Computer Science*, Volume 363, Issue 1,
|
| 235 |
+
Computing and Combinatorics,
|
| 236 |
+
10th Annual International Conference on
|
| 237 |
+
Computing and Combinatorics (COCOON 2004), 25 October 2006, Pages 28--42
|
| 238 |
+
<https://doi.org/10.1016/j.tcs.2006.06.015>
|
| 239 |
+
|
| 240 |
+
.. [3] F. Cazals, C. Karande,
|
| 241 |
+
"A note on the problem of reporting maximal cliques",
|
| 242 |
+
*Theoretical Computer Science*,
|
| 243 |
+
Volume 407, Issues 1--3, 6 November 2008, Pages 564--568,
|
| 244 |
+
<https://doi.org/10.1016/j.tcs.2008.05.010>
|
| 245 |
+
|
| 246 |
+
"""
|
| 247 |
+
if len(G) == 0:
|
| 248 |
+
return
|
| 249 |
+
|
| 250 |
+
adj = {u: {v for v in G[u] if v != u} for u in G}
|
| 251 |
+
|
| 252 |
+
# Initialize Q with the given nodes and subg, cand with their nbrs
|
| 253 |
+
Q = nodes[:] if nodes is not None else []
|
| 254 |
+
cand = set(G)
|
| 255 |
+
for node in Q:
|
| 256 |
+
if node not in cand:
|
| 257 |
+
raise ValueError(f"The given `nodes` {nodes} do not form a clique")
|
| 258 |
+
cand &= adj[node]
|
| 259 |
+
|
| 260 |
+
if not cand:
|
| 261 |
+
yield Q[:]
|
| 262 |
+
return
|
| 263 |
+
|
| 264 |
+
subg = cand.copy()
|
| 265 |
+
stack = []
|
| 266 |
+
Q.append(None)
|
| 267 |
+
|
| 268 |
+
u = max(subg, key=lambda u: len(cand & adj[u]))
|
| 269 |
+
ext_u = cand - adj[u]
|
| 270 |
+
|
| 271 |
+
try:
|
| 272 |
+
while True:
|
| 273 |
+
if ext_u:
|
| 274 |
+
q = ext_u.pop()
|
| 275 |
+
cand.remove(q)
|
| 276 |
+
Q[-1] = q
|
| 277 |
+
adj_q = adj[q]
|
| 278 |
+
subg_q = subg & adj_q
|
| 279 |
+
if not subg_q:
|
| 280 |
+
yield Q[:]
|
| 281 |
+
else:
|
| 282 |
+
cand_q = cand & adj_q
|
| 283 |
+
if cand_q:
|
| 284 |
+
stack.append((subg, cand, ext_u))
|
| 285 |
+
Q.append(None)
|
| 286 |
+
subg = subg_q
|
| 287 |
+
cand = cand_q
|
| 288 |
+
u = max(subg, key=lambda u: len(cand & adj[u]))
|
| 289 |
+
ext_u = cand - adj[u]
|
| 290 |
+
else:
|
| 291 |
+
Q.pop()
|
| 292 |
+
subg, cand, ext_u = stack.pop()
|
| 293 |
+
except IndexError:
|
| 294 |
+
pass
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
# TODO Should this also be not implemented for directed graphs?
|
| 298 |
+
@nx._dispatchable
|
| 299 |
+
def find_cliques_recursive(G, nodes=None):
|
| 300 |
+
"""Returns all maximal cliques in a graph.
|
| 301 |
+
|
| 302 |
+
For each node *v*, a *maximal clique for v* is a largest complete
|
| 303 |
+
subgraph containing *v*. The largest maximal clique is sometimes
|
| 304 |
+
called the *maximum clique*.
|
| 305 |
+
|
| 306 |
+
This function returns an iterator over cliques, each of which is a
|
| 307 |
+
list of nodes. It is a recursive implementation, so may suffer from
|
| 308 |
+
recursion depth issues, but is included for pedagogical reasons.
|
| 309 |
+
For a non-recursive implementation, see :func:`find_cliques`.
|
| 310 |
+
|
| 311 |
+
This function accepts a list of `nodes` and only the maximal cliques
|
| 312 |
+
containing all of these `nodes` are returned. It can considerably speed up
|
| 313 |
+
the running time if some specific cliques are desired.
|
| 314 |
+
|
| 315 |
+
Parameters
|
| 316 |
+
----------
|
| 317 |
+
G : NetworkX graph
|
| 318 |
+
|
| 319 |
+
nodes : list, optional (default=None)
|
| 320 |
+
If provided, only yield *maximal cliques* containing all nodes in `nodes`.
|
| 321 |
+
If `nodes` isn't a clique itself, a ValueError is raised.
|
| 322 |
+
|
| 323 |
+
Returns
|
| 324 |
+
-------
|
| 325 |
+
iterator
|
| 326 |
+
An iterator over maximal cliques, each of which is a list of
|
| 327 |
+
nodes in `G`. If `nodes` is provided, only the maximal cliques
|
| 328 |
+
containing all the nodes in `nodes` are yielded. The order of
|
| 329 |
+
cliques is arbitrary.
|
| 330 |
+
|
| 331 |
+
Raises
|
| 332 |
+
------
|
| 333 |
+
ValueError
|
| 334 |
+
If `nodes` is not a clique.
|
| 335 |
+
|
| 336 |
+
See Also
|
| 337 |
+
--------
|
| 338 |
+
find_cliques
|
| 339 |
+
An iterative version of the same algorithm. See docstring for examples.
|
| 340 |
+
|
| 341 |
+
Notes
|
| 342 |
+
-----
|
| 343 |
+
To obtain a list of all maximal cliques, use
|
| 344 |
+
`list(find_cliques_recursive(G))`. However, be aware that in the
|
| 345 |
+
worst-case, the length of this list can be exponential in the number
|
| 346 |
+
of nodes in the graph. This function avoids storing all cliques in memory
|
| 347 |
+
by only keeping current candidate node lists in memory during its search.
|
| 348 |
+
|
| 349 |
+
This implementation is based on the algorithm published by Bron and
|
| 350 |
+
Kerbosch (1973) [1]_, as adapted by Tomita, Tanaka and Takahashi
|
| 351 |
+
(2006) [2]_ and discussed in Cazals and Karande (2008) [3]_. For a
|
| 352 |
+
non-recursive implementation, see :func:`find_cliques`.
|
| 353 |
+
|
| 354 |
+
This algorithm ignores self-loops and parallel edges, since cliques
|
| 355 |
+
are not conventionally defined with such edges.
|
| 356 |
+
|
| 357 |
+
References
|
| 358 |
+
----------
|
| 359 |
+
.. [1] Bron, C. and Kerbosch, J.
|
| 360 |
+
"Algorithm 457: finding all cliques of an undirected graph".
|
| 361 |
+
*Communications of the ACM* 16, 9 (Sep. 1973), 575--577.
|
| 362 |
+
<http://portal.acm.org/citation.cfm?doid=362342.362367>
|
| 363 |
+
|
| 364 |
+
.. [2] Etsuji Tomita, Akira Tanaka, Haruhisa Takahashi,
|
| 365 |
+
"The worst-case time complexity for generating all maximal
|
| 366 |
+
cliques and computational experiments",
|
| 367 |
+
*Theoretical Computer Science*, Volume 363, Issue 1,
|
| 368 |
+
Computing and Combinatorics,
|
| 369 |
+
10th Annual International Conference on
|
| 370 |
+
Computing and Combinatorics (COCOON 2004), 25 October 2006, Pages 28--42
|
| 371 |
+
<https://doi.org/10.1016/j.tcs.2006.06.015>
|
| 372 |
+
|
| 373 |
+
.. [3] F. Cazals, C. Karande,
|
| 374 |
+
"A note on the problem of reporting maximal cliques",
|
| 375 |
+
*Theoretical Computer Science*,
|
| 376 |
+
Volume 407, Issues 1--3, 6 November 2008, Pages 564--568,
|
| 377 |
+
<https://doi.org/10.1016/j.tcs.2008.05.010>
|
| 378 |
+
|
| 379 |
+
"""
|
| 380 |
+
if len(G) == 0:
|
| 381 |
+
return iter([])
|
| 382 |
+
|
| 383 |
+
adj = {u: {v for v in G[u] if v != u} for u in G}
|
| 384 |
+
|
| 385 |
+
# Initialize Q with the given nodes and subg, cand with their nbrs
|
| 386 |
+
Q = nodes[:] if nodes is not None else []
|
| 387 |
+
cand_init = set(G)
|
| 388 |
+
for node in Q:
|
| 389 |
+
if node not in cand_init:
|
| 390 |
+
raise ValueError(f"The given `nodes` {nodes} do not form a clique")
|
| 391 |
+
cand_init &= adj[node]
|
| 392 |
+
|
| 393 |
+
if not cand_init:
|
| 394 |
+
return iter([Q])
|
| 395 |
+
|
| 396 |
+
subg_init = cand_init.copy()
|
| 397 |
+
|
| 398 |
+
def expand(subg, cand):
|
| 399 |
+
u = max(subg, key=lambda u: len(cand & adj[u]))
|
| 400 |
+
for q in cand - adj[u]:
|
| 401 |
+
cand.remove(q)
|
| 402 |
+
Q.append(q)
|
| 403 |
+
adj_q = adj[q]
|
| 404 |
+
subg_q = subg & adj_q
|
| 405 |
+
if not subg_q:
|
| 406 |
+
yield Q[:]
|
| 407 |
+
else:
|
| 408 |
+
cand_q = cand & adj_q
|
| 409 |
+
if cand_q:
|
| 410 |
+
yield from expand(subg_q, cand_q)
|
| 411 |
+
Q.pop()
|
| 412 |
+
|
| 413 |
+
return expand(subg_init, cand_init)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
@nx._dispatchable(returns_graph=True)
|
| 417 |
+
def make_max_clique_graph(G, create_using=None):
|
| 418 |
+
"""Returns the maximal clique graph of the given graph.
|
| 419 |
+
|
| 420 |
+
The nodes of the maximal clique graph of `G` are the cliques of
|
| 421 |
+
`G` and an edge joins two cliques if the cliques are not disjoint.
|
| 422 |
+
|
| 423 |
+
Parameters
|
| 424 |
+
----------
|
| 425 |
+
G : NetworkX graph
|
| 426 |
+
|
| 427 |
+
create_using : NetworkX graph constructor, optional (default=nx.Graph)
|
| 428 |
+
Graph type to create. If graph instance, then cleared before populated.
|
| 429 |
+
|
| 430 |
+
Returns
|
| 431 |
+
-------
|
| 432 |
+
NetworkX graph
|
| 433 |
+
A graph whose nodes are the cliques of `G` and whose edges
|
| 434 |
+
join two cliques if they are not disjoint.
|
| 435 |
+
|
| 436 |
+
Notes
|
| 437 |
+
-----
|
| 438 |
+
This function behaves like the following code::
|
| 439 |
+
|
| 440 |
+
import networkx as nx
|
| 441 |
+
|
| 442 |
+
G = nx.make_clique_bipartite(G)
|
| 443 |
+
cliques = [v for v in G.nodes() if G.nodes[v]["bipartite"] == 0]
|
| 444 |
+
G = nx.bipartite.projected_graph(G, cliques)
|
| 445 |
+
G = nx.relabel_nodes(G, {-v: v - 1 for v in G})
|
| 446 |
+
|
| 447 |
+
It should be faster, though, since it skips all the intermediate
|
| 448 |
+
steps.
|
| 449 |
+
|
| 450 |
+
"""
|
| 451 |
+
if create_using is None:
|
| 452 |
+
B = G.__class__()
|
| 453 |
+
else:
|
| 454 |
+
B = nx.empty_graph(0, create_using)
|
| 455 |
+
cliques = list(enumerate(set(c) for c in find_cliques(G)))
|
| 456 |
+
# Add a numbered node for each clique.
|
| 457 |
+
B.add_nodes_from(i for i, c in cliques)
|
| 458 |
+
# Join cliques by an edge if they share a node.
|
| 459 |
+
clique_pairs = combinations(cliques, 2)
|
| 460 |
+
B.add_edges_from((i, j) for (i, c1), (j, c2) in clique_pairs if c1 & c2)
|
| 461 |
+
return B
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
@nx._dispatchable(returns_graph=True)
|
| 465 |
+
def make_clique_bipartite(G, fpos=None, create_using=None, name=None):
|
| 466 |
+
"""Returns the bipartite clique graph corresponding to `G`.
|
| 467 |
+
|
| 468 |
+
In the returned bipartite graph, the "bottom" nodes are the nodes of
|
| 469 |
+
`G` and the "top" nodes represent the maximal cliques of `G`.
|
| 470 |
+
There is an edge from node *v* to clique *C* in the returned graph
|
| 471 |
+
if and only if *v* is an element of *C*.
|
| 472 |
+
|
| 473 |
+
Parameters
|
| 474 |
+
----------
|
| 475 |
+
G : NetworkX graph
|
| 476 |
+
An undirected graph.
|
| 477 |
+
|
| 478 |
+
fpos : bool
|
| 479 |
+
If True or not None, the returned graph will have an
|
| 480 |
+
additional attribute, `pos`, a dictionary mapping node to
|
| 481 |
+
position in the Euclidean plane.
|
| 482 |
+
|
| 483 |
+
create_using : NetworkX graph constructor, optional (default=nx.Graph)
|
| 484 |
+
Graph type to create. If graph instance, then cleared before populated.
|
| 485 |
+
|
| 486 |
+
Returns
|
| 487 |
+
-------
|
| 488 |
+
NetworkX graph
|
| 489 |
+
A bipartite graph whose "bottom" set is the nodes of the graph
|
| 490 |
+
`G`, whose "top" set is the cliques of `G`, and whose edges
|
| 491 |
+
join nodes of `G` to the cliques that contain them.
|
| 492 |
+
|
| 493 |
+
The nodes of the graph `G` have the node attribute
|
| 494 |
+
'bipartite' set to 1 and the nodes representing cliques
|
| 495 |
+
have the node attribute 'bipartite' set to 0, as is the
|
| 496 |
+
convention for bipartite graphs in NetworkX.
|
| 497 |
+
|
| 498 |
+
"""
|
| 499 |
+
B = nx.empty_graph(0, create_using)
|
| 500 |
+
B.clear()
|
| 501 |
+
# The "bottom" nodes in the bipartite graph are the nodes of the
|
| 502 |
+
# original graph, G.
|
| 503 |
+
B.add_nodes_from(G, bipartite=1)
|
| 504 |
+
for i, cl in enumerate(find_cliques(G)):
|
| 505 |
+
# The "top" nodes in the bipartite graph are the cliques. These
|
| 506 |
+
# nodes get negative numbers as labels.
|
| 507 |
+
name = -i - 1
|
| 508 |
+
B.add_node(name, bipartite=0)
|
| 509 |
+
B.add_edges_from((v, name) for v in cl)
|
| 510 |
+
return B
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
@nx._dispatchable
|
| 514 |
+
def node_clique_number(G, nodes=None, cliques=None, separate_nodes=False):
|
| 515 |
+
"""Returns the size of the largest maximal clique containing each given node.
|
| 516 |
+
|
| 517 |
+
Returns a single or list depending on input nodes.
|
| 518 |
+
An optional list of cliques can be input if already computed.
|
| 519 |
+
|
| 520 |
+
Parameters
|
| 521 |
+
----------
|
| 522 |
+
G : NetworkX graph
|
| 523 |
+
An undirected graph.
|
| 524 |
+
|
| 525 |
+
cliques : list, optional (default=None)
|
| 526 |
+
A list of cliques, each of which is itself a list of nodes.
|
| 527 |
+
If not specified, the list of all cliques will be computed
|
| 528 |
+
using :func:`find_cliques`.
|
| 529 |
+
|
| 530 |
+
Returns
|
| 531 |
+
-------
|
| 532 |
+
int or dict
|
| 533 |
+
If `nodes` is a single node, returns the size of the
|
| 534 |
+
largest maximal clique in `G` containing that node.
|
| 535 |
+
Otherwise return a dict keyed by node to the size
|
| 536 |
+
of the largest maximal clique containing that node.
|
| 537 |
+
|
| 538 |
+
See Also
|
| 539 |
+
--------
|
| 540 |
+
find_cliques
|
| 541 |
+
find_cliques yields the maximal cliques of G.
|
| 542 |
+
It accepts a `nodes` argument which restricts consideration to
|
| 543 |
+
maximal cliques containing all the given `nodes`.
|
| 544 |
+
The search for the cliques is optimized for `nodes`.
|
| 545 |
+
"""
|
| 546 |
+
if cliques is None:
|
| 547 |
+
if nodes is not None:
|
| 548 |
+
# Use ego_graph to decrease size of graph
|
| 549 |
+
# check for single node
|
| 550 |
+
if nodes in G:
|
| 551 |
+
return max(len(c) for c in find_cliques(nx.ego_graph(G, nodes)))
|
| 552 |
+
# handle multiple nodes
|
| 553 |
+
return {
|
| 554 |
+
n: max(len(c) for c in find_cliques(nx.ego_graph(G, n))) for n in nodes
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
# nodes is None--find all cliques
|
| 558 |
+
cliques = list(find_cliques(G))
|
| 559 |
+
|
| 560 |
+
# single node requested
|
| 561 |
+
if nodes in G:
|
| 562 |
+
return max(len(c) for c in cliques if nodes in c)
|
| 563 |
+
|
| 564 |
+
# multiple nodes requested
|
| 565 |
+
# preprocess all nodes (faster than one at a time for even 2 nodes)
|
| 566 |
+
size_for_n = defaultdict(int)
|
| 567 |
+
for c in cliques:
|
| 568 |
+
size_of_c = len(c)
|
| 569 |
+
for n in c:
|
| 570 |
+
if size_for_n[n] < size_of_c:
|
| 571 |
+
size_for_n[n] = size_of_c
|
| 572 |
+
if nodes is None:
|
| 573 |
+
return size_for_n
|
| 574 |
+
return {n: size_for_n[n] for n in nodes}
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
def number_of_cliques(G, nodes=None, cliques=None):
|
| 578 |
+
"""Returns the number of maximal cliques for each node.
|
| 579 |
+
|
| 580 |
+
Returns a single or list depending on input nodes.
|
| 581 |
+
Optional list of cliques can be input if already computed.
|
| 582 |
+
"""
|
| 583 |
+
if cliques is None:
|
| 584 |
+
cliques = list(find_cliques(G))
|
| 585 |
+
|
| 586 |
+
if nodes is None:
|
| 587 |
+
nodes = list(G.nodes()) # none, get entire graph
|
| 588 |
+
|
| 589 |
+
if not isinstance(nodes, list): # check for a list
|
| 590 |
+
v = nodes
|
| 591 |
+
# assume it is a single value
|
| 592 |
+
numcliq = len([1 for c in cliques if v in c])
|
| 593 |
+
else:
|
| 594 |
+
numcliq = {}
|
| 595 |
+
for v in nodes:
|
| 596 |
+
numcliq[v] = len([1 for c in cliques if v in c])
|
| 597 |
+
return numcliq
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
class MaxWeightClique:
|
| 601 |
+
"""A class for the maximum weight clique algorithm.
|
| 602 |
+
|
| 603 |
+
This class is a helper for the `max_weight_clique` function. The class
|
| 604 |
+
should not normally be used directly.
|
| 605 |
+
|
| 606 |
+
Parameters
|
| 607 |
+
----------
|
| 608 |
+
G : NetworkX graph
|
| 609 |
+
The undirected graph for which a maximum weight clique is sought
|
| 610 |
+
weight : string or None, optional (default='weight')
|
| 611 |
+
The node attribute that holds the integer value used as a weight.
|
| 612 |
+
If None, then each node has weight 1.
|
| 613 |
+
|
| 614 |
+
Attributes
|
| 615 |
+
----------
|
| 616 |
+
G : NetworkX graph
|
| 617 |
+
The undirected graph for which a maximum weight clique is sought
|
| 618 |
+
node_weights: dict
|
| 619 |
+
The weight of each node
|
| 620 |
+
incumbent_nodes : list
|
| 621 |
+
The nodes of the incumbent clique (the best clique found so far)
|
| 622 |
+
incumbent_weight: int
|
| 623 |
+
The weight of the incumbent clique
|
| 624 |
+
"""
|
| 625 |
+
|
| 626 |
+
def __init__(self, G, weight):
|
| 627 |
+
self.G = G
|
| 628 |
+
self.incumbent_nodes = []
|
| 629 |
+
self.incumbent_weight = 0
|
| 630 |
+
|
| 631 |
+
if weight is None:
|
| 632 |
+
self.node_weights = {v: 1 for v in G.nodes()}
|
| 633 |
+
else:
|
| 634 |
+
for v in G.nodes():
|
| 635 |
+
if weight not in G.nodes[v]:
|
| 636 |
+
errmsg = f"Node {v!r} does not have the requested weight field."
|
| 637 |
+
raise KeyError(errmsg)
|
| 638 |
+
if not isinstance(G.nodes[v][weight], int):
|
| 639 |
+
errmsg = f"The {weight!r} field of node {v!r} is not an integer."
|
| 640 |
+
raise ValueError(errmsg)
|
| 641 |
+
self.node_weights = {v: G.nodes[v][weight] for v in G.nodes()}
|
| 642 |
+
|
| 643 |
+
def update_incumbent_if_improved(self, C, C_weight):
|
| 644 |
+
"""Update the incumbent if the node set C has greater weight.
|
| 645 |
+
|
| 646 |
+
C is assumed to be a clique.
|
| 647 |
+
"""
|
| 648 |
+
if C_weight > self.incumbent_weight:
|
| 649 |
+
self.incumbent_nodes = C[:]
|
| 650 |
+
self.incumbent_weight = C_weight
|
| 651 |
+
|
| 652 |
+
def greedily_find_independent_set(self, P):
|
| 653 |
+
"""Greedily find an independent set of nodes from a set of
|
| 654 |
+
nodes P."""
|
| 655 |
+
independent_set = []
|
| 656 |
+
P = P[:]
|
| 657 |
+
while P:
|
| 658 |
+
v = P[0]
|
| 659 |
+
independent_set.append(v)
|
| 660 |
+
P = [w for w in P if v != w and not self.G.has_edge(v, w)]
|
| 661 |
+
return independent_set
|
| 662 |
+
|
| 663 |
+
def find_branching_nodes(self, P, target):
|
| 664 |
+
"""Find a set of nodes to branch on."""
|
| 665 |
+
residual_wt = {v: self.node_weights[v] for v in P}
|
| 666 |
+
total_wt = 0
|
| 667 |
+
P = P[:]
|
| 668 |
+
while P:
|
| 669 |
+
independent_set = self.greedily_find_independent_set(P)
|
| 670 |
+
min_wt_in_class = min(residual_wt[v] for v in independent_set)
|
| 671 |
+
total_wt += min_wt_in_class
|
| 672 |
+
if total_wt > target:
|
| 673 |
+
break
|
| 674 |
+
for v in independent_set:
|
| 675 |
+
residual_wt[v] -= min_wt_in_class
|
| 676 |
+
P = [v for v in P if residual_wt[v] != 0]
|
| 677 |
+
return P
|
| 678 |
+
|
| 679 |
+
def expand(self, C, C_weight, P):
|
| 680 |
+
"""Look for the best clique that contains all the nodes in C and zero or
|
| 681 |
+
more of the nodes in P, backtracking if it can be shown that no such
|
| 682 |
+
clique has greater weight than the incumbent.
|
| 683 |
+
"""
|
| 684 |
+
self.update_incumbent_if_improved(C, C_weight)
|
| 685 |
+
branching_nodes = self.find_branching_nodes(P, self.incumbent_weight - C_weight)
|
| 686 |
+
while branching_nodes:
|
| 687 |
+
v = branching_nodes.pop()
|
| 688 |
+
P.remove(v)
|
| 689 |
+
new_C = C + [v]
|
| 690 |
+
new_C_weight = C_weight + self.node_weights[v]
|
| 691 |
+
new_P = [w for w in P if self.G.has_edge(v, w)]
|
| 692 |
+
self.expand(new_C, new_C_weight, new_P)
|
| 693 |
+
|
| 694 |
+
def find_max_weight_clique(self):
|
| 695 |
+
"""Find a maximum weight clique."""
|
| 696 |
+
# Sort nodes in reverse order of degree for speed
|
| 697 |
+
nodes = sorted(self.G.nodes(), key=lambda v: self.G.degree(v), reverse=True)
|
| 698 |
+
nodes = [v for v in nodes if self.node_weights[v] > 0]
|
| 699 |
+
self.expand([], 0, nodes)
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
@not_implemented_for("directed")
|
| 703 |
+
@nx._dispatchable(node_attrs="weight")
|
| 704 |
+
def max_weight_clique(G, weight="weight"):
|
| 705 |
+
"""Find a maximum weight clique in G.
|
| 706 |
+
|
| 707 |
+
A *clique* in a graph is a set of nodes such that every two distinct nodes
|
| 708 |
+
are adjacent. The *weight* of a clique is the sum of the weights of its
|
| 709 |
+
nodes. A *maximum weight clique* of graph G is a clique C in G such that
|
| 710 |
+
no clique in G has weight greater than the weight of C.
|
| 711 |
+
|
| 712 |
+
Parameters
|
| 713 |
+
----------
|
| 714 |
+
G : NetworkX graph
|
| 715 |
+
Undirected graph
|
| 716 |
+
weight : string or None, optional (default='weight')
|
| 717 |
+
The node attribute that holds the integer value used as a weight.
|
| 718 |
+
If None, then each node has weight 1.
|
| 719 |
+
|
| 720 |
+
Returns
|
| 721 |
+
-------
|
| 722 |
+
clique : list
|
| 723 |
+
the nodes of a maximum weight clique
|
| 724 |
+
weight : int
|
| 725 |
+
the weight of a maximum weight clique
|
| 726 |
+
|
| 727 |
+
Notes
|
| 728 |
+
-----
|
| 729 |
+
The implementation is recursive, and therefore it may run into recursion
|
| 730 |
+
depth issues if G contains a clique whose number of nodes is close to the
|
| 731 |
+
recursion depth limit.
|
| 732 |
+
|
| 733 |
+
At each search node, the algorithm greedily constructs a weighted
|
| 734 |
+
independent set cover of part of the graph in order to find a small set of
|
| 735 |
+
nodes on which to branch. The algorithm is very similar to the algorithm
|
| 736 |
+
of Tavares et al. [1]_, other than the fact that the NetworkX version does
|
| 737 |
+
not use bitsets. This style of algorithm for maximum weight clique (and
|
| 738 |
+
maximum weight independent set, which is the same problem but on the
|
| 739 |
+
complement graph) has a decades-long history. See Algorithm B of Warren
|
| 740 |
+
and Hicks [2]_ and the references in that paper.
|
| 741 |
+
|
| 742 |
+
References
|
| 743 |
+
----------
|
| 744 |
+
.. [1] Tavares, W.A., Neto, M.B.C., Rodrigues, C.D., Michelon, P.: Um
|
| 745 |
+
algoritmo de branch and bound para o problema da clique máxima
|
| 746 |
+
ponderada. Proceedings of XLVII SBPO 1 (2015).
|
| 747 |
+
|
| 748 |
+
.. [2] Warren, Jeffrey S, Hicks, Illya V.: Combinatorial Branch-and-Bound
|
| 749 |
+
for the Maximum Weight Independent Set Problem. Technical Report,
|
| 750 |
+
Texas A&M University (2016).
|
| 751 |
+
"""
|
| 752 |
+
|
| 753 |
+
mwc = MaxWeightClique(G, weight)
|
| 754 |
+
mwc.find_max_weight_clique()
|
| 755 |
+
return mwc.incumbent_nodes, mwc.incumbent_weight
|
.venv/lib/python3.11/site-packages/networkx/algorithms/cluster.py
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Algorithms to characterize the number of triangles in a graph."""
|
| 2 |
+
|
| 3 |
+
from collections import Counter
|
| 4 |
+
from itertools import chain, combinations
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from networkx.utils import not_implemented_for
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"triangles",
|
| 11 |
+
"average_clustering",
|
| 12 |
+
"clustering",
|
| 13 |
+
"transitivity",
|
| 14 |
+
"square_clustering",
|
| 15 |
+
"generalized_degree",
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@not_implemented_for("directed")
|
| 20 |
+
@nx._dispatchable
|
| 21 |
+
def triangles(G, nodes=None):
|
| 22 |
+
"""Compute the number of triangles.
|
| 23 |
+
|
| 24 |
+
Finds the number of triangles that include a node as one vertex.
|
| 25 |
+
|
| 26 |
+
Parameters
|
| 27 |
+
----------
|
| 28 |
+
G : graph
|
| 29 |
+
A networkx graph
|
| 30 |
+
|
| 31 |
+
nodes : node, iterable of nodes, or None (default=None)
|
| 32 |
+
If a singleton node, return the number of triangles for that node.
|
| 33 |
+
If an iterable, compute the number of triangles for each of those nodes.
|
| 34 |
+
If `None` (the default) compute the number of triangles for all nodes in `G`.
|
| 35 |
+
|
| 36 |
+
Returns
|
| 37 |
+
-------
|
| 38 |
+
out : dict or int
|
| 39 |
+
If `nodes` is a container of nodes, returns number of triangles keyed by node (dict).
|
| 40 |
+
If `nodes` is a specific node, returns number of triangles for the node (int).
|
| 41 |
+
|
| 42 |
+
Examples
|
| 43 |
+
--------
|
| 44 |
+
>>> G = nx.complete_graph(5)
|
| 45 |
+
>>> print(nx.triangles(G, 0))
|
| 46 |
+
6
|
| 47 |
+
>>> print(nx.triangles(G))
|
| 48 |
+
{0: 6, 1: 6, 2: 6, 3: 6, 4: 6}
|
| 49 |
+
>>> print(list(nx.triangles(G, [0, 1]).values()))
|
| 50 |
+
[6, 6]
|
| 51 |
+
|
| 52 |
+
Notes
|
| 53 |
+
-----
|
| 54 |
+
Self loops are ignored.
|
| 55 |
+
|
| 56 |
+
"""
|
| 57 |
+
if nodes is not None:
|
| 58 |
+
# If `nodes` represents a single node, return only its number of triangles
|
| 59 |
+
if nodes in G:
|
| 60 |
+
return next(_triangles_and_degree_iter(G, nodes))[2] // 2
|
| 61 |
+
|
| 62 |
+
# if `nodes` is a container of nodes, then return a
|
| 63 |
+
# dictionary mapping node to number of triangles.
|
| 64 |
+
return {v: t // 2 for v, d, t, _ in _triangles_and_degree_iter(G, nodes)}
|
| 65 |
+
|
| 66 |
+
# if nodes is None, then compute triangles for the complete graph
|
| 67 |
+
|
| 68 |
+
# dict used to avoid visiting the same nodes twice
|
| 69 |
+
# this allows calculating/counting each triangle only once
|
| 70 |
+
later_nbrs = {}
|
| 71 |
+
|
| 72 |
+
# iterate over the nodes in a graph
|
| 73 |
+
for node, neighbors in G.adjacency():
|
| 74 |
+
later_nbrs[node] = {n for n in neighbors if n not in later_nbrs and n != node}
|
| 75 |
+
|
| 76 |
+
# instantiate Counter for each node to include isolated nodes
|
| 77 |
+
# add 1 to the count if a nodes neighbor's neighbor is also a neighbor
|
| 78 |
+
triangle_counts = Counter(dict.fromkeys(G, 0))
|
| 79 |
+
for node1, neighbors in later_nbrs.items():
|
| 80 |
+
for node2 in neighbors:
|
| 81 |
+
third_nodes = neighbors & later_nbrs[node2]
|
| 82 |
+
m = len(third_nodes)
|
| 83 |
+
triangle_counts[node1] += m
|
| 84 |
+
triangle_counts[node2] += m
|
| 85 |
+
triangle_counts.update(third_nodes)
|
| 86 |
+
|
| 87 |
+
return dict(triangle_counts)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@not_implemented_for("multigraph")
|
| 91 |
+
def _triangles_and_degree_iter(G, nodes=None):
|
| 92 |
+
"""Return an iterator of (node, degree, triangles, generalized degree).
|
| 93 |
+
|
| 94 |
+
This double counts triangles so you may want to divide by 2.
|
| 95 |
+
See degree(), triangles() and generalized_degree() for definitions
|
| 96 |
+
and details.
|
| 97 |
+
|
| 98 |
+
"""
|
| 99 |
+
if nodes is None:
|
| 100 |
+
nodes_nbrs = G.adj.items()
|
| 101 |
+
else:
|
| 102 |
+
nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
|
| 103 |
+
|
| 104 |
+
for v, v_nbrs in nodes_nbrs:
|
| 105 |
+
vs = set(v_nbrs) - {v}
|
| 106 |
+
gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs)
|
| 107 |
+
ntriangles = sum(k * val for k, val in gen_degree.items())
|
| 108 |
+
yield (v, len(vs), ntriangles, gen_degree)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@not_implemented_for("multigraph")
|
| 112 |
+
def _weighted_triangles_and_degree_iter(G, nodes=None, weight="weight"):
|
| 113 |
+
"""Return an iterator of (node, degree, weighted_triangles).
|
| 114 |
+
|
| 115 |
+
Used for weighted clustering.
|
| 116 |
+
Note: this returns the geometric average weight of edges in the triangle.
|
| 117 |
+
Also, each triangle is counted twice (each direction).
|
| 118 |
+
So you may want to divide by 2.
|
| 119 |
+
|
| 120 |
+
"""
|
| 121 |
+
import numpy as np
|
| 122 |
+
|
| 123 |
+
if weight is None or G.number_of_edges() == 0:
|
| 124 |
+
max_weight = 1
|
| 125 |
+
else:
|
| 126 |
+
max_weight = max(d.get(weight, 1) for u, v, d in G.edges(data=True))
|
| 127 |
+
if nodes is None:
|
| 128 |
+
nodes_nbrs = G.adj.items()
|
| 129 |
+
else:
|
| 130 |
+
nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
|
| 131 |
+
|
| 132 |
+
def wt(u, v):
|
| 133 |
+
return G[u][v].get(weight, 1) / max_weight
|
| 134 |
+
|
| 135 |
+
for i, nbrs in nodes_nbrs:
|
| 136 |
+
inbrs = set(nbrs) - {i}
|
| 137 |
+
weighted_triangles = 0
|
| 138 |
+
seen = set()
|
| 139 |
+
for j in inbrs:
|
| 140 |
+
seen.add(j)
|
| 141 |
+
# This avoids counting twice -- we double at the end.
|
| 142 |
+
jnbrs = set(G[j]) - seen
|
| 143 |
+
# Only compute the edge weight once, before the inner inner
|
| 144 |
+
# loop.
|
| 145 |
+
wij = wt(i, j)
|
| 146 |
+
weighted_triangles += np.cbrt(
|
| 147 |
+
[(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs]
|
| 148 |
+
).sum()
|
| 149 |
+
yield (i, len(inbrs), 2 * float(weighted_triangles))
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
@not_implemented_for("multigraph")
|
| 153 |
+
def _directed_triangles_and_degree_iter(G, nodes=None):
|
| 154 |
+
"""Return an iterator of
|
| 155 |
+
(node, total_degree, reciprocal_degree, directed_triangles).
|
| 156 |
+
|
| 157 |
+
Used for directed clustering.
|
| 158 |
+
Note that unlike `_triangles_and_degree_iter()`, this function counts
|
| 159 |
+
directed triangles so does not count triangles twice.
|
| 160 |
+
|
| 161 |
+
"""
|
| 162 |
+
nodes_nbrs = ((n, G._pred[n], G._succ[n]) for n in G.nbunch_iter(nodes))
|
| 163 |
+
|
| 164 |
+
for i, preds, succs in nodes_nbrs:
|
| 165 |
+
ipreds = set(preds) - {i}
|
| 166 |
+
isuccs = set(succs) - {i}
|
| 167 |
+
|
| 168 |
+
directed_triangles = 0
|
| 169 |
+
for j in chain(ipreds, isuccs):
|
| 170 |
+
jpreds = set(G._pred[j]) - {j}
|
| 171 |
+
jsuccs = set(G._succ[j]) - {j}
|
| 172 |
+
directed_triangles += sum(
|
| 173 |
+
1
|
| 174 |
+
for k in chain(
|
| 175 |
+
(ipreds & jpreds),
|
| 176 |
+
(ipreds & jsuccs),
|
| 177 |
+
(isuccs & jpreds),
|
| 178 |
+
(isuccs & jsuccs),
|
| 179 |
+
)
|
| 180 |
+
)
|
| 181 |
+
dtotal = len(ipreds) + len(isuccs)
|
| 182 |
+
dbidirectional = len(ipreds & isuccs)
|
| 183 |
+
yield (i, dtotal, dbidirectional, directed_triangles)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
@not_implemented_for("multigraph")
|
| 187 |
+
def _directed_weighted_triangles_and_degree_iter(G, nodes=None, weight="weight"):
|
| 188 |
+
"""Return an iterator of
|
| 189 |
+
(node, total_degree, reciprocal_degree, directed_weighted_triangles).
|
| 190 |
+
|
| 191 |
+
Used for directed weighted clustering.
|
| 192 |
+
Note that unlike `_weighted_triangles_and_degree_iter()`, this function counts
|
| 193 |
+
directed triangles so does not count triangles twice.
|
| 194 |
+
|
| 195 |
+
"""
|
| 196 |
+
import numpy as np
|
| 197 |
+
|
| 198 |
+
if weight is None or G.number_of_edges() == 0:
|
| 199 |
+
max_weight = 1
|
| 200 |
+
else:
|
| 201 |
+
max_weight = max(d.get(weight, 1) for u, v, d in G.edges(data=True))
|
| 202 |
+
|
| 203 |
+
nodes_nbrs = ((n, G._pred[n], G._succ[n]) for n in G.nbunch_iter(nodes))
|
| 204 |
+
|
| 205 |
+
def wt(u, v):
|
| 206 |
+
return G[u][v].get(weight, 1) / max_weight
|
| 207 |
+
|
| 208 |
+
for i, preds, succs in nodes_nbrs:
|
| 209 |
+
ipreds = set(preds) - {i}
|
| 210 |
+
isuccs = set(succs) - {i}
|
| 211 |
+
|
| 212 |
+
directed_triangles = 0
|
| 213 |
+
for j in ipreds:
|
| 214 |
+
jpreds = set(G._pred[j]) - {j}
|
| 215 |
+
jsuccs = set(G._succ[j]) - {j}
|
| 216 |
+
directed_triangles += np.cbrt(
|
| 217 |
+
[(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds]
|
| 218 |
+
).sum()
|
| 219 |
+
directed_triangles += np.cbrt(
|
| 220 |
+
[(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs]
|
| 221 |
+
).sum()
|
| 222 |
+
directed_triangles += np.cbrt(
|
| 223 |
+
[(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds]
|
| 224 |
+
).sum()
|
| 225 |
+
directed_triangles += np.cbrt(
|
| 226 |
+
[(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs]
|
| 227 |
+
).sum()
|
| 228 |
+
|
| 229 |
+
for j in isuccs:
|
| 230 |
+
jpreds = set(G._pred[j]) - {j}
|
| 231 |
+
jsuccs = set(G._succ[j]) - {j}
|
| 232 |
+
directed_triangles += np.cbrt(
|
| 233 |
+
[(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds]
|
| 234 |
+
).sum()
|
| 235 |
+
directed_triangles += np.cbrt(
|
| 236 |
+
[(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs]
|
| 237 |
+
).sum()
|
| 238 |
+
directed_triangles += np.cbrt(
|
| 239 |
+
[(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds]
|
| 240 |
+
).sum()
|
| 241 |
+
directed_triangles += np.cbrt(
|
| 242 |
+
[(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs]
|
| 243 |
+
).sum()
|
| 244 |
+
|
| 245 |
+
dtotal = len(ipreds) + len(isuccs)
|
| 246 |
+
dbidirectional = len(ipreds & isuccs)
|
| 247 |
+
yield (i, dtotal, dbidirectional, float(directed_triangles))
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 251 |
+
def average_clustering(G, nodes=None, weight=None, count_zeros=True):
|
| 252 |
+
r"""Compute the average clustering coefficient for the graph G.
|
| 253 |
+
|
| 254 |
+
The clustering coefficient for the graph is the average,
|
| 255 |
+
|
| 256 |
+
.. math::
|
| 257 |
+
|
| 258 |
+
C = \frac{1}{n}\sum_{v \in G} c_v,
|
| 259 |
+
|
| 260 |
+
where :math:`n` is the number of nodes in `G`.
|
| 261 |
+
|
| 262 |
+
Parameters
|
| 263 |
+
----------
|
| 264 |
+
G : graph
|
| 265 |
+
|
| 266 |
+
nodes : container of nodes, optional (default=all nodes in G)
|
| 267 |
+
Compute average clustering for nodes in this container.
|
| 268 |
+
|
| 269 |
+
weight : string or None, optional (default=None)
|
| 270 |
+
The edge attribute that holds the numerical value used as a weight.
|
| 271 |
+
If None, then each edge has weight 1.
|
| 272 |
+
|
| 273 |
+
count_zeros : bool
|
| 274 |
+
If False include only the nodes with nonzero clustering in the average.
|
| 275 |
+
|
| 276 |
+
Returns
|
| 277 |
+
-------
|
| 278 |
+
avg : float
|
| 279 |
+
Average clustering
|
| 280 |
+
|
| 281 |
+
Examples
|
| 282 |
+
--------
|
| 283 |
+
>>> G = nx.complete_graph(5)
|
| 284 |
+
>>> print(nx.average_clustering(G))
|
| 285 |
+
1.0
|
| 286 |
+
|
| 287 |
+
Notes
|
| 288 |
+
-----
|
| 289 |
+
This is a space saving routine; it might be faster
|
| 290 |
+
to use the clustering function to get a list and then take the average.
|
| 291 |
+
|
| 292 |
+
Self loops are ignored.
|
| 293 |
+
|
| 294 |
+
References
|
| 295 |
+
----------
|
| 296 |
+
.. [1] Generalizations of the clustering coefficient to weighted
|
| 297 |
+
complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela,
|
| 298 |
+
K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007).
|
| 299 |
+
http://jponnela.com/web_documents/a9.pdf
|
| 300 |
+
.. [2] Marcus Kaiser, Mean clustering coefficients: the role of isolated
|
| 301 |
+
nodes and leafs on clustering measures for small-world networks.
|
| 302 |
+
https://arxiv.org/abs/0802.2512
|
| 303 |
+
"""
|
| 304 |
+
c = clustering(G, nodes, weight=weight).values()
|
| 305 |
+
if not count_zeros:
|
| 306 |
+
c = [v for v in c if abs(v) > 0]
|
| 307 |
+
return sum(c) / len(c)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 311 |
+
def clustering(G, nodes=None, weight=None):
|
| 312 |
+
r"""Compute the clustering coefficient for nodes.
|
| 313 |
+
|
| 314 |
+
For unweighted graphs, the clustering of a node :math:`u`
|
| 315 |
+
is the fraction of possible triangles through that node that exist,
|
| 316 |
+
|
| 317 |
+
.. math::
|
| 318 |
+
|
| 319 |
+
c_u = \frac{2 T(u)}{deg(u)(deg(u)-1)},
|
| 320 |
+
|
| 321 |
+
where :math:`T(u)` is the number of triangles through node :math:`u` and
|
| 322 |
+
:math:`deg(u)` is the degree of :math:`u`.
|
| 323 |
+
|
| 324 |
+
For weighted graphs, there are several ways to define clustering [1]_.
|
| 325 |
+
the one used here is defined
|
| 326 |
+
as the geometric average of the subgraph edge weights [2]_,
|
| 327 |
+
|
| 328 |
+
.. math::
|
| 329 |
+
|
| 330 |
+
c_u = \frac{1}{deg(u)(deg(u)-1))}
|
| 331 |
+
\sum_{vw} (\hat{w}_{uv} \hat{w}_{uw} \hat{w}_{vw})^{1/3}.
|
| 332 |
+
|
| 333 |
+
The edge weights :math:`\hat{w}_{uv}` are normalized by the maximum weight
|
| 334 |
+
in the network :math:`\hat{w}_{uv} = w_{uv}/\max(w)`.
|
| 335 |
+
|
| 336 |
+
The value of :math:`c_u` is assigned to 0 if :math:`deg(u) < 2`.
|
| 337 |
+
|
| 338 |
+
Additionally, this weighted definition has been generalized to support negative edge weights [3]_.
|
| 339 |
+
|
| 340 |
+
For directed graphs, the clustering is similarly defined as the fraction
|
| 341 |
+
of all possible directed triangles or geometric average of the subgraph
|
| 342 |
+
edge weights for unweighted and weighted directed graph respectively [4]_.
|
| 343 |
+
|
| 344 |
+
.. math::
|
| 345 |
+
|
| 346 |
+
c_u = \frac{T(u)}{2(deg^{tot}(u)(deg^{tot}(u)-1) - 2deg^{\leftrightarrow}(u))},
|
| 347 |
+
|
| 348 |
+
where :math:`T(u)` is the number of directed triangles through node
|
| 349 |
+
:math:`u`, :math:`deg^{tot}(u)` is the sum of in degree and out degree of
|
| 350 |
+
:math:`u` and :math:`deg^{\leftrightarrow}(u)` is the reciprocal degree of
|
| 351 |
+
:math:`u`.
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
Parameters
|
| 355 |
+
----------
|
| 356 |
+
G : graph
|
| 357 |
+
|
| 358 |
+
nodes : node, iterable of nodes, or None (default=None)
|
| 359 |
+
If a singleton node, return the number of triangles for that node.
|
| 360 |
+
If an iterable, compute the number of triangles for each of those nodes.
|
| 361 |
+
If `None` (the default) compute the number of triangles for all nodes in `G`.
|
| 362 |
+
|
| 363 |
+
weight : string or None, optional (default=None)
|
| 364 |
+
The edge attribute that holds the numerical value used as a weight.
|
| 365 |
+
If None, then each edge has weight 1.
|
| 366 |
+
|
| 367 |
+
Returns
|
| 368 |
+
-------
|
| 369 |
+
out : float, or dictionary
|
| 370 |
+
Clustering coefficient at specified nodes
|
| 371 |
+
|
| 372 |
+
Examples
|
| 373 |
+
--------
|
| 374 |
+
>>> G = nx.complete_graph(5)
|
| 375 |
+
>>> print(nx.clustering(G, 0))
|
| 376 |
+
1.0
|
| 377 |
+
>>> print(nx.clustering(G))
|
| 378 |
+
{0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0}
|
| 379 |
+
|
| 380 |
+
Notes
|
| 381 |
+
-----
|
| 382 |
+
Self loops are ignored.
|
| 383 |
+
|
| 384 |
+
References
|
| 385 |
+
----------
|
| 386 |
+
.. [1] Generalizations of the clustering coefficient to weighted
|
| 387 |
+
complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela,
|
| 388 |
+
K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007).
|
| 389 |
+
http://jponnela.com/web_documents/a9.pdf
|
| 390 |
+
.. [2] Intensity and coherence of motifs in weighted complex
|
| 391 |
+
networks by J. P. Onnela, J. Saramäki, J. Kertész, and K. Kaski,
|
| 392 |
+
Physical Review E, 71(6), 065103 (2005).
|
| 393 |
+
.. [3] Generalization of Clustering Coefficients to Signed Correlation Networks
|
| 394 |
+
by G. Costantini and M. Perugini, PloS one, 9(2), e88669 (2014).
|
| 395 |
+
.. [4] Clustering in complex directed networks by G. Fagiolo,
|
| 396 |
+
Physical Review E, 76(2), 026107 (2007).
|
| 397 |
+
"""
|
| 398 |
+
if G.is_directed():
|
| 399 |
+
if weight is not None:
|
| 400 |
+
td_iter = _directed_weighted_triangles_and_degree_iter(G, nodes, weight)
|
| 401 |
+
clusterc = {
|
| 402 |
+
v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2)
|
| 403 |
+
for v, dt, db, t in td_iter
|
| 404 |
+
}
|
| 405 |
+
else:
|
| 406 |
+
td_iter = _directed_triangles_and_degree_iter(G, nodes)
|
| 407 |
+
clusterc = {
|
| 408 |
+
v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2)
|
| 409 |
+
for v, dt, db, t in td_iter
|
| 410 |
+
}
|
| 411 |
+
else:
|
| 412 |
+
# The formula 2*T/(d*(d-1)) from docs is t/(d*(d-1)) here b/c t==2*T
|
| 413 |
+
if weight is not None:
|
| 414 |
+
td_iter = _weighted_triangles_and_degree_iter(G, nodes, weight)
|
| 415 |
+
clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t in td_iter}
|
| 416 |
+
else:
|
| 417 |
+
td_iter = _triangles_and_degree_iter(G, nodes)
|
| 418 |
+
clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t, _ in td_iter}
|
| 419 |
+
if nodes in G:
|
| 420 |
+
# Return the value of the sole entry in the dictionary.
|
| 421 |
+
return clusterc[nodes]
|
| 422 |
+
return clusterc
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
@nx._dispatchable
|
| 426 |
+
def transitivity(G):
|
| 427 |
+
r"""Compute graph transitivity, the fraction of all possible triangles
|
| 428 |
+
present in G.
|
| 429 |
+
|
| 430 |
+
Possible triangles are identified by the number of "triads"
|
| 431 |
+
(two edges with a shared vertex).
|
| 432 |
+
|
| 433 |
+
The transitivity is
|
| 434 |
+
|
| 435 |
+
.. math::
|
| 436 |
+
|
| 437 |
+
T = 3\frac{\#triangles}{\#triads}.
|
| 438 |
+
|
| 439 |
+
Parameters
|
| 440 |
+
----------
|
| 441 |
+
G : graph
|
| 442 |
+
|
| 443 |
+
Returns
|
| 444 |
+
-------
|
| 445 |
+
out : float
|
| 446 |
+
Transitivity
|
| 447 |
+
|
| 448 |
+
Notes
|
| 449 |
+
-----
|
| 450 |
+
Self loops are ignored.
|
| 451 |
+
|
| 452 |
+
Examples
|
| 453 |
+
--------
|
| 454 |
+
>>> G = nx.complete_graph(5)
|
| 455 |
+
>>> print(nx.transitivity(G))
|
| 456 |
+
1.0
|
| 457 |
+
"""
|
| 458 |
+
triangles_contri = [
|
| 459 |
+
(t, d * (d - 1)) for v, d, t, _ in _triangles_and_degree_iter(G)
|
| 460 |
+
]
|
| 461 |
+
# If the graph is empty
|
| 462 |
+
if len(triangles_contri) == 0:
|
| 463 |
+
return 0
|
| 464 |
+
triangles, contri = map(sum, zip(*triangles_contri))
|
| 465 |
+
return 0 if triangles == 0 else triangles / contri
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
@nx._dispatchable
|
| 469 |
+
def square_clustering(G, nodes=None):
|
| 470 |
+
r"""Compute the squares clustering coefficient for nodes.
|
| 471 |
+
|
| 472 |
+
For each node return the fraction of possible squares that exist at
|
| 473 |
+
the node [1]_
|
| 474 |
+
|
| 475 |
+
.. math::
|
| 476 |
+
C_4(v) = \frac{ \sum_{u=1}^{k_v}
|
| 477 |
+
\sum_{w=u+1}^{k_v} q_v(u,w) }{ \sum_{u=1}^{k_v}
|
| 478 |
+
\sum_{w=u+1}^{k_v} [a_v(u,w) + q_v(u,w)]},
|
| 479 |
+
|
| 480 |
+
where :math:`q_v(u,w)` are the number of common neighbors of :math:`u` and
|
| 481 |
+
:math:`w` other than :math:`v` (ie squares), and :math:`a_v(u,w) = (k_u -
|
| 482 |
+
(1+q_v(u,w)+\theta_{uv})) + (k_w - (1+q_v(u,w)+\theta_{uw}))`, where
|
| 483 |
+
:math:`\theta_{uw} = 1` if :math:`u` and :math:`w` are connected and 0
|
| 484 |
+
otherwise. [2]_
|
| 485 |
+
|
| 486 |
+
Parameters
|
| 487 |
+
----------
|
| 488 |
+
G : graph
|
| 489 |
+
|
| 490 |
+
nodes : container of nodes, optional (default=all nodes in G)
|
| 491 |
+
Compute clustering for nodes in this container.
|
| 492 |
+
|
| 493 |
+
Returns
|
| 494 |
+
-------
|
| 495 |
+
c4 : dictionary
|
| 496 |
+
A dictionary keyed by node with the square clustering coefficient value.
|
| 497 |
+
|
| 498 |
+
Examples
|
| 499 |
+
--------
|
| 500 |
+
>>> G = nx.complete_graph(5)
|
| 501 |
+
>>> print(nx.square_clustering(G, 0))
|
| 502 |
+
1.0
|
| 503 |
+
>>> print(nx.square_clustering(G))
|
| 504 |
+
{0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0}
|
| 505 |
+
|
| 506 |
+
Notes
|
| 507 |
+
-----
|
| 508 |
+
While :math:`C_3(v)` (triangle clustering) gives the probability that
|
| 509 |
+
two neighbors of node v are connected with each other, :math:`C_4(v)` is
|
| 510 |
+
the probability that two neighbors of node v share a common
|
| 511 |
+
neighbor different from v. This algorithm can be applied to both
|
| 512 |
+
bipartite and unipartite networks.
|
| 513 |
+
|
| 514 |
+
References
|
| 515 |
+
----------
|
| 516 |
+
.. [1] Pedro G. Lind, Marta C. González, and Hans J. Herrmann. 2005
|
| 517 |
+
Cycles and clustering in bipartite networks.
|
| 518 |
+
Physical Review E (72) 056127.
|
| 519 |
+
.. [2] Zhang, Peng et al. Clustering Coefficient and Community Structure of
|
| 520 |
+
Bipartite Networks. Physica A: Statistical Mechanics and its Applications 387.27 (2008): 6869–6875.
|
| 521 |
+
https://arxiv.org/abs/0710.0117v1
|
| 522 |
+
"""
|
| 523 |
+
if nodes is None:
|
| 524 |
+
node_iter = G
|
| 525 |
+
else:
|
| 526 |
+
node_iter = G.nbunch_iter(nodes)
|
| 527 |
+
clustering = {}
|
| 528 |
+
for v in node_iter:
|
| 529 |
+
clustering[v] = 0
|
| 530 |
+
potential = 0
|
| 531 |
+
for u, w in combinations(G[v], 2):
|
| 532 |
+
squares = len((set(G[u]) & set(G[w])) - {v})
|
| 533 |
+
clustering[v] += squares
|
| 534 |
+
degm = squares + 1
|
| 535 |
+
if w in G[u]:
|
| 536 |
+
degm += 1
|
| 537 |
+
potential += (len(G[u]) - degm) + (len(G[w]) - degm) + squares
|
| 538 |
+
if potential > 0:
|
| 539 |
+
clustering[v] /= potential
|
| 540 |
+
if nodes in G:
|
| 541 |
+
# Return the value of the sole entry in the dictionary.
|
| 542 |
+
return clustering[nodes]
|
| 543 |
+
return clustering
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
@not_implemented_for("directed")
|
| 547 |
+
@nx._dispatchable
|
| 548 |
+
def generalized_degree(G, nodes=None):
|
| 549 |
+
r"""Compute the generalized degree for nodes.
|
| 550 |
+
|
| 551 |
+
For each node, the generalized degree shows how many edges of given
|
| 552 |
+
triangle multiplicity the node is connected to. The triangle multiplicity
|
| 553 |
+
of an edge is the number of triangles an edge participates in. The
|
| 554 |
+
generalized degree of node :math:`i` can be written as a vector
|
| 555 |
+
:math:`\mathbf{k}_i=(k_i^{(0)}, \dotsc, k_i^{(N-2)})` where
|
| 556 |
+
:math:`k_i^{(j)}` is the number of edges attached to node :math:`i` that
|
| 557 |
+
participate in :math:`j` triangles.
|
| 558 |
+
|
| 559 |
+
Parameters
|
| 560 |
+
----------
|
| 561 |
+
G : graph
|
| 562 |
+
|
| 563 |
+
nodes : container of nodes, optional (default=all nodes in G)
|
| 564 |
+
Compute the generalized degree for nodes in this container.
|
| 565 |
+
|
| 566 |
+
Returns
|
| 567 |
+
-------
|
| 568 |
+
out : Counter, or dictionary of Counters
|
| 569 |
+
Generalized degree of specified nodes. The Counter is keyed by edge
|
| 570 |
+
triangle multiplicity.
|
| 571 |
+
|
| 572 |
+
Examples
|
| 573 |
+
--------
|
| 574 |
+
>>> G = nx.complete_graph(5)
|
| 575 |
+
>>> print(nx.generalized_degree(G, 0))
|
| 576 |
+
Counter({3: 4})
|
| 577 |
+
>>> print(nx.generalized_degree(G))
|
| 578 |
+
{0: Counter({3: 4}), 1: Counter({3: 4}), 2: Counter({3: 4}), 3: Counter({3: 4}), 4: Counter({3: 4})}
|
| 579 |
+
|
| 580 |
+
To recover the number of triangles attached to a node:
|
| 581 |
+
|
| 582 |
+
>>> k1 = nx.generalized_degree(G, 0)
|
| 583 |
+
>>> sum([k * v for k, v in k1.items()]) / 2 == nx.triangles(G, 0)
|
| 584 |
+
True
|
| 585 |
+
|
| 586 |
+
Notes
|
| 587 |
+
-----
|
| 588 |
+
Self loops are ignored.
|
| 589 |
+
|
| 590 |
+
In a network of N nodes, the highest triangle multiplicity an edge can have
|
| 591 |
+
is N-2.
|
| 592 |
+
|
| 593 |
+
The return value does not include a `zero` entry if no edges of a
|
| 594 |
+
particular triangle multiplicity are present.
|
| 595 |
+
|
| 596 |
+
The number of triangles node :math:`i` is attached to can be recovered from
|
| 597 |
+
the generalized degree :math:`\mathbf{k}_i=(k_i^{(0)}, \dotsc,
|
| 598 |
+
k_i^{(N-2)})` by :math:`(k_i^{(1)}+2k_i^{(2)}+\dotsc +(N-2)k_i^{(N-2)})/2`.
|
| 599 |
+
|
| 600 |
+
References
|
| 601 |
+
----------
|
| 602 |
+
.. [1] Networks with arbitrary edge multiplicities by V. Zlatić,
|
| 603 |
+
D. Garlaschelli and G. Caldarelli, EPL (Europhysics Letters),
|
| 604 |
+
Volume 97, Number 2 (2012).
|
| 605 |
+
https://iopscience.iop.org/article/10.1209/0295-5075/97/28005
|
| 606 |
+
"""
|
| 607 |
+
if nodes in G:
|
| 608 |
+
return next(_triangles_and_degree_iter(G, nodes))[3]
|
| 609 |
+
return {v: gd for v, d, t, gd in _triangles_and_degree_iter(G, nodes)}
|
.venv/lib/python3.11/site-packages/networkx/algorithms/communicability_alg.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Communicability.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.utils import not_implemented_for
|
| 7 |
+
|
| 8 |
+
__all__ = ["communicability", "communicability_exp"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@not_implemented_for("directed")
|
| 12 |
+
@not_implemented_for("multigraph")
|
| 13 |
+
@nx._dispatchable
|
| 14 |
+
def communicability(G):
|
| 15 |
+
r"""Returns communicability between all pairs of nodes in G.
|
| 16 |
+
|
| 17 |
+
The communicability between pairs of nodes in G is the sum of
|
| 18 |
+
walks of different lengths starting at node u and ending at node v.
|
| 19 |
+
|
| 20 |
+
Parameters
|
| 21 |
+
----------
|
| 22 |
+
G: graph
|
| 23 |
+
|
| 24 |
+
Returns
|
| 25 |
+
-------
|
| 26 |
+
comm: dictionary of dictionaries
|
| 27 |
+
Dictionary of dictionaries keyed by nodes with communicability
|
| 28 |
+
as the value.
|
| 29 |
+
|
| 30 |
+
Raises
|
| 31 |
+
------
|
| 32 |
+
NetworkXError
|
| 33 |
+
If the graph is not undirected and simple.
|
| 34 |
+
|
| 35 |
+
See Also
|
| 36 |
+
--------
|
| 37 |
+
communicability_exp:
|
| 38 |
+
Communicability between all pairs of nodes in G using spectral
|
| 39 |
+
decomposition.
|
| 40 |
+
communicability_betweenness_centrality:
|
| 41 |
+
Communicability betweenness centrality for each node in G.
|
| 42 |
+
|
| 43 |
+
Notes
|
| 44 |
+
-----
|
| 45 |
+
This algorithm uses a spectral decomposition of the adjacency matrix.
|
| 46 |
+
Let G=(V,E) be a simple undirected graph. Using the connection between
|
| 47 |
+
the powers of the adjacency matrix and the number of walks in the graph,
|
| 48 |
+
the communicability between nodes `u` and `v` based on the graph spectrum
|
| 49 |
+
is [1]_
|
| 50 |
+
|
| 51 |
+
.. math::
|
| 52 |
+
C(u,v)=\sum_{j=1}^{n}\phi_{j}(u)\phi_{j}(v)e^{\lambda_{j}},
|
| 53 |
+
|
| 54 |
+
where `\phi_{j}(u)` is the `u\rm{th}` element of the `j\rm{th}` orthonormal
|
| 55 |
+
eigenvector of the adjacency matrix associated with the eigenvalue
|
| 56 |
+
`\lambda_{j}`.
|
| 57 |
+
|
| 58 |
+
References
|
| 59 |
+
----------
|
| 60 |
+
.. [1] Ernesto Estrada, Naomichi Hatano,
|
| 61 |
+
"Communicability in complex networks",
|
| 62 |
+
Phys. Rev. E 77, 036111 (2008).
|
| 63 |
+
https://arxiv.org/abs/0707.0756
|
| 64 |
+
|
| 65 |
+
Examples
|
| 66 |
+
--------
|
| 67 |
+
>>> G = nx.Graph([(0, 1), (1, 2), (1, 5), (5, 4), (2, 4), (2, 3), (4, 3), (3, 6)])
|
| 68 |
+
>>> c = nx.communicability(G)
|
| 69 |
+
"""
|
| 70 |
+
import numpy as np
|
| 71 |
+
|
| 72 |
+
nodelist = list(G) # ordering of nodes in matrix
|
| 73 |
+
A = nx.to_numpy_array(G, nodelist)
|
| 74 |
+
# convert to 0-1 matrix
|
| 75 |
+
A[A != 0.0] = 1
|
| 76 |
+
w, vec = np.linalg.eigh(A)
|
| 77 |
+
expw = np.exp(w)
|
| 78 |
+
mapping = dict(zip(nodelist, range(len(nodelist))))
|
| 79 |
+
c = {}
|
| 80 |
+
# computing communicabilities
|
| 81 |
+
for u in G:
|
| 82 |
+
c[u] = {}
|
| 83 |
+
for v in G:
|
| 84 |
+
s = 0
|
| 85 |
+
p = mapping[u]
|
| 86 |
+
q = mapping[v]
|
| 87 |
+
for j in range(len(nodelist)):
|
| 88 |
+
s += vec[:, j][p] * vec[:, j][q] * expw[j]
|
| 89 |
+
c[u][v] = float(s)
|
| 90 |
+
return c
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@not_implemented_for("directed")
|
| 94 |
+
@not_implemented_for("multigraph")
|
| 95 |
+
@nx._dispatchable
|
| 96 |
+
def communicability_exp(G):
|
| 97 |
+
r"""Returns communicability between all pairs of nodes in G.
|
| 98 |
+
|
| 99 |
+
Communicability between pair of node (u,v) of node in G is the sum of
|
| 100 |
+
walks of different lengths starting at node u and ending at node v.
|
| 101 |
+
|
| 102 |
+
Parameters
|
| 103 |
+
----------
|
| 104 |
+
G: graph
|
| 105 |
+
|
| 106 |
+
Returns
|
| 107 |
+
-------
|
| 108 |
+
comm: dictionary of dictionaries
|
| 109 |
+
Dictionary of dictionaries keyed by nodes with communicability
|
| 110 |
+
as the value.
|
| 111 |
+
|
| 112 |
+
Raises
|
| 113 |
+
------
|
| 114 |
+
NetworkXError
|
| 115 |
+
If the graph is not undirected and simple.
|
| 116 |
+
|
| 117 |
+
See Also
|
| 118 |
+
--------
|
| 119 |
+
communicability:
|
| 120 |
+
Communicability between pairs of nodes in G.
|
| 121 |
+
communicability_betweenness_centrality:
|
| 122 |
+
Communicability betweenness centrality for each node in G.
|
| 123 |
+
|
| 124 |
+
Notes
|
| 125 |
+
-----
|
| 126 |
+
This algorithm uses matrix exponentiation of the adjacency matrix.
|
| 127 |
+
|
| 128 |
+
Let G=(V,E) be a simple undirected graph. Using the connection between
|
| 129 |
+
the powers of the adjacency matrix and the number of walks in the graph,
|
| 130 |
+
the communicability between nodes u and v is [1]_,
|
| 131 |
+
|
| 132 |
+
.. math::
|
| 133 |
+
C(u,v) = (e^A)_{uv},
|
| 134 |
+
|
| 135 |
+
where `A` is the adjacency matrix of G.
|
| 136 |
+
|
| 137 |
+
References
|
| 138 |
+
----------
|
| 139 |
+
.. [1] Ernesto Estrada, Naomichi Hatano,
|
| 140 |
+
"Communicability in complex networks",
|
| 141 |
+
Phys. Rev. E 77, 036111 (2008).
|
| 142 |
+
https://arxiv.org/abs/0707.0756
|
| 143 |
+
|
| 144 |
+
Examples
|
| 145 |
+
--------
|
| 146 |
+
>>> G = nx.Graph([(0, 1), (1, 2), (1, 5), (5, 4), (2, 4), (2, 3), (4, 3), (3, 6)])
|
| 147 |
+
>>> c = nx.communicability_exp(G)
|
| 148 |
+
"""
|
| 149 |
+
import scipy as sp
|
| 150 |
+
|
| 151 |
+
nodelist = list(G) # ordering of nodes in matrix
|
| 152 |
+
A = nx.to_numpy_array(G, nodelist)
|
| 153 |
+
# convert to 0-1 matrix
|
| 154 |
+
A[A != 0.0] = 1
|
| 155 |
+
# communicability matrix
|
| 156 |
+
expA = sp.linalg.expm(A)
|
| 157 |
+
mapping = dict(zip(nodelist, range(len(nodelist))))
|
| 158 |
+
c = {}
|
| 159 |
+
for u in G:
|
| 160 |
+
c[u] = {}
|
| 161 |
+
for v in G:
|
| 162 |
+
c[u][v] = float(expA[mapping[u], mapping[v]])
|
| 163 |
+
return c
|
.venv/lib/python3.11/site-packages/networkx/algorithms/core.py
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Find the k-cores of a graph.
|
| 3 |
+
|
| 4 |
+
The k-core is found by recursively pruning nodes with degrees less than k.
|
| 5 |
+
|
| 6 |
+
See the following references for details:
|
| 7 |
+
|
| 8 |
+
An O(m) Algorithm for Cores Decomposition of Networks
|
| 9 |
+
Vladimir Batagelj and Matjaz Zaversnik, 2003.
|
| 10 |
+
https://arxiv.org/abs/cs.DS/0310049
|
| 11 |
+
|
| 12 |
+
Generalized Cores
|
| 13 |
+
Vladimir Batagelj and Matjaz Zaversnik, 2002.
|
| 14 |
+
https://arxiv.org/pdf/cs/0202039
|
| 15 |
+
|
| 16 |
+
For directed graphs a more general notion is that of D-cores which
|
| 17 |
+
looks at (k, l) restrictions on (in, out) degree. The (k, k) D-core
|
| 18 |
+
is the k-core.
|
| 19 |
+
|
| 20 |
+
D-cores: Measuring Collaboration of Directed Graphs Based on Degeneracy
|
| 21 |
+
Christos Giatsidis, Dimitrios M. Thilikos, Michalis Vazirgiannis, ICDM 2011.
|
| 22 |
+
http://www.graphdegeneracy.org/dcores_ICDM_2011.pdf
|
| 23 |
+
|
| 24 |
+
Multi-scale structure and topological anomaly detection via a new network \
|
| 25 |
+
statistic: The onion decomposition
|
| 26 |
+
L. Hébert-Dufresne, J. A. Grochow, and A. Allard
|
| 27 |
+
Scientific Reports 6, 31708 (2016)
|
| 28 |
+
http://doi.org/10.1038/srep31708
|
| 29 |
+
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
import networkx as nx
|
| 33 |
+
|
| 34 |
+
__all__ = [
|
| 35 |
+
"core_number",
|
| 36 |
+
"k_core",
|
| 37 |
+
"k_shell",
|
| 38 |
+
"k_crust",
|
| 39 |
+
"k_corona",
|
| 40 |
+
"k_truss",
|
| 41 |
+
"onion_layers",
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@nx.utils.not_implemented_for("multigraph")
|
| 46 |
+
@nx._dispatchable
|
| 47 |
+
def core_number(G):
|
| 48 |
+
"""Returns the core number for each node.
|
| 49 |
+
|
| 50 |
+
A k-core is a maximal subgraph that contains nodes of degree k or more.
|
| 51 |
+
|
| 52 |
+
The core number of a node is the largest value k of a k-core containing
|
| 53 |
+
that node.
|
| 54 |
+
|
| 55 |
+
Parameters
|
| 56 |
+
----------
|
| 57 |
+
G : NetworkX graph
|
| 58 |
+
An undirected or directed graph
|
| 59 |
+
|
| 60 |
+
Returns
|
| 61 |
+
-------
|
| 62 |
+
core_number : dictionary
|
| 63 |
+
A dictionary keyed by node to the core number.
|
| 64 |
+
|
| 65 |
+
Raises
|
| 66 |
+
------
|
| 67 |
+
NetworkXNotImplemented
|
| 68 |
+
If `G` is a multigraph or contains self loops.
|
| 69 |
+
|
| 70 |
+
Notes
|
| 71 |
+
-----
|
| 72 |
+
For directed graphs the node degree is defined to be the
|
| 73 |
+
in-degree + out-degree.
|
| 74 |
+
|
| 75 |
+
Examples
|
| 76 |
+
--------
|
| 77 |
+
>>> degrees = [0, 1, 2, 2, 2, 2, 3]
|
| 78 |
+
>>> H = nx.havel_hakimi_graph(degrees)
|
| 79 |
+
>>> nx.core_number(H)
|
| 80 |
+
{0: 1, 1: 2, 2: 2, 3: 2, 4: 1, 5: 2, 6: 0}
|
| 81 |
+
>>> G = nx.DiGraph()
|
| 82 |
+
>>> G.add_edges_from([(1, 2), (2, 1), (2, 3), (2, 4), (3, 4), (4, 3)])
|
| 83 |
+
>>> nx.core_number(G)
|
| 84 |
+
{1: 2, 2: 2, 3: 2, 4: 2}
|
| 85 |
+
|
| 86 |
+
References
|
| 87 |
+
----------
|
| 88 |
+
.. [1] An O(m) Algorithm for Cores Decomposition of Networks
|
| 89 |
+
Vladimir Batagelj and Matjaz Zaversnik, 2003.
|
| 90 |
+
https://arxiv.org/abs/cs.DS/0310049
|
| 91 |
+
"""
|
| 92 |
+
if nx.number_of_selfloops(G) > 0:
|
| 93 |
+
msg = (
|
| 94 |
+
"Input graph has self loops which is not permitted; "
|
| 95 |
+
"Consider using G.remove_edges_from(nx.selfloop_edges(G))."
|
| 96 |
+
)
|
| 97 |
+
raise nx.NetworkXNotImplemented(msg)
|
| 98 |
+
degrees = dict(G.degree())
|
| 99 |
+
# Sort nodes by degree.
|
| 100 |
+
nodes = sorted(degrees, key=degrees.get)
|
| 101 |
+
bin_boundaries = [0]
|
| 102 |
+
curr_degree = 0
|
| 103 |
+
for i, v in enumerate(nodes):
|
| 104 |
+
if degrees[v] > curr_degree:
|
| 105 |
+
bin_boundaries.extend([i] * (degrees[v] - curr_degree))
|
| 106 |
+
curr_degree = degrees[v]
|
| 107 |
+
node_pos = {v: pos for pos, v in enumerate(nodes)}
|
| 108 |
+
# The initial guess for the core number of a node is its degree.
|
| 109 |
+
core = degrees
|
| 110 |
+
nbrs = {v: list(nx.all_neighbors(G, v)) for v in G}
|
| 111 |
+
for v in nodes:
|
| 112 |
+
for u in nbrs[v]:
|
| 113 |
+
if core[u] > core[v]:
|
| 114 |
+
nbrs[u].remove(v)
|
| 115 |
+
pos = node_pos[u]
|
| 116 |
+
bin_start = bin_boundaries[core[u]]
|
| 117 |
+
node_pos[u] = bin_start
|
| 118 |
+
node_pos[nodes[bin_start]] = pos
|
| 119 |
+
nodes[bin_start], nodes[pos] = nodes[pos], nodes[bin_start]
|
| 120 |
+
bin_boundaries[core[u]] += 1
|
| 121 |
+
core[u] -= 1
|
| 122 |
+
return core
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _core_subgraph(G, k_filter, k=None, core=None):
|
| 126 |
+
"""Returns the subgraph induced by nodes passing filter `k_filter`.
|
| 127 |
+
|
| 128 |
+
Parameters
|
| 129 |
+
----------
|
| 130 |
+
G : NetworkX graph
|
| 131 |
+
The graph or directed graph to process
|
| 132 |
+
k_filter : filter function
|
| 133 |
+
This function filters the nodes chosen. It takes three inputs:
|
| 134 |
+
A node of G, the filter's cutoff, and the core dict of the graph.
|
| 135 |
+
The function should return a Boolean value.
|
| 136 |
+
k : int, optional
|
| 137 |
+
The order of the core. If not specified use the max core number.
|
| 138 |
+
This value is used as the cutoff for the filter.
|
| 139 |
+
core : dict, optional
|
| 140 |
+
Precomputed core numbers keyed by node for the graph `G`.
|
| 141 |
+
If not specified, the core numbers will be computed from `G`.
|
| 142 |
+
|
| 143 |
+
"""
|
| 144 |
+
if core is None:
|
| 145 |
+
core = core_number(G)
|
| 146 |
+
if k is None:
|
| 147 |
+
k = max(core.values())
|
| 148 |
+
nodes = (v for v in core if k_filter(v, k, core))
|
| 149 |
+
return G.subgraph(nodes).copy()
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
|
| 153 |
+
def k_core(G, k=None, core_number=None):
|
| 154 |
+
"""Returns the k-core of G.
|
| 155 |
+
|
| 156 |
+
A k-core is a maximal subgraph that contains nodes of degree `k` or more.
|
| 157 |
+
|
| 158 |
+
.. deprecated:: 3.3
|
| 159 |
+
`k_core` will not accept `MultiGraph` objects in version 3.5.
|
| 160 |
+
|
| 161 |
+
Parameters
|
| 162 |
+
----------
|
| 163 |
+
G : NetworkX graph
|
| 164 |
+
A graph or directed graph
|
| 165 |
+
k : int, optional
|
| 166 |
+
The order of the core. If not specified return the main core.
|
| 167 |
+
core_number : dictionary, optional
|
| 168 |
+
Precomputed core numbers for the graph G.
|
| 169 |
+
|
| 170 |
+
Returns
|
| 171 |
+
-------
|
| 172 |
+
G : NetworkX graph
|
| 173 |
+
The k-core subgraph
|
| 174 |
+
|
| 175 |
+
Raises
|
| 176 |
+
------
|
| 177 |
+
NetworkXNotImplemented
|
| 178 |
+
The k-core is not defined for multigraphs or graphs with self loops.
|
| 179 |
+
|
| 180 |
+
Notes
|
| 181 |
+
-----
|
| 182 |
+
The main core is the core with `k` as the largest core_number.
|
| 183 |
+
|
| 184 |
+
For directed graphs the node degree is defined to be the
|
| 185 |
+
in-degree + out-degree.
|
| 186 |
+
|
| 187 |
+
Graph, node, and edge attributes are copied to the subgraph.
|
| 188 |
+
|
| 189 |
+
Examples
|
| 190 |
+
--------
|
| 191 |
+
>>> degrees = [0, 1, 2, 2, 2, 2, 3]
|
| 192 |
+
>>> H = nx.havel_hakimi_graph(degrees)
|
| 193 |
+
>>> H.degree
|
| 194 |
+
DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
|
| 195 |
+
>>> nx.k_core(H).nodes
|
| 196 |
+
NodeView((1, 2, 3, 5))
|
| 197 |
+
|
| 198 |
+
See Also
|
| 199 |
+
--------
|
| 200 |
+
core_number
|
| 201 |
+
|
| 202 |
+
References
|
| 203 |
+
----------
|
| 204 |
+
.. [1] An O(m) Algorithm for Cores Decomposition of Networks
|
| 205 |
+
Vladimir Batagelj and Matjaz Zaversnik, 2003.
|
| 206 |
+
https://arxiv.org/abs/cs.DS/0310049
|
| 207 |
+
"""
|
| 208 |
+
|
| 209 |
+
import warnings
|
| 210 |
+
|
| 211 |
+
if G.is_multigraph():
|
| 212 |
+
warnings.warn(
|
| 213 |
+
(
|
| 214 |
+
"\n\n`k_core` will not accept `MultiGraph` objects in version 3.5.\n"
|
| 215 |
+
"Convert it to an undirected graph instead, using::\n\n"
|
| 216 |
+
"\tG = nx.Graph(G)\n"
|
| 217 |
+
),
|
| 218 |
+
category=DeprecationWarning,
|
| 219 |
+
stacklevel=5,
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
def k_filter(v, k, c):
|
| 223 |
+
return c[v] >= k
|
| 224 |
+
|
| 225 |
+
return _core_subgraph(G, k_filter, k, core_number)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
|
| 229 |
+
def k_shell(G, k=None, core_number=None):
|
| 230 |
+
"""Returns the k-shell of G.
|
| 231 |
+
|
| 232 |
+
The k-shell is the subgraph induced by nodes with core number k.
|
| 233 |
+
That is, nodes in the k-core that are not in the (k+1)-core.
|
| 234 |
+
|
| 235 |
+
.. deprecated:: 3.3
|
| 236 |
+
`k_shell` will not accept `MultiGraph` objects in version 3.5.
|
| 237 |
+
|
| 238 |
+
Parameters
|
| 239 |
+
----------
|
| 240 |
+
G : NetworkX graph
|
| 241 |
+
A graph or directed graph.
|
| 242 |
+
k : int, optional
|
| 243 |
+
The order of the shell. If not specified return the outer shell.
|
| 244 |
+
core_number : dictionary, optional
|
| 245 |
+
Precomputed core numbers for the graph G.
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
Returns
|
| 249 |
+
-------
|
| 250 |
+
G : NetworkX graph
|
| 251 |
+
The k-shell subgraph
|
| 252 |
+
|
| 253 |
+
Raises
|
| 254 |
+
------
|
| 255 |
+
NetworkXNotImplemented
|
| 256 |
+
The k-shell is not implemented for multigraphs or graphs with self loops.
|
| 257 |
+
|
| 258 |
+
Notes
|
| 259 |
+
-----
|
| 260 |
+
This is similar to k_corona but in that case only neighbors in the
|
| 261 |
+
k-core are considered.
|
| 262 |
+
|
| 263 |
+
For directed graphs the node degree is defined to be the
|
| 264 |
+
in-degree + out-degree.
|
| 265 |
+
|
| 266 |
+
Graph, node, and edge attributes are copied to the subgraph.
|
| 267 |
+
|
| 268 |
+
Examples
|
| 269 |
+
--------
|
| 270 |
+
>>> degrees = [0, 1, 2, 2, 2, 2, 3]
|
| 271 |
+
>>> H = nx.havel_hakimi_graph(degrees)
|
| 272 |
+
>>> H.degree
|
| 273 |
+
DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
|
| 274 |
+
>>> nx.k_shell(H, k=1).nodes
|
| 275 |
+
NodeView((0, 4))
|
| 276 |
+
|
| 277 |
+
See Also
|
| 278 |
+
--------
|
| 279 |
+
core_number
|
| 280 |
+
k_corona
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
References
|
| 284 |
+
----------
|
| 285 |
+
.. [1] A model of Internet topology using k-shell decomposition
|
| 286 |
+
Shai Carmi, Shlomo Havlin, Scott Kirkpatrick, Yuval Shavitt,
|
| 287 |
+
and Eran Shir, PNAS July 3, 2007 vol. 104 no. 27 11150-11154
|
| 288 |
+
http://www.pnas.org/content/104/27/11150.full
|
| 289 |
+
"""
|
| 290 |
+
|
| 291 |
+
import warnings
|
| 292 |
+
|
| 293 |
+
if G.is_multigraph():
|
| 294 |
+
warnings.warn(
|
| 295 |
+
(
|
| 296 |
+
"\n\n`k_shell` will not accept `MultiGraph` objects in version 3.5.\n"
|
| 297 |
+
"Convert it to an undirected graph instead, using::\n\n"
|
| 298 |
+
"\tG = nx.Graph(G)\n"
|
| 299 |
+
),
|
| 300 |
+
category=DeprecationWarning,
|
| 301 |
+
stacklevel=5,
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
def k_filter(v, k, c):
|
| 305 |
+
return c[v] == k
|
| 306 |
+
|
| 307 |
+
return _core_subgraph(G, k_filter, k, core_number)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
|
| 311 |
+
def k_crust(G, k=None, core_number=None):
|
| 312 |
+
"""Returns the k-crust of G.
|
| 313 |
+
|
| 314 |
+
The k-crust is the graph G with the edges of the k-core removed
|
| 315 |
+
and isolated nodes found after the removal of edges are also removed.
|
| 316 |
+
|
| 317 |
+
.. deprecated:: 3.3
|
| 318 |
+
`k_crust` will not accept `MultiGraph` objects in version 3.5.
|
| 319 |
+
|
| 320 |
+
Parameters
|
| 321 |
+
----------
|
| 322 |
+
G : NetworkX graph
|
| 323 |
+
A graph or directed graph.
|
| 324 |
+
k : int, optional
|
| 325 |
+
The order of the shell. If not specified return the main crust.
|
| 326 |
+
core_number : dictionary, optional
|
| 327 |
+
Precomputed core numbers for the graph G.
|
| 328 |
+
|
| 329 |
+
Returns
|
| 330 |
+
-------
|
| 331 |
+
G : NetworkX graph
|
| 332 |
+
The k-crust subgraph
|
| 333 |
+
|
| 334 |
+
Raises
|
| 335 |
+
------
|
| 336 |
+
NetworkXNotImplemented
|
| 337 |
+
The k-crust is not implemented for multigraphs or graphs with self loops.
|
| 338 |
+
|
| 339 |
+
Notes
|
| 340 |
+
-----
|
| 341 |
+
This definition of k-crust is different than the definition in [1]_.
|
| 342 |
+
The k-crust in [1]_ is equivalent to the k+1 crust of this algorithm.
|
| 343 |
+
|
| 344 |
+
For directed graphs the node degree is defined to be the
|
| 345 |
+
in-degree + out-degree.
|
| 346 |
+
|
| 347 |
+
Graph, node, and edge attributes are copied to the subgraph.
|
| 348 |
+
|
| 349 |
+
Examples
|
| 350 |
+
--------
|
| 351 |
+
>>> degrees = [0, 1, 2, 2, 2, 2, 3]
|
| 352 |
+
>>> H = nx.havel_hakimi_graph(degrees)
|
| 353 |
+
>>> H.degree
|
| 354 |
+
DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
|
| 355 |
+
>>> nx.k_crust(H, k=1).nodes
|
| 356 |
+
NodeView((0, 4, 6))
|
| 357 |
+
|
| 358 |
+
See Also
|
| 359 |
+
--------
|
| 360 |
+
core_number
|
| 361 |
+
|
| 362 |
+
References
|
| 363 |
+
----------
|
| 364 |
+
.. [1] A model of Internet topology using k-shell decomposition
|
| 365 |
+
Shai Carmi, Shlomo Havlin, Scott Kirkpatrick, Yuval Shavitt,
|
| 366 |
+
and Eran Shir, PNAS July 3, 2007 vol. 104 no. 27 11150-11154
|
| 367 |
+
http://www.pnas.org/content/104/27/11150.full
|
| 368 |
+
"""
|
| 369 |
+
|
| 370 |
+
import warnings
|
| 371 |
+
|
| 372 |
+
if G.is_multigraph():
|
| 373 |
+
warnings.warn(
|
| 374 |
+
(
|
| 375 |
+
"\n\n`k_crust` will not accept `MultiGraph` objects in version 3.5.\n"
|
| 376 |
+
"Convert it to an undirected graph instead, using::\n\n"
|
| 377 |
+
"\tG = nx.Graph(G)\n"
|
| 378 |
+
),
|
| 379 |
+
category=DeprecationWarning,
|
| 380 |
+
stacklevel=5,
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
# Default for k is one less than in _core_subgraph, so just inline.
|
| 384 |
+
# Filter is c[v] <= k
|
| 385 |
+
if core_number is None:
|
| 386 |
+
core_number = nx.core_number(G)
|
| 387 |
+
if k is None:
|
| 388 |
+
k = max(core_number.values()) - 1
|
| 389 |
+
nodes = (v for v in core_number if core_number[v] <= k)
|
| 390 |
+
return G.subgraph(nodes).copy()
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
|
| 394 |
+
def k_corona(G, k, core_number=None):
|
| 395 |
+
"""Returns the k-corona of G.
|
| 396 |
+
|
| 397 |
+
The k-corona is the subgraph of nodes in the k-core which have
|
| 398 |
+
exactly k neighbors in the k-core.
|
| 399 |
+
|
| 400 |
+
.. deprecated:: 3.3
|
| 401 |
+
`k_corona` will not accept `MultiGraph` objects in version 3.5.
|
| 402 |
+
|
| 403 |
+
Parameters
|
| 404 |
+
----------
|
| 405 |
+
G : NetworkX graph
|
| 406 |
+
A graph or directed graph
|
| 407 |
+
k : int
|
| 408 |
+
The order of the corona.
|
| 409 |
+
core_number : dictionary, optional
|
| 410 |
+
Precomputed core numbers for the graph G.
|
| 411 |
+
|
| 412 |
+
Returns
|
| 413 |
+
-------
|
| 414 |
+
G : NetworkX graph
|
| 415 |
+
The k-corona subgraph
|
| 416 |
+
|
| 417 |
+
Raises
|
| 418 |
+
------
|
| 419 |
+
NetworkXNotImplemented
|
| 420 |
+
The k-corona is not defined for multigraphs or graphs with self loops.
|
| 421 |
+
|
| 422 |
+
Notes
|
| 423 |
+
-----
|
| 424 |
+
For directed graphs the node degree is defined to be the
|
| 425 |
+
in-degree + out-degree.
|
| 426 |
+
|
| 427 |
+
Graph, node, and edge attributes are copied to the subgraph.
|
| 428 |
+
|
| 429 |
+
Examples
|
| 430 |
+
--------
|
| 431 |
+
>>> degrees = [0, 1, 2, 2, 2, 2, 3]
|
| 432 |
+
>>> H = nx.havel_hakimi_graph(degrees)
|
| 433 |
+
>>> H.degree
|
| 434 |
+
DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
|
| 435 |
+
>>> nx.k_corona(H, k=2).nodes
|
| 436 |
+
NodeView((1, 2, 3, 5))
|
| 437 |
+
|
| 438 |
+
See Also
|
| 439 |
+
--------
|
| 440 |
+
core_number
|
| 441 |
+
|
| 442 |
+
References
|
| 443 |
+
----------
|
| 444 |
+
.. [1] k -core (bootstrap) percolation on complex networks:
|
| 445 |
+
Critical phenomena and nonlocal effects,
|
| 446 |
+
A. V. Goltsev, S. N. Dorogovtsev, and J. F. F. Mendes,
|
| 447 |
+
Phys. Rev. E 73, 056101 (2006)
|
| 448 |
+
http://link.aps.org/doi/10.1103/PhysRevE.73.056101
|
| 449 |
+
"""
|
| 450 |
+
|
| 451 |
+
import warnings
|
| 452 |
+
|
| 453 |
+
if G.is_multigraph():
|
| 454 |
+
warnings.warn(
|
| 455 |
+
(
|
| 456 |
+
"\n\n`k_corona` will not accept `MultiGraph` objects in version 3.5.\n"
|
| 457 |
+
"Convert it to an undirected graph instead, using::\n\n"
|
| 458 |
+
"\tG = nx.Graph(G)\n"
|
| 459 |
+
),
|
| 460 |
+
category=DeprecationWarning,
|
| 461 |
+
stacklevel=5,
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
def func(v, k, c):
|
| 465 |
+
return c[v] == k and k == sum(1 for w in G[v] if c[w] >= k)
|
| 466 |
+
|
| 467 |
+
return _core_subgraph(G, func, k, core_number)
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
@nx.utils.not_implemented_for("directed")
|
| 471 |
+
@nx.utils.not_implemented_for("multigraph")
|
| 472 |
+
@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
|
| 473 |
+
def k_truss(G, k):
|
| 474 |
+
"""Returns the k-truss of `G`.
|
| 475 |
+
|
| 476 |
+
The k-truss is the maximal induced subgraph of `G` which contains at least
|
| 477 |
+
three vertices where every edge is incident to at least `k-2` triangles.
|
| 478 |
+
|
| 479 |
+
Parameters
|
| 480 |
+
----------
|
| 481 |
+
G : NetworkX graph
|
| 482 |
+
An undirected graph
|
| 483 |
+
k : int
|
| 484 |
+
The order of the truss
|
| 485 |
+
|
| 486 |
+
Returns
|
| 487 |
+
-------
|
| 488 |
+
H : NetworkX graph
|
| 489 |
+
The k-truss subgraph
|
| 490 |
+
|
| 491 |
+
Raises
|
| 492 |
+
------
|
| 493 |
+
NetworkXNotImplemented
|
| 494 |
+
If `G` is a multigraph or directed graph or if it contains self loops.
|
| 495 |
+
|
| 496 |
+
Notes
|
| 497 |
+
-----
|
| 498 |
+
A k-clique is a (k-2)-truss and a k-truss is a (k+1)-core.
|
| 499 |
+
|
| 500 |
+
Graph, node, and edge attributes are copied to the subgraph.
|
| 501 |
+
|
| 502 |
+
K-trusses were originally defined in [2] which states that the k-truss
|
| 503 |
+
is the maximal induced subgraph where each edge belongs to at least
|
| 504 |
+
`k-2` triangles. A more recent paper, [1], uses a slightly different
|
| 505 |
+
definition requiring that each edge belong to at least `k` triangles.
|
| 506 |
+
This implementation uses the original definition of `k-2` triangles.
|
| 507 |
+
|
| 508 |
+
Examples
|
| 509 |
+
--------
|
| 510 |
+
>>> degrees = [0, 1, 2, 2, 2, 2, 3]
|
| 511 |
+
>>> H = nx.havel_hakimi_graph(degrees)
|
| 512 |
+
>>> H.degree
|
| 513 |
+
DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
|
| 514 |
+
>>> nx.k_truss(H, k=2).nodes
|
| 515 |
+
NodeView((0, 1, 2, 3, 4, 5))
|
| 516 |
+
|
| 517 |
+
References
|
| 518 |
+
----------
|
| 519 |
+
.. [1] Bounds and Algorithms for k-truss. Paul Burkhardt, Vance Faber,
|
| 520 |
+
David G. Harris, 2018. https://arxiv.org/abs/1806.05523v2
|
| 521 |
+
.. [2] Trusses: Cohesive Subgraphs for Social Network Analysis. Jonathan
|
| 522 |
+
Cohen, 2005.
|
| 523 |
+
"""
|
| 524 |
+
if nx.number_of_selfloops(G) > 0:
|
| 525 |
+
msg = (
|
| 526 |
+
"Input graph has self loops which is not permitted; "
|
| 527 |
+
"Consider using G.remove_edges_from(nx.selfloop_edges(G))."
|
| 528 |
+
)
|
| 529 |
+
raise nx.NetworkXNotImplemented(msg)
|
| 530 |
+
|
| 531 |
+
H = G.copy()
|
| 532 |
+
|
| 533 |
+
n_dropped = 1
|
| 534 |
+
while n_dropped > 0:
|
| 535 |
+
n_dropped = 0
|
| 536 |
+
to_drop = []
|
| 537 |
+
seen = set()
|
| 538 |
+
for u in H:
|
| 539 |
+
nbrs_u = set(H[u])
|
| 540 |
+
seen.add(u)
|
| 541 |
+
new_nbrs = [v for v in nbrs_u if v not in seen]
|
| 542 |
+
for v in new_nbrs:
|
| 543 |
+
if len(nbrs_u & set(H[v])) < (k - 2):
|
| 544 |
+
to_drop.append((u, v))
|
| 545 |
+
H.remove_edges_from(to_drop)
|
| 546 |
+
n_dropped = len(to_drop)
|
| 547 |
+
H.remove_nodes_from(list(nx.isolates(H)))
|
| 548 |
+
|
| 549 |
+
return H
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
@nx.utils.not_implemented_for("multigraph")
|
| 553 |
+
@nx.utils.not_implemented_for("directed")
|
| 554 |
+
@nx._dispatchable
|
| 555 |
+
def onion_layers(G):
|
| 556 |
+
"""Returns the layer of each vertex in an onion decomposition of the graph.
|
| 557 |
+
|
| 558 |
+
The onion decomposition refines the k-core decomposition by providing
|
| 559 |
+
information on the internal organization of each k-shell. It is usually
|
| 560 |
+
used alongside the `core numbers`.
|
| 561 |
+
|
| 562 |
+
Parameters
|
| 563 |
+
----------
|
| 564 |
+
G : NetworkX graph
|
| 565 |
+
An undirected graph without self loops.
|
| 566 |
+
|
| 567 |
+
Returns
|
| 568 |
+
-------
|
| 569 |
+
od_layers : dictionary
|
| 570 |
+
A dictionary keyed by node to the onion layer. The layers are
|
| 571 |
+
contiguous integers starting at 1.
|
| 572 |
+
|
| 573 |
+
Raises
|
| 574 |
+
------
|
| 575 |
+
NetworkXNotImplemented
|
| 576 |
+
If `G` is a multigraph or directed graph or if it contains self loops.
|
| 577 |
+
|
| 578 |
+
Examples
|
| 579 |
+
--------
|
| 580 |
+
>>> degrees = [0, 1, 2, 2, 2, 2, 3]
|
| 581 |
+
>>> H = nx.havel_hakimi_graph(degrees)
|
| 582 |
+
>>> H.degree
|
| 583 |
+
DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
|
| 584 |
+
>>> nx.onion_layers(H)
|
| 585 |
+
{6: 1, 0: 2, 4: 3, 1: 4, 2: 4, 3: 4, 5: 4}
|
| 586 |
+
|
| 587 |
+
See Also
|
| 588 |
+
--------
|
| 589 |
+
core_number
|
| 590 |
+
|
| 591 |
+
References
|
| 592 |
+
----------
|
| 593 |
+
.. [1] Multi-scale structure and topological anomaly detection via a new
|
| 594 |
+
network statistic: The onion decomposition
|
| 595 |
+
L. Hébert-Dufresne, J. A. Grochow, and A. Allard
|
| 596 |
+
Scientific Reports 6, 31708 (2016)
|
| 597 |
+
http://doi.org/10.1038/srep31708
|
| 598 |
+
.. [2] Percolation and the effective structure of complex networks
|
| 599 |
+
A. Allard and L. Hébert-Dufresne
|
| 600 |
+
Physical Review X 9, 011023 (2019)
|
| 601 |
+
http://doi.org/10.1103/PhysRevX.9.011023
|
| 602 |
+
"""
|
| 603 |
+
if nx.number_of_selfloops(G) > 0:
|
| 604 |
+
msg = (
|
| 605 |
+
"Input graph contains self loops which is not permitted; "
|
| 606 |
+
"Consider using G.remove_edges_from(nx.selfloop_edges(G))."
|
| 607 |
+
)
|
| 608 |
+
raise nx.NetworkXNotImplemented(msg)
|
| 609 |
+
# Dictionaries to register the k-core/onion decompositions.
|
| 610 |
+
od_layers = {}
|
| 611 |
+
# Adjacency list
|
| 612 |
+
neighbors = {v: list(nx.all_neighbors(G, v)) for v in G}
|
| 613 |
+
# Effective degree of nodes.
|
| 614 |
+
degrees = dict(G.degree())
|
| 615 |
+
# Performs the onion decomposition.
|
| 616 |
+
current_core = 1
|
| 617 |
+
current_layer = 1
|
| 618 |
+
# Sets vertices of degree 0 to layer 1, if any.
|
| 619 |
+
isolated_nodes = list(nx.isolates(G))
|
| 620 |
+
if len(isolated_nodes) > 0:
|
| 621 |
+
for v in isolated_nodes:
|
| 622 |
+
od_layers[v] = current_layer
|
| 623 |
+
degrees.pop(v)
|
| 624 |
+
current_layer = 2
|
| 625 |
+
# Finds the layer for the remaining nodes.
|
| 626 |
+
while len(degrees) > 0:
|
| 627 |
+
# Sets the order for looking at nodes.
|
| 628 |
+
nodes = sorted(degrees, key=degrees.get)
|
| 629 |
+
# Sets properly the current core.
|
| 630 |
+
min_degree = degrees[nodes[0]]
|
| 631 |
+
if min_degree > current_core:
|
| 632 |
+
current_core = min_degree
|
| 633 |
+
# Identifies vertices in the current layer.
|
| 634 |
+
this_layer = []
|
| 635 |
+
for n in nodes:
|
| 636 |
+
if degrees[n] > current_core:
|
| 637 |
+
break
|
| 638 |
+
this_layer.append(n)
|
| 639 |
+
# Identifies the core/layer of the vertices in the current layer.
|
| 640 |
+
for v in this_layer:
|
| 641 |
+
od_layers[v] = current_layer
|
| 642 |
+
for n in neighbors[v]:
|
| 643 |
+
neighbors[n].remove(v)
|
| 644 |
+
degrees[n] = degrees[n] - 1
|
| 645 |
+
degrees.pop(v)
|
| 646 |
+
# Updates the layer count.
|
| 647 |
+
current_layer = current_layer + 1
|
| 648 |
+
# Returns the dictionaries containing the onion layer of each vertices.
|
| 649 |
+
return od_layers
|
.venv/lib/python3.11/site-packages/networkx/algorithms/covering.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions related to graph covers."""
|
| 2 |
+
|
| 3 |
+
from functools import partial
|
| 4 |
+
from itertools import chain
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from networkx.utils import arbitrary_element, not_implemented_for
|
| 8 |
+
|
| 9 |
+
__all__ = ["min_edge_cover", "is_edge_cover"]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@not_implemented_for("directed")
|
| 13 |
+
@not_implemented_for("multigraph")
|
| 14 |
+
@nx._dispatchable
|
| 15 |
+
def min_edge_cover(G, matching_algorithm=None):
|
| 16 |
+
"""Returns the min cardinality edge cover of the graph as a set of edges.
|
| 17 |
+
|
| 18 |
+
A smallest edge cover can be found in polynomial time by finding
|
| 19 |
+
a maximum matching and extending it greedily so that all nodes
|
| 20 |
+
are covered. This function follows that process. A maximum matching
|
| 21 |
+
algorithm can be specified for the first step of the algorithm.
|
| 22 |
+
The resulting set may return a set with one 2-tuple for each edge,
|
| 23 |
+
(the usual case) or with both 2-tuples `(u, v)` and `(v, u)` for
|
| 24 |
+
each edge. The latter is only done when a bipartite matching algorithm
|
| 25 |
+
is specified as `matching_algorithm`.
|
| 26 |
+
|
| 27 |
+
Parameters
|
| 28 |
+
----------
|
| 29 |
+
G : NetworkX graph
|
| 30 |
+
An undirected graph.
|
| 31 |
+
|
| 32 |
+
matching_algorithm : function
|
| 33 |
+
A function that returns a maximum cardinality matching for `G`.
|
| 34 |
+
The function must take one input, the graph `G`, and return
|
| 35 |
+
either a set of edges (with only one direction for the pair of nodes)
|
| 36 |
+
or a dictionary mapping each node to its mate. If not specified,
|
| 37 |
+
:func:`~networkx.algorithms.matching.max_weight_matching` is used.
|
| 38 |
+
Common bipartite matching functions include
|
| 39 |
+
:func:`~networkx.algorithms.bipartite.matching.hopcroft_karp_matching`
|
| 40 |
+
or
|
| 41 |
+
:func:`~networkx.algorithms.bipartite.matching.eppstein_matching`.
|
| 42 |
+
|
| 43 |
+
Returns
|
| 44 |
+
-------
|
| 45 |
+
min_cover : set
|
| 46 |
+
|
| 47 |
+
A set of the edges in a minimum edge cover in the form of tuples.
|
| 48 |
+
It contains only one of the equivalent 2-tuples `(u, v)` and `(v, u)`
|
| 49 |
+
for each edge. If a bipartite method is used to compute the matching,
|
| 50 |
+
the returned set contains both the 2-tuples `(u, v)` and `(v, u)`
|
| 51 |
+
for each edge of a minimum edge cover.
|
| 52 |
+
|
| 53 |
+
Examples
|
| 54 |
+
--------
|
| 55 |
+
>>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
|
| 56 |
+
>>> sorted(nx.min_edge_cover(G))
|
| 57 |
+
[(2, 1), (3, 0)]
|
| 58 |
+
|
| 59 |
+
Notes
|
| 60 |
+
-----
|
| 61 |
+
An edge cover of a graph is a set of edges such that every node of
|
| 62 |
+
the graph is incident to at least one edge of the set.
|
| 63 |
+
The minimum edge cover is an edge covering of smallest cardinality.
|
| 64 |
+
|
| 65 |
+
Due to its implementation, the worst-case running time of this algorithm
|
| 66 |
+
is bounded by the worst-case running time of the function
|
| 67 |
+
``matching_algorithm``.
|
| 68 |
+
|
| 69 |
+
Minimum edge cover for `G` can also be found using the `min_edge_covering`
|
| 70 |
+
function in :mod:`networkx.algorithms.bipartite.covering` which is
|
| 71 |
+
simply this function with a default matching algorithm of
|
| 72 |
+
:func:`~networkx.algorithms.bipartite.matching.hopcraft_karp_matching`
|
| 73 |
+
"""
|
| 74 |
+
if len(G) == 0:
|
| 75 |
+
return set()
|
| 76 |
+
if nx.number_of_isolates(G) > 0:
|
| 77 |
+
# ``min_cover`` does not exist as there is an isolated node
|
| 78 |
+
raise nx.NetworkXException(
|
| 79 |
+
"Graph has a node with no edge incident on it, so no edge cover exists."
|
| 80 |
+
)
|
| 81 |
+
if matching_algorithm is None:
|
| 82 |
+
matching_algorithm = partial(nx.max_weight_matching, maxcardinality=True)
|
| 83 |
+
maximum_matching = matching_algorithm(G)
|
| 84 |
+
# ``min_cover`` is superset of ``maximum_matching``
|
| 85 |
+
try:
|
| 86 |
+
# bipartite matching algs return dict so convert if needed
|
| 87 |
+
min_cover = set(maximum_matching.items())
|
| 88 |
+
bipartite_cover = True
|
| 89 |
+
except AttributeError:
|
| 90 |
+
min_cover = maximum_matching
|
| 91 |
+
bipartite_cover = False
|
| 92 |
+
# iterate for uncovered nodes
|
| 93 |
+
uncovered_nodes = set(G) - {v for u, v in min_cover} - {u for u, v in min_cover}
|
| 94 |
+
for v in uncovered_nodes:
|
| 95 |
+
# Since `v` is uncovered, each edge incident to `v` will join it
|
| 96 |
+
# with a covered node (otherwise, if there were an edge joining
|
| 97 |
+
# uncovered nodes `u` and `v`, the maximum matching algorithm
|
| 98 |
+
# would have found it), so we can choose an arbitrary edge
|
| 99 |
+
# incident to `v`. (This applies only in a simple graph, not a
|
| 100 |
+
# multigraph.)
|
| 101 |
+
u = arbitrary_element(G[v])
|
| 102 |
+
min_cover.add((u, v))
|
| 103 |
+
if bipartite_cover:
|
| 104 |
+
min_cover.add((v, u))
|
| 105 |
+
return min_cover
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@not_implemented_for("directed")
|
| 109 |
+
@nx._dispatchable
|
| 110 |
+
def is_edge_cover(G, cover):
|
| 111 |
+
"""Decides whether a set of edges is a valid edge cover of the graph.
|
| 112 |
+
|
| 113 |
+
Given a set of edges, whether it is an edge covering can
|
| 114 |
+
be decided if we just check whether all nodes of the graph
|
| 115 |
+
has an edge from the set, incident on it.
|
| 116 |
+
|
| 117 |
+
Parameters
|
| 118 |
+
----------
|
| 119 |
+
G : NetworkX graph
|
| 120 |
+
An undirected bipartite graph.
|
| 121 |
+
|
| 122 |
+
cover : set
|
| 123 |
+
Set of edges to be checked.
|
| 124 |
+
|
| 125 |
+
Returns
|
| 126 |
+
-------
|
| 127 |
+
bool
|
| 128 |
+
Whether the set of edges is a valid edge cover of the graph.
|
| 129 |
+
|
| 130 |
+
Examples
|
| 131 |
+
--------
|
| 132 |
+
>>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
|
| 133 |
+
>>> cover = {(2, 1), (3, 0)}
|
| 134 |
+
>>> nx.is_edge_cover(G, cover)
|
| 135 |
+
True
|
| 136 |
+
|
| 137 |
+
Notes
|
| 138 |
+
-----
|
| 139 |
+
An edge cover of a graph is a set of edges such that every node of
|
| 140 |
+
the graph is incident to at least one edge of the set.
|
| 141 |
+
"""
|
| 142 |
+
return set(G) <= set(chain.from_iterable(cover))
|
.venv/lib/python3.11/site-packages/networkx/algorithms/cuts.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions for finding and evaluating cuts in a graph."""
|
| 2 |
+
|
| 3 |
+
from itertools import chain
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"boundary_expansion",
|
| 9 |
+
"conductance",
|
| 10 |
+
"cut_size",
|
| 11 |
+
"edge_expansion",
|
| 12 |
+
"mixing_expansion",
|
| 13 |
+
"node_expansion",
|
| 14 |
+
"normalized_cut_size",
|
| 15 |
+
"volume",
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# TODO STILL NEED TO UPDATE ALL THE DOCUMENTATION!
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 23 |
+
def cut_size(G, S, T=None, weight=None):
|
| 24 |
+
"""Returns the size of the cut between two sets of nodes.
|
| 25 |
+
|
| 26 |
+
A *cut* is a partition of the nodes of a graph into two sets. The
|
| 27 |
+
*cut size* is the sum of the weights of the edges "between" the two
|
| 28 |
+
sets of nodes.
|
| 29 |
+
|
| 30 |
+
Parameters
|
| 31 |
+
----------
|
| 32 |
+
G : NetworkX graph
|
| 33 |
+
|
| 34 |
+
S : collection
|
| 35 |
+
A collection of nodes in `G`.
|
| 36 |
+
|
| 37 |
+
T : collection
|
| 38 |
+
A collection of nodes in `G`. If not specified, this is taken to
|
| 39 |
+
be the set complement of `S`.
|
| 40 |
+
|
| 41 |
+
weight : object
|
| 42 |
+
Edge attribute key to use as weight. If not specified, edges
|
| 43 |
+
have weight one.
|
| 44 |
+
|
| 45 |
+
Returns
|
| 46 |
+
-------
|
| 47 |
+
number
|
| 48 |
+
Total weight of all edges from nodes in set `S` to nodes in
|
| 49 |
+
set `T` (and, in the case of directed graphs, all edges from
|
| 50 |
+
nodes in `T` to nodes in `S`).
|
| 51 |
+
|
| 52 |
+
Examples
|
| 53 |
+
--------
|
| 54 |
+
In the graph with two cliques joined by a single edges, the natural
|
| 55 |
+
bipartition of the graph into two blocks, one for each clique,
|
| 56 |
+
yields a cut of weight one::
|
| 57 |
+
|
| 58 |
+
>>> G = nx.barbell_graph(3, 0)
|
| 59 |
+
>>> S = {0, 1, 2}
|
| 60 |
+
>>> T = {3, 4, 5}
|
| 61 |
+
>>> nx.cut_size(G, S, T)
|
| 62 |
+
1
|
| 63 |
+
|
| 64 |
+
Each parallel edge in a multigraph is counted when determining the
|
| 65 |
+
cut size::
|
| 66 |
+
|
| 67 |
+
>>> G = nx.MultiGraph(["ab", "ab"])
|
| 68 |
+
>>> S = {"a"}
|
| 69 |
+
>>> T = {"b"}
|
| 70 |
+
>>> nx.cut_size(G, S, T)
|
| 71 |
+
2
|
| 72 |
+
|
| 73 |
+
Notes
|
| 74 |
+
-----
|
| 75 |
+
In a multigraph, the cut size is the total weight of edges including
|
| 76 |
+
multiplicity.
|
| 77 |
+
|
| 78 |
+
"""
|
| 79 |
+
edges = nx.edge_boundary(G, S, T, data=weight, default=1)
|
| 80 |
+
if G.is_directed():
|
| 81 |
+
edges = chain(edges, nx.edge_boundary(G, T, S, data=weight, default=1))
|
| 82 |
+
return sum(weight for u, v, weight in edges)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 86 |
+
def volume(G, S, weight=None):
|
| 87 |
+
"""Returns the volume of a set of nodes.
|
| 88 |
+
|
| 89 |
+
The *volume* of a set *S* is the sum of the (out-)degrees of nodes
|
| 90 |
+
in *S* (taking into account parallel edges in multigraphs). [1]
|
| 91 |
+
|
| 92 |
+
Parameters
|
| 93 |
+
----------
|
| 94 |
+
G : NetworkX graph
|
| 95 |
+
|
| 96 |
+
S : collection
|
| 97 |
+
A collection of nodes in `G`.
|
| 98 |
+
|
| 99 |
+
weight : object
|
| 100 |
+
Edge attribute key to use as weight. If not specified, edges
|
| 101 |
+
have weight one.
|
| 102 |
+
|
| 103 |
+
Returns
|
| 104 |
+
-------
|
| 105 |
+
number
|
| 106 |
+
The volume of the set of nodes represented by `S` in the graph
|
| 107 |
+
`G`.
|
| 108 |
+
|
| 109 |
+
See also
|
| 110 |
+
--------
|
| 111 |
+
conductance
|
| 112 |
+
cut_size
|
| 113 |
+
edge_expansion
|
| 114 |
+
edge_boundary
|
| 115 |
+
normalized_cut_size
|
| 116 |
+
|
| 117 |
+
References
|
| 118 |
+
----------
|
| 119 |
+
.. [1] David Gleich.
|
| 120 |
+
*Hierarchical Directed Spectral Graph Partitioning*.
|
| 121 |
+
<https://www.cs.purdue.edu/homes/dgleich/publications/Gleich%202005%20-%20hierarchical%20directed%20spectral.pdf>
|
| 122 |
+
|
| 123 |
+
"""
|
| 124 |
+
degree = G.out_degree if G.is_directed() else G.degree
|
| 125 |
+
return sum(d for v, d in degree(S, weight=weight))
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 129 |
+
def normalized_cut_size(G, S, T=None, weight=None):
|
| 130 |
+
"""Returns the normalized size of the cut between two sets of nodes.
|
| 131 |
+
|
| 132 |
+
The *normalized cut size* is the cut size times the sum of the
|
| 133 |
+
reciprocal sizes of the volumes of the two sets. [1]
|
| 134 |
+
|
| 135 |
+
Parameters
|
| 136 |
+
----------
|
| 137 |
+
G : NetworkX graph
|
| 138 |
+
|
| 139 |
+
S : collection
|
| 140 |
+
A collection of nodes in `G`.
|
| 141 |
+
|
| 142 |
+
T : collection
|
| 143 |
+
A collection of nodes in `G`.
|
| 144 |
+
|
| 145 |
+
weight : object
|
| 146 |
+
Edge attribute key to use as weight. If not specified, edges
|
| 147 |
+
have weight one.
|
| 148 |
+
|
| 149 |
+
Returns
|
| 150 |
+
-------
|
| 151 |
+
number
|
| 152 |
+
The normalized cut size between the two sets `S` and `T`.
|
| 153 |
+
|
| 154 |
+
Notes
|
| 155 |
+
-----
|
| 156 |
+
In a multigraph, the cut size is the total weight of edges including
|
| 157 |
+
multiplicity.
|
| 158 |
+
|
| 159 |
+
See also
|
| 160 |
+
--------
|
| 161 |
+
conductance
|
| 162 |
+
cut_size
|
| 163 |
+
edge_expansion
|
| 164 |
+
volume
|
| 165 |
+
|
| 166 |
+
References
|
| 167 |
+
----------
|
| 168 |
+
.. [1] David Gleich.
|
| 169 |
+
*Hierarchical Directed Spectral Graph Partitioning*.
|
| 170 |
+
<https://www.cs.purdue.edu/homes/dgleich/publications/Gleich%202005%20-%20hierarchical%20directed%20spectral.pdf>
|
| 171 |
+
|
| 172 |
+
"""
|
| 173 |
+
if T is None:
|
| 174 |
+
T = set(G) - set(S)
|
| 175 |
+
num_cut_edges = cut_size(G, S, T=T, weight=weight)
|
| 176 |
+
volume_S = volume(G, S, weight=weight)
|
| 177 |
+
volume_T = volume(G, T, weight=weight)
|
| 178 |
+
return num_cut_edges * ((1 / volume_S) + (1 / volume_T))
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 182 |
+
def conductance(G, S, T=None, weight=None):
|
| 183 |
+
"""Returns the conductance of two sets of nodes.
|
| 184 |
+
|
| 185 |
+
The *conductance* is the quotient of the cut size and the smaller of
|
| 186 |
+
the volumes of the two sets. [1]
|
| 187 |
+
|
| 188 |
+
Parameters
|
| 189 |
+
----------
|
| 190 |
+
G : NetworkX graph
|
| 191 |
+
|
| 192 |
+
S : collection
|
| 193 |
+
A collection of nodes in `G`.
|
| 194 |
+
|
| 195 |
+
T : collection
|
| 196 |
+
A collection of nodes in `G`.
|
| 197 |
+
|
| 198 |
+
weight : object
|
| 199 |
+
Edge attribute key to use as weight. If not specified, edges
|
| 200 |
+
have weight one.
|
| 201 |
+
|
| 202 |
+
Returns
|
| 203 |
+
-------
|
| 204 |
+
number
|
| 205 |
+
The conductance between the two sets `S` and `T`.
|
| 206 |
+
|
| 207 |
+
See also
|
| 208 |
+
--------
|
| 209 |
+
cut_size
|
| 210 |
+
edge_expansion
|
| 211 |
+
normalized_cut_size
|
| 212 |
+
volume
|
| 213 |
+
|
| 214 |
+
References
|
| 215 |
+
----------
|
| 216 |
+
.. [1] David Gleich.
|
| 217 |
+
*Hierarchical Directed Spectral Graph Partitioning*.
|
| 218 |
+
<https://www.cs.purdue.edu/homes/dgleich/publications/Gleich%202005%20-%20hierarchical%20directed%20spectral.pdf>
|
| 219 |
+
|
| 220 |
+
"""
|
| 221 |
+
if T is None:
|
| 222 |
+
T = set(G) - set(S)
|
| 223 |
+
num_cut_edges = cut_size(G, S, T, weight=weight)
|
| 224 |
+
volume_S = volume(G, S, weight=weight)
|
| 225 |
+
volume_T = volume(G, T, weight=weight)
|
| 226 |
+
return num_cut_edges / min(volume_S, volume_T)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 230 |
+
def edge_expansion(G, S, T=None, weight=None):
|
| 231 |
+
"""Returns the edge expansion between two node sets.
|
| 232 |
+
|
| 233 |
+
The *edge expansion* is the quotient of the cut size and the smaller
|
| 234 |
+
of the cardinalities of the two sets. [1]
|
| 235 |
+
|
| 236 |
+
Parameters
|
| 237 |
+
----------
|
| 238 |
+
G : NetworkX graph
|
| 239 |
+
|
| 240 |
+
S : collection
|
| 241 |
+
A collection of nodes in `G`.
|
| 242 |
+
|
| 243 |
+
T : collection
|
| 244 |
+
A collection of nodes in `G`.
|
| 245 |
+
|
| 246 |
+
weight : object
|
| 247 |
+
Edge attribute key to use as weight. If not specified, edges
|
| 248 |
+
have weight one.
|
| 249 |
+
|
| 250 |
+
Returns
|
| 251 |
+
-------
|
| 252 |
+
number
|
| 253 |
+
The edge expansion between the two sets `S` and `T`.
|
| 254 |
+
|
| 255 |
+
See also
|
| 256 |
+
--------
|
| 257 |
+
boundary_expansion
|
| 258 |
+
mixing_expansion
|
| 259 |
+
node_expansion
|
| 260 |
+
|
| 261 |
+
References
|
| 262 |
+
----------
|
| 263 |
+
.. [1] Fan Chung.
|
| 264 |
+
*Spectral Graph Theory*.
|
| 265 |
+
(CBMS Regional Conference Series in Mathematics, No. 92),
|
| 266 |
+
American Mathematical Society, 1997, ISBN 0-8218-0315-8
|
| 267 |
+
<http://www.math.ucsd.edu/~fan/research/revised.html>
|
| 268 |
+
|
| 269 |
+
"""
|
| 270 |
+
if T is None:
|
| 271 |
+
T = set(G) - set(S)
|
| 272 |
+
num_cut_edges = cut_size(G, S, T=T, weight=weight)
|
| 273 |
+
return num_cut_edges / min(len(S), len(T))
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 277 |
+
def mixing_expansion(G, S, T=None, weight=None):
|
| 278 |
+
"""Returns the mixing expansion between two node sets.
|
| 279 |
+
|
| 280 |
+
The *mixing expansion* is the quotient of the cut size and twice the
|
| 281 |
+
number of edges in the graph. [1]
|
| 282 |
+
|
| 283 |
+
Parameters
|
| 284 |
+
----------
|
| 285 |
+
G : NetworkX graph
|
| 286 |
+
|
| 287 |
+
S : collection
|
| 288 |
+
A collection of nodes in `G`.
|
| 289 |
+
|
| 290 |
+
T : collection
|
| 291 |
+
A collection of nodes in `G`.
|
| 292 |
+
|
| 293 |
+
weight : object
|
| 294 |
+
Edge attribute key to use as weight. If not specified, edges
|
| 295 |
+
have weight one.
|
| 296 |
+
|
| 297 |
+
Returns
|
| 298 |
+
-------
|
| 299 |
+
number
|
| 300 |
+
The mixing expansion between the two sets `S` and `T`.
|
| 301 |
+
|
| 302 |
+
See also
|
| 303 |
+
--------
|
| 304 |
+
boundary_expansion
|
| 305 |
+
edge_expansion
|
| 306 |
+
node_expansion
|
| 307 |
+
|
| 308 |
+
References
|
| 309 |
+
----------
|
| 310 |
+
.. [1] Vadhan, Salil P.
|
| 311 |
+
"Pseudorandomness."
|
| 312 |
+
*Foundations and Trends
|
| 313 |
+
in Theoretical Computer Science* 7.1–3 (2011): 1–336.
|
| 314 |
+
<https://doi.org/10.1561/0400000010>
|
| 315 |
+
|
| 316 |
+
"""
|
| 317 |
+
num_cut_edges = cut_size(G, S, T=T, weight=weight)
|
| 318 |
+
num_total_edges = G.number_of_edges()
|
| 319 |
+
return num_cut_edges / (2 * num_total_edges)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
# TODO What is the generalization to two arguments, S and T? Does the
|
| 323 |
+
# denominator become `min(len(S), len(T))`?
|
| 324 |
+
@nx._dispatchable
|
| 325 |
+
def node_expansion(G, S):
|
| 326 |
+
"""Returns the node expansion of the set `S`.
|
| 327 |
+
|
| 328 |
+
The *node expansion* is the quotient of the size of the node
|
| 329 |
+
boundary of *S* and the cardinality of *S*. [1]
|
| 330 |
+
|
| 331 |
+
Parameters
|
| 332 |
+
----------
|
| 333 |
+
G : NetworkX graph
|
| 334 |
+
|
| 335 |
+
S : collection
|
| 336 |
+
A collection of nodes in `G`.
|
| 337 |
+
|
| 338 |
+
Returns
|
| 339 |
+
-------
|
| 340 |
+
number
|
| 341 |
+
The node expansion of the set `S`.
|
| 342 |
+
|
| 343 |
+
See also
|
| 344 |
+
--------
|
| 345 |
+
boundary_expansion
|
| 346 |
+
edge_expansion
|
| 347 |
+
mixing_expansion
|
| 348 |
+
|
| 349 |
+
References
|
| 350 |
+
----------
|
| 351 |
+
.. [1] Vadhan, Salil P.
|
| 352 |
+
"Pseudorandomness."
|
| 353 |
+
*Foundations and Trends
|
| 354 |
+
in Theoretical Computer Science* 7.1–3 (2011): 1–336.
|
| 355 |
+
<https://doi.org/10.1561/0400000010>
|
| 356 |
+
|
| 357 |
+
"""
|
| 358 |
+
neighborhood = set(chain.from_iterable(G.neighbors(v) for v in S))
|
| 359 |
+
return len(neighborhood) / len(S)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
# TODO What is the generalization to two arguments, S and T? Does the
|
| 363 |
+
# denominator become `min(len(S), len(T))`?
|
| 364 |
+
@nx._dispatchable
|
| 365 |
+
def boundary_expansion(G, S):
|
| 366 |
+
"""Returns the boundary expansion of the set `S`.
|
| 367 |
+
|
| 368 |
+
The *boundary expansion* is the quotient of the size
|
| 369 |
+
of the node boundary and the cardinality of *S*. [1]
|
| 370 |
+
|
| 371 |
+
Parameters
|
| 372 |
+
----------
|
| 373 |
+
G : NetworkX graph
|
| 374 |
+
|
| 375 |
+
S : collection
|
| 376 |
+
A collection of nodes in `G`.
|
| 377 |
+
|
| 378 |
+
Returns
|
| 379 |
+
-------
|
| 380 |
+
number
|
| 381 |
+
The boundary expansion of the set `S`.
|
| 382 |
+
|
| 383 |
+
See also
|
| 384 |
+
--------
|
| 385 |
+
edge_expansion
|
| 386 |
+
mixing_expansion
|
| 387 |
+
node_expansion
|
| 388 |
+
|
| 389 |
+
References
|
| 390 |
+
----------
|
| 391 |
+
.. [1] Vadhan, Salil P.
|
| 392 |
+
"Pseudorandomness."
|
| 393 |
+
*Foundations and Trends in Theoretical Computer Science*
|
| 394 |
+
7.1–3 (2011): 1–336.
|
| 395 |
+
<https://doi.org/10.1561/0400000010>
|
| 396 |
+
|
| 397 |
+
"""
|
| 398 |
+
return len(nx.node_boundary(G, S)) / len(S)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/cycles.py
ADDED
|
@@ -0,0 +1,1230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
========================
|
| 3 |
+
Cycle finding algorithms
|
| 4 |
+
========================
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from collections import Counter, defaultdict
|
| 8 |
+
from itertools import combinations, product
|
| 9 |
+
from math import inf
|
| 10 |
+
|
| 11 |
+
import networkx as nx
|
| 12 |
+
from networkx.utils import not_implemented_for, pairwise
|
| 13 |
+
|
| 14 |
+
__all__ = [
|
| 15 |
+
"cycle_basis",
|
| 16 |
+
"simple_cycles",
|
| 17 |
+
"recursive_simple_cycles",
|
| 18 |
+
"find_cycle",
|
| 19 |
+
"minimum_cycle_basis",
|
| 20 |
+
"chordless_cycles",
|
| 21 |
+
"girth",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@not_implemented_for("directed")
|
| 26 |
+
@not_implemented_for("multigraph")
|
| 27 |
+
@nx._dispatchable
|
| 28 |
+
def cycle_basis(G, root=None):
|
| 29 |
+
"""Returns a list of cycles which form a basis for cycles of G.
|
| 30 |
+
|
| 31 |
+
A basis for cycles of a network is a minimal collection of
|
| 32 |
+
cycles such that any cycle in the network can be written
|
| 33 |
+
as a sum of cycles in the basis. Here summation of cycles
|
| 34 |
+
is defined as "exclusive or" of the edges. Cycle bases are
|
| 35 |
+
useful, e.g. when deriving equations for electric circuits
|
| 36 |
+
using Kirchhoff's Laws.
|
| 37 |
+
|
| 38 |
+
Parameters
|
| 39 |
+
----------
|
| 40 |
+
G : NetworkX Graph
|
| 41 |
+
root : node, optional
|
| 42 |
+
Specify starting node for basis.
|
| 43 |
+
|
| 44 |
+
Returns
|
| 45 |
+
-------
|
| 46 |
+
A list of cycle lists. Each cycle list is a list of nodes
|
| 47 |
+
which forms a cycle (loop) in G.
|
| 48 |
+
|
| 49 |
+
Examples
|
| 50 |
+
--------
|
| 51 |
+
>>> G = nx.Graph()
|
| 52 |
+
>>> nx.add_cycle(G, [0, 1, 2, 3])
|
| 53 |
+
>>> nx.add_cycle(G, [0, 3, 4, 5])
|
| 54 |
+
>>> nx.cycle_basis(G, 0)
|
| 55 |
+
[[3, 4, 5, 0], [1, 2, 3, 0]]
|
| 56 |
+
|
| 57 |
+
Notes
|
| 58 |
+
-----
|
| 59 |
+
This is adapted from algorithm CACM 491 [1]_.
|
| 60 |
+
|
| 61 |
+
References
|
| 62 |
+
----------
|
| 63 |
+
.. [1] Paton, K. An algorithm for finding a fundamental set of
|
| 64 |
+
cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518.
|
| 65 |
+
|
| 66 |
+
See Also
|
| 67 |
+
--------
|
| 68 |
+
simple_cycles
|
| 69 |
+
minimum_cycle_basis
|
| 70 |
+
"""
|
| 71 |
+
gnodes = dict.fromkeys(G) # set-like object that maintains node order
|
| 72 |
+
cycles = []
|
| 73 |
+
while gnodes: # loop over connected components
|
| 74 |
+
if root is None:
|
| 75 |
+
root = gnodes.popitem()[0]
|
| 76 |
+
stack = [root]
|
| 77 |
+
pred = {root: root}
|
| 78 |
+
used = {root: set()}
|
| 79 |
+
while stack: # walk the spanning tree finding cycles
|
| 80 |
+
z = stack.pop() # use last-in so cycles easier to find
|
| 81 |
+
zused = used[z]
|
| 82 |
+
for nbr in G[z]:
|
| 83 |
+
if nbr not in used: # new node
|
| 84 |
+
pred[nbr] = z
|
| 85 |
+
stack.append(nbr)
|
| 86 |
+
used[nbr] = {z}
|
| 87 |
+
elif nbr == z: # self loops
|
| 88 |
+
cycles.append([z])
|
| 89 |
+
elif nbr not in zused: # found a cycle
|
| 90 |
+
pn = used[nbr]
|
| 91 |
+
cycle = [nbr, z]
|
| 92 |
+
p = pred[z]
|
| 93 |
+
while p not in pn:
|
| 94 |
+
cycle.append(p)
|
| 95 |
+
p = pred[p]
|
| 96 |
+
cycle.append(p)
|
| 97 |
+
cycles.append(cycle)
|
| 98 |
+
used[nbr].add(z)
|
| 99 |
+
for node in pred:
|
| 100 |
+
gnodes.pop(node, None)
|
| 101 |
+
root = None
|
| 102 |
+
return cycles
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@nx._dispatchable
|
| 106 |
+
def simple_cycles(G, length_bound=None):
|
| 107 |
+
"""Find simple cycles (elementary circuits) of a graph.
|
| 108 |
+
|
| 109 |
+
A "simple cycle", or "elementary circuit", is a closed path where
|
| 110 |
+
no node appears twice. In a directed graph, two simple cycles are distinct
|
| 111 |
+
if they are not cyclic permutations of each other. In an undirected graph,
|
| 112 |
+
two simple cycles are distinct if they are not cyclic permutations of each
|
| 113 |
+
other nor of the other's reversal.
|
| 114 |
+
|
| 115 |
+
Optionally, the cycles are bounded in length. In the unbounded case, we use
|
| 116 |
+
a nonrecursive, iterator/generator version of Johnson's algorithm [1]_. In
|
| 117 |
+
the bounded case, we use a version of the algorithm of Gupta and
|
| 118 |
+
Suzumura [2]_. There may be better algorithms for some cases [3]_ [4]_ [5]_.
|
| 119 |
+
|
| 120 |
+
The algorithms of Johnson, and Gupta and Suzumura, are enhanced by some
|
| 121 |
+
well-known preprocessing techniques. When `G` is directed, we restrict our
|
| 122 |
+
attention to strongly connected components of `G`, generate all simple cycles
|
| 123 |
+
containing a certain node, remove that node, and further decompose the
|
| 124 |
+
remainder into strongly connected components. When `G` is undirected, we
|
| 125 |
+
restrict our attention to biconnected components, generate all simple cycles
|
| 126 |
+
containing a particular edge, remove that edge, and further decompose the
|
| 127 |
+
remainder into biconnected components.
|
| 128 |
+
|
| 129 |
+
Note that multigraphs are supported by this function -- and in undirected
|
| 130 |
+
multigraphs, a pair of parallel edges is considered a cycle of length 2.
|
| 131 |
+
Likewise, self-loops are considered to be cycles of length 1. We define
|
| 132 |
+
cycles as sequences of nodes; so the presence of loops and parallel edges
|
| 133 |
+
does not change the number of simple cycles in a graph.
|
| 134 |
+
|
| 135 |
+
Parameters
|
| 136 |
+
----------
|
| 137 |
+
G : NetworkX Graph
|
| 138 |
+
A networkx graph. Undirected, directed, and multigraphs are all supported.
|
| 139 |
+
|
| 140 |
+
length_bound : int or None, optional (default=None)
|
| 141 |
+
If `length_bound` is an int, generate all simple cycles of `G` with length at
|
| 142 |
+
most `length_bound`. Otherwise, generate all simple cycles of `G`.
|
| 143 |
+
|
| 144 |
+
Yields
|
| 145 |
+
------
|
| 146 |
+
list of nodes
|
| 147 |
+
Each cycle is represented by a list of nodes along the cycle.
|
| 148 |
+
|
| 149 |
+
Examples
|
| 150 |
+
--------
|
| 151 |
+
>>> G = nx.DiGraph([(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)])
|
| 152 |
+
>>> sorted(nx.simple_cycles(G))
|
| 153 |
+
[[0], [0, 1, 2], [0, 2], [1, 2], [2]]
|
| 154 |
+
|
| 155 |
+
To filter the cycles so that they don't include certain nodes or edges,
|
| 156 |
+
copy your graph and eliminate those nodes or edges before calling.
|
| 157 |
+
For example, to exclude self-loops from the above example:
|
| 158 |
+
|
| 159 |
+
>>> H = G.copy()
|
| 160 |
+
>>> H.remove_edges_from(nx.selfloop_edges(G))
|
| 161 |
+
>>> sorted(nx.simple_cycles(H))
|
| 162 |
+
[[0, 1, 2], [0, 2], [1, 2]]
|
| 163 |
+
|
| 164 |
+
Notes
|
| 165 |
+
-----
|
| 166 |
+
When `length_bound` is None, the time complexity is $O((n+e)(c+1))$ for $n$
|
| 167 |
+
nodes, $e$ edges and $c$ simple circuits. Otherwise, when ``length_bound > 1``,
|
| 168 |
+
the time complexity is $O((c+n)(k-1)d^k)$ where $d$ is the average degree of
|
| 169 |
+
the nodes of `G` and $k$ = `length_bound`.
|
| 170 |
+
|
| 171 |
+
Raises
|
| 172 |
+
------
|
| 173 |
+
ValueError
|
| 174 |
+
when ``length_bound < 0``.
|
| 175 |
+
|
| 176 |
+
References
|
| 177 |
+
----------
|
| 178 |
+
.. [1] Finding all the elementary circuits of a directed graph.
|
| 179 |
+
D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
|
| 180 |
+
https://doi.org/10.1137/0204007
|
| 181 |
+
.. [2] Finding All Bounded-Length Simple Cycles in a Directed Graph
|
| 182 |
+
A. Gupta and T. Suzumura https://arxiv.org/abs/2105.10094
|
| 183 |
+
.. [3] Enumerating the cycles of a digraph: a new preprocessing strategy.
|
| 184 |
+
G. Loizou and P. Thanish, Information Sciences, v. 27, 163-182, 1982.
|
| 185 |
+
.. [4] A search strategy for the elementary cycles of a directed graph.
|
| 186 |
+
J.L. Szwarcfiter and P.E. Lauer, BIT NUMERICAL MATHEMATICS,
|
| 187 |
+
v. 16, no. 2, 192-204, 1976.
|
| 188 |
+
.. [5] Optimal Listing of Cycles and st-Paths in Undirected Graphs
|
| 189 |
+
R. Ferreira and R. Grossi and A. Marino and N. Pisanti and R. Rizzi and
|
| 190 |
+
G. Sacomoto https://arxiv.org/abs/1205.2766
|
| 191 |
+
|
| 192 |
+
See Also
|
| 193 |
+
--------
|
| 194 |
+
cycle_basis
|
| 195 |
+
chordless_cycles
|
| 196 |
+
"""
|
| 197 |
+
|
| 198 |
+
if length_bound is not None:
|
| 199 |
+
if length_bound == 0:
|
| 200 |
+
return
|
| 201 |
+
elif length_bound < 0:
|
| 202 |
+
raise ValueError("length bound must be non-negative")
|
| 203 |
+
|
| 204 |
+
directed = G.is_directed()
|
| 205 |
+
yield from ([v] for v, Gv in G.adj.items() if v in Gv)
|
| 206 |
+
|
| 207 |
+
if length_bound is not None and length_bound == 1:
|
| 208 |
+
return
|
| 209 |
+
|
| 210 |
+
if G.is_multigraph() and not directed:
|
| 211 |
+
visited = set()
|
| 212 |
+
for u, Gu in G.adj.items():
|
| 213 |
+
multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited)
|
| 214 |
+
yield from ([u, v] for v, m in multiplicity if m > 1)
|
| 215 |
+
visited.add(u)
|
| 216 |
+
|
| 217 |
+
# explicitly filter out loops; implicitly filter out parallel edges
|
| 218 |
+
if directed:
|
| 219 |
+
G = nx.DiGraph((u, v) for u, Gu in G.adj.items() for v in Gu if v != u)
|
| 220 |
+
else:
|
| 221 |
+
G = nx.Graph((u, v) for u, Gu in G.adj.items() for v in Gu if v != u)
|
| 222 |
+
|
| 223 |
+
# this case is not strictly necessary but improves performance
|
| 224 |
+
if length_bound is not None and length_bound == 2:
|
| 225 |
+
if directed:
|
| 226 |
+
visited = set()
|
| 227 |
+
for u, Gu in G.adj.items():
|
| 228 |
+
yield from (
|
| 229 |
+
[v, u] for v in visited.intersection(Gu) if G.has_edge(v, u)
|
| 230 |
+
)
|
| 231 |
+
visited.add(u)
|
| 232 |
+
return
|
| 233 |
+
|
| 234 |
+
if directed:
|
| 235 |
+
yield from _directed_cycle_search(G, length_bound)
|
| 236 |
+
else:
|
| 237 |
+
yield from _undirected_cycle_search(G, length_bound)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _directed_cycle_search(G, length_bound):
|
| 241 |
+
"""A dispatch function for `simple_cycles` for directed graphs.
|
| 242 |
+
|
| 243 |
+
We generate all cycles of G through binary partition.
|
| 244 |
+
|
| 245 |
+
1. Pick a node v in G which belongs to at least one cycle
|
| 246 |
+
a. Generate all cycles of G which contain the node v.
|
| 247 |
+
b. Recursively generate all cycles of G \\ v.
|
| 248 |
+
|
| 249 |
+
This is accomplished through the following:
|
| 250 |
+
|
| 251 |
+
1. Compute the strongly connected components SCC of G.
|
| 252 |
+
2. Select and remove a biconnected component C from BCC. Select a
|
| 253 |
+
non-tree edge (u, v) of a depth-first search of G[C].
|
| 254 |
+
3. For each simple cycle P containing v in G[C], yield P.
|
| 255 |
+
4. Add the biconnected components of G[C \\ v] to BCC.
|
| 256 |
+
|
| 257 |
+
If the parameter length_bound is not None, then step 3 will be limited to
|
| 258 |
+
simple cycles of length at most length_bound.
|
| 259 |
+
|
| 260 |
+
Parameters
|
| 261 |
+
----------
|
| 262 |
+
G : NetworkX DiGraph
|
| 263 |
+
A directed graph
|
| 264 |
+
|
| 265 |
+
length_bound : int or None
|
| 266 |
+
If length_bound is an int, generate all simple cycles of G with length at most length_bound.
|
| 267 |
+
Otherwise, generate all simple cycles of G.
|
| 268 |
+
|
| 269 |
+
Yields
|
| 270 |
+
------
|
| 271 |
+
list of nodes
|
| 272 |
+
Each cycle is represented by a list of nodes along the cycle.
|
| 273 |
+
"""
|
| 274 |
+
|
| 275 |
+
scc = nx.strongly_connected_components
|
| 276 |
+
components = [c for c in scc(G) if len(c) >= 2]
|
| 277 |
+
while components:
|
| 278 |
+
c = components.pop()
|
| 279 |
+
Gc = G.subgraph(c)
|
| 280 |
+
v = next(iter(c))
|
| 281 |
+
if length_bound is None:
|
| 282 |
+
yield from _johnson_cycle_search(Gc, [v])
|
| 283 |
+
else:
|
| 284 |
+
yield from _bounded_cycle_search(Gc, [v], length_bound)
|
| 285 |
+
# delete v after searching G, to make sure we can find v
|
| 286 |
+
G.remove_node(v)
|
| 287 |
+
components.extend(c for c in scc(Gc) if len(c) >= 2)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def _undirected_cycle_search(G, length_bound):
|
| 291 |
+
"""A dispatch function for `simple_cycles` for undirected graphs.
|
| 292 |
+
|
| 293 |
+
We generate all cycles of G through binary partition.
|
| 294 |
+
|
| 295 |
+
1. Pick an edge (u, v) in G which belongs to at least one cycle
|
| 296 |
+
a. Generate all cycles of G which contain the edge (u, v)
|
| 297 |
+
b. Recursively generate all cycles of G \\ (u, v)
|
| 298 |
+
|
| 299 |
+
This is accomplished through the following:
|
| 300 |
+
|
| 301 |
+
1. Compute the biconnected components BCC of G.
|
| 302 |
+
2. Select and remove a biconnected component C from BCC. Select a
|
| 303 |
+
non-tree edge (u, v) of a depth-first search of G[C].
|
| 304 |
+
3. For each (v -> u) path P remaining in G[C] \\ (u, v), yield P.
|
| 305 |
+
4. Add the biconnected components of G[C] \\ (u, v) to BCC.
|
| 306 |
+
|
| 307 |
+
If the parameter length_bound is not None, then step 3 will be limited to simple paths
|
| 308 |
+
of length at most length_bound.
|
| 309 |
+
|
| 310 |
+
Parameters
|
| 311 |
+
----------
|
| 312 |
+
G : NetworkX Graph
|
| 313 |
+
An undirected graph
|
| 314 |
+
|
| 315 |
+
length_bound : int or None
|
| 316 |
+
If length_bound is an int, generate all simple cycles of G with length at most length_bound.
|
| 317 |
+
Otherwise, generate all simple cycles of G.
|
| 318 |
+
|
| 319 |
+
Yields
|
| 320 |
+
------
|
| 321 |
+
list of nodes
|
| 322 |
+
Each cycle is represented by a list of nodes along the cycle.
|
| 323 |
+
"""
|
| 324 |
+
|
| 325 |
+
bcc = nx.biconnected_components
|
| 326 |
+
components = [c for c in bcc(G) if len(c) >= 3]
|
| 327 |
+
while components:
|
| 328 |
+
c = components.pop()
|
| 329 |
+
Gc = G.subgraph(c)
|
| 330 |
+
uv = list(next(iter(Gc.edges)))
|
| 331 |
+
G.remove_edge(*uv)
|
| 332 |
+
# delete (u, v) before searching G, to avoid fake 3-cycles [u, v, u]
|
| 333 |
+
if length_bound is None:
|
| 334 |
+
yield from _johnson_cycle_search(Gc, uv)
|
| 335 |
+
else:
|
| 336 |
+
yield from _bounded_cycle_search(Gc, uv, length_bound)
|
| 337 |
+
components.extend(c for c in bcc(Gc) if len(c) >= 3)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class _NeighborhoodCache(dict):
|
| 341 |
+
"""Very lightweight graph wrapper which caches neighborhoods as list.
|
| 342 |
+
|
| 343 |
+
This dict subclass uses the __missing__ functionality to query graphs for
|
| 344 |
+
their neighborhoods, and store the result as a list. This is used to avoid
|
| 345 |
+
the performance penalty incurred by subgraph views.
|
| 346 |
+
"""
|
| 347 |
+
|
| 348 |
+
def __init__(self, G):
|
| 349 |
+
self.G = G
|
| 350 |
+
|
| 351 |
+
def __missing__(self, v):
|
| 352 |
+
Gv = self[v] = list(self.G[v])
|
| 353 |
+
return Gv
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def _johnson_cycle_search(G, path):
|
| 357 |
+
"""The main loop of the cycle-enumeration algorithm of Johnson.
|
| 358 |
+
|
| 359 |
+
Parameters
|
| 360 |
+
----------
|
| 361 |
+
G : NetworkX Graph or DiGraph
|
| 362 |
+
A graph
|
| 363 |
+
|
| 364 |
+
path : list
|
| 365 |
+
A cycle prefix. All cycles generated will begin with this prefix.
|
| 366 |
+
|
| 367 |
+
Yields
|
| 368 |
+
------
|
| 369 |
+
list of nodes
|
| 370 |
+
Each cycle is represented by a list of nodes along the cycle.
|
| 371 |
+
|
| 372 |
+
References
|
| 373 |
+
----------
|
| 374 |
+
.. [1] Finding all the elementary circuits of a directed graph.
|
| 375 |
+
D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
|
| 376 |
+
https://doi.org/10.1137/0204007
|
| 377 |
+
|
| 378 |
+
"""
|
| 379 |
+
|
| 380 |
+
G = _NeighborhoodCache(G)
|
| 381 |
+
blocked = set(path)
|
| 382 |
+
B = defaultdict(set) # graph portions that yield no elementary circuit
|
| 383 |
+
start = path[0]
|
| 384 |
+
stack = [iter(G[path[-1]])]
|
| 385 |
+
closed = [False]
|
| 386 |
+
while stack:
|
| 387 |
+
nbrs = stack[-1]
|
| 388 |
+
for w in nbrs:
|
| 389 |
+
if w == start:
|
| 390 |
+
yield path[:]
|
| 391 |
+
closed[-1] = True
|
| 392 |
+
elif w not in blocked:
|
| 393 |
+
path.append(w)
|
| 394 |
+
closed.append(False)
|
| 395 |
+
stack.append(iter(G[w]))
|
| 396 |
+
blocked.add(w)
|
| 397 |
+
break
|
| 398 |
+
else: # no more nbrs
|
| 399 |
+
stack.pop()
|
| 400 |
+
v = path.pop()
|
| 401 |
+
if closed.pop():
|
| 402 |
+
if closed:
|
| 403 |
+
closed[-1] = True
|
| 404 |
+
unblock_stack = {v}
|
| 405 |
+
while unblock_stack:
|
| 406 |
+
u = unblock_stack.pop()
|
| 407 |
+
if u in blocked:
|
| 408 |
+
blocked.remove(u)
|
| 409 |
+
unblock_stack.update(B[u])
|
| 410 |
+
B[u].clear()
|
| 411 |
+
else:
|
| 412 |
+
for w in G[v]:
|
| 413 |
+
B[w].add(v)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _bounded_cycle_search(G, path, length_bound):
|
| 417 |
+
"""The main loop of the cycle-enumeration algorithm of Gupta and Suzumura.
|
| 418 |
+
|
| 419 |
+
Parameters
|
| 420 |
+
----------
|
| 421 |
+
G : NetworkX Graph or DiGraph
|
| 422 |
+
A graph
|
| 423 |
+
|
| 424 |
+
path : list
|
| 425 |
+
A cycle prefix. All cycles generated will begin with this prefix.
|
| 426 |
+
|
| 427 |
+
length_bound: int
|
| 428 |
+
A length bound. All cycles generated will have length at most length_bound.
|
| 429 |
+
|
| 430 |
+
Yields
|
| 431 |
+
------
|
| 432 |
+
list of nodes
|
| 433 |
+
Each cycle is represented by a list of nodes along the cycle.
|
| 434 |
+
|
| 435 |
+
References
|
| 436 |
+
----------
|
| 437 |
+
.. [1] Finding All Bounded-Length Simple Cycles in a Directed Graph
|
| 438 |
+
A. Gupta and T. Suzumura https://arxiv.org/abs/2105.10094
|
| 439 |
+
|
| 440 |
+
"""
|
| 441 |
+
G = _NeighborhoodCache(G)
|
| 442 |
+
lock = {v: 0 for v in path}
|
| 443 |
+
B = defaultdict(set)
|
| 444 |
+
start = path[0]
|
| 445 |
+
stack = [iter(G[path[-1]])]
|
| 446 |
+
blen = [length_bound]
|
| 447 |
+
while stack:
|
| 448 |
+
nbrs = stack[-1]
|
| 449 |
+
for w in nbrs:
|
| 450 |
+
if w == start:
|
| 451 |
+
yield path[:]
|
| 452 |
+
blen[-1] = 1
|
| 453 |
+
elif len(path) < lock.get(w, length_bound):
|
| 454 |
+
path.append(w)
|
| 455 |
+
blen.append(length_bound)
|
| 456 |
+
lock[w] = len(path)
|
| 457 |
+
stack.append(iter(G[w]))
|
| 458 |
+
break
|
| 459 |
+
else:
|
| 460 |
+
stack.pop()
|
| 461 |
+
v = path.pop()
|
| 462 |
+
bl = blen.pop()
|
| 463 |
+
if blen:
|
| 464 |
+
blen[-1] = min(blen[-1], bl)
|
| 465 |
+
if bl < length_bound:
|
| 466 |
+
relax_stack = [(bl, v)]
|
| 467 |
+
while relax_stack:
|
| 468 |
+
bl, u = relax_stack.pop()
|
| 469 |
+
if lock.get(u, length_bound) < length_bound - bl + 1:
|
| 470 |
+
lock[u] = length_bound - bl + 1
|
| 471 |
+
relax_stack.extend((bl + 1, w) for w in B[u].difference(path))
|
| 472 |
+
else:
|
| 473 |
+
for w in G[v]:
|
| 474 |
+
B[w].add(v)
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
@nx._dispatchable
|
| 478 |
+
def chordless_cycles(G, length_bound=None):
|
| 479 |
+
"""Find simple chordless cycles of a graph.
|
| 480 |
+
|
| 481 |
+
A `simple cycle` is a closed path where no node appears twice. In a simple
|
| 482 |
+
cycle, a `chord` is an additional edge between two nodes in the cycle. A
|
| 483 |
+
`chordless cycle` is a simple cycle without chords. Said differently, a
|
| 484 |
+
chordless cycle is a cycle C in a graph G where the number of edges in the
|
| 485 |
+
induced graph G[C] is equal to the length of `C`.
|
| 486 |
+
|
| 487 |
+
Note that some care must be taken in the case that G is not a simple graph
|
| 488 |
+
nor a simple digraph. Some authors limit the definition of chordless cycles
|
| 489 |
+
to have a prescribed minimum length; we do not.
|
| 490 |
+
|
| 491 |
+
1. We interpret self-loops to be chordless cycles, except in multigraphs
|
| 492 |
+
with multiple loops in parallel. Likewise, in a chordless cycle of
|
| 493 |
+
length greater than 1, there can be no nodes with self-loops.
|
| 494 |
+
|
| 495 |
+
2. We interpret directed two-cycles to be chordless cycles, except in
|
| 496 |
+
multi-digraphs when any edge in a two-cycle has a parallel copy.
|
| 497 |
+
|
| 498 |
+
3. We interpret parallel pairs of undirected edges as two-cycles, except
|
| 499 |
+
when a third (or more) parallel edge exists between the two nodes.
|
| 500 |
+
|
| 501 |
+
4. Generalizing the above, edges with parallel clones may not occur in
|
| 502 |
+
chordless cycles.
|
| 503 |
+
|
| 504 |
+
In a directed graph, two chordless cycles are distinct if they are not
|
| 505 |
+
cyclic permutations of each other. In an undirected graph, two chordless
|
| 506 |
+
cycles are distinct if they are not cyclic permutations of each other nor of
|
| 507 |
+
the other's reversal.
|
| 508 |
+
|
| 509 |
+
Optionally, the cycles are bounded in length.
|
| 510 |
+
|
| 511 |
+
We use an algorithm strongly inspired by that of Dias et al [1]_. It has
|
| 512 |
+
been modified in the following ways:
|
| 513 |
+
|
| 514 |
+
1. Recursion is avoided, per Python's limitations
|
| 515 |
+
|
| 516 |
+
2. The labeling function is not necessary, because the starting paths
|
| 517 |
+
are chosen (and deleted from the host graph) to prevent multiple
|
| 518 |
+
occurrences of the same path
|
| 519 |
+
|
| 520 |
+
3. The search is optionally bounded at a specified length
|
| 521 |
+
|
| 522 |
+
4. Support for directed graphs is provided by extending cycles along
|
| 523 |
+
forward edges, and blocking nodes along forward and reverse edges
|
| 524 |
+
|
| 525 |
+
5. Support for multigraphs is provided by omitting digons from the set
|
| 526 |
+
of forward edges
|
| 527 |
+
|
| 528 |
+
Parameters
|
| 529 |
+
----------
|
| 530 |
+
G : NetworkX DiGraph
|
| 531 |
+
A directed graph
|
| 532 |
+
|
| 533 |
+
length_bound : int or None, optional (default=None)
|
| 534 |
+
If length_bound is an int, generate all simple cycles of G with length at
|
| 535 |
+
most length_bound. Otherwise, generate all simple cycles of G.
|
| 536 |
+
|
| 537 |
+
Yields
|
| 538 |
+
------
|
| 539 |
+
list of nodes
|
| 540 |
+
Each cycle is represented by a list of nodes along the cycle.
|
| 541 |
+
|
| 542 |
+
Examples
|
| 543 |
+
--------
|
| 544 |
+
>>> sorted(list(nx.chordless_cycles(nx.complete_graph(4))))
|
| 545 |
+
[[1, 0, 2], [1, 0, 3], [2, 0, 3], [2, 1, 3]]
|
| 546 |
+
|
| 547 |
+
Notes
|
| 548 |
+
-----
|
| 549 |
+
When length_bound is None, and the graph is simple, the time complexity is
|
| 550 |
+
$O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ chordless cycles.
|
| 551 |
+
|
| 552 |
+
Raises
|
| 553 |
+
------
|
| 554 |
+
ValueError
|
| 555 |
+
when length_bound < 0.
|
| 556 |
+
|
| 557 |
+
References
|
| 558 |
+
----------
|
| 559 |
+
.. [1] Efficient enumeration of chordless cycles
|
| 560 |
+
E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi
|
| 561 |
+
https://arxiv.org/abs/1309.1051
|
| 562 |
+
|
| 563 |
+
See Also
|
| 564 |
+
--------
|
| 565 |
+
simple_cycles
|
| 566 |
+
"""
|
| 567 |
+
|
| 568 |
+
if length_bound is not None:
|
| 569 |
+
if length_bound == 0:
|
| 570 |
+
return
|
| 571 |
+
elif length_bound < 0:
|
| 572 |
+
raise ValueError("length bound must be non-negative")
|
| 573 |
+
|
| 574 |
+
directed = G.is_directed()
|
| 575 |
+
multigraph = G.is_multigraph()
|
| 576 |
+
|
| 577 |
+
if multigraph:
|
| 578 |
+
yield from ([v] for v, Gv in G.adj.items() if len(Gv.get(v, ())) == 1)
|
| 579 |
+
else:
|
| 580 |
+
yield from ([v] for v, Gv in G.adj.items() if v in Gv)
|
| 581 |
+
|
| 582 |
+
if length_bound is not None and length_bound == 1:
|
| 583 |
+
return
|
| 584 |
+
|
| 585 |
+
# Nodes with loops cannot belong to longer cycles. Let's delete them here.
|
| 586 |
+
# also, we implicitly reduce the multiplicity of edges down to 1 in the case
|
| 587 |
+
# of multiedges.
|
| 588 |
+
if directed:
|
| 589 |
+
F = nx.DiGraph((u, v) for u, Gu in G.adj.items() if u not in Gu for v in Gu)
|
| 590 |
+
B = F.to_undirected(as_view=False)
|
| 591 |
+
else:
|
| 592 |
+
F = nx.Graph((u, v) for u, Gu in G.adj.items() if u not in Gu for v in Gu)
|
| 593 |
+
B = None
|
| 594 |
+
|
| 595 |
+
# If we're given a multigraph, we have a few cases to consider with parallel
|
| 596 |
+
# edges.
|
| 597 |
+
#
|
| 598 |
+
# 1. If we have 2 or more edges in parallel between the nodes (u, v), we
|
| 599 |
+
# must not construct longer cycles along (u, v).
|
| 600 |
+
# 2. If G is not directed, then a pair of parallel edges between (u, v) is a
|
| 601 |
+
# chordless cycle unless there exists a third (or more) parallel edge.
|
| 602 |
+
# 3. If G is directed, then parallel edges do not form cycles, but do
|
| 603 |
+
# preclude back-edges from forming cycles (handled in the next section),
|
| 604 |
+
# Thus, if an edge (u, v) is duplicated and the reverse (v, u) is also
|
| 605 |
+
# present, then we remove both from F.
|
| 606 |
+
#
|
| 607 |
+
# In directed graphs, we need to consider both directions that edges can
|
| 608 |
+
# take, so iterate over all edges (u, v) and possibly (v, u). In undirected
|
| 609 |
+
# graphs, we need to be a little careful to only consider every edge once,
|
| 610 |
+
# so we use a "visited" set to emulate node-order comparisons.
|
| 611 |
+
|
| 612 |
+
if multigraph:
|
| 613 |
+
if not directed:
|
| 614 |
+
B = F.copy()
|
| 615 |
+
visited = set()
|
| 616 |
+
for u, Gu in G.adj.items():
|
| 617 |
+
if directed:
|
| 618 |
+
multiplicity = ((v, len(Guv)) for v, Guv in Gu.items())
|
| 619 |
+
for v, m in multiplicity:
|
| 620 |
+
if m > 1:
|
| 621 |
+
F.remove_edges_from(((u, v), (v, u)))
|
| 622 |
+
else:
|
| 623 |
+
multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited)
|
| 624 |
+
for v, m in multiplicity:
|
| 625 |
+
if m == 2:
|
| 626 |
+
yield [u, v]
|
| 627 |
+
if m > 1:
|
| 628 |
+
F.remove_edge(u, v)
|
| 629 |
+
visited.add(u)
|
| 630 |
+
|
| 631 |
+
# If we're given a directed graphs, we need to think about digons. If we
|
| 632 |
+
# have two edges (u, v) and (v, u), then that's a two-cycle. If either edge
|
| 633 |
+
# was duplicated above, then we removed both from F. So, any digons we find
|
| 634 |
+
# here are chordless. After finding digons, we remove their edges from F
|
| 635 |
+
# to avoid traversing them in the search for chordless cycles.
|
| 636 |
+
if directed:
|
| 637 |
+
for u, Fu in F.adj.items():
|
| 638 |
+
digons = [[u, v] for v in Fu if F.has_edge(v, u)]
|
| 639 |
+
yield from digons
|
| 640 |
+
F.remove_edges_from(digons)
|
| 641 |
+
F.remove_edges_from(e[::-1] for e in digons)
|
| 642 |
+
|
| 643 |
+
if length_bound is not None and length_bound == 2:
|
| 644 |
+
return
|
| 645 |
+
|
| 646 |
+
# Now, we prepare to search for cycles. We have removed all cycles of
|
| 647 |
+
# lengths 1 and 2, so F is a simple graph or simple digraph. We repeatedly
|
| 648 |
+
# separate digraphs into their strongly connected components, and undirected
|
| 649 |
+
# graphs into their biconnected components. For each component, we pick a
|
| 650 |
+
# node v, search for chordless cycles based at each "stem" (u, v, w), and
|
| 651 |
+
# then remove v from that component before separating the graph again.
|
| 652 |
+
if directed:
|
| 653 |
+
separate = nx.strongly_connected_components
|
| 654 |
+
|
| 655 |
+
# Directed stems look like (u -> v -> w), so we use the product of
|
| 656 |
+
# predecessors of v with successors of v.
|
| 657 |
+
def stems(C, v):
|
| 658 |
+
for u, w in product(C.pred[v], C.succ[v]):
|
| 659 |
+
if not G.has_edge(u, w): # omit stems with acyclic chords
|
| 660 |
+
yield [u, v, w], F.has_edge(w, u)
|
| 661 |
+
|
| 662 |
+
else:
|
| 663 |
+
separate = nx.biconnected_components
|
| 664 |
+
|
| 665 |
+
# Undirected stems look like (u ~ v ~ w), but we must not also search
|
| 666 |
+
# (w ~ v ~ u), so we use combinations of v's neighbors of length 2.
|
| 667 |
+
def stems(C, v):
|
| 668 |
+
yield from (([u, v, w], F.has_edge(w, u)) for u, w in combinations(C[v], 2))
|
| 669 |
+
|
| 670 |
+
components = [c for c in separate(F) if len(c) > 2]
|
| 671 |
+
while components:
|
| 672 |
+
c = components.pop()
|
| 673 |
+
v = next(iter(c))
|
| 674 |
+
Fc = F.subgraph(c)
|
| 675 |
+
Fcc = Bcc = None
|
| 676 |
+
for S, is_triangle in stems(Fc, v):
|
| 677 |
+
if is_triangle:
|
| 678 |
+
yield S
|
| 679 |
+
else:
|
| 680 |
+
if Fcc is None:
|
| 681 |
+
Fcc = _NeighborhoodCache(Fc)
|
| 682 |
+
Bcc = Fcc if B is None else _NeighborhoodCache(B.subgraph(c))
|
| 683 |
+
yield from _chordless_cycle_search(Fcc, Bcc, S, length_bound)
|
| 684 |
+
|
| 685 |
+
components.extend(c for c in separate(F.subgraph(c - {v})) if len(c) > 2)
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
def _chordless_cycle_search(F, B, path, length_bound):
|
| 689 |
+
"""The main loop for chordless cycle enumeration.
|
| 690 |
+
|
| 691 |
+
This algorithm is strongly inspired by that of Dias et al [1]_. It has been
|
| 692 |
+
modified in the following ways:
|
| 693 |
+
|
| 694 |
+
1. Recursion is avoided, per Python's limitations
|
| 695 |
+
|
| 696 |
+
2. The labeling function is not necessary, because the starting paths
|
| 697 |
+
are chosen (and deleted from the host graph) to prevent multiple
|
| 698 |
+
occurrences of the same path
|
| 699 |
+
|
| 700 |
+
3. The search is optionally bounded at a specified length
|
| 701 |
+
|
| 702 |
+
4. Support for directed graphs is provided by extending cycles along
|
| 703 |
+
forward edges, and blocking nodes along forward and reverse edges
|
| 704 |
+
|
| 705 |
+
5. Support for multigraphs is provided by omitting digons from the set
|
| 706 |
+
of forward edges
|
| 707 |
+
|
| 708 |
+
Parameters
|
| 709 |
+
----------
|
| 710 |
+
F : _NeighborhoodCache
|
| 711 |
+
A graph of forward edges to follow in constructing cycles
|
| 712 |
+
|
| 713 |
+
B : _NeighborhoodCache
|
| 714 |
+
A graph of blocking edges to prevent the production of chordless cycles
|
| 715 |
+
|
| 716 |
+
path : list
|
| 717 |
+
A cycle prefix. All cycles generated will begin with this prefix.
|
| 718 |
+
|
| 719 |
+
length_bound : int
|
| 720 |
+
A length bound. All cycles generated will have length at most length_bound.
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
Yields
|
| 724 |
+
------
|
| 725 |
+
list of nodes
|
| 726 |
+
Each cycle is represented by a list of nodes along the cycle.
|
| 727 |
+
|
| 728 |
+
References
|
| 729 |
+
----------
|
| 730 |
+
.. [1] Efficient enumeration of chordless cycles
|
| 731 |
+
E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi
|
| 732 |
+
https://arxiv.org/abs/1309.1051
|
| 733 |
+
|
| 734 |
+
"""
|
| 735 |
+
blocked = defaultdict(int)
|
| 736 |
+
target = path[0]
|
| 737 |
+
blocked[path[1]] = 1
|
| 738 |
+
for w in path[1:]:
|
| 739 |
+
for v in B[w]:
|
| 740 |
+
blocked[v] += 1
|
| 741 |
+
|
| 742 |
+
stack = [iter(F[path[2]])]
|
| 743 |
+
while stack:
|
| 744 |
+
nbrs = stack[-1]
|
| 745 |
+
for w in nbrs:
|
| 746 |
+
if blocked[w] == 1 and (length_bound is None or len(path) < length_bound):
|
| 747 |
+
Fw = F[w]
|
| 748 |
+
if target in Fw:
|
| 749 |
+
yield path + [w]
|
| 750 |
+
else:
|
| 751 |
+
Bw = B[w]
|
| 752 |
+
if target in Bw:
|
| 753 |
+
continue
|
| 754 |
+
for v in Bw:
|
| 755 |
+
blocked[v] += 1
|
| 756 |
+
path.append(w)
|
| 757 |
+
stack.append(iter(Fw))
|
| 758 |
+
break
|
| 759 |
+
else:
|
| 760 |
+
stack.pop()
|
| 761 |
+
for v in B[path.pop()]:
|
| 762 |
+
blocked[v] -= 1
|
| 763 |
+
|
| 764 |
+
|
| 765 |
+
@not_implemented_for("undirected")
|
| 766 |
+
@nx._dispatchable(mutates_input=True)
|
| 767 |
+
def recursive_simple_cycles(G):
|
| 768 |
+
"""Find simple cycles (elementary circuits) of a directed graph.
|
| 769 |
+
|
| 770 |
+
A `simple cycle`, or `elementary circuit`, is a closed path where
|
| 771 |
+
no node appears twice. Two elementary circuits are distinct if they
|
| 772 |
+
are not cyclic permutations of each other.
|
| 773 |
+
|
| 774 |
+
This version uses a recursive algorithm to build a list of cycles.
|
| 775 |
+
You should probably use the iterator version called simple_cycles().
|
| 776 |
+
Warning: This recursive version uses lots of RAM!
|
| 777 |
+
It appears in NetworkX for pedagogical value.
|
| 778 |
+
|
| 779 |
+
Parameters
|
| 780 |
+
----------
|
| 781 |
+
G : NetworkX DiGraph
|
| 782 |
+
A directed graph
|
| 783 |
+
|
| 784 |
+
Returns
|
| 785 |
+
-------
|
| 786 |
+
A list of cycles, where each cycle is represented by a list of nodes
|
| 787 |
+
along the cycle.
|
| 788 |
+
|
| 789 |
+
Example:
|
| 790 |
+
|
| 791 |
+
>>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
|
| 792 |
+
>>> G = nx.DiGraph(edges)
|
| 793 |
+
>>> nx.recursive_simple_cycles(G)
|
| 794 |
+
[[0], [2], [0, 1, 2], [0, 2], [1, 2]]
|
| 795 |
+
|
| 796 |
+
Notes
|
| 797 |
+
-----
|
| 798 |
+
The implementation follows pp. 79-80 in [1]_.
|
| 799 |
+
|
| 800 |
+
The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$
|
| 801 |
+
elementary circuits.
|
| 802 |
+
|
| 803 |
+
References
|
| 804 |
+
----------
|
| 805 |
+
.. [1] Finding all the elementary circuits of a directed graph.
|
| 806 |
+
D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
|
| 807 |
+
https://doi.org/10.1137/0204007
|
| 808 |
+
|
| 809 |
+
See Also
|
| 810 |
+
--------
|
| 811 |
+
simple_cycles, cycle_basis
|
| 812 |
+
"""
|
| 813 |
+
|
| 814 |
+
# Jon Olav Vik, 2010-08-09
|
| 815 |
+
def _unblock(thisnode):
|
| 816 |
+
"""Recursively unblock and remove nodes from B[thisnode]."""
|
| 817 |
+
if blocked[thisnode]:
|
| 818 |
+
blocked[thisnode] = False
|
| 819 |
+
while B[thisnode]:
|
| 820 |
+
_unblock(B[thisnode].pop())
|
| 821 |
+
|
| 822 |
+
def circuit(thisnode, startnode, component):
|
| 823 |
+
closed = False # set to True if elementary path is closed
|
| 824 |
+
path.append(thisnode)
|
| 825 |
+
blocked[thisnode] = True
|
| 826 |
+
for nextnode in component[thisnode]: # direct successors of thisnode
|
| 827 |
+
if nextnode == startnode:
|
| 828 |
+
result.append(path[:])
|
| 829 |
+
closed = True
|
| 830 |
+
elif not blocked[nextnode]:
|
| 831 |
+
if circuit(nextnode, startnode, component):
|
| 832 |
+
closed = True
|
| 833 |
+
if closed:
|
| 834 |
+
_unblock(thisnode)
|
| 835 |
+
else:
|
| 836 |
+
for nextnode in component[thisnode]:
|
| 837 |
+
if thisnode not in B[nextnode]: # TODO: use set for speedup?
|
| 838 |
+
B[nextnode].append(thisnode)
|
| 839 |
+
path.pop() # remove thisnode from path
|
| 840 |
+
return closed
|
| 841 |
+
|
| 842 |
+
path = [] # stack of nodes in current path
|
| 843 |
+
blocked = defaultdict(bool) # vertex: blocked from search?
|
| 844 |
+
B = defaultdict(list) # graph portions that yield no elementary circuit
|
| 845 |
+
result = [] # list to accumulate the circuits found
|
| 846 |
+
|
| 847 |
+
# Johnson's algorithm exclude self cycle edges like (v, v)
|
| 848 |
+
# To be backward compatible, we record those cycles in advance
|
| 849 |
+
# and then remove from subG
|
| 850 |
+
for v in G:
|
| 851 |
+
if G.has_edge(v, v):
|
| 852 |
+
result.append([v])
|
| 853 |
+
G.remove_edge(v, v)
|
| 854 |
+
|
| 855 |
+
# Johnson's algorithm requires some ordering of the nodes.
|
| 856 |
+
# They might not be sortable so we assign an arbitrary ordering.
|
| 857 |
+
ordering = dict(zip(G, range(len(G))))
|
| 858 |
+
for s in ordering:
|
| 859 |
+
# Build the subgraph induced by s and following nodes in the ordering
|
| 860 |
+
subgraph = G.subgraph(node for node in G if ordering[node] >= ordering[s])
|
| 861 |
+
# Find the strongly connected component in the subgraph
|
| 862 |
+
# that contains the least node according to the ordering
|
| 863 |
+
strongcomp = nx.strongly_connected_components(subgraph)
|
| 864 |
+
mincomp = min(strongcomp, key=lambda ns: min(ordering[n] for n in ns))
|
| 865 |
+
component = G.subgraph(mincomp)
|
| 866 |
+
if len(component) > 1:
|
| 867 |
+
# smallest node in the component according to the ordering
|
| 868 |
+
startnode = min(component, key=ordering.__getitem__)
|
| 869 |
+
for node in component:
|
| 870 |
+
blocked[node] = False
|
| 871 |
+
B[node][:] = []
|
| 872 |
+
dummy = circuit(startnode, startnode, component)
|
| 873 |
+
return result
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
@nx._dispatchable
|
| 877 |
+
def find_cycle(G, source=None, orientation=None):
|
| 878 |
+
"""Returns a cycle found via depth-first traversal.
|
| 879 |
+
|
| 880 |
+
The cycle is a list of edges indicating the cyclic path.
|
| 881 |
+
Orientation of directed edges is controlled by `orientation`.
|
| 882 |
+
|
| 883 |
+
Parameters
|
| 884 |
+
----------
|
| 885 |
+
G : graph
|
| 886 |
+
A directed/undirected graph/multigraph.
|
| 887 |
+
|
| 888 |
+
source : node, list of nodes
|
| 889 |
+
The node from which the traversal begins. If None, then a source
|
| 890 |
+
is chosen arbitrarily and repeatedly until all edges from each node in
|
| 891 |
+
the graph are searched.
|
| 892 |
+
|
| 893 |
+
orientation : None | 'original' | 'reverse' | 'ignore' (default: None)
|
| 894 |
+
For directed graphs and directed multigraphs, edge traversals need not
|
| 895 |
+
respect the original orientation of the edges.
|
| 896 |
+
When set to 'reverse' every edge is traversed in the reverse direction.
|
| 897 |
+
When set to 'ignore', every edge is treated as undirected.
|
| 898 |
+
When set to 'original', every edge is treated as directed.
|
| 899 |
+
In all three cases, the yielded edge tuples add a last entry to
|
| 900 |
+
indicate the direction in which that edge was traversed.
|
| 901 |
+
If orientation is None, the yielded edge has no direction indicated.
|
| 902 |
+
The direction is respected, but not reported.
|
| 903 |
+
|
| 904 |
+
Returns
|
| 905 |
+
-------
|
| 906 |
+
edges : directed edges
|
| 907 |
+
A list of directed edges indicating the path taken for the loop.
|
| 908 |
+
If no cycle is found, then an exception is raised.
|
| 909 |
+
For graphs, an edge is of the form `(u, v)` where `u` and `v`
|
| 910 |
+
are the tail and head of the edge as determined by the traversal.
|
| 911 |
+
For multigraphs, an edge is of the form `(u, v, key)`, where `key` is
|
| 912 |
+
the key of the edge. When the graph is directed, then `u` and `v`
|
| 913 |
+
are always in the order of the actual directed edge.
|
| 914 |
+
If orientation is not None then the edge tuple is extended to include
|
| 915 |
+
the direction of traversal ('forward' or 'reverse') on that edge.
|
| 916 |
+
|
| 917 |
+
Raises
|
| 918 |
+
------
|
| 919 |
+
NetworkXNoCycle
|
| 920 |
+
If no cycle was found.
|
| 921 |
+
|
| 922 |
+
Examples
|
| 923 |
+
--------
|
| 924 |
+
In this example, we construct a DAG and find, in the first call, that there
|
| 925 |
+
are no directed cycles, and so an exception is raised. In the second call,
|
| 926 |
+
we ignore edge orientations and find that there is an undirected cycle.
|
| 927 |
+
Note that the second call finds a directed cycle while effectively
|
| 928 |
+
traversing an undirected graph, and so, we found an "undirected cycle".
|
| 929 |
+
This means that this DAG structure does not form a directed tree (which
|
| 930 |
+
is also known as a polytree).
|
| 931 |
+
|
| 932 |
+
>>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2)])
|
| 933 |
+
>>> nx.find_cycle(G, orientation="original")
|
| 934 |
+
Traceback (most recent call last):
|
| 935 |
+
...
|
| 936 |
+
networkx.exception.NetworkXNoCycle: No cycle found.
|
| 937 |
+
>>> list(nx.find_cycle(G, orientation="ignore"))
|
| 938 |
+
[(0, 1, 'forward'), (1, 2, 'forward'), (0, 2, 'reverse')]
|
| 939 |
+
|
| 940 |
+
See Also
|
| 941 |
+
--------
|
| 942 |
+
simple_cycles
|
| 943 |
+
"""
|
| 944 |
+
if not G.is_directed() or orientation in (None, "original"):
|
| 945 |
+
|
| 946 |
+
def tailhead(edge):
|
| 947 |
+
return edge[:2]
|
| 948 |
+
|
| 949 |
+
elif orientation == "reverse":
|
| 950 |
+
|
| 951 |
+
def tailhead(edge):
|
| 952 |
+
return edge[1], edge[0]
|
| 953 |
+
|
| 954 |
+
elif orientation == "ignore":
|
| 955 |
+
|
| 956 |
+
def tailhead(edge):
|
| 957 |
+
if edge[-1] == "reverse":
|
| 958 |
+
return edge[1], edge[0]
|
| 959 |
+
return edge[:2]
|
| 960 |
+
|
| 961 |
+
explored = set()
|
| 962 |
+
cycle = []
|
| 963 |
+
final_node = None
|
| 964 |
+
for start_node in G.nbunch_iter(source):
|
| 965 |
+
if start_node in explored:
|
| 966 |
+
# No loop is possible.
|
| 967 |
+
continue
|
| 968 |
+
|
| 969 |
+
edges = []
|
| 970 |
+
# All nodes seen in this iteration of edge_dfs
|
| 971 |
+
seen = {start_node}
|
| 972 |
+
# Nodes in active path.
|
| 973 |
+
active_nodes = {start_node}
|
| 974 |
+
previous_head = None
|
| 975 |
+
|
| 976 |
+
for edge in nx.edge_dfs(G, start_node, orientation):
|
| 977 |
+
# Determine if this edge is a continuation of the active path.
|
| 978 |
+
tail, head = tailhead(edge)
|
| 979 |
+
if head in explored:
|
| 980 |
+
# Then we've already explored it. No loop is possible.
|
| 981 |
+
continue
|
| 982 |
+
if previous_head is not None and tail != previous_head:
|
| 983 |
+
# This edge results from backtracking.
|
| 984 |
+
# Pop until we get a node whose head equals the current tail.
|
| 985 |
+
# So for example, we might have:
|
| 986 |
+
# (0, 1), (1, 2), (2, 3), (1, 4)
|
| 987 |
+
# which must become:
|
| 988 |
+
# (0, 1), (1, 4)
|
| 989 |
+
while True:
|
| 990 |
+
try:
|
| 991 |
+
popped_edge = edges.pop()
|
| 992 |
+
except IndexError:
|
| 993 |
+
edges = []
|
| 994 |
+
active_nodes = {tail}
|
| 995 |
+
break
|
| 996 |
+
else:
|
| 997 |
+
popped_head = tailhead(popped_edge)[1]
|
| 998 |
+
active_nodes.remove(popped_head)
|
| 999 |
+
|
| 1000 |
+
if edges:
|
| 1001 |
+
last_head = tailhead(edges[-1])[1]
|
| 1002 |
+
if tail == last_head:
|
| 1003 |
+
break
|
| 1004 |
+
edges.append(edge)
|
| 1005 |
+
|
| 1006 |
+
if head in active_nodes:
|
| 1007 |
+
# We have a loop!
|
| 1008 |
+
cycle.extend(edges)
|
| 1009 |
+
final_node = head
|
| 1010 |
+
break
|
| 1011 |
+
else:
|
| 1012 |
+
seen.add(head)
|
| 1013 |
+
active_nodes.add(head)
|
| 1014 |
+
previous_head = head
|
| 1015 |
+
|
| 1016 |
+
if cycle:
|
| 1017 |
+
break
|
| 1018 |
+
else:
|
| 1019 |
+
explored.update(seen)
|
| 1020 |
+
|
| 1021 |
+
else:
|
| 1022 |
+
assert len(cycle) == 0
|
| 1023 |
+
raise nx.exception.NetworkXNoCycle("No cycle found.")
|
| 1024 |
+
|
| 1025 |
+
# We now have a list of edges which ends on a cycle.
|
| 1026 |
+
# So we need to remove from the beginning edges that are not relevant.
|
| 1027 |
+
|
| 1028 |
+
for i, edge in enumerate(cycle):
|
| 1029 |
+
tail, head = tailhead(edge)
|
| 1030 |
+
if tail == final_node:
|
| 1031 |
+
break
|
| 1032 |
+
|
| 1033 |
+
return cycle[i:]
|
| 1034 |
+
|
| 1035 |
+
|
| 1036 |
+
@not_implemented_for("directed")
|
| 1037 |
+
@not_implemented_for("multigraph")
|
| 1038 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 1039 |
+
def minimum_cycle_basis(G, weight=None):
|
| 1040 |
+
"""Returns a minimum weight cycle basis for G
|
| 1041 |
+
|
| 1042 |
+
Minimum weight means a cycle basis for which the total weight
|
| 1043 |
+
(length for unweighted graphs) of all the cycles is minimum.
|
| 1044 |
+
|
| 1045 |
+
Parameters
|
| 1046 |
+
----------
|
| 1047 |
+
G : NetworkX Graph
|
| 1048 |
+
weight: string
|
| 1049 |
+
name of the edge attribute to use for edge weights
|
| 1050 |
+
|
| 1051 |
+
Returns
|
| 1052 |
+
-------
|
| 1053 |
+
A list of cycle lists. Each cycle list is a list of nodes
|
| 1054 |
+
which forms a cycle (loop) in G. Note that the nodes are not
|
| 1055 |
+
necessarily returned in a order by which they appear in the cycle
|
| 1056 |
+
|
| 1057 |
+
Examples
|
| 1058 |
+
--------
|
| 1059 |
+
>>> G = nx.Graph()
|
| 1060 |
+
>>> nx.add_cycle(G, [0, 1, 2, 3])
|
| 1061 |
+
>>> nx.add_cycle(G, [0, 3, 4, 5])
|
| 1062 |
+
>>> nx.minimum_cycle_basis(G)
|
| 1063 |
+
[[5, 4, 3, 0], [3, 2, 1, 0]]
|
| 1064 |
+
|
| 1065 |
+
References:
|
| 1066 |
+
[1] Kavitha, Telikepalli, et al. "An O(m^2n) Algorithm for
|
| 1067 |
+
Minimum Cycle Basis of Graphs."
|
| 1068 |
+
http://link.springer.com/article/10.1007/s00453-007-9064-z
|
| 1069 |
+
[2] de Pina, J. 1995. Applications of shortest path methods.
|
| 1070 |
+
Ph.D. thesis, University of Amsterdam, Netherlands
|
| 1071 |
+
|
| 1072 |
+
See Also
|
| 1073 |
+
--------
|
| 1074 |
+
simple_cycles, cycle_basis
|
| 1075 |
+
"""
|
| 1076 |
+
# We first split the graph in connected subgraphs
|
| 1077 |
+
return sum(
|
| 1078 |
+
(_min_cycle_basis(G.subgraph(c), weight) for c in nx.connected_components(G)),
|
| 1079 |
+
[],
|
| 1080 |
+
)
|
| 1081 |
+
|
| 1082 |
+
|
| 1083 |
+
def _min_cycle_basis(G, weight):
|
| 1084 |
+
cb = []
|
| 1085 |
+
# We extract the edges not in a spanning tree. We do not really need a
|
| 1086 |
+
# *minimum* spanning tree. That is why we call the next function with
|
| 1087 |
+
# weight=None. Depending on implementation, it may be faster as well
|
| 1088 |
+
tree_edges = list(nx.minimum_spanning_edges(G, weight=None, data=False))
|
| 1089 |
+
chords = G.edges - tree_edges - {(v, u) for u, v in tree_edges}
|
| 1090 |
+
|
| 1091 |
+
# We maintain a set of vectors orthogonal to sofar found cycles
|
| 1092 |
+
set_orth = [{edge} for edge in chords]
|
| 1093 |
+
while set_orth:
|
| 1094 |
+
base = set_orth.pop()
|
| 1095 |
+
# kth cycle is "parallel" to kth vector in set_orth
|
| 1096 |
+
cycle_edges = _min_cycle(G, base, weight)
|
| 1097 |
+
cb.append([v for u, v in cycle_edges])
|
| 1098 |
+
|
| 1099 |
+
# now update set_orth so that k+1,k+2... th elements are
|
| 1100 |
+
# orthogonal to the newly found cycle, as per [p. 336, 1]
|
| 1101 |
+
set_orth = [
|
| 1102 |
+
(
|
| 1103 |
+
{e for e in orth if e not in base if e[::-1] not in base}
|
| 1104 |
+
| {e for e in base if e not in orth if e[::-1] not in orth}
|
| 1105 |
+
)
|
| 1106 |
+
if sum((e in orth or e[::-1] in orth) for e in cycle_edges) % 2
|
| 1107 |
+
else orth
|
| 1108 |
+
for orth in set_orth
|
| 1109 |
+
]
|
| 1110 |
+
return cb
|
| 1111 |
+
|
| 1112 |
+
|
| 1113 |
+
def _min_cycle(G, orth, weight):
|
| 1114 |
+
"""
|
| 1115 |
+
Computes the minimum weight cycle in G,
|
| 1116 |
+
orthogonal to the vector orth as per [p. 338, 1]
|
| 1117 |
+
Use (u, 1) to indicate the lifted copy of u (denoted u' in paper).
|
| 1118 |
+
"""
|
| 1119 |
+
Gi = nx.Graph()
|
| 1120 |
+
|
| 1121 |
+
# Add 2 copies of each edge in G to Gi.
|
| 1122 |
+
# If edge is in orth, add cross edge; otherwise in-plane edge
|
| 1123 |
+
for u, v, wt in G.edges(data=weight, default=1):
|
| 1124 |
+
if (u, v) in orth or (v, u) in orth:
|
| 1125 |
+
Gi.add_edges_from([(u, (v, 1)), ((u, 1), v)], Gi_weight=wt)
|
| 1126 |
+
else:
|
| 1127 |
+
Gi.add_edges_from([(u, v), ((u, 1), (v, 1))], Gi_weight=wt)
|
| 1128 |
+
|
| 1129 |
+
# find the shortest length in Gi between n and (n, 1) for each n
|
| 1130 |
+
# Note: Use "Gi_weight" for name of weight attribute
|
| 1131 |
+
spl = nx.shortest_path_length
|
| 1132 |
+
lift = {n: spl(Gi, source=n, target=(n, 1), weight="Gi_weight") for n in G}
|
| 1133 |
+
|
| 1134 |
+
# Now compute that short path in Gi, which translates to a cycle in G
|
| 1135 |
+
start = min(lift, key=lift.get)
|
| 1136 |
+
end = (start, 1)
|
| 1137 |
+
min_path_i = nx.shortest_path(Gi, source=start, target=end, weight="Gi_weight")
|
| 1138 |
+
|
| 1139 |
+
# Now we obtain the actual path, re-map nodes in Gi to those in G
|
| 1140 |
+
min_path = [n if n in G else n[0] for n in min_path_i]
|
| 1141 |
+
|
| 1142 |
+
# Now remove the edges that occur two times
|
| 1143 |
+
# two passes: flag which edges get kept, then build it
|
| 1144 |
+
edgelist = list(pairwise(min_path))
|
| 1145 |
+
edgeset = set()
|
| 1146 |
+
for e in edgelist:
|
| 1147 |
+
if e in edgeset:
|
| 1148 |
+
edgeset.remove(e)
|
| 1149 |
+
elif e[::-1] in edgeset:
|
| 1150 |
+
edgeset.remove(e[::-1])
|
| 1151 |
+
else:
|
| 1152 |
+
edgeset.add(e)
|
| 1153 |
+
|
| 1154 |
+
min_edgelist = []
|
| 1155 |
+
for e in edgelist:
|
| 1156 |
+
if e in edgeset:
|
| 1157 |
+
min_edgelist.append(e)
|
| 1158 |
+
edgeset.remove(e)
|
| 1159 |
+
elif e[::-1] in edgeset:
|
| 1160 |
+
min_edgelist.append(e[::-1])
|
| 1161 |
+
edgeset.remove(e[::-1])
|
| 1162 |
+
|
| 1163 |
+
return min_edgelist
|
| 1164 |
+
|
| 1165 |
+
|
| 1166 |
+
@not_implemented_for("directed")
|
| 1167 |
+
@not_implemented_for("multigraph")
|
| 1168 |
+
@nx._dispatchable
|
| 1169 |
+
def girth(G):
|
| 1170 |
+
"""Returns the girth of the graph.
|
| 1171 |
+
|
| 1172 |
+
The girth of a graph is the length of its shortest cycle, or infinity if
|
| 1173 |
+
the graph is acyclic. The algorithm follows the description given on the
|
| 1174 |
+
Wikipedia page [1]_, and runs in time O(mn) on a graph with m edges and n
|
| 1175 |
+
nodes.
|
| 1176 |
+
|
| 1177 |
+
Parameters
|
| 1178 |
+
----------
|
| 1179 |
+
G : NetworkX Graph
|
| 1180 |
+
|
| 1181 |
+
Returns
|
| 1182 |
+
-------
|
| 1183 |
+
int or math.inf
|
| 1184 |
+
|
| 1185 |
+
Examples
|
| 1186 |
+
--------
|
| 1187 |
+
All examples below (except P_5) can easily be checked using Wikipedia,
|
| 1188 |
+
which has a page for each of these famous graphs.
|
| 1189 |
+
|
| 1190 |
+
>>> nx.girth(nx.chvatal_graph())
|
| 1191 |
+
4
|
| 1192 |
+
>>> nx.girth(nx.tutte_graph())
|
| 1193 |
+
4
|
| 1194 |
+
>>> nx.girth(nx.petersen_graph())
|
| 1195 |
+
5
|
| 1196 |
+
>>> nx.girth(nx.heawood_graph())
|
| 1197 |
+
6
|
| 1198 |
+
>>> nx.girth(nx.pappus_graph())
|
| 1199 |
+
6
|
| 1200 |
+
>>> nx.girth(nx.path_graph(5))
|
| 1201 |
+
inf
|
| 1202 |
+
|
| 1203 |
+
References
|
| 1204 |
+
----------
|
| 1205 |
+
.. [1] `Wikipedia: Girth <https://en.wikipedia.org/wiki/Girth_(graph_theory)>`_
|
| 1206 |
+
|
| 1207 |
+
"""
|
| 1208 |
+
girth = depth_limit = inf
|
| 1209 |
+
tree_edge = nx.algorithms.traversal.breadth_first_search.TREE_EDGE
|
| 1210 |
+
level_edge = nx.algorithms.traversal.breadth_first_search.LEVEL_EDGE
|
| 1211 |
+
for n in G:
|
| 1212 |
+
# run a BFS from source n, keeping track of distances; since we want
|
| 1213 |
+
# the shortest cycle, no need to explore beyond the current minimum length
|
| 1214 |
+
depth = {n: 0}
|
| 1215 |
+
for u, v, label in nx.bfs_labeled_edges(G, n):
|
| 1216 |
+
du = depth[u]
|
| 1217 |
+
if du > depth_limit:
|
| 1218 |
+
break
|
| 1219 |
+
if label is tree_edge:
|
| 1220 |
+
depth[v] = du + 1
|
| 1221 |
+
else:
|
| 1222 |
+
# if (u, v) is a level edge, the length is du + du + 1 (odd)
|
| 1223 |
+
# otherwise, it's a forward edge; length is du + (du + 1) + 1 (even)
|
| 1224 |
+
delta = label is level_edge
|
| 1225 |
+
length = du + du + 2 - delta
|
| 1226 |
+
if length < girth:
|
| 1227 |
+
girth = length
|
| 1228 |
+
depth_limit = du - delta
|
| 1229 |
+
|
| 1230 |
+
return girth
|
.venv/lib/python3.11/site-packages/networkx/algorithms/d_separation.py
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Algorithm for testing d-separation in DAGs.
|
| 3 |
+
|
| 4 |
+
*d-separation* is a test for conditional independence in probability
|
| 5 |
+
distributions that can be factorized using DAGs. It is a purely
|
| 6 |
+
graphical test that uses the underlying graph and makes no reference
|
| 7 |
+
to the actual distribution parameters. See [1]_ for a formal
|
| 8 |
+
definition.
|
| 9 |
+
|
| 10 |
+
The implementation is based on the conceptually simple linear time
|
| 11 |
+
algorithm presented in [2]_. Refer to [3]_, [4]_ for a couple of
|
| 12 |
+
alternative algorithms.
|
| 13 |
+
|
| 14 |
+
The functional interface in NetworkX consists of three functions:
|
| 15 |
+
|
| 16 |
+
- `find_minimal_d_separator` returns a minimal d-separator set ``z``.
|
| 17 |
+
That is, removing any node or nodes from it makes it no longer a d-separator.
|
| 18 |
+
- `is_d_separator` checks if a given set is a d-separator.
|
| 19 |
+
- `is_minimal_d_separator` checks if a given set is a minimal d-separator.
|
| 20 |
+
|
| 21 |
+
D-separators
|
| 22 |
+
------------
|
| 23 |
+
|
| 24 |
+
Here, we provide a brief overview of d-separation and related concepts that
|
| 25 |
+
are relevant for understanding it:
|
| 26 |
+
|
| 27 |
+
The ideas of d-separation and d-connection relate to paths being open or blocked.
|
| 28 |
+
|
| 29 |
+
- A "path" is a sequence of nodes connected in order by edges. Unlike for most
|
| 30 |
+
graph theory analysis, the direction of the edges is ignored. Thus the path
|
| 31 |
+
can be thought of as a traditional path on the undirected version of the graph.
|
| 32 |
+
- A "candidate d-separator" ``z`` is a set of nodes being considered as
|
| 33 |
+
possibly blocking all paths between two prescribed sets ``x`` and ``y`` of nodes.
|
| 34 |
+
We refer to each node in the candidate d-separator as "known".
|
| 35 |
+
- A "collider" node on a path is a node that is a successor of its two neighbor
|
| 36 |
+
nodes on the path. That is, ``c`` is a collider if the edge directions
|
| 37 |
+
along the path look like ``... u -> c <- v ...``.
|
| 38 |
+
- If a collider node or any of its descendants are "known", the collider
|
| 39 |
+
is called an "open collider". Otherwise it is a "blocking collider".
|
| 40 |
+
- Any path can be "blocked" in two ways. If the path contains a "known" node
|
| 41 |
+
that is not a collider, the path is blocked. Also, if the path contains a
|
| 42 |
+
collider that is not a "known" node, the path is blocked.
|
| 43 |
+
- A path is "open" if it is not blocked. That is, it is open if every node is
|
| 44 |
+
either an open collider or not a "known". Said another way, every
|
| 45 |
+
"known" in the path is a collider and every collider is open (has a
|
| 46 |
+
"known" as a inclusive descendant). The concept of "open path" is meant to
|
| 47 |
+
demonstrate a probabilistic conditional dependence between two nodes given
|
| 48 |
+
prescribed knowledge ("known" nodes).
|
| 49 |
+
- Two sets ``x`` and ``y`` of nodes are "d-separated" by a set of nodes ``z``
|
| 50 |
+
if all paths between nodes in ``x`` and nodes in ``y`` are blocked. That is,
|
| 51 |
+
if there are no open paths from any node in ``x`` to any node in ``y``.
|
| 52 |
+
Such a set ``z`` is a "d-separator" of ``x`` and ``y``.
|
| 53 |
+
- A "minimal d-separator" is a d-separator ``z`` for which no node or subset
|
| 54 |
+
of nodes can be removed with it still being a d-separator.
|
| 55 |
+
|
| 56 |
+
The d-separator blocks some paths between ``x`` and ``y`` but opens others.
|
| 57 |
+
Nodes in the d-separator block paths if the nodes are not colliders.
|
| 58 |
+
But if a collider or its descendant nodes are in the d-separation set, the
|
| 59 |
+
colliders are open, allowing a path through that collider.
|
| 60 |
+
|
| 61 |
+
Illustration of D-separation with examples
|
| 62 |
+
------------------------------------------
|
| 63 |
+
|
| 64 |
+
A pair of two nodes, ``u`` and ``v``, are d-connected if there is a path
|
| 65 |
+
from ``u`` to ``v`` that is not blocked. That means, there is an open
|
| 66 |
+
path from ``u`` to ``v``.
|
| 67 |
+
|
| 68 |
+
For example, if the d-separating set is the empty set, then the following paths are
|
| 69 |
+
open between ``u`` and ``v``:
|
| 70 |
+
|
| 71 |
+
- u <- n -> v
|
| 72 |
+
- u -> w -> ... -> n -> v
|
| 73 |
+
|
| 74 |
+
If on the other hand, ``n`` is in the d-separating set, then ``n`` blocks
|
| 75 |
+
those paths between ``u`` and ``v``.
|
| 76 |
+
|
| 77 |
+
Colliders block a path if they and their descendants are not included
|
| 78 |
+
in the d-separating set. An example of a path that is blocked when the
|
| 79 |
+
d-separating set is empty is:
|
| 80 |
+
|
| 81 |
+
- u -> w -> ... -> n <- v
|
| 82 |
+
|
| 83 |
+
The node ``n`` is a collider in this path and is not in the d-separating set.
|
| 84 |
+
So ``n`` blocks this path. However, if ``n`` or a descendant of ``n`` is
|
| 85 |
+
included in the d-separating set, then the path through the collider
|
| 86 |
+
at ``n`` (... -> n <- ...) is "open".
|
| 87 |
+
|
| 88 |
+
D-separation is concerned with blocking all paths between nodes from ``x`` to ``y``.
|
| 89 |
+
A d-separating set between ``x`` and ``y`` is one where all paths are blocked.
|
| 90 |
+
|
| 91 |
+
D-separation and its applications in probability
|
| 92 |
+
------------------------------------------------
|
| 93 |
+
|
| 94 |
+
D-separation is commonly used in probabilistic causal-graph models. D-separation
|
| 95 |
+
connects the idea of probabilistic "dependence" with separation in a graph. If
|
| 96 |
+
one assumes the causal Markov condition [5]_, (every node is conditionally
|
| 97 |
+
independent of its non-descendants, given its parents) then d-separation implies
|
| 98 |
+
conditional independence in probability distributions.
|
| 99 |
+
Symmetrically, d-connection implies dependence.
|
| 100 |
+
|
| 101 |
+
The intuition is as follows. The edges on a causal graph indicate which nodes
|
| 102 |
+
influence the outcome of other nodes directly. An edge from u to v
|
| 103 |
+
implies that the outcome of event ``u`` influences the probabilities for
|
| 104 |
+
the outcome of event ``v``. Certainly knowing ``u`` changes predictions for ``v``.
|
| 105 |
+
But also knowing ``v`` changes predictions for ``u``. The outcomes are dependent.
|
| 106 |
+
Furthermore, an edge from ``v`` to ``w`` would mean that ``w`` and ``v`` are dependent
|
| 107 |
+
and thus that ``u`` could indirectly influence ``w``.
|
| 108 |
+
|
| 109 |
+
Without any knowledge about the system (candidate d-separating set is empty)
|
| 110 |
+
a causal graph ``u -> v -> w`` allows all three nodes to be dependent. But
|
| 111 |
+
if we know the outcome of ``v``, the conditional probabilities of outcomes for
|
| 112 |
+
``u`` and ``w`` are independent of each other. That is, once we know the outcome
|
| 113 |
+
for ```v`, the probabilities for ``w`` do not depend on the outcome for ``u``.
|
| 114 |
+
This is the idea behind ``v`` blocking the path if it is "known" (in the candidate
|
| 115 |
+
d-separating set).
|
| 116 |
+
|
| 117 |
+
The same argument works whether the direction of the edges are both
|
| 118 |
+
left-going and when both arrows head out from the middle. Having a "known"
|
| 119 |
+
node on a path blocks the collider-free path because those relationships
|
| 120 |
+
make the conditional probabilities independent.
|
| 121 |
+
|
| 122 |
+
The direction of the causal edges does impact dependence precisely in the
|
| 123 |
+
case of a collider e.g. ``u -> v <- w``. In that situation, both ``u`` and ``w``
|
| 124 |
+
influence ``v```. But they do not directly influence each other. So without any
|
| 125 |
+
knowledge of any outcomes, ``u`` and ``w`` are independent. That is the idea behind
|
| 126 |
+
colliders blocking the path. But, if ``v`` is known, the conditional probabilities
|
| 127 |
+
of ``u`` and ``w`` can be dependent. This is the heart of Berkson's Paradox [6]_.
|
| 128 |
+
For example, suppose ``u`` and ``w`` are boolean events (they either happen or do not)
|
| 129 |
+
and ``v`` represents the outcome "at least one of ``u`` and ``w`` occur". Then knowing
|
| 130 |
+
``v`` is true makes the conditional probabilities of ``u`` and ``w`` dependent.
|
| 131 |
+
Essentially, knowing that at least one of them is true raises the probability of
|
| 132 |
+
each. But further knowledge that ``w`` is true (or false) change the conditional
|
| 133 |
+
probability of ``u`` to either the original value or 1. So the conditional
|
| 134 |
+
probability of ``u`` depends on the outcome of ``w`` even though there is no
|
| 135 |
+
causal relationship between them. When a collider is known, dependence can
|
| 136 |
+
occur across paths through that collider. This is the reason open colliders
|
| 137 |
+
do not block paths.
|
| 138 |
+
|
| 139 |
+
Furthermore, even if ``v`` is not "known", if one of its descendants is "known"
|
| 140 |
+
we can use that information to know more about ``v`` which again makes
|
| 141 |
+
``u`` and ``w`` potentially dependent. Suppose the chance of ``n`` occurring
|
| 142 |
+
is much higher when ``v`` occurs ("at least one of ``u`` and ``w`` occur").
|
| 143 |
+
Then if we know ``n`` occurred, it is more likely that ``v`` occurred and that
|
| 144 |
+
makes the chance of ``u`` and ``w`` dependent. This is the idea behind why
|
| 145 |
+
a collider does no block a path if any descendant of the collider is "known".
|
| 146 |
+
|
| 147 |
+
When two sets of nodes ``x`` and ``y`` are d-separated by a set ``z``,
|
| 148 |
+
it means that given the outcomes of the nodes in ``z``, the probabilities
|
| 149 |
+
of outcomes of the nodes in ``x`` are independent of the outcomes of the
|
| 150 |
+
nodes in ``y`` and vice versa.
|
| 151 |
+
|
| 152 |
+
Examples
|
| 153 |
+
--------
|
| 154 |
+
A Hidden Markov Model with 5 observed states and 5 hidden states
|
| 155 |
+
where the hidden states have causal relationships resulting in
|
| 156 |
+
a path results in the following causal network. We check that
|
| 157 |
+
early states along the path are separated from late state in
|
| 158 |
+
the path by the d-separator of the middle hidden state.
|
| 159 |
+
Thus if we condition on the middle hidden state, the early
|
| 160 |
+
state probabilities are independent of the late state outcomes.
|
| 161 |
+
|
| 162 |
+
>>> G = nx.DiGraph()
|
| 163 |
+
>>> G.add_edges_from(
|
| 164 |
+
... [
|
| 165 |
+
... ("H1", "H2"),
|
| 166 |
+
... ("H2", "H3"),
|
| 167 |
+
... ("H3", "H4"),
|
| 168 |
+
... ("H4", "H5"),
|
| 169 |
+
... ("H1", "O1"),
|
| 170 |
+
... ("H2", "O2"),
|
| 171 |
+
... ("H3", "O3"),
|
| 172 |
+
... ("H4", "O4"),
|
| 173 |
+
... ("H5", "O5"),
|
| 174 |
+
... ]
|
| 175 |
+
... )
|
| 176 |
+
>>> x, y, z = ({"H1", "O1"}, {"H5", "O5"}, {"H3"})
|
| 177 |
+
>>> nx.is_d_separator(G, x, y, z)
|
| 178 |
+
True
|
| 179 |
+
>>> nx.is_minimal_d_separator(G, x, y, z)
|
| 180 |
+
True
|
| 181 |
+
>>> nx.is_minimal_d_separator(G, x, y, z | {"O3"})
|
| 182 |
+
False
|
| 183 |
+
>>> z = nx.find_minimal_d_separator(G, x | y, {"O2", "O3", "O4"})
|
| 184 |
+
>>> z == {"H2", "H4"}
|
| 185 |
+
True
|
| 186 |
+
|
| 187 |
+
If no minimal_d_separator exists, `None` is returned
|
| 188 |
+
|
| 189 |
+
>>> other_z = nx.find_minimal_d_separator(G, x | y, {"H2", "H3"})
|
| 190 |
+
>>> other_z is None
|
| 191 |
+
True
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
References
|
| 195 |
+
----------
|
| 196 |
+
|
| 197 |
+
.. [1] Pearl, J. (2009). Causality. Cambridge: Cambridge University Press.
|
| 198 |
+
|
| 199 |
+
.. [2] Darwiche, A. (2009). Modeling and reasoning with Bayesian networks.
|
| 200 |
+
Cambridge: Cambridge University Press.
|
| 201 |
+
|
| 202 |
+
.. [3] Shachter, Ross D. "Bayes-ball: The rational pastime (for
|
| 203 |
+
determining irrelevance and requisite information in belief networks
|
| 204 |
+
and influence diagrams)." In Proceedings of the Fourteenth Conference
|
| 205 |
+
on Uncertainty in Artificial Intelligence (UAI), (pp. 480–487). 1998.
|
| 206 |
+
|
| 207 |
+
.. [4] Koller, D., & Friedman, N. (2009).
|
| 208 |
+
Probabilistic graphical models: principles and techniques. The MIT Press.
|
| 209 |
+
|
| 210 |
+
.. [5] https://en.wikipedia.org/wiki/Causal_Markov_condition
|
| 211 |
+
|
| 212 |
+
.. [6] https://en.wikipedia.org/wiki/Berkson%27s_paradox
|
| 213 |
+
|
| 214 |
+
"""
|
| 215 |
+
|
| 216 |
+
from collections import deque
|
| 217 |
+
from itertools import chain
|
| 218 |
+
|
| 219 |
+
import networkx as nx
|
| 220 |
+
from networkx.utils import UnionFind, not_implemented_for
|
| 221 |
+
|
| 222 |
+
__all__ = [
|
| 223 |
+
"is_d_separator",
|
| 224 |
+
"is_minimal_d_separator",
|
| 225 |
+
"find_minimal_d_separator",
|
| 226 |
+
"d_separated",
|
| 227 |
+
"minimal_d_separator",
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
@not_implemented_for("undirected")
|
| 232 |
+
@nx._dispatchable
|
| 233 |
+
def is_d_separator(G, x, y, z):
|
| 234 |
+
"""Return whether node sets `x` and `y` are d-separated by `z`.
|
| 235 |
+
|
| 236 |
+
Parameters
|
| 237 |
+
----------
|
| 238 |
+
G : nx.DiGraph
|
| 239 |
+
A NetworkX DAG.
|
| 240 |
+
|
| 241 |
+
x : node or set of nodes
|
| 242 |
+
First node or set of nodes in `G`.
|
| 243 |
+
|
| 244 |
+
y : node or set of nodes
|
| 245 |
+
Second node or set of nodes in `G`.
|
| 246 |
+
|
| 247 |
+
z : node or set of nodes
|
| 248 |
+
Potential separator (set of conditioning nodes in `G`). Can be empty set.
|
| 249 |
+
|
| 250 |
+
Returns
|
| 251 |
+
-------
|
| 252 |
+
b : bool
|
| 253 |
+
A boolean that is true if `x` is d-separated from `y` given `z` in `G`.
|
| 254 |
+
|
| 255 |
+
Raises
|
| 256 |
+
------
|
| 257 |
+
NetworkXError
|
| 258 |
+
The *d-separation* test is commonly used on disjoint sets of
|
| 259 |
+
nodes in acyclic directed graphs. Accordingly, the algorithm
|
| 260 |
+
raises a :exc:`NetworkXError` if the node sets are not
|
| 261 |
+
disjoint or if the input graph is not a DAG.
|
| 262 |
+
|
| 263 |
+
NodeNotFound
|
| 264 |
+
If any of the input nodes are not found in the graph,
|
| 265 |
+
a :exc:`NodeNotFound` exception is raised
|
| 266 |
+
|
| 267 |
+
Notes
|
| 268 |
+
-----
|
| 269 |
+
A d-separating set in a DAG is a set of nodes that
|
| 270 |
+
blocks all paths between the two sets. Nodes in `z`
|
| 271 |
+
block a path if they are part of the path and are not a collider,
|
| 272 |
+
or a descendant of a collider. Also colliders that are not in `z`
|
| 273 |
+
block a path. A collider structure along a path
|
| 274 |
+
is ``... -> c <- ...`` where ``c`` is the collider node.
|
| 275 |
+
|
| 276 |
+
https://en.wikipedia.org/wiki/Bayesian_network#d-separation
|
| 277 |
+
"""
|
| 278 |
+
try:
|
| 279 |
+
x = {x} if x in G else x
|
| 280 |
+
y = {y} if y in G else y
|
| 281 |
+
z = {z} if z in G else z
|
| 282 |
+
|
| 283 |
+
intersection = x & y or x & z or y & z
|
| 284 |
+
if intersection:
|
| 285 |
+
raise nx.NetworkXError(
|
| 286 |
+
f"The sets are not disjoint, with intersection {intersection}"
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
set_v = x | y | z
|
| 290 |
+
if set_v - G.nodes:
|
| 291 |
+
raise nx.NodeNotFound(f"The node(s) {set_v - G.nodes} are not found in G")
|
| 292 |
+
except TypeError:
|
| 293 |
+
raise nx.NodeNotFound("One of x, y, or z is not a node or a set of nodes in G")
|
| 294 |
+
|
| 295 |
+
if not nx.is_directed_acyclic_graph(G):
|
| 296 |
+
raise nx.NetworkXError("graph should be directed acyclic")
|
| 297 |
+
|
| 298 |
+
# contains -> and <-> edges from starting node T
|
| 299 |
+
forward_deque = deque([])
|
| 300 |
+
forward_visited = set()
|
| 301 |
+
|
| 302 |
+
# contains <- and - edges from starting node T
|
| 303 |
+
backward_deque = deque(x)
|
| 304 |
+
backward_visited = set()
|
| 305 |
+
|
| 306 |
+
ancestors_or_z = set().union(*[nx.ancestors(G, node) for node in x]) | z | x
|
| 307 |
+
|
| 308 |
+
while forward_deque or backward_deque:
|
| 309 |
+
if backward_deque:
|
| 310 |
+
node = backward_deque.popleft()
|
| 311 |
+
backward_visited.add(node)
|
| 312 |
+
if node in y:
|
| 313 |
+
return False
|
| 314 |
+
if node in z:
|
| 315 |
+
continue
|
| 316 |
+
|
| 317 |
+
# add <- edges to backward deque
|
| 318 |
+
backward_deque.extend(G.pred[node].keys() - backward_visited)
|
| 319 |
+
# add -> edges to forward deque
|
| 320 |
+
forward_deque.extend(G.succ[node].keys() - forward_visited)
|
| 321 |
+
|
| 322 |
+
if forward_deque:
|
| 323 |
+
node = forward_deque.popleft()
|
| 324 |
+
forward_visited.add(node)
|
| 325 |
+
if node in y:
|
| 326 |
+
return False
|
| 327 |
+
|
| 328 |
+
# Consider if -> node <- is opened due to ancestor of node in z
|
| 329 |
+
if node in ancestors_or_z:
|
| 330 |
+
# add <- edges to backward deque
|
| 331 |
+
backward_deque.extend(G.pred[node].keys() - backward_visited)
|
| 332 |
+
if node not in z:
|
| 333 |
+
# add -> edges to forward deque
|
| 334 |
+
forward_deque.extend(G.succ[node].keys() - forward_visited)
|
| 335 |
+
|
| 336 |
+
return True
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
@not_implemented_for("undirected")
|
| 340 |
+
@nx._dispatchable
|
| 341 |
+
def find_minimal_d_separator(G, x, y, *, included=None, restricted=None):
|
| 342 |
+
"""Returns a minimal d-separating set between `x` and `y` if possible
|
| 343 |
+
|
| 344 |
+
A d-separating set in a DAG is a set of nodes that blocks all
|
| 345 |
+
paths between the two sets of nodes, `x` and `y`. This function
|
| 346 |
+
constructs a d-separating set that is "minimal", meaning no nodes can
|
| 347 |
+
be removed without it losing the d-separating property for `x` and `y`.
|
| 348 |
+
If no d-separating sets exist for `x` and `y`, this returns `None`.
|
| 349 |
+
|
| 350 |
+
In a DAG there may be more than one minimal d-separator between two
|
| 351 |
+
sets of nodes. Minimal d-separators are not always unique. This function
|
| 352 |
+
returns one minimal d-separator, or `None` if no d-separator exists.
|
| 353 |
+
|
| 354 |
+
Uses the algorithm presented in [1]_. The complexity of the algorithm
|
| 355 |
+
is :math:`O(m)`, where :math:`m` stands for the number of edges in
|
| 356 |
+
the subgraph of G consisting of only the ancestors of `x` and `y`.
|
| 357 |
+
For full details, see [1]_.
|
| 358 |
+
|
| 359 |
+
Parameters
|
| 360 |
+
----------
|
| 361 |
+
G : graph
|
| 362 |
+
A networkx DAG.
|
| 363 |
+
x : set | node
|
| 364 |
+
A node or set of nodes in the graph.
|
| 365 |
+
y : set | node
|
| 366 |
+
A node or set of nodes in the graph.
|
| 367 |
+
included : set | node | None
|
| 368 |
+
A node or set of nodes which must be included in the found separating set,
|
| 369 |
+
default is None, which means the empty set.
|
| 370 |
+
restricted : set | node | None
|
| 371 |
+
Restricted node or set of nodes to consider. Only these nodes can be in
|
| 372 |
+
the found separating set, default is None meaning all nodes in ``G``.
|
| 373 |
+
|
| 374 |
+
Returns
|
| 375 |
+
-------
|
| 376 |
+
z : set | None
|
| 377 |
+
The minimal d-separating set, if at least one d-separating set exists,
|
| 378 |
+
otherwise None.
|
| 379 |
+
|
| 380 |
+
Raises
|
| 381 |
+
------
|
| 382 |
+
NetworkXError
|
| 383 |
+
Raises a :exc:`NetworkXError` if the input graph is not a DAG
|
| 384 |
+
or if node sets `x`, `y`, and `included` are not disjoint.
|
| 385 |
+
|
| 386 |
+
NodeNotFound
|
| 387 |
+
If any of the input nodes are not found in the graph,
|
| 388 |
+
a :exc:`NodeNotFound` exception is raised.
|
| 389 |
+
|
| 390 |
+
References
|
| 391 |
+
----------
|
| 392 |
+
.. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
|
| 393 |
+
minimal d-separators in linear time and applications." In
|
| 394 |
+
Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
|
| 395 |
+
"""
|
| 396 |
+
if not nx.is_directed_acyclic_graph(G):
|
| 397 |
+
raise nx.NetworkXError("graph should be directed acyclic")
|
| 398 |
+
|
| 399 |
+
try:
|
| 400 |
+
x = {x} if x in G else x
|
| 401 |
+
y = {y} if y in G else y
|
| 402 |
+
|
| 403 |
+
if included is None:
|
| 404 |
+
included = set()
|
| 405 |
+
elif included in G:
|
| 406 |
+
included = {included}
|
| 407 |
+
|
| 408 |
+
if restricted is None:
|
| 409 |
+
restricted = set(G)
|
| 410 |
+
elif restricted in G:
|
| 411 |
+
restricted = {restricted}
|
| 412 |
+
|
| 413 |
+
set_y = x | y | included | restricted
|
| 414 |
+
if set_y - G.nodes:
|
| 415 |
+
raise nx.NodeNotFound(f"The node(s) {set_y - G.nodes} are not found in G")
|
| 416 |
+
except TypeError:
|
| 417 |
+
raise nx.NodeNotFound(
|
| 418 |
+
"One of x, y, included or restricted is not a node or set of nodes in G"
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
if not included <= restricted:
|
| 422 |
+
raise nx.NetworkXError(
|
| 423 |
+
f"Included nodes {included} must be in restricted nodes {restricted}"
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
intersection = x & y or x & included or y & included
|
| 427 |
+
if intersection:
|
| 428 |
+
raise nx.NetworkXError(
|
| 429 |
+
f"The sets x, y, included are not disjoint. Overlap: {intersection}"
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
nodeset = x | y | included
|
| 433 |
+
ancestors_x_y_included = nodeset.union(*[nx.ancestors(G, node) for node in nodeset])
|
| 434 |
+
|
| 435 |
+
z_init = restricted & (ancestors_x_y_included - (x | y))
|
| 436 |
+
|
| 437 |
+
x_closure = _reachable(G, x, ancestors_x_y_included, z_init)
|
| 438 |
+
if x_closure & y:
|
| 439 |
+
return None
|
| 440 |
+
|
| 441 |
+
z_updated = z_init & (x_closure | included)
|
| 442 |
+
y_closure = _reachable(G, y, ancestors_x_y_included, z_updated)
|
| 443 |
+
return z_updated & (y_closure | included)
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
@not_implemented_for("undirected")
|
| 447 |
+
@nx._dispatchable
|
| 448 |
+
def is_minimal_d_separator(G, x, y, z, *, included=None, restricted=None):
|
| 449 |
+
"""Determine if `z` is a minimal d-separator for `x` and `y`.
|
| 450 |
+
|
| 451 |
+
A d-separator, `z`, in a DAG is a set of nodes that blocks
|
| 452 |
+
all paths from nodes in set `x` to nodes in set `y`.
|
| 453 |
+
A minimal d-separator is a d-separator `z` such that removing
|
| 454 |
+
any subset of nodes makes it no longer a d-separator.
|
| 455 |
+
|
| 456 |
+
Note: This function checks whether `z` is a d-separator AND is
|
| 457 |
+
minimal. One can use the function `is_d_separator` to only check if
|
| 458 |
+
`z` is a d-separator. See examples below.
|
| 459 |
+
|
| 460 |
+
Parameters
|
| 461 |
+
----------
|
| 462 |
+
G : nx.DiGraph
|
| 463 |
+
A NetworkX DAG.
|
| 464 |
+
x : node | set
|
| 465 |
+
A node or set of nodes in the graph.
|
| 466 |
+
y : node | set
|
| 467 |
+
A node or set of nodes in the graph.
|
| 468 |
+
z : node | set
|
| 469 |
+
The node or set of nodes to check if it is a minimal d-separating set.
|
| 470 |
+
The function :func:`is_d_separator` is called inside this function
|
| 471 |
+
to verify that `z` is in fact a d-separator.
|
| 472 |
+
included : set | node | None
|
| 473 |
+
A node or set of nodes which must be included in the found separating set,
|
| 474 |
+
default is ``None``, which means the empty set.
|
| 475 |
+
restricted : set | node | None
|
| 476 |
+
Restricted node or set of nodes to consider. Only these nodes can be in
|
| 477 |
+
the found separating set, default is ``None`` meaning all nodes in ``G``.
|
| 478 |
+
|
| 479 |
+
Returns
|
| 480 |
+
-------
|
| 481 |
+
bool
|
| 482 |
+
Whether or not the set `z` is a minimal d-separator subject to
|
| 483 |
+
`restricted` nodes and `included` node constraints.
|
| 484 |
+
|
| 485 |
+
Examples
|
| 486 |
+
--------
|
| 487 |
+
>>> G = nx.path_graph([0, 1, 2, 3], create_using=nx.DiGraph)
|
| 488 |
+
>>> G.add_node(4)
|
| 489 |
+
>>> nx.is_minimal_d_separator(G, 0, 2, {1})
|
| 490 |
+
True
|
| 491 |
+
>>> # since {1} is the minimal d-separator, {1, 3, 4} is not minimal
|
| 492 |
+
>>> nx.is_minimal_d_separator(G, 0, 2, {1, 3, 4})
|
| 493 |
+
False
|
| 494 |
+
>>> # alternatively, if we only want to check that {1, 3, 4} is a d-separator
|
| 495 |
+
>>> nx.is_d_separator(G, 0, 2, {1, 3, 4})
|
| 496 |
+
True
|
| 497 |
+
|
| 498 |
+
Raises
|
| 499 |
+
------
|
| 500 |
+
NetworkXError
|
| 501 |
+
Raises a :exc:`NetworkXError` if the input graph is not a DAG.
|
| 502 |
+
|
| 503 |
+
NodeNotFound
|
| 504 |
+
If any of the input nodes are not found in the graph,
|
| 505 |
+
a :exc:`NodeNotFound` exception is raised.
|
| 506 |
+
|
| 507 |
+
References
|
| 508 |
+
----------
|
| 509 |
+
.. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
|
| 510 |
+
minimal d-separators in linear time and applications." In
|
| 511 |
+
Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
|
| 512 |
+
|
| 513 |
+
Notes
|
| 514 |
+
-----
|
| 515 |
+
This function works on verifying that a set is minimal and
|
| 516 |
+
d-separating between two nodes. Uses criterion (a), (b), (c) on
|
| 517 |
+
page 4 of [1]_. a) closure(`x`) and `y` are disjoint. b) `z` contains
|
| 518 |
+
all nodes from `included` and is contained in the `restricted`
|
| 519 |
+
nodes and in the union of ancestors of `x`, `y`, and `included`.
|
| 520 |
+
c) the nodes in `z` not in `included` are contained in both
|
| 521 |
+
closure(x) and closure(y). The closure of a set is the set of nodes
|
| 522 |
+
connected to the set by a directed path in G.
|
| 523 |
+
|
| 524 |
+
The complexity is :math:`O(m)`, where :math:`m` stands for the
|
| 525 |
+
number of edges in the subgraph of G consisting of only the
|
| 526 |
+
ancestors of `x` and `y`.
|
| 527 |
+
|
| 528 |
+
For full details, see [1]_.
|
| 529 |
+
"""
|
| 530 |
+
if not nx.is_directed_acyclic_graph(G):
|
| 531 |
+
raise nx.NetworkXError("graph should be directed acyclic")
|
| 532 |
+
|
| 533 |
+
try:
|
| 534 |
+
x = {x} if x in G else x
|
| 535 |
+
y = {y} if y in G else y
|
| 536 |
+
z = {z} if z in G else z
|
| 537 |
+
|
| 538 |
+
if included is None:
|
| 539 |
+
included = set()
|
| 540 |
+
elif included in G:
|
| 541 |
+
included = {included}
|
| 542 |
+
|
| 543 |
+
if restricted is None:
|
| 544 |
+
restricted = set(G)
|
| 545 |
+
elif restricted in G:
|
| 546 |
+
restricted = {restricted}
|
| 547 |
+
|
| 548 |
+
set_y = x | y | included | restricted
|
| 549 |
+
if set_y - G.nodes:
|
| 550 |
+
raise nx.NodeNotFound(f"The node(s) {set_y - G.nodes} are not found in G")
|
| 551 |
+
except TypeError:
|
| 552 |
+
raise nx.NodeNotFound(
|
| 553 |
+
"One of x, y, z, included or restricted is not a node or set of nodes in G"
|
| 554 |
+
)
|
| 555 |
+
|
| 556 |
+
if not included <= z:
|
| 557 |
+
raise nx.NetworkXError(
|
| 558 |
+
f"Included nodes {included} must be in proposed separating set z {x}"
|
| 559 |
+
)
|
| 560 |
+
if not z <= restricted:
|
| 561 |
+
raise nx.NetworkXError(
|
| 562 |
+
f"Separating set {z} must be contained in restricted set {restricted}"
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
intersection = x.intersection(y) or x.intersection(z) or y.intersection(z)
|
| 566 |
+
if intersection:
|
| 567 |
+
raise nx.NetworkXError(
|
| 568 |
+
f"The sets are not disjoint, with intersection {intersection}"
|
| 569 |
+
)
|
| 570 |
+
|
| 571 |
+
nodeset = x | y | included
|
| 572 |
+
ancestors_x_y_included = nodeset.union(*[nx.ancestors(G, n) for n in nodeset])
|
| 573 |
+
|
| 574 |
+
# criterion (a) -- check that z is actually a separator
|
| 575 |
+
x_closure = _reachable(G, x, ancestors_x_y_included, z)
|
| 576 |
+
if x_closure & y:
|
| 577 |
+
return False
|
| 578 |
+
|
| 579 |
+
# criterion (b) -- basic constraint; included and restricted already checked above
|
| 580 |
+
if not (z <= ancestors_x_y_included):
|
| 581 |
+
return False
|
| 582 |
+
|
| 583 |
+
# criterion (c) -- check that z is minimal
|
| 584 |
+
y_closure = _reachable(G, y, ancestors_x_y_included, z)
|
| 585 |
+
if not ((z - included) <= (x_closure & y_closure)):
|
| 586 |
+
return False
|
| 587 |
+
return True
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
@not_implemented_for("undirected")
|
| 591 |
+
def _reachable(G, x, a, z):
|
| 592 |
+
"""Modified Bayes-Ball algorithm for finding d-connected nodes.
|
| 593 |
+
|
| 594 |
+
Find all nodes in `a` that are d-connected to those in `x` by
|
| 595 |
+
those in `z`. This is an implementation of the function
|
| 596 |
+
`REACHABLE` in [1]_ (which is itself a modification of the
|
| 597 |
+
Bayes-Ball algorithm [2]_) when restricted to DAGs.
|
| 598 |
+
|
| 599 |
+
Parameters
|
| 600 |
+
----------
|
| 601 |
+
G : nx.DiGraph
|
| 602 |
+
A NetworkX DAG.
|
| 603 |
+
x : node | set
|
| 604 |
+
A node in the DAG, or a set of nodes.
|
| 605 |
+
a : node | set
|
| 606 |
+
A (set of) node(s) in the DAG containing the ancestors of `x`.
|
| 607 |
+
z : node | set
|
| 608 |
+
The node or set of nodes conditioned on when checking d-connectedness.
|
| 609 |
+
|
| 610 |
+
Returns
|
| 611 |
+
-------
|
| 612 |
+
w : set
|
| 613 |
+
The closure of `x` in `a` with respect to d-connectedness
|
| 614 |
+
given `z`.
|
| 615 |
+
|
| 616 |
+
References
|
| 617 |
+
----------
|
| 618 |
+
.. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
|
| 619 |
+
minimal d-separators in linear time and applications." In
|
| 620 |
+
Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
|
| 621 |
+
|
| 622 |
+
.. [2] Shachter, Ross D. "Bayes-ball: The rational pastime
|
| 623 |
+
(for determining irrelevance and requisite information in
|
| 624 |
+
belief networks and influence diagrams)." In Proceedings of the
|
| 625 |
+
Fourteenth Conference on Uncertainty in Artificial Intelligence
|
| 626 |
+
(UAI), (pp. 480–487). 1998.
|
| 627 |
+
"""
|
| 628 |
+
|
| 629 |
+
def _pass(e, v, f, n):
|
| 630 |
+
"""Whether a ball entering node `v` along edge `e` passes to `n` along `f`.
|
| 631 |
+
|
| 632 |
+
Boolean function defined on page 6 of [1]_.
|
| 633 |
+
|
| 634 |
+
Parameters
|
| 635 |
+
----------
|
| 636 |
+
e : bool
|
| 637 |
+
Directed edge by which the ball got to node `v`; `True` iff directed into `v`.
|
| 638 |
+
v : node
|
| 639 |
+
Node where the ball is.
|
| 640 |
+
f : bool
|
| 641 |
+
Directed edge connecting nodes `v` and `n`; `True` iff directed `n`.
|
| 642 |
+
n : node
|
| 643 |
+
Checking whether the ball passes to this node.
|
| 644 |
+
|
| 645 |
+
Returns
|
| 646 |
+
-------
|
| 647 |
+
b : bool
|
| 648 |
+
Whether the ball passes or not.
|
| 649 |
+
|
| 650 |
+
References
|
| 651 |
+
----------
|
| 652 |
+
.. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
|
| 653 |
+
minimal d-separators in linear time and applications." In
|
| 654 |
+
Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
|
| 655 |
+
"""
|
| 656 |
+
is_element_of_A = n in a
|
| 657 |
+
# almost_definite_status = True # always true for DAGs; not so for RCGs
|
| 658 |
+
collider_if_in_Z = v not in z or (e and not f)
|
| 659 |
+
return is_element_of_A and collider_if_in_Z # and almost_definite_status
|
| 660 |
+
|
| 661 |
+
queue = deque([])
|
| 662 |
+
for node in x:
|
| 663 |
+
if bool(G.pred[node]):
|
| 664 |
+
queue.append((True, node))
|
| 665 |
+
if bool(G.succ[node]):
|
| 666 |
+
queue.append((False, node))
|
| 667 |
+
processed = queue.copy()
|
| 668 |
+
|
| 669 |
+
while any(queue):
|
| 670 |
+
e, v = queue.popleft()
|
| 671 |
+
preds = ((False, n) for n in G.pred[v])
|
| 672 |
+
succs = ((True, n) for n in G.succ[v])
|
| 673 |
+
f_n_pairs = chain(preds, succs)
|
| 674 |
+
for f, n in f_n_pairs:
|
| 675 |
+
if (f, n) not in processed and _pass(e, v, f, n):
|
| 676 |
+
queue.append((f, n))
|
| 677 |
+
processed.append((f, n))
|
| 678 |
+
|
| 679 |
+
return {w for (_, w) in processed}
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
# Deprecated functions:
|
| 683 |
+
def d_separated(G, x, y, z):
|
| 684 |
+
"""Return whether nodes sets ``x`` and ``y`` are d-separated by ``z``.
|
| 685 |
+
|
| 686 |
+
.. deprecated:: 3.3
|
| 687 |
+
|
| 688 |
+
This function is deprecated and will be removed in NetworkX v3.5.
|
| 689 |
+
Please use `is_d_separator(G, x, y, z)`.
|
| 690 |
+
|
| 691 |
+
"""
|
| 692 |
+
import warnings
|
| 693 |
+
|
| 694 |
+
warnings.warn(
|
| 695 |
+
"d_separated is deprecated and will be removed in NetworkX v3.5."
|
| 696 |
+
"Please use `is_d_separator(G, x, y, z)`.",
|
| 697 |
+
category=DeprecationWarning,
|
| 698 |
+
stacklevel=2,
|
| 699 |
+
)
|
| 700 |
+
return nx.is_d_separator(G, x, y, z)
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
def minimal_d_separator(G, u, v):
|
| 704 |
+
"""Returns a minimal_d-separating set between `x` and `y` if possible
|
| 705 |
+
|
| 706 |
+
.. deprecated:: 3.3
|
| 707 |
+
|
| 708 |
+
minimal_d_separator is deprecated and will be removed in NetworkX v3.5.
|
| 709 |
+
Please use `find_minimal_d_separator(G, x, y)`.
|
| 710 |
+
|
| 711 |
+
"""
|
| 712 |
+
import warnings
|
| 713 |
+
|
| 714 |
+
warnings.warn(
|
| 715 |
+
(
|
| 716 |
+
"This function is deprecated and will be removed in NetworkX v3.5."
|
| 717 |
+
"Please use `is_d_separator(G, x, y)`."
|
| 718 |
+
),
|
| 719 |
+
category=DeprecationWarning,
|
| 720 |
+
stacklevel=2,
|
| 721 |
+
)
|
| 722 |
+
return nx.find_minimal_d_separator(G, u, v)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/dag.py
ADDED
|
@@ -0,0 +1,1418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Algorithms for directed acyclic graphs (DAGs).
|
| 2 |
+
|
| 3 |
+
Note that most of these functions are only guaranteed to work for DAGs.
|
| 4 |
+
In general, these functions do not check for acyclic-ness, so it is up
|
| 5 |
+
to the user to check for that.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import heapq
|
| 9 |
+
from collections import deque
|
| 10 |
+
from functools import partial
|
| 11 |
+
from itertools import chain, combinations, product, starmap
|
| 12 |
+
from math import gcd
|
| 13 |
+
|
| 14 |
+
import networkx as nx
|
| 15 |
+
from networkx.utils import arbitrary_element, not_implemented_for, pairwise
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
"descendants",
|
| 19 |
+
"ancestors",
|
| 20 |
+
"topological_sort",
|
| 21 |
+
"lexicographical_topological_sort",
|
| 22 |
+
"all_topological_sorts",
|
| 23 |
+
"topological_generations",
|
| 24 |
+
"is_directed_acyclic_graph",
|
| 25 |
+
"is_aperiodic",
|
| 26 |
+
"transitive_closure",
|
| 27 |
+
"transitive_closure_dag",
|
| 28 |
+
"transitive_reduction",
|
| 29 |
+
"antichains",
|
| 30 |
+
"dag_longest_path",
|
| 31 |
+
"dag_longest_path_length",
|
| 32 |
+
"dag_to_branching",
|
| 33 |
+
"compute_v_structures",
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
chaini = chain.from_iterable
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@nx._dispatchable
|
| 40 |
+
def descendants(G, source):
|
| 41 |
+
"""Returns all nodes reachable from `source` in `G`.
|
| 42 |
+
|
| 43 |
+
Parameters
|
| 44 |
+
----------
|
| 45 |
+
G : NetworkX Graph
|
| 46 |
+
source : node in `G`
|
| 47 |
+
|
| 48 |
+
Returns
|
| 49 |
+
-------
|
| 50 |
+
set()
|
| 51 |
+
The descendants of `source` in `G`
|
| 52 |
+
|
| 53 |
+
Raises
|
| 54 |
+
------
|
| 55 |
+
NetworkXError
|
| 56 |
+
If node `source` is not in `G`.
|
| 57 |
+
|
| 58 |
+
Examples
|
| 59 |
+
--------
|
| 60 |
+
>>> DG = nx.path_graph(5, create_using=nx.DiGraph)
|
| 61 |
+
>>> sorted(nx.descendants(DG, 2))
|
| 62 |
+
[3, 4]
|
| 63 |
+
|
| 64 |
+
The `source` node is not a descendant of itself, but can be included manually:
|
| 65 |
+
|
| 66 |
+
>>> sorted(nx.descendants(DG, 2) | {2})
|
| 67 |
+
[2, 3, 4]
|
| 68 |
+
|
| 69 |
+
See also
|
| 70 |
+
--------
|
| 71 |
+
ancestors
|
| 72 |
+
"""
|
| 73 |
+
return {child for parent, child in nx.bfs_edges(G, source)}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@nx._dispatchable
|
| 77 |
+
def ancestors(G, source):
|
| 78 |
+
"""Returns all nodes having a path to `source` in `G`.
|
| 79 |
+
|
| 80 |
+
Parameters
|
| 81 |
+
----------
|
| 82 |
+
G : NetworkX Graph
|
| 83 |
+
source : node in `G`
|
| 84 |
+
|
| 85 |
+
Returns
|
| 86 |
+
-------
|
| 87 |
+
set()
|
| 88 |
+
The ancestors of `source` in `G`
|
| 89 |
+
|
| 90 |
+
Raises
|
| 91 |
+
------
|
| 92 |
+
NetworkXError
|
| 93 |
+
If node `source` is not in `G`.
|
| 94 |
+
|
| 95 |
+
Examples
|
| 96 |
+
--------
|
| 97 |
+
>>> DG = nx.path_graph(5, create_using=nx.DiGraph)
|
| 98 |
+
>>> sorted(nx.ancestors(DG, 2))
|
| 99 |
+
[0, 1]
|
| 100 |
+
|
| 101 |
+
The `source` node is not an ancestor of itself, but can be included manually:
|
| 102 |
+
|
| 103 |
+
>>> sorted(nx.ancestors(DG, 2) | {2})
|
| 104 |
+
[0, 1, 2]
|
| 105 |
+
|
| 106 |
+
See also
|
| 107 |
+
--------
|
| 108 |
+
descendants
|
| 109 |
+
"""
|
| 110 |
+
return {child for parent, child in nx.bfs_edges(G, source, reverse=True)}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@nx._dispatchable
|
| 114 |
+
def has_cycle(G):
|
| 115 |
+
"""Decides whether the directed graph has a cycle."""
|
| 116 |
+
try:
|
| 117 |
+
# Feed the entire iterator into a zero-length deque.
|
| 118 |
+
deque(topological_sort(G), maxlen=0)
|
| 119 |
+
except nx.NetworkXUnfeasible:
|
| 120 |
+
return True
|
| 121 |
+
else:
|
| 122 |
+
return False
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@nx._dispatchable
|
| 126 |
+
def is_directed_acyclic_graph(G):
|
| 127 |
+
"""Returns True if the graph `G` is a directed acyclic graph (DAG) or
|
| 128 |
+
False if not.
|
| 129 |
+
|
| 130 |
+
Parameters
|
| 131 |
+
----------
|
| 132 |
+
G : NetworkX graph
|
| 133 |
+
|
| 134 |
+
Returns
|
| 135 |
+
-------
|
| 136 |
+
bool
|
| 137 |
+
True if `G` is a DAG, False otherwise
|
| 138 |
+
|
| 139 |
+
Examples
|
| 140 |
+
--------
|
| 141 |
+
Undirected graph::
|
| 142 |
+
|
| 143 |
+
>>> G = nx.Graph([(1, 2), (2, 3)])
|
| 144 |
+
>>> nx.is_directed_acyclic_graph(G)
|
| 145 |
+
False
|
| 146 |
+
|
| 147 |
+
Directed graph with cycle::
|
| 148 |
+
|
| 149 |
+
>>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
|
| 150 |
+
>>> nx.is_directed_acyclic_graph(G)
|
| 151 |
+
False
|
| 152 |
+
|
| 153 |
+
Directed acyclic graph::
|
| 154 |
+
|
| 155 |
+
>>> G = nx.DiGraph([(1, 2), (2, 3)])
|
| 156 |
+
>>> nx.is_directed_acyclic_graph(G)
|
| 157 |
+
True
|
| 158 |
+
|
| 159 |
+
See also
|
| 160 |
+
--------
|
| 161 |
+
topological_sort
|
| 162 |
+
"""
|
| 163 |
+
return G.is_directed() and not has_cycle(G)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@nx._dispatchable
|
| 167 |
+
def topological_generations(G):
|
| 168 |
+
"""Stratifies a DAG into generations.
|
| 169 |
+
|
| 170 |
+
A topological generation is node collection in which ancestors of a node in each
|
| 171 |
+
generation are guaranteed to be in a previous generation, and any descendants of
|
| 172 |
+
a node are guaranteed to be in a following generation. Nodes are guaranteed to
|
| 173 |
+
be in the earliest possible generation that they can belong to.
|
| 174 |
+
|
| 175 |
+
Parameters
|
| 176 |
+
----------
|
| 177 |
+
G : NetworkX digraph
|
| 178 |
+
A directed acyclic graph (DAG)
|
| 179 |
+
|
| 180 |
+
Yields
|
| 181 |
+
------
|
| 182 |
+
sets of nodes
|
| 183 |
+
Yields sets of nodes representing each generation.
|
| 184 |
+
|
| 185 |
+
Raises
|
| 186 |
+
------
|
| 187 |
+
NetworkXError
|
| 188 |
+
Generations are defined for directed graphs only. If the graph
|
| 189 |
+
`G` is undirected, a :exc:`NetworkXError` is raised.
|
| 190 |
+
|
| 191 |
+
NetworkXUnfeasible
|
| 192 |
+
If `G` is not a directed acyclic graph (DAG) no topological generations
|
| 193 |
+
exist and a :exc:`NetworkXUnfeasible` exception is raised. This can also
|
| 194 |
+
be raised if `G` is changed while the returned iterator is being processed
|
| 195 |
+
|
| 196 |
+
RuntimeError
|
| 197 |
+
If `G` is changed while the returned iterator is being processed.
|
| 198 |
+
|
| 199 |
+
Examples
|
| 200 |
+
--------
|
| 201 |
+
>>> DG = nx.DiGraph([(2, 1), (3, 1)])
|
| 202 |
+
>>> [sorted(generation) for generation in nx.topological_generations(DG)]
|
| 203 |
+
[[2, 3], [1]]
|
| 204 |
+
|
| 205 |
+
Notes
|
| 206 |
+
-----
|
| 207 |
+
The generation in which a node resides can also be determined by taking the
|
| 208 |
+
max-path-distance from the node to the farthest leaf node. That value can
|
| 209 |
+
be obtained with this function using `enumerate(topological_generations(G))`.
|
| 210 |
+
|
| 211 |
+
See also
|
| 212 |
+
--------
|
| 213 |
+
topological_sort
|
| 214 |
+
"""
|
| 215 |
+
if not G.is_directed():
|
| 216 |
+
raise nx.NetworkXError("Topological sort not defined on undirected graphs.")
|
| 217 |
+
|
| 218 |
+
multigraph = G.is_multigraph()
|
| 219 |
+
indegree_map = {v: d for v, d in G.in_degree() if d > 0}
|
| 220 |
+
zero_indegree = [v for v, d in G.in_degree() if d == 0]
|
| 221 |
+
|
| 222 |
+
while zero_indegree:
|
| 223 |
+
this_generation = zero_indegree
|
| 224 |
+
zero_indegree = []
|
| 225 |
+
for node in this_generation:
|
| 226 |
+
if node not in G:
|
| 227 |
+
raise RuntimeError("Graph changed during iteration")
|
| 228 |
+
for child in G.neighbors(node):
|
| 229 |
+
try:
|
| 230 |
+
indegree_map[child] -= len(G[node][child]) if multigraph else 1
|
| 231 |
+
except KeyError as err:
|
| 232 |
+
raise RuntimeError("Graph changed during iteration") from err
|
| 233 |
+
if indegree_map[child] == 0:
|
| 234 |
+
zero_indegree.append(child)
|
| 235 |
+
del indegree_map[child]
|
| 236 |
+
yield this_generation
|
| 237 |
+
|
| 238 |
+
if indegree_map:
|
| 239 |
+
raise nx.NetworkXUnfeasible(
|
| 240 |
+
"Graph contains a cycle or graph changed during iteration"
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
@nx._dispatchable
|
| 245 |
+
def topological_sort(G):
|
| 246 |
+
"""Returns a generator of nodes in topologically sorted order.
|
| 247 |
+
|
| 248 |
+
A topological sort is a nonunique permutation of the nodes of a
|
| 249 |
+
directed graph such that an edge from u to v implies that u
|
| 250 |
+
appears before v in the topological sort order. This ordering is
|
| 251 |
+
valid only if the graph has no directed cycles.
|
| 252 |
+
|
| 253 |
+
Parameters
|
| 254 |
+
----------
|
| 255 |
+
G : NetworkX digraph
|
| 256 |
+
A directed acyclic graph (DAG)
|
| 257 |
+
|
| 258 |
+
Yields
|
| 259 |
+
------
|
| 260 |
+
nodes
|
| 261 |
+
Yields the nodes in topological sorted order.
|
| 262 |
+
|
| 263 |
+
Raises
|
| 264 |
+
------
|
| 265 |
+
NetworkXError
|
| 266 |
+
Topological sort is defined for directed graphs only. If the graph `G`
|
| 267 |
+
is undirected, a :exc:`NetworkXError` is raised.
|
| 268 |
+
|
| 269 |
+
NetworkXUnfeasible
|
| 270 |
+
If `G` is not a directed acyclic graph (DAG) no topological sort exists
|
| 271 |
+
and a :exc:`NetworkXUnfeasible` exception is raised. This can also be
|
| 272 |
+
raised if `G` is changed while the returned iterator is being processed
|
| 273 |
+
|
| 274 |
+
RuntimeError
|
| 275 |
+
If `G` is changed while the returned iterator is being processed.
|
| 276 |
+
|
| 277 |
+
Examples
|
| 278 |
+
--------
|
| 279 |
+
To get the reverse order of the topological sort:
|
| 280 |
+
|
| 281 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 3)])
|
| 282 |
+
>>> list(reversed(list(nx.topological_sort(DG))))
|
| 283 |
+
[3, 2, 1]
|
| 284 |
+
|
| 285 |
+
If your DiGraph naturally has the edges representing tasks/inputs
|
| 286 |
+
and nodes representing people/processes that initiate tasks, then
|
| 287 |
+
topological_sort is not quite what you need. You will have to change
|
| 288 |
+
the tasks to nodes with dependence reflected by edges. The result is
|
| 289 |
+
a kind of topological sort of the edges. This can be done
|
| 290 |
+
with :func:`networkx.line_graph` as follows:
|
| 291 |
+
|
| 292 |
+
>>> list(nx.topological_sort(nx.line_graph(DG)))
|
| 293 |
+
[(1, 2), (2, 3)]
|
| 294 |
+
|
| 295 |
+
Notes
|
| 296 |
+
-----
|
| 297 |
+
This algorithm is based on a description and proof in
|
| 298 |
+
"Introduction to Algorithms: A Creative Approach" [1]_ .
|
| 299 |
+
|
| 300 |
+
See also
|
| 301 |
+
--------
|
| 302 |
+
is_directed_acyclic_graph, lexicographical_topological_sort
|
| 303 |
+
|
| 304 |
+
References
|
| 305 |
+
----------
|
| 306 |
+
.. [1] Manber, U. (1989).
|
| 307 |
+
*Introduction to Algorithms - A Creative Approach.* Addison-Wesley.
|
| 308 |
+
"""
|
| 309 |
+
for generation in nx.topological_generations(G):
|
| 310 |
+
yield from generation
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@nx._dispatchable
|
| 314 |
+
def lexicographical_topological_sort(G, key=None):
|
| 315 |
+
"""Generate the nodes in the unique lexicographical topological sort order.
|
| 316 |
+
|
| 317 |
+
Generates a unique ordering of nodes by first sorting topologically (for which there are often
|
| 318 |
+
multiple valid orderings) and then additionally by sorting lexicographically.
|
| 319 |
+
|
| 320 |
+
A topological sort arranges the nodes of a directed graph so that the
|
| 321 |
+
upstream node of each directed edge precedes the downstream node.
|
| 322 |
+
It is always possible to find a solution for directed graphs that have no cycles.
|
| 323 |
+
There may be more than one valid solution.
|
| 324 |
+
|
| 325 |
+
Lexicographical sorting is just sorting alphabetically. It is used here to break ties in the
|
| 326 |
+
topological sort and to determine a single, unique ordering. This can be useful in comparing
|
| 327 |
+
sort results.
|
| 328 |
+
|
| 329 |
+
The lexicographical order can be customized by providing a function to the `key=` parameter.
|
| 330 |
+
The definition of the key function is the same as used in python's built-in `sort()`.
|
| 331 |
+
The function takes a single argument and returns a key to use for sorting purposes.
|
| 332 |
+
|
| 333 |
+
Lexicographical sorting can fail if the node names are un-sortable. See the example below.
|
| 334 |
+
The solution is to provide a function to the `key=` argument that returns sortable keys.
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
Parameters
|
| 338 |
+
----------
|
| 339 |
+
G : NetworkX digraph
|
| 340 |
+
A directed acyclic graph (DAG)
|
| 341 |
+
|
| 342 |
+
key : function, optional
|
| 343 |
+
A function of one argument that converts a node name to a comparison key.
|
| 344 |
+
It defines and resolves ambiguities in the sort order. Defaults to the identity function.
|
| 345 |
+
|
| 346 |
+
Yields
|
| 347 |
+
------
|
| 348 |
+
nodes
|
| 349 |
+
Yields the nodes of G in lexicographical topological sort order.
|
| 350 |
+
|
| 351 |
+
Raises
|
| 352 |
+
------
|
| 353 |
+
NetworkXError
|
| 354 |
+
Topological sort is defined for directed graphs only. If the graph `G`
|
| 355 |
+
is undirected, a :exc:`NetworkXError` is raised.
|
| 356 |
+
|
| 357 |
+
NetworkXUnfeasible
|
| 358 |
+
If `G` is not a directed acyclic graph (DAG) no topological sort exists
|
| 359 |
+
and a :exc:`NetworkXUnfeasible` exception is raised. This can also be
|
| 360 |
+
raised if `G` is changed while the returned iterator is being processed
|
| 361 |
+
|
| 362 |
+
RuntimeError
|
| 363 |
+
If `G` is changed while the returned iterator is being processed.
|
| 364 |
+
|
| 365 |
+
TypeError
|
| 366 |
+
Results from un-sortable node names.
|
| 367 |
+
Consider using `key=` parameter to resolve ambiguities in the sort order.
|
| 368 |
+
|
| 369 |
+
Examples
|
| 370 |
+
--------
|
| 371 |
+
>>> DG = nx.DiGraph([(2, 1), (2, 5), (1, 3), (1, 4), (5, 4)])
|
| 372 |
+
>>> list(nx.lexicographical_topological_sort(DG))
|
| 373 |
+
[2, 1, 3, 5, 4]
|
| 374 |
+
>>> list(nx.lexicographical_topological_sort(DG, key=lambda x: -x))
|
| 375 |
+
[2, 5, 1, 4, 3]
|
| 376 |
+
|
| 377 |
+
The sort will fail for any graph with integer and string nodes. Comparison of integer to strings
|
| 378 |
+
is not defined in python. Is 3 greater or less than 'red'?
|
| 379 |
+
|
| 380 |
+
>>> DG = nx.DiGraph([(1, "red"), (3, "red"), (1, "green"), (2, "blue")])
|
| 381 |
+
>>> list(nx.lexicographical_topological_sort(DG))
|
| 382 |
+
Traceback (most recent call last):
|
| 383 |
+
...
|
| 384 |
+
TypeError: '<' not supported between instances of 'str' and 'int'
|
| 385 |
+
...
|
| 386 |
+
|
| 387 |
+
Incomparable nodes can be resolved using a `key` function. This example function
|
| 388 |
+
allows comparison of integers and strings by returning a tuple where the first
|
| 389 |
+
element is True for `str`, False otherwise. The second element is the node name.
|
| 390 |
+
This groups the strings and integers separately so they can be compared only among themselves.
|
| 391 |
+
|
| 392 |
+
>>> key = lambda node: (isinstance(node, str), node)
|
| 393 |
+
>>> list(nx.lexicographical_topological_sort(DG, key=key))
|
| 394 |
+
[1, 2, 3, 'blue', 'green', 'red']
|
| 395 |
+
|
| 396 |
+
Notes
|
| 397 |
+
-----
|
| 398 |
+
This algorithm is based on a description and proof in
|
| 399 |
+
"Introduction to Algorithms: A Creative Approach" [1]_ .
|
| 400 |
+
|
| 401 |
+
See also
|
| 402 |
+
--------
|
| 403 |
+
topological_sort
|
| 404 |
+
|
| 405 |
+
References
|
| 406 |
+
----------
|
| 407 |
+
.. [1] Manber, U. (1989).
|
| 408 |
+
*Introduction to Algorithms - A Creative Approach.* Addison-Wesley.
|
| 409 |
+
"""
|
| 410 |
+
if not G.is_directed():
|
| 411 |
+
msg = "Topological sort not defined on undirected graphs."
|
| 412 |
+
raise nx.NetworkXError(msg)
|
| 413 |
+
|
| 414 |
+
if key is None:
|
| 415 |
+
|
| 416 |
+
def key(node):
|
| 417 |
+
return node
|
| 418 |
+
|
| 419 |
+
nodeid_map = {n: i for i, n in enumerate(G)}
|
| 420 |
+
|
| 421 |
+
def create_tuple(node):
|
| 422 |
+
return key(node), nodeid_map[node], node
|
| 423 |
+
|
| 424 |
+
indegree_map = {v: d for v, d in G.in_degree() if d > 0}
|
| 425 |
+
# These nodes have zero indegree and ready to be returned.
|
| 426 |
+
zero_indegree = [create_tuple(v) for v, d in G.in_degree() if d == 0]
|
| 427 |
+
heapq.heapify(zero_indegree)
|
| 428 |
+
|
| 429 |
+
while zero_indegree:
|
| 430 |
+
_, _, node = heapq.heappop(zero_indegree)
|
| 431 |
+
|
| 432 |
+
if node not in G:
|
| 433 |
+
raise RuntimeError("Graph changed during iteration")
|
| 434 |
+
for _, child in G.edges(node):
|
| 435 |
+
try:
|
| 436 |
+
indegree_map[child] -= 1
|
| 437 |
+
except KeyError as err:
|
| 438 |
+
raise RuntimeError("Graph changed during iteration") from err
|
| 439 |
+
if indegree_map[child] == 0:
|
| 440 |
+
try:
|
| 441 |
+
heapq.heappush(zero_indegree, create_tuple(child))
|
| 442 |
+
except TypeError as err:
|
| 443 |
+
raise TypeError(
|
| 444 |
+
f"{err}\nConsider using `key=` parameter to resolve ambiguities in the sort order."
|
| 445 |
+
)
|
| 446 |
+
del indegree_map[child]
|
| 447 |
+
|
| 448 |
+
yield node
|
| 449 |
+
|
| 450 |
+
if indegree_map:
|
| 451 |
+
msg = "Graph contains a cycle or graph changed during iteration"
|
| 452 |
+
raise nx.NetworkXUnfeasible(msg)
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
@not_implemented_for("undirected")
|
| 456 |
+
@nx._dispatchable
|
| 457 |
+
def all_topological_sorts(G):
|
| 458 |
+
"""Returns a generator of _all_ topological sorts of the directed graph G.
|
| 459 |
+
|
| 460 |
+
A topological sort is a nonunique permutation of the nodes such that an
|
| 461 |
+
edge from u to v implies that u appears before v in the topological sort
|
| 462 |
+
order.
|
| 463 |
+
|
| 464 |
+
Parameters
|
| 465 |
+
----------
|
| 466 |
+
G : NetworkX DiGraph
|
| 467 |
+
A directed graph
|
| 468 |
+
|
| 469 |
+
Yields
|
| 470 |
+
------
|
| 471 |
+
topological_sort_order : list
|
| 472 |
+
a list of nodes in `G`, representing one of the topological sort orders
|
| 473 |
+
|
| 474 |
+
Raises
|
| 475 |
+
------
|
| 476 |
+
NetworkXNotImplemented
|
| 477 |
+
If `G` is not directed
|
| 478 |
+
NetworkXUnfeasible
|
| 479 |
+
If `G` is not acyclic
|
| 480 |
+
|
| 481 |
+
Examples
|
| 482 |
+
--------
|
| 483 |
+
To enumerate all topological sorts of directed graph:
|
| 484 |
+
|
| 485 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 3), (2, 4)])
|
| 486 |
+
>>> list(nx.all_topological_sorts(DG))
|
| 487 |
+
[[1, 2, 4, 3], [1, 2, 3, 4]]
|
| 488 |
+
|
| 489 |
+
Notes
|
| 490 |
+
-----
|
| 491 |
+
Implements an iterative version of the algorithm given in [1].
|
| 492 |
+
|
| 493 |
+
References
|
| 494 |
+
----------
|
| 495 |
+
.. [1] Knuth, Donald E., Szwarcfiter, Jayme L. (1974).
|
| 496 |
+
"A Structured Program to Generate All Topological Sorting Arrangements"
|
| 497 |
+
Information Processing Letters, Volume 2, Issue 6, 1974, Pages 153-157,
|
| 498 |
+
ISSN 0020-0190,
|
| 499 |
+
https://doi.org/10.1016/0020-0190(74)90001-5.
|
| 500 |
+
Elsevier (North-Holland), Amsterdam
|
| 501 |
+
"""
|
| 502 |
+
if not G.is_directed():
|
| 503 |
+
raise nx.NetworkXError("Topological sort not defined on undirected graphs.")
|
| 504 |
+
|
| 505 |
+
# the names of count and D are chosen to match the global variables in [1]
|
| 506 |
+
# number of edges originating in a vertex v
|
| 507 |
+
count = dict(G.in_degree())
|
| 508 |
+
# vertices with indegree 0
|
| 509 |
+
D = deque([v for v, d in G.in_degree() if d == 0])
|
| 510 |
+
# stack of first value chosen at a position k in the topological sort
|
| 511 |
+
bases = []
|
| 512 |
+
current_sort = []
|
| 513 |
+
|
| 514 |
+
# do-while construct
|
| 515 |
+
while True:
|
| 516 |
+
assert all(count[v] == 0 for v in D)
|
| 517 |
+
|
| 518 |
+
if len(current_sort) == len(G):
|
| 519 |
+
yield list(current_sort)
|
| 520 |
+
|
| 521 |
+
# clean-up stack
|
| 522 |
+
while len(current_sort) > 0:
|
| 523 |
+
assert len(bases) == len(current_sort)
|
| 524 |
+
q = current_sort.pop()
|
| 525 |
+
|
| 526 |
+
# "restores" all edges (q, x)
|
| 527 |
+
# NOTE: it is important to iterate over edges instead
|
| 528 |
+
# of successors, so count is updated correctly in multigraphs
|
| 529 |
+
for _, j in G.out_edges(q):
|
| 530 |
+
count[j] += 1
|
| 531 |
+
assert count[j] >= 0
|
| 532 |
+
# remove entries from D
|
| 533 |
+
while len(D) > 0 and count[D[-1]] > 0:
|
| 534 |
+
D.pop()
|
| 535 |
+
|
| 536 |
+
# corresponds to a circular shift of the values in D
|
| 537 |
+
# if the first value chosen (the base) is in the first
|
| 538 |
+
# position of D again, we are done and need to consider the
|
| 539 |
+
# previous condition
|
| 540 |
+
D.appendleft(q)
|
| 541 |
+
if D[-1] == bases[-1]:
|
| 542 |
+
# all possible values have been chosen at current position
|
| 543 |
+
# remove corresponding marker
|
| 544 |
+
bases.pop()
|
| 545 |
+
else:
|
| 546 |
+
# there are still elements that have not been fixed
|
| 547 |
+
# at the current position in the topological sort
|
| 548 |
+
# stop removing elements, escape inner loop
|
| 549 |
+
break
|
| 550 |
+
|
| 551 |
+
else:
|
| 552 |
+
if len(D) == 0:
|
| 553 |
+
raise nx.NetworkXUnfeasible("Graph contains a cycle.")
|
| 554 |
+
|
| 555 |
+
# choose next node
|
| 556 |
+
q = D.pop()
|
| 557 |
+
# "erase" all edges (q, x)
|
| 558 |
+
# NOTE: it is important to iterate over edges instead
|
| 559 |
+
# of successors, so count is updated correctly in multigraphs
|
| 560 |
+
for _, j in G.out_edges(q):
|
| 561 |
+
count[j] -= 1
|
| 562 |
+
assert count[j] >= 0
|
| 563 |
+
if count[j] == 0:
|
| 564 |
+
D.append(j)
|
| 565 |
+
current_sort.append(q)
|
| 566 |
+
|
| 567 |
+
# base for current position might _not_ be fixed yet
|
| 568 |
+
if len(bases) < len(current_sort):
|
| 569 |
+
bases.append(q)
|
| 570 |
+
|
| 571 |
+
if len(bases) == 0:
|
| 572 |
+
break
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
@nx._dispatchable
|
| 576 |
+
def is_aperiodic(G):
|
| 577 |
+
"""Returns True if `G` is aperiodic.
|
| 578 |
+
|
| 579 |
+
A directed graph is aperiodic if there is no integer k > 1 that
|
| 580 |
+
divides the length of every cycle in the graph.
|
| 581 |
+
|
| 582 |
+
Parameters
|
| 583 |
+
----------
|
| 584 |
+
G : NetworkX DiGraph
|
| 585 |
+
A directed graph
|
| 586 |
+
|
| 587 |
+
Returns
|
| 588 |
+
-------
|
| 589 |
+
bool
|
| 590 |
+
True if the graph is aperiodic False otherwise
|
| 591 |
+
|
| 592 |
+
Raises
|
| 593 |
+
------
|
| 594 |
+
NetworkXError
|
| 595 |
+
If `G` is not directed
|
| 596 |
+
|
| 597 |
+
Examples
|
| 598 |
+
--------
|
| 599 |
+
A graph consisting of one cycle, the length of which is 2. Therefore ``k = 2``
|
| 600 |
+
divides the length of every cycle in the graph and thus the graph
|
| 601 |
+
is *not aperiodic*::
|
| 602 |
+
|
| 603 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 1)])
|
| 604 |
+
>>> nx.is_aperiodic(DG)
|
| 605 |
+
False
|
| 606 |
+
|
| 607 |
+
A graph consisting of two cycles: one of length 2 and the other of length 3.
|
| 608 |
+
The cycle lengths are coprime, so there is no single value of k where ``k > 1``
|
| 609 |
+
that divides each cycle length and therefore the graph is *aperiodic*::
|
| 610 |
+
|
| 611 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1), (1, 4), (4, 1)])
|
| 612 |
+
>>> nx.is_aperiodic(DG)
|
| 613 |
+
True
|
| 614 |
+
|
| 615 |
+
A graph consisting of two cycles: one of length 2 and the other of length 4.
|
| 616 |
+
The lengths of the cycles share a common factor ``k = 2``, and therefore
|
| 617 |
+
the graph is *not aperiodic*::
|
| 618 |
+
|
| 619 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 1), (3, 4), (4, 5), (5, 6), (6, 3)])
|
| 620 |
+
>>> nx.is_aperiodic(DG)
|
| 621 |
+
False
|
| 622 |
+
|
| 623 |
+
An acyclic graph, therefore the graph is *not aperiodic*::
|
| 624 |
+
|
| 625 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 3)])
|
| 626 |
+
>>> nx.is_aperiodic(DG)
|
| 627 |
+
False
|
| 628 |
+
|
| 629 |
+
Notes
|
| 630 |
+
-----
|
| 631 |
+
This uses the method outlined in [1]_, which runs in $O(m)$ time
|
| 632 |
+
given $m$ edges in `G`. Note that a graph is not aperiodic if it is
|
| 633 |
+
acyclic as every integer trivial divides length 0 cycles.
|
| 634 |
+
|
| 635 |
+
References
|
| 636 |
+
----------
|
| 637 |
+
.. [1] Jarvis, J. P.; Shier, D. R. (1996),
|
| 638 |
+
"Graph-theoretic analysis of finite Markov chains,"
|
| 639 |
+
in Shier, D. R.; Wallenius, K. T., Applied Mathematical Modeling:
|
| 640 |
+
A Multidisciplinary Approach, CRC Press.
|
| 641 |
+
"""
|
| 642 |
+
if not G.is_directed():
|
| 643 |
+
raise nx.NetworkXError("is_aperiodic not defined for undirected graphs")
|
| 644 |
+
if len(G) == 0:
|
| 645 |
+
raise nx.NetworkXPointlessConcept("Graph has no nodes.")
|
| 646 |
+
s = arbitrary_element(G)
|
| 647 |
+
levels = {s: 0}
|
| 648 |
+
this_level = [s]
|
| 649 |
+
g = 0
|
| 650 |
+
lev = 1
|
| 651 |
+
while this_level:
|
| 652 |
+
next_level = []
|
| 653 |
+
for u in this_level:
|
| 654 |
+
for v in G[u]:
|
| 655 |
+
if v in levels: # Non-Tree Edge
|
| 656 |
+
g = gcd(g, levels[u] - levels[v] + 1)
|
| 657 |
+
else: # Tree Edge
|
| 658 |
+
next_level.append(v)
|
| 659 |
+
levels[v] = lev
|
| 660 |
+
this_level = next_level
|
| 661 |
+
lev += 1
|
| 662 |
+
if len(levels) == len(G): # All nodes in tree
|
| 663 |
+
return g == 1
|
| 664 |
+
else:
|
| 665 |
+
return g == 1 and nx.is_aperiodic(G.subgraph(set(G) - set(levels)))
|
| 666 |
+
|
| 667 |
+
|
| 668 |
+
@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
|
| 669 |
+
def transitive_closure(G, reflexive=False):
|
| 670 |
+
"""Returns transitive closure of a graph
|
| 671 |
+
|
| 672 |
+
The transitive closure of G = (V,E) is a graph G+ = (V,E+) such that
|
| 673 |
+
for all v, w in V there is an edge (v, w) in E+ if and only if there
|
| 674 |
+
is a path from v to w in G.
|
| 675 |
+
|
| 676 |
+
Handling of paths from v to v has some flexibility within this definition.
|
| 677 |
+
A reflexive transitive closure creates a self-loop for the path
|
| 678 |
+
from v to v of length 0. The usual transitive closure creates a
|
| 679 |
+
self-loop only if a cycle exists (a path from v to v with length > 0).
|
| 680 |
+
We also allow an option for no self-loops.
|
| 681 |
+
|
| 682 |
+
Parameters
|
| 683 |
+
----------
|
| 684 |
+
G : NetworkX Graph
|
| 685 |
+
A directed/undirected graph/multigraph.
|
| 686 |
+
reflexive : Bool or None, optional (default: False)
|
| 687 |
+
Determines when cycles create self-loops in the Transitive Closure.
|
| 688 |
+
If True, trivial cycles (length 0) create self-loops. The result
|
| 689 |
+
is a reflexive transitive closure of G.
|
| 690 |
+
If False (the default) non-trivial cycles create self-loops.
|
| 691 |
+
If None, self-loops are not created.
|
| 692 |
+
|
| 693 |
+
Returns
|
| 694 |
+
-------
|
| 695 |
+
NetworkX graph
|
| 696 |
+
The transitive closure of `G`
|
| 697 |
+
|
| 698 |
+
Raises
|
| 699 |
+
------
|
| 700 |
+
NetworkXError
|
| 701 |
+
If `reflexive` not in `{None, True, False}`
|
| 702 |
+
|
| 703 |
+
Examples
|
| 704 |
+
--------
|
| 705 |
+
The treatment of trivial (i.e. length 0) cycles is controlled by the
|
| 706 |
+
`reflexive` parameter.
|
| 707 |
+
|
| 708 |
+
Trivial (i.e. length 0) cycles do not create self-loops when
|
| 709 |
+
``reflexive=False`` (the default)::
|
| 710 |
+
|
| 711 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 3)])
|
| 712 |
+
>>> TC = nx.transitive_closure(DG, reflexive=False)
|
| 713 |
+
>>> TC.edges()
|
| 714 |
+
OutEdgeView([(1, 2), (1, 3), (2, 3)])
|
| 715 |
+
|
| 716 |
+
However, nontrivial (i.e. length greater than 0) cycles create self-loops
|
| 717 |
+
when ``reflexive=False`` (the default)::
|
| 718 |
+
|
| 719 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
|
| 720 |
+
>>> TC = nx.transitive_closure(DG, reflexive=False)
|
| 721 |
+
>>> TC.edges()
|
| 722 |
+
OutEdgeView([(1, 2), (1, 3), (1, 1), (2, 3), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3)])
|
| 723 |
+
|
| 724 |
+
Trivial cycles (length 0) create self-loops when ``reflexive=True``::
|
| 725 |
+
|
| 726 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 3)])
|
| 727 |
+
>>> TC = nx.transitive_closure(DG, reflexive=True)
|
| 728 |
+
>>> TC.edges()
|
| 729 |
+
OutEdgeView([(1, 2), (1, 1), (1, 3), (2, 3), (2, 2), (3, 3)])
|
| 730 |
+
|
| 731 |
+
And the third option is not to create self-loops at all when ``reflexive=None``::
|
| 732 |
+
|
| 733 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
|
| 734 |
+
>>> TC = nx.transitive_closure(DG, reflexive=None)
|
| 735 |
+
>>> TC.edges()
|
| 736 |
+
OutEdgeView([(1, 2), (1, 3), (2, 3), (2, 1), (3, 1), (3, 2)])
|
| 737 |
+
|
| 738 |
+
References
|
| 739 |
+
----------
|
| 740 |
+
.. [1] https://www.ics.uci.edu/~eppstein/PADS/PartialOrder.py
|
| 741 |
+
"""
|
| 742 |
+
TC = G.copy()
|
| 743 |
+
|
| 744 |
+
if reflexive not in {None, True, False}:
|
| 745 |
+
raise nx.NetworkXError("Incorrect value for the parameter `reflexive`")
|
| 746 |
+
|
| 747 |
+
for v in G:
|
| 748 |
+
if reflexive is None:
|
| 749 |
+
TC.add_edges_from((v, u) for u in nx.descendants(G, v) if u not in TC[v])
|
| 750 |
+
elif reflexive is True:
|
| 751 |
+
TC.add_edges_from(
|
| 752 |
+
(v, u) for u in nx.descendants(G, v) | {v} if u not in TC[v]
|
| 753 |
+
)
|
| 754 |
+
elif reflexive is False:
|
| 755 |
+
TC.add_edges_from((v, e[1]) for e in nx.edge_bfs(G, v) if e[1] not in TC[v])
|
| 756 |
+
|
| 757 |
+
return TC
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
@not_implemented_for("undirected")
|
| 761 |
+
@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
|
| 762 |
+
def transitive_closure_dag(G, topo_order=None):
|
| 763 |
+
"""Returns the transitive closure of a directed acyclic graph.
|
| 764 |
+
|
| 765 |
+
This function is faster than the function `transitive_closure`, but fails
|
| 766 |
+
if the graph has a cycle.
|
| 767 |
+
|
| 768 |
+
The transitive closure of G = (V,E) is a graph G+ = (V,E+) such that
|
| 769 |
+
for all v, w in V there is an edge (v, w) in E+ if and only if there
|
| 770 |
+
is a non-null path from v to w in G.
|
| 771 |
+
|
| 772 |
+
Parameters
|
| 773 |
+
----------
|
| 774 |
+
G : NetworkX DiGraph
|
| 775 |
+
A directed acyclic graph (DAG)
|
| 776 |
+
|
| 777 |
+
topo_order: list or tuple, optional
|
| 778 |
+
A topological order for G (if None, the function will compute one)
|
| 779 |
+
|
| 780 |
+
Returns
|
| 781 |
+
-------
|
| 782 |
+
NetworkX DiGraph
|
| 783 |
+
The transitive closure of `G`
|
| 784 |
+
|
| 785 |
+
Raises
|
| 786 |
+
------
|
| 787 |
+
NetworkXNotImplemented
|
| 788 |
+
If `G` is not directed
|
| 789 |
+
NetworkXUnfeasible
|
| 790 |
+
If `G` has a cycle
|
| 791 |
+
|
| 792 |
+
Examples
|
| 793 |
+
--------
|
| 794 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 3)])
|
| 795 |
+
>>> TC = nx.transitive_closure_dag(DG)
|
| 796 |
+
>>> TC.edges()
|
| 797 |
+
OutEdgeView([(1, 2), (1, 3), (2, 3)])
|
| 798 |
+
|
| 799 |
+
Notes
|
| 800 |
+
-----
|
| 801 |
+
This algorithm is probably simple enough to be well-known but I didn't find
|
| 802 |
+
a mention in the literature.
|
| 803 |
+
"""
|
| 804 |
+
if topo_order is None:
|
| 805 |
+
topo_order = list(topological_sort(G))
|
| 806 |
+
|
| 807 |
+
TC = G.copy()
|
| 808 |
+
|
| 809 |
+
# idea: traverse vertices following a reverse topological order, connecting
|
| 810 |
+
# each vertex to its descendants at distance 2 as we go
|
| 811 |
+
for v in reversed(topo_order):
|
| 812 |
+
TC.add_edges_from((v, u) for u in nx.descendants_at_distance(TC, v, 2))
|
| 813 |
+
|
| 814 |
+
return TC
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
@not_implemented_for("undirected")
|
| 818 |
+
@nx._dispatchable(returns_graph=True)
|
| 819 |
+
def transitive_reduction(G):
|
| 820 |
+
"""Returns transitive reduction of a directed graph
|
| 821 |
+
|
| 822 |
+
The transitive reduction of G = (V,E) is a graph G- = (V,E-) such that
|
| 823 |
+
for all v,w in V there is an edge (v,w) in E- if and only if (v,w) is
|
| 824 |
+
in E and there is no path from v to w in G with length greater than 1.
|
| 825 |
+
|
| 826 |
+
Parameters
|
| 827 |
+
----------
|
| 828 |
+
G : NetworkX DiGraph
|
| 829 |
+
A directed acyclic graph (DAG)
|
| 830 |
+
|
| 831 |
+
Returns
|
| 832 |
+
-------
|
| 833 |
+
NetworkX DiGraph
|
| 834 |
+
The transitive reduction of `G`
|
| 835 |
+
|
| 836 |
+
Raises
|
| 837 |
+
------
|
| 838 |
+
NetworkXError
|
| 839 |
+
If `G` is not a directed acyclic graph (DAG) transitive reduction is
|
| 840 |
+
not uniquely defined and a :exc:`NetworkXError` exception is raised.
|
| 841 |
+
|
| 842 |
+
Examples
|
| 843 |
+
--------
|
| 844 |
+
To perform transitive reduction on a DiGraph:
|
| 845 |
+
|
| 846 |
+
>>> DG = nx.DiGraph([(1, 2), (2, 3), (1, 3)])
|
| 847 |
+
>>> TR = nx.transitive_reduction(DG)
|
| 848 |
+
>>> list(TR.edges)
|
| 849 |
+
[(1, 2), (2, 3)]
|
| 850 |
+
|
| 851 |
+
To avoid unnecessary data copies, this implementation does not return a
|
| 852 |
+
DiGraph with node/edge data.
|
| 853 |
+
To perform transitive reduction on a DiGraph and transfer node/edge data:
|
| 854 |
+
|
| 855 |
+
>>> DG = nx.DiGraph()
|
| 856 |
+
>>> DG.add_edges_from([(1, 2), (2, 3), (1, 3)], color="red")
|
| 857 |
+
>>> TR = nx.transitive_reduction(DG)
|
| 858 |
+
>>> TR.add_nodes_from(DG.nodes(data=True))
|
| 859 |
+
>>> TR.add_edges_from((u, v, DG.edges[u, v]) for u, v in TR.edges)
|
| 860 |
+
>>> list(TR.edges(data=True))
|
| 861 |
+
[(1, 2, {'color': 'red'}), (2, 3, {'color': 'red'})]
|
| 862 |
+
|
| 863 |
+
References
|
| 864 |
+
----------
|
| 865 |
+
https://en.wikipedia.org/wiki/Transitive_reduction
|
| 866 |
+
|
| 867 |
+
"""
|
| 868 |
+
if not is_directed_acyclic_graph(G):
|
| 869 |
+
msg = "Directed Acyclic Graph required for transitive_reduction"
|
| 870 |
+
raise nx.NetworkXError(msg)
|
| 871 |
+
TR = nx.DiGraph()
|
| 872 |
+
TR.add_nodes_from(G.nodes())
|
| 873 |
+
descendants = {}
|
| 874 |
+
# count before removing set stored in descendants
|
| 875 |
+
check_count = dict(G.in_degree)
|
| 876 |
+
for u in G:
|
| 877 |
+
u_nbrs = set(G[u])
|
| 878 |
+
for v in G[u]:
|
| 879 |
+
if v in u_nbrs:
|
| 880 |
+
if v not in descendants:
|
| 881 |
+
descendants[v] = {y for x, y in nx.dfs_edges(G, v)}
|
| 882 |
+
u_nbrs -= descendants[v]
|
| 883 |
+
check_count[v] -= 1
|
| 884 |
+
if check_count[v] == 0:
|
| 885 |
+
del descendants[v]
|
| 886 |
+
TR.add_edges_from((u, v) for v in u_nbrs)
|
| 887 |
+
return TR
|
| 888 |
+
|
| 889 |
+
|
| 890 |
+
@not_implemented_for("undirected")
|
| 891 |
+
@nx._dispatchable
|
| 892 |
+
def antichains(G, topo_order=None):
|
| 893 |
+
"""Generates antichains from a directed acyclic graph (DAG).
|
| 894 |
+
|
| 895 |
+
An antichain is a subset of a partially ordered set such that any
|
| 896 |
+
two elements in the subset are incomparable.
|
| 897 |
+
|
| 898 |
+
Parameters
|
| 899 |
+
----------
|
| 900 |
+
G : NetworkX DiGraph
|
| 901 |
+
A directed acyclic graph (DAG)
|
| 902 |
+
|
| 903 |
+
topo_order: list or tuple, optional
|
| 904 |
+
A topological order for G (if None, the function will compute one)
|
| 905 |
+
|
| 906 |
+
Yields
|
| 907 |
+
------
|
| 908 |
+
antichain : list
|
| 909 |
+
a list of nodes in `G` representing an antichain
|
| 910 |
+
|
| 911 |
+
Raises
|
| 912 |
+
------
|
| 913 |
+
NetworkXNotImplemented
|
| 914 |
+
If `G` is not directed
|
| 915 |
+
|
| 916 |
+
NetworkXUnfeasible
|
| 917 |
+
If `G` contains a cycle
|
| 918 |
+
|
| 919 |
+
Examples
|
| 920 |
+
--------
|
| 921 |
+
>>> DG = nx.DiGraph([(1, 2), (1, 3)])
|
| 922 |
+
>>> list(nx.antichains(DG))
|
| 923 |
+
[[], [3], [2], [2, 3], [1]]
|
| 924 |
+
|
| 925 |
+
Notes
|
| 926 |
+
-----
|
| 927 |
+
This function was originally developed by Peter Jipsen and Franco Saliola
|
| 928 |
+
for the SAGE project. It's included in NetworkX with permission from the
|
| 929 |
+
authors. Original SAGE code at:
|
| 930 |
+
|
| 931 |
+
https://github.com/sagemath/sage/blob/master/src/sage/combinat/posets/hasse_diagram.py
|
| 932 |
+
|
| 933 |
+
References
|
| 934 |
+
----------
|
| 935 |
+
.. [1] Free Lattices, by R. Freese, J. Jezek and J. B. Nation,
|
| 936 |
+
AMS, Vol 42, 1995, p. 226.
|
| 937 |
+
"""
|
| 938 |
+
if topo_order is None:
|
| 939 |
+
topo_order = list(nx.topological_sort(G))
|
| 940 |
+
|
| 941 |
+
TC = nx.transitive_closure_dag(G, topo_order)
|
| 942 |
+
antichains_stacks = [([], list(reversed(topo_order)))]
|
| 943 |
+
|
| 944 |
+
while antichains_stacks:
|
| 945 |
+
(antichain, stack) = antichains_stacks.pop()
|
| 946 |
+
# Invariant:
|
| 947 |
+
# - the elements of antichain are independent
|
| 948 |
+
# - the elements of stack are independent from those of antichain
|
| 949 |
+
yield antichain
|
| 950 |
+
while stack:
|
| 951 |
+
x = stack.pop()
|
| 952 |
+
new_antichain = antichain + [x]
|
| 953 |
+
new_stack = [t for t in stack if not ((t in TC[x]) or (x in TC[t]))]
|
| 954 |
+
antichains_stacks.append((new_antichain, new_stack))
|
| 955 |
+
|
| 956 |
+
|
| 957 |
+
@not_implemented_for("undirected")
|
| 958 |
+
@nx._dispatchable(edge_attrs={"weight": "default_weight"})
|
| 959 |
+
def dag_longest_path(G, weight="weight", default_weight=1, topo_order=None):
|
| 960 |
+
"""Returns the longest path in a directed acyclic graph (DAG).
|
| 961 |
+
|
| 962 |
+
If `G` has edges with `weight` attribute the edge data are used as
|
| 963 |
+
weight values.
|
| 964 |
+
|
| 965 |
+
Parameters
|
| 966 |
+
----------
|
| 967 |
+
G : NetworkX DiGraph
|
| 968 |
+
A directed acyclic graph (DAG)
|
| 969 |
+
|
| 970 |
+
weight : str, optional
|
| 971 |
+
Edge data key to use for weight
|
| 972 |
+
|
| 973 |
+
default_weight : int, optional
|
| 974 |
+
The weight of edges that do not have a weight attribute
|
| 975 |
+
|
| 976 |
+
topo_order: list or tuple, optional
|
| 977 |
+
A topological order for `G` (if None, the function will compute one)
|
| 978 |
+
|
| 979 |
+
Returns
|
| 980 |
+
-------
|
| 981 |
+
list
|
| 982 |
+
Longest path
|
| 983 |
+
|
| 984 |
+
Raises
|
| 985 |
+
------
|
| 986 |
+
NetworkXNotImplemented
|
| 987 |
+
If `G` is not directed
|
| 988 |
+
|
| 989 |
+
Examples
|
| 990 |
+
--------
|
| 991 |
+
>>> DG = nx.DiGraph(
|
| 992 |
+
... [(0, 1, {"cost": 1}), (1, 2, {"cost": 1}), (0, 2, {"cost": 42})]
|
| 993 |
+
... )
|
| 994 |
+
>>> list(nx.all_simple_paths(DG, 0, 2))
|
| 995 |
+
[[0, 1, 2], [0, 2]]
|
| 996 |
+
>>> nx.dag_longest_path(DG)
|
| 997 |
+
[0, 1, 2]
|
| 998 |
+
>>> nx.dag_longest_path(DG, weight="cost")
|
| 999 |
+
[0, 2]
|
| 1000 |
+
|
| 1001 |
+
In the case where multiple valid topological orderings exist, `topo_order`
|
| 1002 |
+
can be used to specify a specific ordering:
|
| 1003 |
+
|
| 1004 |
+
>>> DG = nx.DiGraph([(0, 1), (0, 2)])
|
| 1005 |
+
>>> sorted(nx.all_topological_sorts(DG)) # Valid topological orderings
|
| 1006 |
+
[[0, 1, 2], [0, 2, 1]]
|
| 1007 |
+
>>> nx.dag_longest_path(DG, topo_order=[0, 1, 2])
|
| 1008 |
+
[0, 1]
|
| 1009 |
+
>>> nx.dag_longest_path(DG, topo_order=[0, 2, 1])
|
| 1010 |
+
[0, 2]
|
| 1011 |
+
|
| 1012 |
+
See also
|
| 1013 |
+
--------
|
| 1014 |
+
dag_longest_path_length
|
| 1015 |
+
|
| 1016 |
+
"""
|
| 1017 |
+
if not G:
|
| 1018 |
+
return []
|
| 1019 |
+
|
| 1020 |
+
if topo_order is None:
|
| 1021 |
+
topo_order = nx.topological_sort(G)
|
| 1022 |
+
|
| 1023 |
+
dist = {} # stores {v : (length, u)}
|
| 1024 |
+
for v in topo_order:
|
| 1025 |
+
us = [
|
| 1026 |
+
(
|
| 1027 |
+
dist[u][0]
|
| 1028 |
+
+ (
|
| 1029 |
+
max(data.values(), key=lambda x: x.get(weight, default_weight))
|
| 1030 |
+
if G.is_multigraph()
|
| 1031 |
+
else data
|
| 1032 |
+
).get(weight, default_weight),
|
| 1033 |
+
u,
|
| 1034 |
+
)
|
| 1035 |
+
for u, data in G.pred[v].items()
|
| 1036 |
+
]
|
| 1037 |
+
|
| 1038 |
+
# Use the best predecessor if there is one and its distance is
|
| 1039 |
+
# non-negative, otherwise terminate.
|
| 1040 |
+
maxu = max(us, key=lambda x: x[0]) if us else (0, v)
|
| 1041 |
+
dist[v] = maxu if maxu[0] >= 0 else (0, v)
|
| 1042 |
+
|
| 1043 |
+
u = None
|
| 1044 |
+
v = max(dist, key=lambda x: dist[x][0])
|
| 1045 |
+
path = []
|
| 1046 |
+
while u != v:
|
| 1047 |
+
path.append(v)
|
| 1048 |
+
u = v
|
| 1049 |
+
v = dist[v][1]
|
| 1050 |
+
|
| 1051 |
+
path.reverse()
|
| 1052 |
+
return path
|
| 1053 |
+
|
| 1054 |
+
|
| 1055 |
+
@not_implemented_for("undirected")
|
| 1056 |
+
@nx._dispatchable(edge_attrs={"weight": "default_weight"})
|
| 1057 |
+
def dag_longest_path_length(G, weight="weight", default_weight=1):
|
| 1058 |
+
"""Returns the longest path length in a DAG
|
| 1059 |
+
|
| 1060 |
+
Parameters
|
| 1061 |
+
----------
|
| 1062 |
+
G : NetworkX DiGraph
|
| 1063 |
+
A directed acyclic graph (DAG)
|
| 1064 |
+
|
| 1065 |
+
weight : string, optional
|
| 1066 |
+
Edge data key to use for weight
|
| 1067 |
+
|
| 1068 |
+
default_weight : int, optional
|
| 1069 |
+
The weight of edges that do not have a weight attribute
|
| 1070 |
+
|
| 1071 |
+
Returns
|
| 1072 |
+
-------
|
| 1073 |
+
int
|
| 1074 |
+
Longest path length
|
| 1075 |
+
|
| 1076 |
+
Raises
|
| 1077 |
+
------
|
| 1078 |
+
NetworkXNotImplemented
|
| 1079 |
+
If `G` is not directed
|
| 1080 |
+
|
| 1081 |
+
Examples
|
| 1082 |
+
--------
|
| 1083 |
+
>>> DG = nx.DiGraph(
|
| 1084 |
+
... [(0, 1, {"cost": 1}), (1, 2, {"cost": 1}), (0, 2, {"cost": 42})]
|
| 1085 |
+
... )
|
| 1086 |
+
>>> list(nx.all_simple_paths(DG, 0, 2))
|
| 1087 |
+
[[0, 1, 2], [0, 2]]
|
| 1088 |
+
>>> nx.dag_longest_path_length(DG)
|
| 1089 |
+
2
|
| 1090 |
+
>>> nx.dag_longest_path_length(DG, weight="cost")
|
| 1091 |
+
42
|
| 1092 |
+
|
| 1093 |
+
See also
|
| 1094 |
+
--------
|
| 1095 |
+
dag_longest_path
|
| 1096 |
+
"""
|
| 1097 |
+
path = nx.dag_longest_path(G, weight, default_weight)
|
| 1098 |
+
path_length = 0
|
| 1099 |
+
if G.is_multigraph():
|
| 1100 |
+
for u, v in pairwise(path):
|
| 1101 |
+
i = max(G[u][v], key=lambda x: G[u][v][x].get(weight, default_weight))
|
| 1102 |
+
path_length += G[u][v][i].get(weight, default_weight)
|
| 1103 |
+
else:
|
| 1104 |
+
for u, v in pairwise(path):
|
| 1105 |
+
path_length += G[u][v].get(weight, default_weight)
|
| 1106 |
+
|
| 1107 |
+
return path_length
|
| 1108 |
+
|
| 1109 |
+
|
| 1110 |
+
@nx._dispatchable
|
| 1111 |
+
def root_to_leaf_paths(G):
|
| 1112 |
+
"""Yields root-to-leaf paths in a directed acyclic graph.
|
| 1113 |
+
|
| 1114 |
+
`G` must be a directed acyclic graph. If not, the behavior of this
|
| 1115 |
+
function is undefined. A "root" in this graph is a node of in-degree
|
| 1116 |
+
zero and a "leaf" a node of out-degree zero.
|
| 1117 |
+
|
| 1118 |
+
When invoked, this function iterates over each path from any root to
|
| 1119 |
+
any leaf. A path is a list of nodes.
|
| 1120 |
+
|
| 1121 |
+
"""
|
| 1122 |
+
roots = (v for v, d in G.in_degree() if d == 0)
|
| 1123 |
+
leaves = (v for v, d in G.out_degree() if d == 0)
|
| 1124 |
+
all_paths = partial(nx.all_simple_paths, G)
|
| 1125 |
+
# TODO In Python 3, this would be better as `yield from ...`.
|
| 1126 |
+
return chaini(starmap(all_paths, product(roots, leaves)))
|
| 1127 |
+
|
| 1128 |
+
|
| 1129 |
+
@not_implemented_for("multigraph")
|
| 1130 |
+
@not_implemented_for("undirected")
|
| 1131 |
+
@nx._dispatchable(returns_graph=True)
|
| 1132 |
+
def dag_to_branching(G):
|
| 1133 |
+
"""Returns a branching representing all (overlapping) paths from
|
| 1134 |
+
root nodes to leaf nodes in the given directed acyclic graph.
|
| 1135 |
+
|
| 1136 |
+
As described in :mod:`networkx.algorithms.tree.recognition`, a
|
| 1137 |
+
*branching* is a directed forest in which each node has at most one
|
| 1138 |
+
parent. In other words, a branching is a disjoint union of
|
| 1139 |
+
*arborescences*. For this function, each node of in-degree zero in
|
| 1140 |
+
`G` becomes a root of one of the arborescences, and there will be
|
| 1141 |
+
one leaf node for each distinct path from that root to a leaf node
|
| 1142 |
+
in `G`.
|
| 1143 |
+
|
| 1144 |
+
Each node `v` in `G` with *k* parents becomes *k* distinct nodes in
|
| 1145 |
+
the returned branching, one for each parent, and the sub-DAG rooted
|
| 1146 |
+
at `v` is duplicated for each copy. The algorithm then recurses on
|
| 1147 |
+
the children of each copy of `v`.
|
| 1148 |
+
|
| 1149 |
+
Parameters
|
| 1150 |
+
----------
|
| 1151 |
+
G : NetworkX graph
|
| 1152 |
+
A directed acyclic graph.
|
| 1153 |
+
|
| 1154 |
+
Returns
|
| 1155 |
+
-------
|
| 1156 |
+
DiGraph
|
| 1157 |
+
The branching in which there is a bijection between root-to-leaf
|
| 1158 |
+
paths in `G` (in which multiple paths may share the same leaf)
|
| 1159 |
+
and root-to-leaf paths in the branching (in which there is a
|
| 1160 |
+
unique path from a root to a leaf).
|
| 1161 |
+
|
| 1162 |
+
Each node has an attribute 'source' whose value is the original
|
| 1163 |
+
node to which this node corresponds. No other graph, node, or
|
| 1164 |
+
edge attributes are copied into this new graph.
|
| 1165 |
+
|
| 1166 |
+
Raises
|
| 1167 |
+
------
|
| 1168 |
+
NetworkXNotImplemented
|
| 1169 |
+
If `G` is not directed, or if `G` is a multigraph.
|
| 1170 |
+
|
| 1171 |
+
HasACycle
|
| 1172 |
+
If `G` is not acyclic.
|
| 1173 |
+
|
| 1174 |
+
Examples
|
| 1175 |
+
--------
|
| 1176 |
+
To examine which nodes in the returned branching were produced by
|
| 1177 |
+
which original node in the directed acyclic graph, we can collect
|
| 1178 |
+
the mapping from source node to new nodes into a dictionary. For
|
| 1179 |
+
example, consider the directed diamond graph::
|
| 1180 |
+
|
| 1181 |
+
>>> from collections import defaultdict
|
| 1182 |
+
>>> from operator import itemgetter
|
| 1183 |
+
>>>
|
| 1184 |
+
>>> G = nx.DiGraph(nx.utils.pairwise("abd"))
|
| 1185 |
+
>>> G.add_edges_from(nx.utils.pairwise("acd"))
|
| 1186 |
+
>>> B = nx.dag_to_branching(G)
|
| 1187 |
+
>>>
|
| 1188 |
+
>>> sources = defaultdict(set)
|
| 1189 |
+
>>> for v, source in B.nodes(data="source"):
|
| 1190 |
+
... sources[source].add(v)
|
| 1191 |
+
>>> len(sources["a"])
|
| 1192 |
+
1
|
| 1193 |
+
>>> len(sources["d"])
|
| 1194 |
+
2
|
| 1195 |
+
|
| 1196 |
+
To copy node attributes from the original graph to the new graph,
|
| 1197 |
+
you can use a dictionary like the one constructed in the above
|
| 1198 |
+
example::
|
| 1199 |
+
|
| 1200 |
+
>>> for source, nodes in sources.items():
|
| 1201 |
+
... for v in nodes:
|
| 1202 |
+
... B.nodes[v].update(G.nodes[source])
|
| 1203 |
+
|
| 1204 |
+
Notes
|
| 1205 |
+
-----
|
| 1206 |
+
This function is not idempotent in the sense that the node labels in
|
| 1207 |
+
the returned branching may be uniquely generated each time the
|
| 1208 |
+
function is invoked. In fact, the node labels may not be integers;
|
| 1209 |
+
in order to relabel the nodes to be more readable, you can use the
|
| 1210 |
+
:func:`networkx.convert_node_labels_to_integers` function.
|
| 1211 |
+
|
| 1212 |
+
The current implementation of this function uses
|
| 1213 |
+
:func:`networkx.prefix_tree`, so it is subject to the limitations of
|
| 1214 |
+
that function.
|
| 1215 |
+
|
| 1216 |
+
"""
|
| 1217 |
+
if has_cycle(G):
|
| 1218 |
+
msg = "dag_to_branching is only defined for acyclic graphs"
|
| 1219 |
+
raise nx.HasACycle(msg)
|
| 1220 |
+
paths = root_to_leaf_paths(G)
|
| 1221 |
+
B = nx.prefix_tree(paths)
|
| 1222 |
+
# Remove the synthetic `root`(0) and `NIL`(-1) nodes from the tree
|
| 1223 |
+
B.remove_node(0)
|
| 1224 |
+
B.remove_node(-1)
|
| 1225 |
+
return B
|
| 1226 |
+
|
| 1227 |
+
|
| 1228 |
+
@not_implemented_for("undirected")
|
| 1229 |
+
@nx._dispatchable
|
| 1230 |
+
def compute_v_structures(G):
|
| 1231 |
+
"""Yields 3-node tuples that represent the v-structures in `G`.
|
| 1232 |
+
|
| 1233 |
+
.. deprecated:: 3.4
|
| 1234 |
+
|
| 1235 |
+
`compute_v_structures` actually yields colliders. It will be removed in
|
| 1236 |
+
version 3.6. Use `nx.dag.v_structures` or `nx.dag.colliders` instead.
|
| 1237 |
+
|
| 1238 |
+
Colliders are triples in the directed acyclic graph (DAG) where two parent nodes
|
| 1239 |
+
point to the same child node. V-structures are colliders where the two parent
|
| 1240 |
+
nodes are not adjacent. In a causal graph setting, the parents do not directly
|
| 1241 |
+
depend on each other, but conditioning on the child node provides an association.
|
| 1242 |
+
|
| 1243 |
+
Parameters
|
| 1244 |
+
----------
|
| 1245 |
+
G : graph
|
| 1246 |
+
A networkx `~networkx.DiGraph`.
|
| 1247 |
+
|
| 1248 |
+
Yields
|
| 1249 |
+
------
|
| 1250 |
+
A 3-tuple representation of a v-structure
|
| 1251 |
+
Each v-structure is a 3-tuple with the parent, collider, and other parent.
|
| 1252 |
+
|
| 1253 |
+
Raises
|
| 1254 |
+
------
|
| 1255 |
+
NetworkXNotImplemented
|
| 1256 |
+
If `G` is an undirected graph.
|
| 1257 |
+
|
| 1258 |
+
Examples
|
| 1259 |
+
--------
|
| 1260 |
+
>>> G = nx.DiGraph([(1, 2), (0, 4), (3, 1), (2, 4), (0, 5), (4, 5), (1, 5)])
|
| 1261 |
+
>>> nx.is_directed_acyclic_graph(G)
|
| 1262 |
+
True
|
| 1263 |
+
>>> list(nx.compute_v_structures(G))
|
| 1264 |
+
[(0, 4, 2), (0, 5, 4), (0, 5, 1), (4, 5, 1)]
|
| 1265 |
+
|
| 1266 |
+
See Also
|
| 1267 |
+
--------
|
| 1268 |
+
v_structures
|
| 1269 |
+
colliders
|
| 1270 |
+
|
| 1271 |
+
Notes
|
| 1272 |
+
-----
|
| 1273 |
+
This function was written to be used on DAGs, however it works on cyclic graphs
|
| 1274 |
+
too. Since colliders are referred to in the cyclic causal graph literature
|
| 1275 |
+
[2]_ we allow cyclic graphs in this function. It is suggested that you test if
|
| 1276 |
+
your input graph is acyclic as in the example if you want that property.
|
| 1277 |
+
|
| 1278 |
+
References
|
| 1279 |
+
----------
|
| 1280 |
+
.. [1] `Pearl's PRIMER <https://bayes.cs.ucla.edu/PRIMER/primer-ch2.pdf>`_
|
| 1281 |
+
Ch-2 page 50: v-structures def.
|
| 1282 |
+
.. [2] A Hyttinen, P.O. Hoyer, F. Eberhardt, M J ̈arvisalo, (2013)
|
| 1283 |
+
"Discovering cyclic causal models with latent variables:
|
| 1284 |
+
a general SAT-based procedure", UAI'13: Proceedings of the Twenty-Ninth
|
| 1285 |
+
Conference on Uncertainty in Artificial Intelligence, pg 301–310,
|
| 1286 |
+
`doi:10.5555/3023638.3023669 <https://dl.acm.org/doi/10.5555/3023638.3023669>`_
|
| 1287 |
+
"""
|
| 1288 |
+
import warnings
|
| 1289 |
+
|
| 1290 |
+
warnings.warn(
|
| 1291 |
+
(
|
| 1292 |
+
"\n\n`compute_v_structures` actually yields colliders. It will be\n"
|
| 1293 |
+
"removed in version 3.6. Use `nx.dag.v_structures` or `nx.dag.colliders`\n"
|
| 1294 |
+
"instead.\n"
|
| 1295 |
+
),
|
| 1296 |
+
category=DeprecationWarning,
|
| 1297 |
+
stacklevel=5,
|
| 1298 |
+
)
|
| 1299 |
+
|
| 1300 |
+
return colliders(G)
|
| 1301 |
+
|
| 1302 |
+
|
| 1303 |
+
@not_implemented_for("undirected")
|
| 1304 |
+
@nx._dispatchable
|
| 1305 |
+
def v_structures(G):
|
| 1306 |
+
"""Yields 3-node tuples that represent the v-structures in `G`.
|
| 1307 |
+
|
| 1308 |
+
Colliders are triples in the directed acyclic graph (DAG) where two parent nodes
|
| 1309 |
+
point to the same child node. V-structures are colliders where the two parent
|
| 1310 |
+
nodes are not adjacent. In a causal graph setting, the parents do not directly
|
| 1311 |
+
depend on each other, but conditioning on the child node provides an association.
|
| 1312 |
+
|
| 1313 |
+
Parameters
|
| 1314 |
+
----------
|
| 1315 |
+
G : graph
|
| 1316 |
+
A networkx `~networkx.DiGraph`.
|
| 1317 |
+
|
| 1318 |
+
Yields
|
| 1319 |
+
------
|
| 1320 |
+
A 3-tuple representation of a v-structure
|
| 1321 |
+
Each v-structure is a 3-tuple with the parent, collider, and other parent.
|
| 1322 |
+
|
| 1323 |
+
Raises
|
| 1324 |
+
------
|
| 1325 |
+
NetworkXNotImplemented
|
| 1326 |
+
If `G` is an undirected graph.
|
| 1327 |
+
|
| 1328 |
+
Examples
|
| 1329 |
+
--------
|
| 1330 |
+
>>> G = nx.DiGraph([(1, 2), (0, 4), (3, 1), (2, 4), (0, 5), (4, 5), (1, 5)])
|
| 1331 |
+
>>> nx.is_directed_acyclic_graph(G)
|
| 1332 |
+
True
|
| 1333 |
+
>>> list(nx.dag.v_structures(G))
|
| 1334 |
+
[(0, 4, 2), (0, 5, 1), (4, 5, 1)]
|
| 1335 |
+
|
| 1336 |
+
See Also
|
| 1337 |
+
--------
|
| 1338 |
+
colliders
|
| 1339 |
+
|
| 1340 |
+
Notes
|
| 1341 |
+
-----
|
| 1342 |
+
This function was written to be used on DAGs, however it works on cyclic graphs
|
| 1343 |
+
too. Since colliders are referred to in the cyclic causal graph literature
|
| 1344 |
+
[2]_ we allow cyclic graphs in this function. It is suggested that you test if
|
| 1345 |
+
your input graph is acyclic as in the example if you want that property.
|
| 1346 |
+
|
| 1347 |
+
References
|
| 1348 |
+
----------
|
| 1349 |
+
.. [1] `Pearl's PRIMER <https://bayes.cs.ucla.edu/PRIMER/primer-ch2.pdf>`_
|
| 1350 |
+
Ch-2 page 50: v-structures def.
|
| 1351 |
+
.. [2] A Hyttinen, P.O. Hoyer, F. Eberhardt, M J ̈arvisalo, (2013)
|
| 1352 |
+
"Discovering cyclic causal models with latent variables:
|
| 1353 |
+
a general SAT-based procedure", UAI'13: Proceedings of the Twenty-Ninth
|
| 1354 |
+
Conference on Uncertainty in Artificial Intelligence, pg 301–310,
|
| 1355 |
+
`doi:10.5555/3023638.3023669 <https://dl.acm.org/doi/10.5555/3023638.3023669>`_
|
| 1356 |
+
"""
|
| 1357 |
+
for p1, c, p2 in colliders(G):
|
| 1358 |
+
if not (G.has_edge(p1, p2) or G.has_edge(p2, p1)):
|
| 1359 |
+
yield (p1, c, p2)
|
| 1360 |
+
|
| 1361 |
+
|
| 1362 |
+
@not_implemented_for("undirected")
|
| 1363 |
+
@nx._dispatchable
|
| 1364 |
+
def colliders(G):
|
| 1365 |
+
"""Yields 3-node tuples that represent the colliders in `G`.
|
| 1366 |
+
|
| 1367 |
+
In a Directed Acyclic Graph (DAG), if you have three nodes A, B, and C, and
|
| 1368 |
+
there are edges from A to C and from B to C, then C is a collider [1]_ . In
|
| 1369 |
+
a causal graph setting, this means that both events A and B are "causing" C,
|
| 1370 |
+
and conditioning on C provide an association between A and B even if
|
| 1371 |
+
no direct causal relationship exists between A and B.
|
| 1372 |
+
|
| 1373 |
+
Parameters
|
| 1374 |
+
----------
|
| 1375 |
+
G : graph
|
| 1376 |
+
A networkx `~networkx.DiGraph`.
|
| 1377 |
+
|
| 1378 |
+
Yields
|
| 1379 |
+
------
|
| 1380 |
+
A 3-tuple representation of a collider
|
| 1381 |
+
Each collider is a 3-tuple with the parent, collider, and other parent.
|
| 1382 |
+
|
| 1383 |
+
Raises
|
| 1384 |
+
------
|
| 1385 |
+
NetworkXNotImplemented
|
| 1386 |
+
If `G` is an undirected graph.
|
| 1387 |
+
|
| 1388 |
+
Examples
|
| 1389 |
+
--------
|
| 1390 |
+
>>> G = nx.DiGraph([(1, 2), (0, 4), (3, 1), (2, 4), (0, 5), (4, 5), (1, 5)])
|
| 1391 |
+
>>> nx.is_directed_acyclic_graph(G)
|
| 1392 |
+
True
|
| 1393 |
+
>>> list(nx.dag.colliders(G))
|
| 1394 |
+
[(0, 4, 2), (0, 5, 4), (0, 5, 1), (4, 5, 1)]
|
| 1395 |
+
|
| 1396 |
+
See Also
|
| 1397 |
+
--------
|
| 1398 |
+
v_structures
|
| 1399 |
+
|
| 1400 |
+
Notes
|
| 1401 |
+
-----
|
| 1402 |
+
This function was written to be used on DAGs, however it works on cyclic graphs
|
| 1403 |
+
too. Since colliders are referred to in the cyclic causal graph literature
|
| 1404 |
+
[2]_ we allow cyclic graphs in this function. It is suggested that you test if
|
| 1405 |
+
your input graph is acyclic as in the example if you want that property.
|
| 1406 |
+
|
| 1407 |
+
References
|
| 1408 |
+
----------
|
| 1409 |
+
.. [1] `Wikipedia: Collider in causal graphs <https://en.wikipedia.org/wiki/Collider_(statistics)>`_
|
| 1410 |
+
.. [2] A Hyttinen, P.O. Hoyer, F. Eberhardt, M J ̈arvisalo, (2013)
|
| 1411 |
+
"Discovering cyclic causal models with latent variables:
|
| 1412 |
+
a general SAT-based procedure", UAI'13: Proceedings of the Twenty-Ninth
|
| 1413 |
+
Conference on Uncertainty in Artificial Intelligence, pg 301–310,
|
| 1414 |
+
`doi:10.5555/3023638.3023669 <https://dl.acm.org/doi/10.5555/3023638.3023669>`_
|
| 1415 |
+
"""
|
| 1416 |
+
for node in G.nodes:
|
| 1417 |
+
for p1, p2 in combinations(G.predecessors(node), 2):
|
| 1418 |
+
yield (p1, node, p2)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/distance_measures.py
ADDED
|
@@ -0,0 +1,1022 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Graph diameter, radius, eccentricity and other properties."""
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.utils import not_implemented_for
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"eccentricity",
|
| 10 |
+
"diameter",
|
| 11 |
+
"harmonic_diameter",
|
| 12 |
+
"radius",
|
| 13 |
+
"periphery",
|
| 14 |
+
"center",
|
| 15 |
+
"barycenter",
|
| 16 |
+
"resistance_distance",
|
| 17 |
+
"kemeny_constant",
|
| 18 |
+
"effective_graph_resistance",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _extrema_bounding(G, compute="diameter", weight=None):
|
| 23 |
+
"""Compute requested extreme distance metric of undirected graph G
|
| 24 |
+
|
| 25 |
+
Computation is based on smart lower and upper bounds, and in practice
|
| 26 |
+
linear in the number of nodes, rather than quadratic (except for some
|
| 27 |
+
border cases such as complete graphs or circle shaped graphs).
|
| 28 |
+
|
| 29 |
+
Parameters
|
| 30 |
+
----------
|
| 31 |
+
G : NetworkX graph
|
| 32 |
+
An undirected graph
|
| 33 |
+
|
| 34 |
+
compute : string denoting the requesting metric
|
| 35 |
+
"diameter" for the maximal eccentricity value,
|
| 36 |
+
"radius" for the minimal eccentricity value,
|
| 37 |
+
"periphery" for the set of nodes with eccentricity equal to the diameter,
|
| 38 |
+
"center" for the set of nodes with eccentricity equal to the radius,
|
| 39 |
+
"eccentricities" for the maximum distance from each node to all other nodes in G
|
| 40 |
+
|
| 41 |
+
weight : string, function, or None
|
| 42 |
+
If this is a string, then edge weights will be accessed via the
|
| 43 |
+
edge attribute with this key (that is, the weight of the edge
|
| 44 |
+
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
|
| 45 |
+
such edge attribute exists, the weight of the edge is assumed to
|
| 46 |
+
be one.
|
| 47 |
+
|
| 48 |
+
If this is a function, the weight of an edge is the value
|
| 49 |
+
returned by the function. The function must accept exactly three
|
| 50 |
+
positional arguments: the two endpoints of an edge and the
|
| 51 |
+
dictionary of edge attributes for that edge. The function must
|
| 52 |
+
return a number.
|
| 53 |
+
|
| 54 |
+
If this is None, every edge has weight/distance/cost 1.
|
| 55 |
+
|
| 56 |
+
Weights stored as floating point values can lead to small round-off
|
| 57 |
+
errors in distances. Use integer weights to avoid this.
|
| 58 |
+
|
| 59 |
+
Weights should be positive, since they are distances.
|
| 60 |
+
|
| 61 |
+
Returns
|
| 62 |
+
-------
|
| 63 |
+
value : value of the requested metric
|
| 64 |
+
int for "diameter" and "radius" or
|
| 65 |
+
list of nodes for "center" and "periphery" or
|
| 66 |
+
dictionary of eccentricity values keyed by node for "eccentricities"
|
| 67 |
+
|
| 68 |
+
Raises
|
| 69 |
+
------
|
| 70 |
+
NetworkXError
|
| 71 |
+
If the graph consists of multiple components
|
| 72 |
+
ValueError
|
| 73 |
+
If `compute` is not one of "diameter", "radius", "periphery", "center", or "eccentricities".
|
| 74 |
+
|
| 75 |
+
Notes
|
| 76 |
+
-----
|
| 77 |
+
This algorithm was proposed in [1]_ and discussed further in [2]_ and [3]_.
|
| 78 |
+
|
| 79 |
+
References
|
| 80 |
+
----------
|
| 81 |
+
.. [1] F. W. Takes, W. A. Kosters,
|
| 82 |
+
"Determining the diameter of small world networks."
|
| 83 |
+
Proceedings of the 20th ACM international conference on Information and knowledge management, 2011
|
| 84 |
+
https://dl.acm.org/doi/abs/10.1145/2063576.2063748
|
| 85 |
+
.. [2] F. W. Takes, W. A. Kosters,
|
| 86 |
+
"Computing the Eccentricity Distribution of Large Graphs."
|
| 87 |
+
Algorithms, 2013
|
| 88 |
+
https://www.mdpi.com/1999-4893/6/1/100
|
| 89 |
+
.. [3] M. Borassi, P. Crescenzi, M. Habib, W. A. Kosters, A. Marino, F. W. Takes,
|
| 90 |
+
"Fast diameter and radius BFS-based computation in (weakly connected) real-world graphs: With an application to the six degrees of separation games. "
|
| 91 |
+
Theoretical Computer Science, 2015
|
| 92 |
+
https://www.sciencedirect.com/science/article/pii/S0304397515001644
|
| 93 |
+
"""
|
| 94 |
+
# init variables
|
| 95 |
+
degrees = dict(G.degree()) # start with the highest degree node
|
| 96 |
+
minlowernode = max(degrees, key=degrees.get)
|
| 97 |
+
N = len(degrees) # number of nodes
|
| 98 |
+
# alternate between smallest lower and largest upper bound
|
| 99 |
+
high = False
|
| 100 |
+
# status variables
|
| 101 |
+
ecc_lower = dict.fromkeys(G, 0)
|
| 102 |
+
ecc_upper = dict.fromkeys(G, N)
|
| 103 |
+
candidates = set(G)
|
| 104 |
+
|
| 105 |
+
# (re)set bound extremes
|
| 106 |
+
minlower = N
|
| 107 |
+
maxlower = 0
|
| 108 |
+
minupper = N
|
| 109 |
+
maxupper = 0
|
| 110 |
+
|
| 111 |
+
# repeat the following until there are no more candidates
|
| 112 |
+
while candidates:
|
| 113 |
+
if high:
|
| 114 |
+
current = maxuppernode # select node with largest upper bound
|
| 115 |
+
else:
|
| 116 |
+
current = minlowernode # select node with smallest lower bound
|
| 117 |
+
high = not high
|
| 118 |
+
|
| 119 |
+
# get distances from/to current node and derive eccentricity
|
| 120 |
+
dist = nx.shortest_path_length(G, source=current, weight=weight)
|
| 121 |
+
|
| 122 |
+
if len(dist) != N:
|
| 123 |
+
msg = "Cannot compute metric because graph is not connected."
|
| 124 |
+
raise nx.NetworkXError(msg)
|
| 125 |
+
current_ecc = max(dist.values())
|
| 126 |
+
|
| 127 |
+
# print status update
|
| 128 |
+
# print ("ecc of " + str(current) + " (" + str(ecc_lower[current]) + "/"
|
| 129 |
+
# + str(ecc_upper[current]) + ", deg: " + str(dist[current]) + ") is "
|
| 130 |
+
# + str(current_ecc))
|
| 131 |
+
# print(ecc_upper)
|
| 132 |
+
|
| 133 |
+
# (re)set bound extremes
|
| 134 |
+
maxuppernode = None
|
| 135 |
+
minlowernode = None
|
| 136 |
+
|
| 137 |
+
# update node bounds
|
| 138 |
+
for i in candidates:
|
| 139 |
+
# update eccentricity bounds
|
| 140 |
+
d = dist[i]
|
| 141 |
+
ecc_lower[i] = low = max(ecc_lower[i], max(d, (current_ecc - d)))
|
| 142 |
+
ecc_upper[i] = upp = min(ecc_upper[i], current_ecc + d)
|
| 143 |
+
|
| 144 |
+
# update min/max values of lower and upper bounds
|
| 145 |
+
minlower = min(ecc_lower[i], minlower)
|
| 146 |
+
maxlower = max(ecc_lower[i], maxlower)
|
| 147 |
+
minupper = min(ecc_upper[i], minupper)
|
| 148 |
+
maxupper = max(ecc_upper[i], maxupper)
|
| 149 |
+
|
| 150 |
+
# update candidate set
|
| 151 |
+
if compute == "diameter":
|
| 152 |
+
ruled_out = {
|
| 153 |
+
i
|
| 154 |
+
for i in candidates
|
| 155 |
+
if ecc_upper[i] <= maxlower and 2 * ecc_lower[i] >= maxupper
|
| 156 |
+
}
|
| 157 |
+
elif compute == "radius":
|
| 158 |
+
ruled_out = {
|
| 159 |
+
i
|
| 160 |
+
for i in candidates
|
| 161 |
+
if ecc_lower[i] >= minupper and ecc_upper[i] + 1 <= 2 * minlower
|
| 162 |
+
}
|
| 163 |
+
elif compute == "periphery":
|
| 164 |
+
ruled_out = {
|
| 165 |
+
i
|
| 166 |
+
for i in candidates
|
| 167 |
+
if ecc_upper[i] < maxlower
|
| 168 |
+
and (maxlower == maxupper or ecc_lower[i] > maxupper)
|
| 169 |
+
}
|
| 170 |
+
elif compute == "center":
|
| 171 |
+
ruled_out = {
|
| 172 |
+
i
|
| 173 |
+
for i in candidates
|
| 174 |
+
if ecc_lower[i] > minupper
|
| 175 |
+
and (minlower == minupper or ecc_upper[i] + 1 < 2 * minlower)
|
| 176 |
+
}
|
| 177 |
+
elif compute == "eccentricities":
|
| 178 |
+
ruled_out = set()
|
| 179 |
+
else:
|
| 180 |
+
msg = "compute must be one of 'diameter', 'radius', 'periphery', 'center', 'eccentricities'"
|
| 181 |
+
raise ValueError(msg)
|
| 182 |
+
|
| 183 |
+
ruled_out.update(i for i in candidates if ecc_lower[i] == ecc_upper[i])
|
| 184 |
+
candidates -= ruled_out
|
| 185 |
+
|
| 186 |
+
# for i in ruled_out:
|
| 187 |
+
# print("removing %g: ecc_u: %g maxl: %g ecc_l: %g maxu: %g"%
|
| 188 |
+
# (i,ecc_upper[i],maxlower,ecc_lower[i],maxupper))
|
| 189 |
+
# print("node %g: ecc_u: %g maxl: %g ecc_l: %g maxu: %g"%
|
| 190 |
+
# (4,ecc_upper[4],maxlower,ecc_lower[4],maxupper))
|
| 191 |
+
# print("NODE 4: %g"%(ecc_upper[4] <= maxlower))
|
| 192 |
+
# print("NODE 4: %g"%(2 * ecc_lower[4] >= maxupper))
|
| 193 |
+
# print("NODE 4: %g"%(ecc_upper[4] <= maxlower
|
| 194 |
+
# and 2 * ecc_lower[4] >= maxupper))
|
| 195 |
+
|
| 196 |
+
# updating maxuppernode and minlowernode for selection in next round
|
| 197 |
+
for i in candidates:
|
| 198 |
+
if (
|
| 199 |
+
minlowernode is None
|
| 200 |
+
or (
|
| 201 |
+
ecc_lower[i] == ecc_lower[minlowernode]
|
| 202 |
+
and degrees[i] > degrees[minlowernode]
|
| 203 |
+
)
|
| 204 |
+
or (ecc_lower[i] < ecc_lower[minlowernode])
|
| 205 |
+
):
|
| 206 |
+
minlowernode = i
|
| 207 |
+
|
| 208 |
+
if (
|
| 209 |
+
maxuppernode is None
|
| 210 |
+
or (
|
| 211 |
+
ecc_upper[i] == ecc_upper[maxuppernode]
|
| 212 |
+
and degrees[i] > degrees[maxuppernode]
|
| 213 |
+
)
|
| 214 |
+
or (ecc_upper[i] > ecc_upper[maxuppernode])
|
| 215 |
+
):
|
| 216 |
+
maxuppernode = i
|
| 217 |
+
|
| 218 |
+
# print status update
|
| 219 |
+
# print (" min=" + str(minlower) + "/" + str(minupper) +
|
| 220 |
+
# " max=" + str(maxlower) + "/" + str(maxupper) +
|
| 221 |
+
# " candidates: " + str(len(candidates)))
|
| 222 |
+
# print("cand:",candidates)
|
| 223 |
+
# print("ecc_l",ecc_lower)
|
| 224 |
+
# print("ecc_u",ecc_upper)
|
| 225 |
+
# wait = input("press Enter to continue")
|
| 226 |
+
|
| 227 |
+
# return the correct value of the requested metric
|
| 228 |
+
if compute == "diameter":
|
| 229 |
+
return maxlower
|
| 230 |
+
if compute == "radius":
|
| 231 |
+
return minupper
|
| 232 |
+
if compute == "periphery":
|
| 233 |
+
p = [v for v in G if ecc_lower[v] == maxlower]
|
| 234 |
+
return p
|
| 235 |
+
if compute == "center":
|
| 236 |
+
c = [v for v in G if ecc_upper[v] == minupper]
|
| 237 |
+
return c
|
| 238 |
+
if compute == "eccentricities":
|
| 239 |
+
return ecc_lower
|
| 240 |
+
return None
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 244 |
+
def eccentricity(G, v=None, sp=None, weight=None):
|
| 245 |
+
"""Returns the eccentricity of nodes in G.
|
| 246 |
+
|
| 247 |
+
The eccentricity of a node v is the maximum distance from v to
|
| 248 |
+
all other nodes in G.
|
| 249 |
+
|
| 250 |
+
Parameters
|
| 251 |
+
----------
|
| 252 |
+
G : NetworkX graph
|
| 253 |
+
A graph
|
| 254 |
+
|
| 255 |
+
v : node, optional
|
| 256 |
+
Return value of specified node
|
| 257 |
+
|
| 258 |
+
sp : dict of dicts, optional
|
| 259 |
+
All pairs shortest path lengths as a dictionary of dictionaries
|
| 260 |
+
|
| 261 |
+
weight : string, function, or None (default=None)
|
| 262 |
+
If this is a string, then edge weights will be accessed via the
|
| 263 |
+
edge attribute with this key (that is, the weight of the edge
|
| 264 |
+
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
|
| 265 |
+
such edge attribute exists, the weight of the edge is assumed to
|
| 266 |
+
be one.
|
| 267 |
+
|
| 268 |
+
If this is a function, the weight of an edge is the value
|
| 269 |
+
returned by the function. The function must accept exactly three
|
| 270 |
+
positional arguments: the two endpoints of an edge and the
|
| 271 |
+
dictionary of edge attributes for that edge. The function must
|
| 272 |
+
return a number.
|
| 273 |
+
|
| 274 |
+
If this is None, every edge has weight/distance/cost 1.
|
| 275 |
+
|
| 276 |
+
Weights stored as floating point values can lead to small round-off
|
| 277 |
+
errors in distances. Use integer weights to avoid this.
|
| 278 |
+
|
| 279 |
+
Weights should be positive, since they are distances.
|
| 280 |
+
|
| 281 |
+
Returns
|
| 282 |
+
-------
|
| 283 |
+
ecc : dictionary
|
| 284 |
+
A dictionary of eccentricity values keyed by node.
|
| 285 |
+
|
| 286 |
+
Examples
|
| 287 |
+
--------
|
| 288 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
|
| 289 |
+
>>> dict(nx.eccentricity(G))
|
| 290 |
+
{1: 2, 2: 3, 3: 2, 4: 2, 5: 3}
|
| 291 |
+
|
| 292 |
+
>>> dict(
|
| 293 |
+
... nx.eccentricity(G, v=[1, 5])
|
| 294 |
+
... ) # This returns the eccentricity of node 1 & 5
|
| 295 |
+
{1: 2, 5: 3}
|
| 296 |
+
|
| 297 |
+
"""
|
| 298 |
+
# if v is None: # none, use entire graph
|
| 299 |
+
# nodes=G.nodes()
|
| 300 |
+
# elif v in G: # is v a single node
|
| 301 |
+
# nodes=[v]
|
| 302 |
+
# else: # assume v is a container of nodes
|
| 303 |
+
# nodes=v
|
| 304 |
+
order = G.order()
|
| 305 |
+
e = {}
|
| 306 |
+
for n in G.nbunch_iter(v):
|
| 307 |
+
if sp is None:
|
| 308 |
+
length = nx.shortest_path_length(G, source=n, weight=weight)
|
| 309 |
+
|
| 310 |
+
L = len(length)
|
| 311 |
+
else:
|
| 312 |
+
try:
|
| 313 |
+
length = sp[n]
|
| 314 |
+
L = len(length)
|
| 315 |
+
except TypeError as err:
|
| 316 |
+
raise nx.NetworkXError('Format of "sp" is invalid.') from err
|
| 317 |
+
if L != order:
|
| 318 |
+
if G.is_directed():
|
| 319 |
+
msg = (
|
| 320 |
+
"Found infinite path length because the digraph is not"
|
| 321 |
+
" strongly connected"
|
| 322 |
+
)
|
| 323 |
+
else:
|
| 324 |
+
msg = "Found infinite path length because the graph is not" " connected"
|
| 325 |
+
raise nx.NetworkXError(msg)
|
| 326 |
+
|
| 327 |
+
e[n] = max(length.values())
|
| 328 |
+
|
| 329 |
+
if v in G:
|
| 330 |
+
return e[v] # return single value
|
| 331 |
+
return e
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 335 |
+
def diameter(G, e=None, usebounds=False, weight=None):
|
| 336 |
+
"""Returns the diameter of the graph G.
|
| 337 |
+
|
| 338 |
+
The diameter is the maximum eccentricity.
|
| 339 |
+
|
| 340 |
+
Parameters
|
| 341 |
+
----------
|
| 342 |
+
G : NetworkX graph
|
| 343 |
+
A graph
|
| 344 |
+
|
| 345 |
+
e : eccentricity dictionary, optional
|
| 346 |
+
A precomputed dictionary of eccentricities.
|
| 347 |
+
|
| 348 |
+
weight : string, function, or None
|
| 349 |
+
If this is a string, then edge weights will be accessed via the
|
| 350 |
+
edge attribute with this key (that is, the weight of the edge
|
| 351 |
+
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
|
| 352 |
+
such edge attribute exists, the weight of the edge is assumed to
|
| 353 |
+
be one.
|
| 354 |
+
|
| 355 |
+
If this is a function, the weight of an edge is the value
|
| 356 |
+
returned by the function. The function must accept exactly three
|
| 357 |
+
positional arguments: the two endpoints of an edge and the
|
| 358 |
+
dictionary of edge attributes for that edge. The function must
|
| 359 |
+
return a number.
|
| 360 |
+
|
| 361 |
+
If this is None, every edge has weight/distance/cost 1.
|
| 362 |
+
|
| 363 |
+
Weights stored as floating point values can lead to small round-off
|
| 364 |
+
errors in distances. Use integer weights to avoid this.
|
| 365 |
+
|
| 366 |
+
Weights should be positive, since they are distances.
|
| 367 |
+
|
| 368 |
+
Returns
|
| 369 |
+
-------
|
| 370 |
+
d : integer
|
| 371 |
+
Diameter of graph
|
| 372 |
+
|
| 373 |
+
Examples
|
| 374 |
+
--------
|
| 375 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
|
| 376 |
+
>>> nx.diameter(G)
|
| 377 |
+
3
|
| 378 |
+
|
| 379 |
+
See Also
|
| 380 |
+
--------
|
| 381 |
+
eccentricity
|
| 382 |
+
"""
|
| 383 |
+
if usebounds is True and e is None and not G.is_directed():
|
| 384 |
+
return _extrema_bounding(G, compute="diameter", weight=weight)
|
| 385 |
+
if e is None:
|
| 386 |
+
e = eccentricity(G, weight=weight)
|
| 387 |
+
return max(e.values())
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
@nx._dispatchable
|
| 391 |
+
def harmonic_diameter(G, sp=None):
|
| 392 |
+
"""Returns the harmonic diameter of the graph G.
|
| 393 |
+
|
| 394 |
+
The harmonic diameter of a graph is the harmonic mean of the distances
|
| 395 |
+
between all pairs of distinct vertices. Graphs that are not strongly
|
| 396 |
+
connected have infinite diameter and mean distance, making such
|
| 397 |
+
measures not useful. Restricting the diameter or mean distance to
|
| 398 |
+
finite distances yields paradoxical values (e.g., a perfect match
|
| 399 |
+
would have diameter one). The harmonic mean handles gracefully
|
| 400 |
+
infinite distances (e.g., a perfect match has harmonic diameter equal
|
| 401 |
+
to the number of vertices minus one), making it possible to assign a
|
| 402 |
+
meaningful value to all graphs.
|
| 403 |
+
|
| 404 |
+
Note that in [1] the harmonic diameter is called "connectivity length":
|
| 405 |
+
however, "harmonic diameter" is a more standard name from the
|
| 406 |
+
theory of metric spaces. The name "harmonic mean distance" is perhaps
|
| 407 |
+
a more descriptive name, but is not used in the literature, so we use the
|
| 408 |
+
name "harmonic diameter" here.
|
| 409 |
+
|
| 410 |
+
Parameters
|
| 411 |
+
----------
|
| 412 |
+
G : NetworkX graph
|
| 413 |
+
A graph
|
| 414 |
+
|
| 415 |
+
sp : dict of dicts, optional
|
| 416 |
+
All-pairs shortest path lengths as a dictionary of dictionaries
|
| 417 |
+
|
| 418 |
+
Returns
|
| 419 |
+
-------
|
| 420 |
+
hd : float
|
| 421 |
+
Harmonic diameter of graph
|
| 422 |
+
|
| 423 |
+
References
|
| 424 |
+
----------
|
| 425 |
+
.. [1] Massimo Marchiori and Vito Latora, "Harmony in the small-world".
|
| 426 |
+
*Physica A: Statistical Mechanics and Its Applications*
|
| 427 |
+
285(3-4), pages 539-546, 2000.
|
| 428 |
+
<https://doi.org/10.1016/S0378-4371(00)00311-3>
|
| 429 |
+
"""
|
| 430 |
+
order = G.order()
|
| 431 |
+
|
| 432 |
+
sum_invd = 0
|
| 433 |
+
for n in G:
|
| 434 |
+
if sp is None:
|
| 435 |
+
length = nx.single_source_shortest_path_length(G, n)
|
| 436 |
+
else:
|
| 437 |
+
try:
|
| 438 |
+
length = sp[n]
|
| 439 |
+
L = len(length)
|
| 440 |
+
except TypeError as err:
|
| 441 |
+
raise nx.NetworkXError('Format of "sp" is invalid.') from err
|
| 442 |
+
|
| 443 |
+
for d in length.values():
|
| 444 |
+
# Note that this will skip the zero distance from n to itself,
|
| 445 |
+
# as it should be, but also zero-weight paths in weighted graphs.
|
| 446 |
+
if d != 0:
|
| 447 |
+
sum_invd += 1 / d
|
| 448 |
+
|
| 449 |
+
if sum_invd != 0:
|
| 450 |
+
return order * (order - 1) / sum_invd
|
| 451 |
+
if order > 1:
|
| 452 |
+
return math.inf
|
| 453 |
+
return math.nan
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 457 |
+
def periphery(G, e=None, usebounds=False, weight=None):
|
| 458 |
+
"""Returns the periphery of the graph G.
|
| 459 |
+
|
| 460 |
+
The periphery is the set of nodes with eccentricity equal to the diameter.
|
| 461 |
+
|
| 462 |
+
Parameters
|
| 463 |
+
----------
|
| 464 |
+
G : NetworkX graph
|
| 465 |
+
A graph
|
| 466 |
+
|
| 467 |
+
e : eccentricity dictionary, optional
|
| 468 |
+
A precomputed dictionary of eccentricities.
|
| 469 |
+
|
| 470 |
+
weight : string, function, or None
|
| 471 |
+
If this is a string, then edge weights will be accessed via the
|
| 472 |
+
edge attribute with this key (that is, the weight of the edge
|
| 473 |
+
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
|
| 474 |
+
such edge attribute exists, the weight of the edge is assumed to
|
| 475 |
+
be one.
|
| 476 |
+
|
| 477 |
+
If this is a function, the weight of an edge is the value
|
| 478 |
+
returned by the function. The function must accept exactly three
|
| 479 |
+
positional arguments: the two endpoints of an edge and the
|
| 480 |
+
dictionary of edge attributes for that edge. The function must
|
| 481 |
+
return a number.
|
| 482 |
+
|
| 483 |
+
If this is None, every edge has weight/distance/cost 1.
|
| 484 |
+
|
| 485 |
+
Weights stored as floating point values can lead to small round-off
|
| 486 |
+
errors in distances. Use integer weights to avoid this.
|
| 487 |
+
|
| 488 |
+
Weights should be positive, since they are distances.
|
| 489 |
+
|
| 490 |
+
Returns
|
| 491 |
+
-------
|
| 492 |
+
p : list
|
| 493 |
+
List of nodes in periphery
|
| 494 |
+
|
| 495 |
+
Examples
|
| 496 |
+
--------
|
| 497 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
|
| 498 |
+
>>> nx.periphery(G)
|
| 499 |
+
[2, 5]
|
| 500 |
+
|
| 501 |
+
See Also
|
| 502 |
+
--------
|
| 503 |
+
barycenter
|
| 504 |
+
center
|
| 505 |
+
"""
|
| 506 |
+
if usebounds is True and e is None and not G.is_directed():
|
| 507 |
+
return _extrema_bounding(G, compute="periphery", weight=weight)
|
| 508 |
+
if e is None:
|
| 509 |
+
e = eccentricity(G, weight=weight)
|
| 510 |
+
diameter = max(e.values())
|
| 511 |
+
p = [v for v in e if e[v] == diameter]
|
| 512 |
+
return p
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 516 |
+
def radius(G, e=None, usebounds=False, weight=None):
|
| 517 |
+
"""Returns the radius of the graph G.
|
| 518 |
+
|
| 519 |
+
The radius is the minimum eccentricity.
|
| 520 |
+
|
| 521 |
+
Parameters
|
| 522 |
+
----------
|
| 523 |
+
G : NetworkX graph
|
| 524 |
+
A graph
|
| 525 |
+
|
| 526 |
+
e : eccentricity dictionary, optional
|
| 527 |
+
A precomputed dictionary of eccentricities.
|
| 528 |
+
|
| 529 |
+
weight : string, function, or None
|
| 530 |
+
If this is a string, then edge weights will be accessed via the
|
| 531 |
+
edge attribute with this key (that is, the weight of the edge
|
| 532 |
+
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
|
| 533 |
+
such edge attribute exists, the weight of the edge is assumed to
|
| 534 |
+
be one.
|
| 535 |
+
|
| 536 |
+
If this is a function, the weight of an edge is the value
|
| 537 |
+
returned by the function. The function must accept exactly three
|
| 538 |
+
positional arguments: the two endpoints of an edge and the
|
| 539 |
+
dictionary of edge attributes for that edge. The function must
|
| 540 |
+
return a number.
|
| 541 |
+
|
| 542 |
+
If this is None, every edge has weight/distance/cost 1.
|
| 543 |
+
|
| 544 |
+
Weights stored as floating point values can lead to small round-off
|
| 545 |
+
errors in distances. Use integer weights to avoid this.
|
| 546 |
+
|
| 547 |
+
Weights should be positive, since they are distances.
|
| 548 |
+
|
| 549 |
+
Returns
|
| 550 |
+
-------
|
| 551 |
+
r : integer
|
| 552 |
+
Radius of graph
|
| 553 |
+
|
| 554 |
+
Examples
|
| 555 |
+
--------
|
| 556 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
|
| 557 |
+
>>> nx.radius(G)
|
| 558 |
+
2
|
| 559 |
+
|
| 560 |
+
"""
|
| 561 |
+
if usebounds is True and e is None and not G.is_directed():
|
| 562 |
+
return _extrema_bounding(G, compute="radius", weight=weight)
|
| 563 |
+
if e is None:
|
| 564 |
+
e = eccentricity(G, weight=weight)
|
| 565 |
+
return min(e.values())
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 569 |
+
def center(G, e=None, usebounds=False, weight=None):
|
| 570 |
+
"""Returns the center of the graph G.
|
| 571 |
+
|
| 572 |
+
The center is the set of nodes with eccentricity equal to radius.
|
| 573 |
+
|
| 574 |
+
Parameters
|
| 575 |
+
----------
|
| 576 |
+
G : NetworkX graph
|
| 577 |
+
A graph
|
| 578 |
+
|
| 579 |
+
e : eccentricity dictionary, optional
|
| 580 |
+
A precomputed dictionary of eccentricities.
|
| 581 |
+
|
| 582 |
+
weight : string, function, or None
|
| 583 |
+
If this is a string, then edge weights will be accessed via the
|
| 584 |
+
edge attribute with this key (that is, the weight of the edge
|
| 585 |
+
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
|
| 586 |
+
such edge attribute exists, the weight of the edge is assumed to
|
| 587 |
+
be one.
|
| 588 |
+
|
| 589 |
+
If this is a function, the weight of an edge is the value
|
| 590 |
+
returned by the function. The function must accept exactly three
|
| 591 |
+
positional arguments: the two endpoints of an edge and the
|
| 592 |
+
dictionary of edge attributes for that edge. The function must
|
| 593 |
+
return a number.
|
| 594 |
+
|
| 595 |
+
If this is None, every edge has weight/distance/cost 1.
|
| 596 |
+
|
| 597 |
+
Weights stored as floating point values can lead to small round-off
|
| 598 |
+
errors in distances. Use integer weights to avoid this.
|
| 599 |
+
|
| 600 |
+
Weights should be positive, since they are distances.
|
| 601 |
+
|
| 602 |
+
Returns
|
| 603 |
+
-------
|
| 604 |
+
c : list
|
| 605 |
+
List of nodes in center
|
| 606 |
+
|
| 607 |
+
Examples
|
| 608 |
+
--------
|
| 609 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
|
| 610 |
+
>>> list(nx.center(G))
|
| 611 |
+
[1, 3, 4]
|
| 612 |
+
|
| 613 |
+
See Also
|
| 614 |
+
--------
|
| 615 |
+
barycenter
|
| 616 |
+
periphery
|
| 617 |
+
"""
|
| 618 |
+
if usebounds is True and e is None and not G.is_directed():
|
| 619 |
+
return _extrema_bounding(G, compute="center", weight=weight)
|
| 620 |
+
if e is None:
|
| 621 |
+
e = eccentricity(G, weight=weight)
|
| 622 |
+
radius = min(e.values())
|
| 623 |
+
p = [v for v in e if e[v] == radius]
|
| 624 |
+
return p
|
| 625 |
+
|
| 626 |
+
|
| 627 |
+
@nx._dispatchable(edge_attrs="weight", mutates_input={"attr": 2})
|
| 628 |
+
def barycenter(G, weight=None, attr=None, sp=None):
|
| 629 |
+
r"""Calculate barycenter of a connected graph, optionally with edge weights.
|
| 630 |
+
|
| 631 |
+
The :dfn:`barycenter` a
|
| 632 |
+
:func:`connected <networkx.algorithms.components.is_connected>` graph
|
| 633 |
+
:math:`G` is the subgraph induced by the set of its nodes :math:`v`
|
| 634 |
+
minimizing the objective function
|
| 635 |
+
|
| 636 |
+
.. math::
|
| 637 |
+
|
| 638 |
+
\sum_{u \in V(G)} d_G(u, v),
|
| 639 |
+
|
| 640 |
+
where :math:`d_G` is the (possibly weighted) :func:`path length
|
| 641 |
+
<networkx.algorithms.shortest_paths.generic.shortest_path_length>`.
|
| 642 |
+
The barycenter is also called the :dfn:`median`. See [West01]_, p. 78.
|
| 643 |
+
|
| 644 |
+
Parameters
|
| 645 |
+
----------
|
| 646 |
+
G : :class:`networkx.Graph`
|
| 647 |
+
The connected graph :math:`G`.
|
| 648 |
+
weight : :class:`str`, optional
|
| 649 |
+
Passed through to
|
| 650 |
+
:func:`~networkx.algorithms.shortest_paths.generic.shortest_path_length`.
|
| 651 |
+
attr : :class:`str`, optional
|
| 652 |
+
If given, write the value of the objective function to each node's
|
| 653 |
+
`attr` attribute. Otherwise do not store the value.
|
| 654 |
+
sp : dict of dicts, optional
|
| 655 |
+
All pairs shortest path lengths as a dictionary of dictionaries
|
| 656 |
+
|
| 657 |
+
Returns
|
| 658 |
+
-------
|
| 659 |
+
list
|
| 660 |
+
Nodes of `G` that induce the barycenter of `G`.
|
| 661 |
+
|
| 662 |
+
Raises
|
| 663 |
+
------
|
| 664 |
+
NetworkXNoPath
|
| 665 |
+
If `G` is disconnected. `G` may appear disconnected to
|
| 666 |
+
:func:`barycenter` if `sp` is given but is missing shortest path
|
| 667 |
+
lengths for any pairs.
|
| 668 |
+
ValueError
|
| 669 |
+
If `sp` and `weight` are both given.
|
| 670 |
+
|
| 671 |
+
Examples
|
| 672 |
+
--------
|
| 673 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
|
| 674 |
+
>>> nx.barycenter(G)
|
| 675 |
+
[1, 3, 4]
|
| 676 |
+
|
| 677 |
+
See Also
|
| 678 |
+
--------
|
| 679 |
+
center
|
| 680 |
+
periphery
|
| 681 |
+
"""
|
| 682 |
+
if sp is None:
|
| 683 |
+
sp = nx.shortest_path_length(G, weight=weight)
|
| 684 |
+
else:
|
| 685 |
+
sp = sp.items()
|
| 686 |
+
if weight is not None:
|
| 687 |
+
raise ValueError("Cannot use both sp, weight arguments together")
|
| 688 |
+
smallest, barycenter_vertices, n = float("inf"), [], len(G)
|
| 689 |
+
for v, dists in sp:
|
| 690 |
+
if len(dists) < n:
|
| 691 |
+
raise nx.NetworkXNoPath(
|
| 692 |
+
f"Input graph {G} is disconnected, so every induced subgraph "
|
| 693 |
+
"has infinite barycentricity."
|
| 694 |
+
)
|
| 695 |
+
barycentricity = sum(dists.values())
|
| 696 |
+
if attr is not None:
|
| 697 |
+
G.nodes[v][attr] = barycentricity
|
| 698 |
+
if barycentricity < smallest:
|
| 699 |
+
smallest = barycentricity
|
| 700 |
+
barycenter_vertices = [v]
|
| 701 |
+
elif barycentricity == smallest:
|
| 702 |
+
barycenter_vertices.append(v)
|
| 703 |
+
if attr is not None:
|
| 704 |
+
nx._clear_cache(G)
|
| 705 |
+
return barycenter_vertices
|
| 706 |
+
|
| 707 |
+
|
| 708 |
+
@not_implemented_for("directed")
|
| 709 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 710 |
+
def resistance_distance(G, nodeA=None, nodeB=None, weight=None, invert_weight=True):
|
| 711 |
+
"""Returns the resistance distance between pairs of nodes in graph G.
|
| 712 |
+
|
| 713 |
+
The resistance distance between two nodes of a graph is akin to treating
|
| 714 |
+
the graph as a grid of resistors with a resistance equal to the provided
|
| 715 |
+
weight [1]_, [2]_.
|
| 716 |
+
|
| 717 |
+
If weight is not provided, then a weight of 1 is used for all edges.
|
| 718 |
+
|
| 719 |
+
If two nodes are the same, the resistance distance is zero.
|
| 720 |
+
|
| 721 |
+
Parameters
|
| 722 |
+
----------
|
| 723 |
+
G : NetworkX graph
|
| 724 |
+
A graph
|
| 725 |
+
|
| 726 |
+
nodeA : node or None, optional (default=None)
|
| 727 |
+
A node within graph G.
|
| 728 |
+
If None, compute resistance distance using all nodes as source nodes.
|
| 729 |
+
|
| 730 |
+
nodeB : node or None, optional (default=None)
|
| 731 |
+
A node within graph G.
|
| 732 |
+
If None, compute resistance distance using all nodes as target nodes.
|
| 733 |
+
|
| 734 |
+
weight : string or None, optional (default=None)
|
| 735 |
+
The edge data key used to compute the resistance distance.
|
| 736 |
+
If None, then each edge has weight 1.
|
| 737 |
+
|
| 738 |
+
invert_weight : boolean (default=True)
|
| 739 |
+
Proper calculation of resistance distance requires building the
|
| 740 |
+
Laplacian matrix with the reciprocal of the weight. Not required
|
| 741 |
+
if the weight is already inverted. Weight cannot be zero.
|
| 742 |
+
|
| 743 |
+
Returns
|
| 744 |
+
-------
|
| 745 |
+
rd : dict or float
|
| 746 |
+
If `nodeA` and `nodeB` are given, resistance distance between `nodeA`
|
| 747 |
+
and `nodeB`. If `nodeA` or `nodeB` is unspecified (the default), a
|
| 748 |
+
dictionary of nodes with resistance distances as the value.
|
| 749 |
+
|
| 750 |
+
Raises
|
| 751 |
+
------
|
| 752 |
+
NetworkXNotImplemented
|
| 753 |
+
If `G` is a directed graph.
|
| 754 |
+
|
| 755 |
+
NetworkXError
|
| 756 |
+
If `G` is not connected, or contains no nodes,
|
| 757 |
+
or `nodeA` is not in `G` or `nodeB` is not in `G`.
|
| 758 |
+
|
| 759 |
+
Examples
|
| 760 |
+
--------
|
| 761 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
|
| 762 |
+
>>> round(nx.resistance_distance(G, 1, 3), 10)
|
| 763 |
+
0.625
|
| 764 |
+
|
| 765 |
+
Notes
|
| 766 |
+
-----
|
| 767 |
+
The implementation is based on Theorem A in [2]_. Self-loops are ignored.
|
| 768 |
+
Multi-edges are contracted in one edge with weight equal to the harmonic sum of the weights.
|
| 769 |
+
|
| 770 |
+
References
|
| 771 |
+
----------
|
| 772 |
+
.. [1] Wikipedia
|
| 773 |
+
"Resistance distance."
|
| 774 |
+
https://en.wikipedia.org/wiki/Resistance_distance
|
| 775 |
+
.. [2] D. J. Klein and M. Randic.
|
| 776 |
+
Resistance distance.
|
| 777 |
+
J. of Math. Chem. 12:81-95, 1993.
|
| 778 |
+
"""
|
| 779 |
+
import numpy as np
|
| 780 |
+
|
| 781 |
+
if len(G) == 0:
|
| 782 |
+
raise nx.NetworkXError("Graph G must contain at least one node.")
|
| 783 |
+
if not nx.is_connected(G):
|
| 784 |
+
raise nx.NetworkXError("Graph G must be strongly connected.")
|
| 785 |
+
if nodeA is not None and nodeA not in G:
|
| 786 |
+
raise nx.NetworkXError("Node A is not in graph G.")
|
| 787 |
+
if nodeB is not None and nodeB not in G:
|
| 788 |
+
raise nx.NetworkXError("Node B is not in graph G.")
|
| 789 |
+
|
| 790 |
+
G = G.copy()
|
| 791 |
+
node_list = list(G)
|
| 792 |
+
|
| 793 |
+
# Invert weights
|
| 794 |
+
if invert_weight and weight is not None:
|
| 795 |
+
if G.is_multigraph():
|
| 796 |
+
for u, v, k, d in G.edges(keys=True, data=True):
|
| 797 |
+
d[weight] = 1 / d[weight]
|
| 798 |
+
else:
|
| 799 |
+
for u, v, d in G.edges(data=True):
|
| 800 |
+
d[weight] = 1 / d[weight]
|
| 801 |
+
|
| 802 |
+
# Compute resistance distance using the Pseudo-inverse of the Laplacian
|
| 803 |
+
# Self-loops are ignored
|
| 804 |
+
L = nx.laplacian_matrix(G, weight=weight).todense()
|
| 805 |
+
Linv = np.linalg.pinv(L, hermitian=True)
|
| 806 |
+
|
| 807 |
+
# Return relevant distances
|
| 808 |
+
if nodeA is not None and nodeB is not None:
|
| 809 |
+
i = node_list.index(nodeA)
|
| 810 |
+
j = node_list.index(nodeB)
|
| 811 |
+
return Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i)
|
| 812 |
+
|
| 813 |
+
elif nodeA is not None:
|
| 814 |
+
i = node_list.index(nodeA)
|
| 815 |
+
d = {}
|
| 816 |
+
for n in G:
|
| 817 |
+
j = node_list.index(n)
|
| 818 |
+
d[n] = Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i)
|
| 819 |
+
return d
|
| 820 |
+
|
| 821 |
+
elif nodeB is not None:
|
| 822 |
+
j = node_list.index(nodeB)
|
| 823 |
+
d = {}
|
| 824 |
+
for n in G:
|
| 825 |
+
i = node_list.index(n)
|
| 826 |
+
d[n] = Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i)
|
| 827 |
+
return d
|
| 828 |
+
|
| 829 |
+
else:
|
| 830 |
+
d = {}
|
| 831 |
+
for n in G:
|
| 832 |
+
i = node_list.index(n)
|
| 833 |
+
d[n] = {}
|
| 834 |
+
for n2 in G:
|
| 835 |
+
j = node_list.index(n2)
|
| 836 |
+
d[n][n2] = (
|
| 837 |
+
Linv.item(i, i)
|
| 838 |
+
+ Linv.item(j, j)
|
| 839 |
+
- Linv.item(i, j)
|
| 840 |
+
- Linv.item(j, i)
|
| 841 |
+
)
|
| 842 |
+
return d
|
| 843 |
+
|
| 844 |
+
|
| 845 |
+
@not_implemented_for("directed")
|
| 846 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 847 |
+
def effective_graph_resistance(G, weight=None, invert_weight=True):
|
| 848 |
+
"""Returns the Effective graph resistance of G.
|
| 849 |
+
|
| 850 |
+
Also known as the Kirchhoff index.
|
| 851 |
+
|
| 852 |
+
The effective graph resistance is defined as the sum
|
| 853 |
+
of the resistance distance of every node pair in G [1]_.
|
| 854 |
+
|
| 855 |
+
If weight is not provided, then a weight of 1 is used for all edges.
|
| 856 |
+
|
| 857 |
+
The effective graph resistance of a disconnected graph is infinite.
|
| 858 |
+
|
| 859 |
+
Parameters
|
| 860 |
+
----------
|
| 861 |
+
G : NetworkX graph
|
| 862 |
+
A graph
|
| 863 |
+
|
| 864 |
+
weight : string or None, optional (default=None)
|
| 865 |
+
The edge data key used to compute the effective graph resistance.
|
| 866 |
+
If None, then each edge has weight 1.
|
| 867 |
+
|
| 868 |
+
invert_weight : boolean (default=True)
|
| 869 |
+
Proper calculation of resistance distance requires building the
|
| 870 |
+
Laplacian matrix with the reciprocal of the weight. Not required
|
| 871 |
+
if the weight is already inverted. Weight cannot be zero.
|
| 872 |
+
|
| 873 |
+
Returns
|
| 874 |
+
-------
|
| 875 |
+
RG : float
|
| 876 |
+
The effective graph resistance of `G`.
|
| 877 |
+
|
| 878 |
+
Raises
|
| 879 |
+
------
|
| 880 |
+
NetworkXNotImplemented
|
| 881 |
+
If `G` is a directed graph.
|
| 882 |
+
|
| 883 |
+
NetworkXError
|
| 884 |
+
If `G` does not contain any nodes.
|
| 885 |
+
|
| 886 |
+
Examples
|
| 887 |
+
--------
|
| 888 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
|
| 889 |
+
>>> round(nx.effective_graph_resistance(G), 10)
|
| 890 |
+
10.25
|
| 891 |
+
|
| 892 |
+
Notes
|
| 893 |
+
-----
|
| 894 |
+
The implementation is based on Theorem 2.2 in [2]_. Self-loops are ignored.
|
| 895 |
+
Multi-edges are contracted in one edge with weight equal to the harmonic sum of the weights.
|
| 896 |
+
|
| 897 |
+
References
|
| 898 |
+
----------
|
| 899 |
+
.. [1] Wolfram
|
| 900 |
+
"Kirchhoff Index."
|
| 901 |
+
https://mathworld.wolfram.com/KirchhoffIndex.html
|
| 902 |
+
.. [2] W. Ellens, F. M. Spieksma, P. Van Mieghem, A. Jamakovic, R. E. Kooij.
|
| 903 |
+
Effective graph resistance.
|
| 904 |
+
Lin. Alg. Appl. 435:2491-2506, 2011.
|
| 905 |
+
"""
|
| 906 |
+
import numpy as np
|
| 907 |
+
|
| 908 |
+
if len(G) == 0:
|
| 909 |
+
raise nx.NetworkXError("Graph G must contain at least one node.")
|
| 910 |
+
|
| 911 |
+
# Disconnected graphs have infinite Effective graph resistance
|
| 912 |
+
if not nx.is_connected(G):
|
| 913 |
+
return float("inf")
|
| 914 |
+
|
| 915 |
+
# Invert weights
|
| 916 |
+
G = G.copy()
|
| 917 |
+
if invert_weight and weight is not None:
|
| 918 |
+
if G.is_multigraph():
|
| 919 |
+
for u, v, k, d in G.edges(keys=True, data=True):
|
| 920 |
+
d[weight] = 1 / d[weight]
|
| 921 |
+
else:
|
| 922 |
+
for u, v, d in G.edges(data=True):
|
| 923 |
+
d[weight] = 1 / d[weight]
|
| 924 |
+
|
| 925 |
+
# Get Laplacian eigenvalues
|
| 926 |
+
mu = np.sort(nx.laplacian_spectrum(G, weight=weight))
|
| 927 |
+
|
| 928 |
+
# Compute Effective graph resistance based on spectrum of the Laplacian
|
| 929 |
+
# Self-loops are ignored
|
| 930 |
+
return float(np.sum(1 / mu[1:]) * G.number_of_nodes())
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
@nx.utils.not_implemented_for("directed")
|
| 934 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 935 |
+
def kemeny_constant(G, *, weight=None):
|
| 936 |
+
"""Returns the Kemeny constant of the given graph.
|
| 937 |
+
|
| 938 |
+
The *Kemeny constant* (or Kemeny's constant) of a graph `G`
|
| 939 |
+
can be computed by regarding the graph as a Markov chain.
|
| 940 |
+
The Kemeny constant is then the expected number of time steps
|
| 941 |
+
to transition from a starting state i to a random destination state
|
| 942 |
+
sampled from the Markov chain's stationary distribution.
|
| 943 |
+
The Kemeny constant is independent of the chosen initial state [1]_.
|
| 944 |
+
|
| 945 |
+
The Kemeny constant measures the time needed for spreading
|
| 946 |
+
across a graph. Low values indicate a closely connected graph
|
| 947 |
+
whereas high values indicate a spread-out graph.
|
| 948 |
+
|
| 949 |
+
If weight is not provided, then a weight of 1 is used for all edges.
|
| 950 |
+
|
| 951 |
+
Since `G` represents a Markov chain, the weights must be positive.
|
| 952 |
+
|
| 953 |
+
Parameters
|
| 954 |
+
----------
|
| 955 |
+
G : NetworkX graph
|
| 956 |
+
|
| 957 |
+
weight : string or None, optional (default=None)
|
| 958 |
+
The edge data key used to compute the Kemeny constant.
|
| 959 |
+
If None, then each edge has weight 1.
|
| 960 |
+
|
| 961 |
+
Returns
|
| 962 |
+
-------
|
| 963 |
+
float
|
| 964 |
+
The Kemeny constant of the graph `G`.
|
| 965 |
+
|
| 966 |
+
Raises
|
| 967 |
+
------
|
| 968 |
+
NetworkXNotImplemented
|
| 969 |
+
If the graph `G` is directed.
|
| 970 |
+
|
| 971 |
+
NetworkXError
|
| 972 |
+
If the graph `G` is not connected, or contains no nodes,
|
| 973 |
+
or has edges with negative weights.
|
| 974 |
+
|
| 975 |
+
Examples
|
| 976 |
+
--------
|
| 977 |
+
>>> G = nx.complete_graph(5)
|
| 978 |
+
>>> round(nx.kemeny_constant(G), 10)
|
| 979 |
+
3.2
|
| 980 |
+
|
| 981 |
+
Notes
|
| 982 |
+
-----
|
| 983 |
+
The implementation is based on equation (3.3) in [2]_.
|
| 984 |
+
Self-loops are allowed and indicate a Markov chain where
|
| 985 |
+
the state can remain the same. Multi-edges are contracted
|
| 986 |
+
in one edge with weight equal to the sum of the weights.
|
| 987 |
+
|
| 988 |
+
References
|
| 989 |
+
----------
|
| 990 |
+
.. [1] Wikipedia
|
| 991 |
+
"Kemeny's constant."
|
| 992 |
+
https://en.wikipedia.org/wiki/Kemeny%27s_constant
|
| 993 |
+
.. [2] Lovász L.
|
| 994 |
+
Random walks on graphs: A survey.
|
| 995 |
+
Paul Erdös is Eighty, vol. 2, Bolyai Society,
|
| 996 |
+
Mathematical Studies, Keszthely, Hungary (1993), pp. 1-46
|
| 997 |
+
"""
|
| 998 |
+
import numpy as np
|
| 999 |
+
import scipy as sp
|
| 1000 |
+
|
| 1001 |
+
if len(G) == 0:
|
| 1002 |
+
raise nx.NetworkXError("Graph G must contain at least one node.")
|
| 1003 |
+
if not nx.is_connected(G):
|
| 1004 |
+
raise nx.NetworkXError("Graph G must be connected.")
|
| 1005 |
+
if nx.is_negatively_weighted(G, weight=weight):
|
| 1006 |
+
raise nx.NetworkXError("The weights of graph G must be nonnegative.")
|
| 1007 |
+
|
| 1008 |
+
# Compute matrix H = D^-1/2 A D^-1/2
|
| 1009 |
+
A = nx.adjacency_matrix(G, weight=weight)
|
| 1010 |
+
n, m = A.shape
|
| 1011 |
+
diags = A.sum(axis=1)
|
| 1012 |
+
with np.errstate(divide="ignore"):
|
| 1013 |
+
diags_sqrt = 1.0 / np.sqrt(diags)
|
| 1014 |
+
diags_sqrt[np.isinf(diags_sqrt)] = 0
|
| 1015 |
+
DH = sp.sparse.csr_array(sp.sparse.spdiags(diags_sqrt, 0, m, n, format="csr"))
|
| 1016 |
+
H = DH @ (A @ DH)
|
| 1017 |
+
|
| 1018 |
+
# Compute eigenvalues of H
|
| 1019 |
+
eig = np.sort(sp.linalg.eigvalsh(H.todense()))
|
| 1020 |
+
|
| 1021 |
+
# Compute the Kemeny constant
|
| 1022 |
+
return float(np.sum(1 / (1 - eig[:-1])))
|
.venv/lib/python3.11/site-packages/networkx/algorithms/distance_regular.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
=======================
|
| 3 |
+
Distance-regular graphs
|
| 4 |
+
=======================
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
from networkx.utils import not_implemented_for
|
| 9 |
+
|
| 10 |
+
from .distance_measures import diameter
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"is_distance_regular",
|
| 14 |
+
"is_strongly_regular",
|
| 15 |
+
"intersection_array",
|
| 16 |
+
"global_parameters",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@nx._dispatchable
|
| 21 |
+
def is_distance_regular(G):
|
| 22 |
+
"""Returns True if the graph is distance regular, False otherwise.
|
| 23 |
+
|
| 24 |
+
A connected graph G is distance-regular if for any nodes x,y
|
| 25 |
+
and any integers i,j=0,1,...,d (where d is the graph
|
| 26 |
+
diameter), the number of vertices at distance i from x and
|
| 27 |
+
distance j from y depends only on i,j and the graph distance
|
| 28 |
+
between x and y, independently of the choice of x and y.
|
| 29 |
+
|
| 30 |
+
Parameters
|
| 31 |
+
----------
|
| 32 |
+
G: Networkx graph (undirected)
|
| 33 |
+
|
| 34 |
+
Returns
|
| 35 |
+
-------
|
| 36 |
+
bool
|
| 37 |
+
True if the graph is Distance Regular, False otherwise
|
| 38 |
+
|
| 39 |
+
Examples
|
| 40 |
+
--------
|
| 41 |
+
>>> G = nx.hypercube_graph(6)
|
| 42 |
+
>>> nx.is_distance_regular(G)
|
| 43 |
+
True
|
| 44 |
+
|
| 45 |
+
See Also
|
| 46 |
+
--------
|
| 47 |
+
intersection_array, global_parameters
|
| 48 |
+
|
| 49 |
+
Notes
|
| 50 |
+
-----
|
| 51 |
+
For undirected and simple graphs only
|
| 52 |
+
|
| 53 |
+
References
|
| 54 |
+
----------
|
| 55 |
+
.. [1] Brouwer, A. E.; Cohen, A. M.; and Neumaier, A.
|
| 56 |
+
Distance-Regular Graphs. New York: Springer-Verlag, 1989.
|
| 57 |
+
.. [2] Weisstein, Eric W. "Distance-Regular Graph."
|
| 58 |
+
http://mathworld.wolfram.com/Distance-RegularGraph.html
|
| 59 |
+
|
| 60 |
+
"""
|
| 61 |
+
try:
|
| 62 |
+
intersection_array(G)
|
| 63 |
+
return True
|
| 64 |
+
except nx.NetworkXError:
|
| 65 |
+
return False
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def global_parameters(b, c):
|
| 69 |
+
"""Returns global parameters for a given intersection array.
|
| 70 |
+
|
| 71 |
+
Given a distance-regular graph G with integers b_i, c_i,i = 0,....,d
|
| 72 |
+
such that for any 2 vertices x,y in G at a distance i=d(x,y), there
|
| 73 |
+
are exactly c_i neighbors of y at a distance of i-1 from x and b_i
|
| 74 |
+
neighbors of y at a distance of i+1 from x.
|
| 75 |
+
|
| 76 |
+
Thus, a distance regular graph has the global parameters,
|
| 77 |
+
[[c_0,a_0,b_0],[c_1,a_1,b_1],......,[c_d,a_d,b_d]] for the
|
| 78 |
+
intersection array [b_0,b_1,.....b_{d-1};c_1,c_2,.....c_d]
|
| 79 |
+
where a_i+b_i+c_i=k , k= degree of every vertex.
|
| 80 |
+
|
| 81 |
+
Parameters
|
| 82 |
+
----------
|
| 83 |
+
b : list
|
| 84 |
+
|
| 85 |
+
c : list
|
| 86 |
+
|
| 87 |
+
Returns
|
| 88 |
+
-------
|
| 89 |
+
iterable
|
| 90 |
+
An iterable over three tuples.
|
| 91 |
+
|
| 92 |
+
Examples
|
| 93 |
+
--------
|
| 94 |
+
>>> G = nx.dodecahedral_graph()
|
| 95 |
+
>>> b, c = nx.intersection_array(G)
|
| 96 |
+
>>> list(nx.global_parameters(b, c))
|
| 97 |
+
[(0, 0, 3), (1, 0, 2), (1, 1, 1), (1, 1, 1), (2, 0, 1), (3, 0, 0)]
|
| 98 |
+
|
| 99 |
+
References
|
| 100 |
+
----------
|
| 101 |
+
.. [1] Weisstein, Eric W. "Global Parameters."
|
| 102 |
+
From MathWorld--A Wolfram Web Resource.
|
| 103 |
+
http://mathworld.wolfram.com/GlobalParameters.html
|
| 104 |
+
|
| 105 |
+
See Also
|
| 106 |
+
--------
|
| 107 |
+
intersection_array
|
| 108 |
+
"""
|
| 109 |
+
return ((y, b[0] - x - y, x) for x, y in zip(b + [0], [0] + c))
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@not_implemented_for("directed")
|
| 113 |
+
@not_implemented_for("multigraph")
|
| 114 |
+
@nx._dispatchable
|
| 115 |
+
def intersection_array(G):
|
| 116 |
+
"""Returns the intersection array of a distance-regular graph.
|
| 117 |
+
|
| 118 |
+
Given a distance-regular graph G with integers b_i, c_i,i = 0,....,d
|
| 119 |
+
such that for any 2 vertices x,y in G at a distance i=d(x,y), there
|
| 120 |
+
are exactly c_i neighbors of y at a distance of i-1 from x and b_i
|
| 121 |
+
neighbors of y at a distance of i+1 from x.
|
| 122 |
+
|
| 123 |
+
A distance regular graph's intersection array is given by,
|
| 124 |
+
[b_0,b_1,.....b_{d-1};c_1,c_2,.....c_d]
|
| 125 |
+
|
| 126 |
+
Parameters
|
| 127 |
+
----------
|
| 128 |
+
G: Networkx graph (undirected)
|
| 129 |
+
|
| 130 |
+
Returns
|
| 131 |
+
-------
|
| 132 |
+
b,c: tuple of lists
|
| 133 |
+
|
| 134 |
+
Examples
|
| 135 |
+
--------
|
| 136 |
+
>>> G = nx.icosahedral_graph()
|
| 137 |
+
>>> nx.intersection_array(G)
|
| 138 |
+
([5, 2, 1], [1, 2, 5])
|
| 139 |
+
|
| 140 |
+
References
|
| 141 |
+
----------
|
| 142 |
+
.. [1] Weisstein, Eric W. "Intersection Array."
|
| 143 |
+
From MathWorld--A Wolfram Web Resource.
|
| 144 |
+
http://mathworld.wolfram.com/IntersectionArray.html
|
| 145 |
+
|
| 146 |
+
See Also
|
| 147 |
+
--------
|
| 148 |
+
global_parameters
|
| 149 |
+
"""
|
| 150 |
+
# test for regular graph (all degrees must be equal)
|
| 151 |
+
if len(G) == 0:
|
| 152 |
+
raise nx.NetworkXPointlessConcept("Graph has no nodes.")
|
| 153 |
+
degree = iter(G.degree())
|
| 154 |
+
(_, k) = next(degree)
|
| 155 |
+
for _, knext in degree:
|
| 156 |
+
if knext != k:
|
| 157 |
+
raise nx.NetworkXError("Graph is not distance regular.")
|
| 158 |
+
k = knext
|
| 159 |
+
path_length = dict(nx.all_pairs_shortest_path_length(G))
|
| 160 |
+
diameter = max(max(path_length[n].values()) for n in path_length)
|
| 161 |
+
bint = {} # 'b' intersection array
|
| 162 |
+
cint = {} # 'c' intersection array
|
| 163 |
+
for u in G:
|
| 164 |
+
for v in G:
|
| 165 |
+
try:
|
| 166 |
+
i = path_length[u][v]
|
| 167 |
+
except KeyError as err: # graph must be connected
|
| 168 |
+
raise nx.NetworkXError("Graph is not distance regular.") from err
|
| 169 |
+
# number of neighbors of v at a distance of i-1 from u
|
| 170 |
+
c = len([n for n in G[v] if path_length[n][u] == i - 1])
|
| 171 |
+
# number of neighbors of v at a distance of i+1 from u
|
| 172 |
+
b = len([n for n in G[v] if path_length[n][u] == i + 1])
|
| 173 |
+
# b,c are independent of u and v
|
| 174 |
+
if cint.get(i, c) != c or bint.get(i, b) != b:
|
| 175 |
+
raise nx.NetworkXError("Graph is not distance regular")
|
| 176 |
+
bint[i] = b
|
| 177 |
+
cint[i] = c
|
| 178 |
+
return (
|
| 179 |
+
[bint.get(j, 0) for j in range(diameter)],
|
| 180 |
+
[cint.get(j + 1, 0) for j in range(diameter)],
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
# TODO There is a definition for directed strongly regular graphs.
|
| 185 |
+
@not_implemented_for("directed")
|
| 186 |
+
@not_implemented_for("multigraph")
|
| 187 |
+
@nx._dispatchable
|
| 188 |
+
def is_strongly_regular(G):
|
| 189 |
+
"""Returns True if and only if the given graph is strongly
|
| 190 |
+
regular.
|
| 191 |
+
|
| 192 |
+
An undirected graph is *strongly regular* if
|
| 193 |
+
|
| 194 |
+
* it is regular,
|
| 195 |
+
* each pair of adjacent vertices has the same number of neighbors in
|
| 196 |
+
common,
|
| 197 |
+
* each pair of nonadjacent vertices has the same number of neighbors
|
| 198 |
+
in common.
|
| 199 |
+
|
| 200 |
+
Each strongly regular graph is a distance-regular graph.
|
| 201 |
+
Conversely, if a distance-regular graph has diameter two, then it is
|
| 202 |
+
a strongly regular graph. For more information on distance-regular
|
| 203 |
+
graphs, see :func:`is_distance_regular`.
|
| 204 |
+
|
| 205 |
+
Parameters
|
| 206 |
+
----------
|
| 207 |
+
G : NetworkX graph
|
| 208 |
+
An undirected graph.
|
| 209 |
+
|
| 210 |
+
Returns
|
| 211 |
+
-------
|
| 212 |
+
bool
|
| 213 |
+
Whether `G` is strongly regular.
|
| 214 |
+
|
| 215 |
+
Examples
|
| 216 |
+
--------
|
| 217 |
+
|
| 218 |
+
The cycle graph on five vertices is strongly regular. It is
|
| 219 |
+
two-regular, each pair of adjacent vertices has no shared neighbors,
|
| 220 |
+
and each pair of nonadjacent vertices has one shared neighbor::
|
| 221 |
+
|
| 222 |
+
>>> G = nx.cycle_graph(5)
|
| 223 |
+
>>> nx.is_strongly_regular(G)
|
| 224 |
+
True
|
| 225 |
+
|
| 226 |
+
"""
|
| 227 |
+
# Here is an alternate implementation based directly on the
|
| 228 |
+
# definition of strongly regular graphs:
|
| 229 |
+
#
|
| 230 |
+
# return (all_equal(G.degree().values())
|
| 231 |
+
# and all_equal(len(common_neighbors(G, u, v))
|
| 232 |
+
# for u, v in G.edges())
|
| 233 |
+
# and all_equal(len(common_neighbors(G, u, v))
|
| 234 |
+
# for u, v in non_edges(G)))
|
| 235 |
+
#
|
| 236 |
+
# We instead use the fact that a distance-regular graph of diameter
|
| 237 |
+
# two is strongly regular.
|
| 238 |
+
return is_distance_regular(G) and diameter(G) == 2
|
.venv/lib/python3.11/site-packages/networkx/algorithms/dominance.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Dominance algorithms.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from functools import reduce
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
from networkx.utils import not_implemented_for
|
| 9 |
+
|
| 10 |
+
__all__ = ["immediate_dominators", "dominance_frontiers"]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@not_implemented_for("undirected")
|
| 14 |
+
@nx._dispatchable
|
| 15 |
+
def immediate_dominators(G, start):
|
| 16 |
+
"""Returns the immediate dominators of all nodes of a directed graph.
|
| 17 |
+
|
| 18 |
+
Parameters
|
| 19 |
+
----------
|
| 20 |
+
G : a DiGraph or MultiDiGraph
|
| 21 |
+
The graph where dominance is to be computed.
|
| 22 |
+
|
| 23 |
+
start : node
|
| 24 |
+
The start node of dominance computation.
|
| 25 |
+
|
| 26 |
+
Returns
|
| 27 |
+
-------
|
| 28 |
+
idom : dict keyed by nodes
|
| 29 |
+
A dict containing the immediate dominators of each node reachable from
|
| 30 |
+
`start`.
|
| 31 |
+
|
| 32 |
+
Raises
|
| 33 |
+
------
|
| 34 |
+
NetworkXNotImplemented
|
| 35 |
+
If `G` is undirected.
|
| 36 |
+
|
| 37 |
+
NetworkXError
|
| 38 |
+
If `start` is not in `G`.
|
| 39 |
+
|
| 40 |
+
Notes
|
| 41 |
+
-----
|
| 42 |
+
Except for `start`, the immediate dominators are the parents of their
|
| 43 |
+
corresponding nodes in the dominator tree.
|
| 44 |
+
|
| 45 |
+
Examples
|
| 46 |
+
--------
|
| 47 |
+
>>> G = nx.DiGraph([(1, 2), (1, 3), (2, 5), (3, 4), (4, 5)])
|
| 48 |
+
>>> sorted(nx.immediate_dominators(G, 1).items())
|
| 49 |
+
[(1, 1), (2, 1), (3, 1), (4, 3), (5, 1)]
|
| 50 |
+
|
| 51 |
+
References
|
| 52 |
+
----------
|
| 53 |
+
.. [1] Cooper, Keith D., Harvey, Timothy J. and Kennedy, Ken.
|
| 54 |
+
"A simple, fast dominance algorithm." (2006).
|
| 55 |
+
https://hdl.handle.net/1911/96345
|
| 56 |
+
"""
|
| 57 |
+
if start not in G:
|
| 58 |
+
raise nx.NetworkXError("start is not in G")
|
| 59 |
+
|
| 60 |
+
idom = {start: start}
|
| 61 |
+
|
| 62 |
+
order = list(nx.dfs_postorder_nodes(G, start))
|
| 63 |
+
dfn = {u: i for i, u in enumerate(order)}
|
| 64 |
+
order.pop()
|
| 65 |
+
order.reverse()
|
| 66 |
+
|
| 67 |
+
def intersect(u, v):
|
| 68 |
+
while u != v:
|
| 69 |
+
while dfn[u] < dfn[v]:
|
| 70 |
+
u = idom[u]
|
| 71 |
+
while dfn[u] > dfn[v]:
|
| 72 |
+
v = idom[v]
|
| 73 |
+
return u
|
| 74 |
+
|
| 75 |
+
changed = True
|
| 76 |
+
while changed:
|
| 77 |
+
changed = False
|
| 78 |
+
for u in order:
|
| 79 |
+
new_idom = reduce(intersect, (v for v in G.pred[u] if v in idom))
|
| 80 |
+
if u not in idom or idom[u] != new_idom:
|
| 81 |
+
idom[u] = new_idom
|
| 82 |
+
changed = True
|
| 83 |
+
|
| 84 |
+
return idom
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@nx._dispatchable
|
| 88 |
+
def dominance_frontiers(G, start):
|
| 89 |
+
"""Returns the dominance frontiers of all nodes of a directed graph.
|
| 90 |
+
|
| 91 |
+
Parameters
|
| 92 |
+
----------
|
| 93 |
+
G : a DiGraph or MultiDiGraph
|
| 94 |
+
The graph where dominance is to be computed.
|
| 95 |
+
|
| 96 |
+
start : node
|
| 97 |
+
The start node of dominance computation.
|
| 98 |
+
|
| 99 |
+
Returns
|
| 100 |
+
-------
|
| 101 |
+
df : dict keyed by nodes
|
| 102 |
+
A dict containing the dominance frontiers of each node reachable from
|
| 103 |
+
`start` as lists.
|
| 104 |
+
|
| 105 |
+
Raises
|
| 106 |
+
------
|
| 107 |
+
NetworkXNotImplemented
|
| 108 |
+
If `G` is undirected.
|
| 109 |
+
|
| 110 |
+
NetworkXError
|
| 111 |
+
If `start` is not in `G`.
|
| 112 |
+
|
| 113 |
+
Examples
|
| 114 |
+
--------
|
| 115 |
+
>>> G = nx.DiGraph([(1, 2), (1, 3), (2, 5), (3, 4), (4, 5)])
|
| 116 |
+
>>> sorted((u, sorted(df)) for u, df in nx.dominance_frontiers(G, 1).items())
|
| 117 |
+
[(1, []), (2, [5]), (3, [5]), (4, [5]), (5, [])]
|
| 118 |
+
|
| 119 |
+
References
|
| 120 |
+
----------
|
| 121 |
+
.. [1] Cooper, Keith D., Harvey, Timothy J. and Kennedy, Ken.
|
| 122 |
+
"A simple, fast dominance algorithm." (2006).
|
| 123 |
+
https://hdl.handle.net/1911/96345
|
| 124 |
+
"""
|
| 125 |
+
idom = nx.immediate_dominators(G, start)
|
| 126 |
+
|
| 127 |
+
df = {u: set() for u in idom}
|
| 128 |
+
for u in idom:
|
| 129 |
+
if len(G.pred[u]) >= 2:
|
| 130 |
+
for v in G.pred[u]:
|
| 131 |
+
if v in idom:
|
| 132 |
+
while v != idom[u]:
|
| 133 |
+
df[v].add(u)
|
| 134 |
+
v = idom[v]
|
| 135 |
+
return df
|
.venv/lib/python3.11/site-packages/networkx/algorithms/efficiency_measures.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Provides functions for computing the efficiency of nodes and graphs."""
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from networkx.exception import NetworkXNoPath
|
| 5 |
+
|
| 6 |
+
from ..utils import not_implemented_for
|
| 7 |
+
|
| 8 |
+
__all__ = ["efficiency", "local_efficiency", "global_efficiency"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@not_implemented_for("directed")
|
| 12 |
+
@nx._dispatchable
|
| 13 |
+
def efficiency(G, u, v):
|
| 14 |
+
"""Returns the efficiency of a pair of nodes in a graph.
|
| 15 |
+
|
| 16 |
+
The *efficiency* of a pair of nodes is the multiplicative inverse of the
|
| 17 |
+
shortest path distance between the nodes [1]_. Returns 0 if no path
|
| 18 |
+
between nodes.
|
| 19 |
+
|
| 20 |
+
Parameters
|
| 21 |
+
----------
|
| 22 |
+
G : :class:`networkx.Graph`
|
| 23 |
+
An undirected graph for which to compute the average local efficiency.
|
| 24 |
+
u, v : node
|
| 25 |
+
Nodes in the graph ``G``.
|
| 26 |
+
|
| 27 |
+
Returns
|
| 28 |
+
-------
|
| 29 |
+
float
|
| 30 |
+
Multiplicative inverse of the shortest path distance between the nodes.
|
| 31 |
+
|
| 32 |
+
Examples
|
| 33 |
+
--------
|
| 34 |
+
>>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
|
| 35 |
+
>>> nx.efficiency(G, 2, 3) # this gives efficiency for node 2 and 3
|
| 36 |
+
0.5
|
| 37 |
+
|
| 38 |
+
Notes
|
| 39 |
+
-----
|
| 40 |
+
Edge weights are ignored when computing the shortest path distances.
|
| 41 |
+
|
| 42 |
+
See also
|
| 43 |
+
--------
|
| 44 |
+
local_efficiency
|
| 45 |
+
global_efficiency
|
| 46 |
+
|
| 47 |
+
References
|
| 48 |
+
----------
|
| 49 |
+
.. [1] Latora, Vito, and Massimo Marchiori.
|
| 50 |
+
"Efficient behavior of small-world networks."
|
| 51 |
+
*Physical Review Letters* 87.19 (2001): 198701.
|
| 52 |
+
<https://doi.org/10.1103/PhysRevLett.87.198701>
|
| 53 |
+
|
| 54 |
+
"""
|
| 55 |
+
try:
|
| 56 |
+
eff = 1 / nx.shortest_path_length(G, u, v)
|
| 57 |
+
except NetworkXNoPath:
|
| 58 |
+
eff = 0
|
| 59 |
+
return eff
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@not_implemented_for("directed")
|
| 63 |
+
@nx._dispatchable
|
| 64 |
+
def global_efficiency(G):
|
| 65 |
+
"""Returns the average global efficiency of the graph.
|
| 66 |
+
|
| 67 |
+
The *efficiency* of a pair of nodes in a graph is the multiplicative
|
| 68 |
+
inverse of the shortest path distance between the nodes. The *average
|
| 69 |
+
global efficiency* of a graph is the average efficiency of all pairs of
|
| 70 |
+
nodes [1]_.
|
| 71 |
+
|
| 72 |
+
Parameters
|
| 73 |
+
----------
|
| 74 |
+
G : :class:`networkx.Graph`
|
| 75 |
+
An undirected graph for which to compute the average global efficiency.
|
| 76 |
+
|
| 77 |
+
Returns
|
| 78 |
+
-------
|
| 79 |
+
float
|
| 80 |
+
The average global efficiency of the graph.
|
| 81 |
+
|
| 82 |
+
Examples
|
| 83 |
+
--------
|
| 84 |
+
>>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
|
| 85 |
+
>>> round(nx.global_efficiency(G), 12)
|
| 86 |
+
0.916666666667
|
| 87 |
+
|
| 88 |
+
Notes
|
| 89 |
+
-----
|
| 90 |
+
Edge weights are ignored when computing the shortest path distances.
|
| 91 |
+
|
| 92 |
+
See also
|
| 93 |
+
--------
|
| 94 |
+
local_efficiency
|
| 95 |
+
|
| 96 |
+
References
|
| 97 |
+
----------
|
| 98 |
+
.. [1] Latora, Vito, and Massimo Marchiori.
|
| 99 |
+
"Efficient behavior of small-world networks."
|
| 100 |
+
*Physical Review Letters* 87.19 (2001): 198701.
|
| 101 |
+
<https://doi.org/10.1103/PhysRevLett.87.198701>
|
| 102 |
+
|
| 103 |
+
"""
|
| 104 |
+
n = len(G)
|
| 105 |
+
denom = n * (n - 1)
|
| 106 |
+
if denom != 0:
|
| 107 |
+
lengths = nx.all_pairs_shortest_path_length(G)
|
| 108 |
+
g_eff = 0
|
| 109 |
+
for source, targets in lengths:
|
| 110 |
+
for target, distance in targets.items():
|
| 111 |
+
if distance > 0:
|
| 112 |
+
g_eff += 1 / distance
|
| 113 |
+
g_eff /= denom
|
| 114 |
+
# g_eff = sum(1 / d for s, tgts in lengths
|
| 115 |
+
# for t, d in tgts.items() if d > 0) / denom
|
| 116 |
+
else:
|
| 117 |
+
g_eff = 0
|
| 118 |
+
# TODO This can be made more efficient by computing all pairs shortest
|
| 119 |
+
# path lengths in parallel.
|
| 120 |
+
return g_eff
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
@not_implemented_for("directed")
|
| 124 |
+
@nx._dispatchable
|
| 125 |
+
def local_efficiency(G):
|
| 126 |
+
"""Returns the average local efficiency of the graph.
|
| 127 |
+
|
| 128 |
+
The *efficiency* of a pair of nodes in a graph is the multiplicative
|
| 129 |
+
inverse of the shortest path distance between the nodes. The *local
|
| 130 |
+
efficiency* of a node in the graph is the average global efficiency of the
|
| 131 |
+
subgraph induced by the neighbors of the node. The *average local
|
| 132 |
+
efficiency* is the average of the local efficiencies of each node [1]_.
|
| 133 |
+
|
| 134 |
+
Parameters
|
| 135 |
+
----------
|
| 136 |
+
G : :class:`networkx.Graph`
|
| 137 |
+
An undirected graph for which to compute the average local efficiency.
|
| 138 |
+
|
| 139 |
+
Returns
|
| 140 |
+
-------
|
| 141 |
+
float
|
| 142 |
+
The average local efficiency of the graph.
|
| 143 |
+
|
| 144 |
+
Examples
|
| 145 |
+
--------
|
| 146 |
+
>>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
|
| 147 |
+
>>> nx.local_efficiency(G)
|
| 148 |
+
0.9166666666666667
|
| 149 |
+
|
| 150 |
+
Notes
|
| 151 |
+
-----
|
| 152 |
+
Edge weights are ignored when computing the shortest path distances.
|
| 153 |
+
|
| 154 |
+
See also
|
| 155 |
+
--------
|
| 156 |
+
global_efficiency
|
| 157 |
+
|
| 158 |
+
References
|
| 159 |
+
----------
|
| 160 |
+
.. [1] Latora, Vito, and Massimo Marchiori.
|
| 161 |
+
"Efficient behavior of small-world networks."
|
| 162 |
+
*Physical Review Letters* 87.19 (2001): 198701.
|
| 163 |
+
<https://doi.org/10.1103/PhysRevLett.87.198701>
|
| 164 |
+
|
| 165 |
+
"""
|
| 166 |
+
efficiency_list = (global_efficiency(G.subgraph(G[v])) for v in G)
|
| 167 |
+
return sum(efficiency_list) / len(G)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/euler.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Eulerian circuits and graphs.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from itertools import combinations
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
|
| 9 |
+
from ..utils import arbitrary_element, not_implemented_for
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"is_eulerian",
|
| 13 |
+
"eulerian_circuit",
|
| 14 |
+
"eulerize",
|
| 15 |
+
"is_semieulerian",
|
| 16 |
+
"has_eulerian_path",
|
| 17 |
+
"eulerian_path",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@nx._dispatchable
|
| 22 |
+
def is_eulerian(G):
|
| 23 |
+
"""Returns True if and only if `G` is Eulerian.
|
| 24 |
+
|
| 25 |
+
A graph is *Eulerian* if it has an Eulerian circuit. An *Eulerian
|
| 26 |
+
circuit* is a closed walk that includes each edge of a graph exactly
|
| 27 |
+
once.
|
| 28 |
+
|
| 29 |
+
Graphs with isolated vertices (i.e. vertices with zero degree) are not
|
| 30 |
+
considered to have Eulerian circuits. Therefore, if the graph is not
|
| 31 |
+
connected (or not strongly connected, for directed graphs), this function
|
| 32 |
+
returns False.
|
| 33 |
+
|
| 34 |
+
Parameters
|
| 35 |
+
----------
|
| 36 |
+
G : NetworkX graph
|
| 37 |
+
A graph, either directed or undirected.
|
| 38 |
+
|
| 39 |
+
Examples
|
| 40 |
+
--------
|
| 41 |
+
>>> nx.is_eulerian(nx.DiGraph({0: [3], 1: [2], 2: [3], 3: [0, 1]}))
|
| 42 |
+
True
|
| 43 |
+
>>> nx.is_eulerian(nx.complete_graph(5))
|
| 44 |
+
True
|
| 45 |
+
>>> nx.is_eulerian(nx.petersen_graph())
|
| 46 |
+
False
|
| 47 |
+
|
| 48 |
+
If you prefer to allow graphs with isolated vertices to have Eulerian circuits,
|
| 49 |
+
you can first remove such vertices and then call `is_eulerian` as below example shows.
|
| 50 |
+
|
| 51 |
+
>>> G = nx.Graph([(0, 1), (1, 2), (0, 2)])
|
| 52 |
+
>>> G.add_node(3)
|
| 53 |
+
>>> nx.is_eulerian(G)
|
| 54 |
+
False
|
| 55 |
+
|
| 56 |
+
>>> G.remove_nodes_from(list(nx.isolates(G)))
|
| 57 |
+
>>> nx.is_eulerian(G)
|
| 58 |
+
True
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
"""
|
| 62 |
+
if G.is_directed():
|
| 63 |
+
# Every node must have equal in degree and out degree and the
|
| 64 |
+
# graph must be strongly connected
|
| 65 |
+
return all(
|
| 66 |
+
G.in_degree(n) == G.out_degree(n) for n in G
|
| 67 |
+
) and nx.is_strongly_connected(G)
|
| 68 |
+
# An undirected Eulerian graph has no vertices of odd degree and
|
| 69 |
+
# must be connected.
|
| 70 |
+
return all(d % 2 == 0 for v, d in G.degree()) and nx.is_connected(G)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@nx._dispatchable
|
| 74 |
+
def is_semieulerian(G):
|
| 75 |
+
"""Return True iff `G` is semi-Eulerian.
|
| 76 |
+
|
| 77 |
+
G is semi-Eulerian if it has an Eulerian path but no Eulerian circuit.
|
| 78 |
+
|
| 79 |
+
See Also
|
| 80 |
+
--------
|
| 81 |
+
has_eulerian_path
|
| 82 |
+
is_eulerian
|
| 83 |
+
"""
|
| 84 |
+
return has_eulerian_path(G) and not is_eulerian(G)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _find_path_start(G):
|
| 88 |
+
"""Return a suitable starting vertex for an Eulerian path.
|
| 89 |
+
|
| 90 |
+
If no path exists, return None.
|
| 91 |
+
"""
|
| 92 |
+
if not has_eulerian_path(G):
|
| 93 |
+
return None
|
| 94 |
+
|
| 95 |
+
if is_eulerian(G):
|
| 96 |
+
return arbitrary_element(G)
|
| 97 |
+
|
| 98 |
+
if G.is_directed():
|
| 99 |
+
v1, v2 = (v for v in G if G.in_degree(v) != G.out_degree(v))
|
| 100 |
+
# Determines which is the 'start' node (as opposed to the 'end')
|
| 101 |
+
if G.out_degree(v1) > G.in_degree(v1):
|
| 102 |
+
return v1
|
| 103 |
+
else:
|
| 104 |
+
return v2
|
| 105 |
+
|
| 106 |
+
else:
|
| 107 |
+
# In an undirected graph randomly choose one of the possibilities
|
| 108 |
+
start = [v for v in G if G.degree(v) % 2 != 0][0]
|
| 109 |
+
return start
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _simplegraph_eulerian_circuit(G, source):
|
| 113 |
+
if G.is_directed():
|
| 114 |
+
degree = G.out_degree
|
| 115 |
+
edges = G.out_edges
|
| 116 |
+
else:
|
| 117 |
+
degree = G.degree
|
| 118 |
+
edges = G.edges
|
| 119 |
+
vertex_stack = [source]
|
| 120 |
+
last_vertex = None
|
| 121 |
+
while vertex_stack:
|
| 122 |
+
current_vertex = vertex_stack[-1]
|
| 123 |
+
if degree(current_vertex) == 0:
|
| 124 |
+
if last_vertex is not None:
|
| 125 |
+
yield (last_vertex, current_vertex)
|
| 126 |
+
last_vertex = current_vertex
|
| 127 |
+
vertex_stack.pop()
|
| 128 |
+
else:
|
| 129 |
+
_, next_vertex = arbitrary_element(edges(current_vertex))
|
| 130 |
+
vertex_stack.append(next_vertex)
|
| 131 |
+
G.remove_edge(current_vertex, next_vertex)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _multigraph_eulerian_circuit(G, source):
|
| 135 |
+
if G.is_directed():
|
| 136 |
+
degree = G.out_degree
|
| 137 |
+
edges = G.out_edges
|
| 138 |
+
else:
|
| 139 |
+
degree = G.degree
|
| 140 |
+
edges = G.edges
|
| 141 |
+
vertex_stack = [(source, None)]
|
| 142 |
+
last_vertex = None
|
| 143 |
+
last_key = None
|
| 144 |
+
while vertex_stack:
|
| 145 |
+
current_vertex, current_key = vertex_stack[-1]
|
| 146 |
+
if degree(current_vertex) == 0:
|
| 147 |
+
if last_vertex is not None:
|
| 148 |
+
yield (last_vertex, current_vertex, last_key)
|
| 149 |
+
last_vertex, last_key = current_vertex, current_key
|
| 150 |
+
vertex_stack.pop()
|
| 151 |
+
else:
|
| 152 |
+
triple = arbitrary_element(edges(current_vertex, keys=True))
|
| 153 |
+
_, next_vertex, next_key = triple
|
| 154 |
+
vertex_stack.append((next_vertex, next_key))
|
| 155 |
+
G.remove_edge(current_vertex, next_vertex, next_key)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
@nx._dispatchable
|
| 159 |
+
def eulerian_circuit(G, source=None, keys=False):
|
| 160 |
+
"""Returns an iterator over the edges of an Eulerian circuit in `G`.
|
| 161 |
+
|
| 162 |
+
An *Eulerian circuit* is a closed walk that includes each edge of a
|
| 163 |
+
graph exactly once.
|
| 164 |
+
|
| 165 |
+
Parameters
|
| 166 |
+
----------
|
| 167 |
+
G : NetworkX graph
|
| 168 |
+
A graph, either directed or undirected.
|
| 169 |
+
|
| 170 |
+
source : node, optional
|
| 171 |
+
Starting node for circuit.
|
| 172 |
+
|
| 173 |
+
keys : bool
|
| 174 |
+
If False, edges generated by this function will be of the form
|
| 175 |
+
``(u, v)``. Otherwise, edges will be of the form ``(u, v, k)``.
|
| 176 |
+
This option is ignored unless `G` is a multigraph.
|
| 177 |
+
|
| 178 |
+
Returns
|
| 179 |
+
-------
|
| 180 |
+
edges : iterator
|
| 181 |
+
An iterator over edges in the Eulerian circuit.
|
| 182 |
+
|
| 183 |
+
Raises
|
| 184 |
+
------
|
| 185 |
+
NetworkXError
|
| 186 |
+
If the graph is not Eulerian.
|
| 187 |
+
|
| 188 |
+
See Also
|
| 189 |
+
--------
|
| 190 |
+
is_eulerian
|
| 191 |
+
|
| 192 |
+
Notes
|
| 193 |
+
-----
|
| 194 |
+
This is a linear time implementation of an algorithm adapted from [1]_.
|
| 195 |
+
|
| 196 |
+
For general information about Euler tours, see [2]_.
|
| 197 |
+
|
| 198 |
+
References
|
| 199 |
+
----------
|
| 200 |
+
.. [1] J. Edmonds, E. L. Johnson.
|
| 201 |
+
Matching, Euler tours and the Chinese postman.
|
| 202 |
+
Mathematical programming, Volume 5, Issue 1 (1973), 111-114.
|
| 203 |
+
.. [2] https://en.wikipedia.org/wiki/Eulerian_path
|
| 204 |
+
|
| 205 |
+
Examples
|
| 206 |
+
--------
|
| 207 |
+
To get an Eulerian circuit in an undirected graph::
|
| 208 |
+
|
| 209 |
+
>>> G = nx.complete_graph(3)
|
| 210 |
+
>>> list(nx.eulerian_circuit(G))
|
| 211 |
+
[(0, 2), (2, 1), (1, 0)]
|
| 212 |
+
>>> list(nx.eulerian_circuit(G, source=1))
|
| 213 |
+
[(1, 2), (2, 0), (0, 1)]
|
| 214 |
+
|
| 215 |
+
To get the sequence of vertices in an Eulerian circuit::
|
| 216 |
+
|
| 217 |
+
>>> [u for u, v in nx.eulerian_circuit(G)]
|
| 218 |
+
[0, 2, 1]
|
| 219 |
+
|
| 220 |
+
"""
|
| 221 |
+
if not is_eulerian(G):
|
| 222 |
+
raise nx.NetworkXError("G is not Eulerian.")
|
| 223 |
+
if G.is_directed():
|
| 224 |
+
G = G.reverse()
|
| 225 |
+
else:
|
| 226 |
+
G = G.copy()
|
| 227 |
+
if source is None:
|
| 228 |
+
source = arbitrary_element(G)
|
| 229 |
+
if G.is_multigraph():
|
| 230 |
+
for u, v, k in _multigraph_eulerian_circuit(G, source):
|
| 231 |
+
if keys:
|
| 232 |
+
yield u, v, k
|
| 233 |
+
else:
|
| 234 |
+
yield u, v
|
| 235 |
+
else:
|
| 236 |
+
yield from _simplegraph_eulerian_circuit(G, source)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
@nx._dispatchable
|
| 240 |
+
def has_eulerian_path(G, source=None):
|
| 241 |
+
"""Return True iff `G` has an Eulerian path.
|
| 242 |
+
|
| 243 |
+
An Eulerian path is a path in a graph which uses each edge of a graph
|
| 244 |
+
exactly once. If `source` is specified, then this function checks
|
| 245 |
+
whether an Eulerian path that starts at node `source` exists.
|
| 246 |
+
|
| 247 |
+
A directed graph has an Eulerian path iff:
|
| 248 |
+
- at most one vertex has out_degree - in_degree = 1,
|
| 249 |
+
- at most one vertex has in_degree - out_degree = 1,
|
| 250 |
+
- every other vertex has equal in_degree and out_degree,
|
| 251 |
+
- and all of its vertices belong to a single connected
|
| 252 |
+
component of the underlying undirected graph.
|
| 253 |
+
|
| 254 |
+
If `source` is not None, an Eulerian path starting at `source` exists if no
|
| 255 |
+
other node has out_degree - in_degree = 1. This is equivalent to either
|
| 256 |
+
there exists an Eulerian circuit or `source` has out_degree - in_degree = 1
|
| 257 |
+
and the conditions above hold.
|
| 258 |
+
|
| 259 |
+
An undirected graph has an Eulerian path iff:
|
| 260 |
+
- exactly zero or two vertices have odd degree,
|
| 261 |
+
- and all of its vertices belong to a single connected component.
|
| 262 |
+
|
| 263 |
+
If `source` is not None, an Eulerian path starting at `source` exists if
|
| 264 |
+
either there exists an Eulerian circuit or `source` has an odd degree and the
|
| 265 |
+
conditions above hold.
|
| 266 |
+
|
| 267 |
+
Graphs with isolated vertices (i.e. vertices with zero degree) are not considered
|
| 268 |
+
to have an Eulerian path. Therefore, if the graph is not connected (or not strongly
|
| 269 |
+
connected, for directed graphs), this function returns False.
|
| 270 |
+
|
| 271 |
+
Parameters
|
| 272 |
+
----------
|
| 273 |
+
G : NetworkX Graph
|
| 274 |
+
The graph to find an euler path in.
|
| 275 |
+
|
| 276 |
+
source : node, optional
|
| 277 |
+
Starting node for path.
|
| 278 |
+
|
| 279 |
+
Returns
|
| 280 |
+
-------
|
| 281 |
+
Bool : True if G has an Eulerian path.
|
| 282 |
+
|
| 283 |
+
Examples
|
| 284 |
+
--------
|
| 285 |
+
If you prefer to allow graphs with isolated vertices to have Eulerian path,
|
| 286 |
+
you can first remove such vertices and then call `has_eulerian_path` as below example shows.
|
| 287 |
+
|
| 288 |
+
>>> G = nx.Graph([(0, 1), (1, 2), (0, 2)])
|
| 289 |
+
>>> G.add_node(3)
|
| 290 |
+
>>> nx.has_eulerian_path(G)
|
| 291 |
+
False
|
| 292 |
+
|
| 293 |
+
>>> G.remove_nodes_from(list(nx.isolates(G)))
|
| 294 |
+
>>> nx.has_eulerian_path(G)
|
| 295 |
+
True
|
| 296 |
+
|
| 297 |
+
See Also
|
| 298 |
+
--------
|
| 299 |
+
is_eulerian
|
| 300 |
+
eulerian_path
|
| 301 |
+
"""
|
| 302 |
+
if nx.is_eulerian(G):
|
| 303 |
+
return True
|
| 304 |
+
|
| 305 |
+
if G.is_directed():
|
| 306 |
+
ins = G.in_degree
|
| 307 |
+
outs = G.out_degree
|
| 308 |
+
# Since we know it is not eulerian, outs - ins must be 1 for source
|
| 309 |
+
if source is not None and outs[source] - ins[source] != 1:
|
| 310 |
+
return False
|
| 311 |
+
|
| 312 |
+
unbalanced_ins = 0
|
| 313 |
+
unbalanced_outs = 0
|
| 314 |
+
for v in G:
|
| 315 |
+
if ins[v] - outs[v] == 1:
|
| 316 |
+
unbalanced_ins += 1
|
| 317 |
+
elif outs[v] - ins[v] == 1:
|
| 318 |
+
unbalanced_outs += 1
|
| 319 |
+
elif ins[v] != outs[v]:
|
| 320 |
+
return False
|
| 321 |
+
|
| 322 |
+
return (
|
| 323 |
+
unbalanced_ins <= 1 and unbalanced_outs <= 1 and nx.is_weakly_connected(G)
|
| 324 |
+
)
|
| 325 |
+
else:
|
| 326 |
+
# We know it is not eulerian, so degree of source must be odd.
|
| 327 |
+
if source is not None and G.degree[source] % 2 != 1:
|
| 328 |
+
return False
|
| 329 |
+
|
| 330 |
+
# Sum is 2 since we know it is not eulerian (which implies sum is 0)
|
| 331 |
+
return sum(d % 2 == 1 for v, d in G.degree()) == 2 and nx.is_connected(G)
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
@nx._dispatchable
|
| 335 |
+
def eulerian_path(G, source=None, keys=False):
|
| 336 |
+
"""Return an iterator over the edges of an Eulerian path in `G`.
|
| 337 |
+
|
| 338 |
+
Parameters
|
| 339 |
+
----------
|
| 340 |
+
G : NetworkX Graph
|
| 341 |
+
The graph in which to look for an eulerian path.
|
| 342 |
+
source : node or None (default: None)
|
| 343 |
+
The node at which to start the search. None means search over all
|
| 344 |
+
starting nodes.
|
| 345 |
+
keys : Bool (default: False)
|
| 346 |
+
Indicates whether to yield edge 3-tuples (u, v, edge_key).
|
| 347 |
+
The default yields edge 2-tuples
|
| 348 |
+
|
| 349 |
+
Yields
|
| 350 |
+
------
|
| 351 |
+
Edge tuples along the eulerian path.
|
| 352 |
+
|
| 353 |
+
Warning: If `source` provided is not the start node of an Euler path
|
| 354 |
+
will raise error even if an Euler Path exists.
|
| 355 |
+
"""
|
| 356 |
+
if not has_eulerian_path(G, source):
|
| 357 |
+
raise nx.NetworkXError("Graph has no Eulerian paths.")
|
| 358 |
+
if G.is_directed():
|
| 359 |
+
G = G.reverse()
|
| 360 |
+
if source is None or nx.is_eulerian(G) is False:
|
| 361 |
+
source = _find_path_start(G)
|
| 362 |
+
if G.is_multigraph():
|
| 363 |
+
for u, v, k in _multigraph_eulerian_circuit(G, source):
|
| 364 |
+
if keys:
|
| 365 |
+
yield u, v, k
|
| 366 |
+
else:
|
| 367 |
+
yield u, v
|
| 368 |
+
else:
|
| 369 |
+
yield from _simplegraph_eulerian_circuit(G, source)
|
| 370 |
+
else:
|
| 371 |
+
G = G.copy()
|
| 372 |
+
if source is None:
|
| 373 |
+
source = _find_path_start(G)
|
| 374 |
+
if G.is_multigraph():
|
| 375 |
+
if keys:
|
| 376 |
+
yield from reversed(
|
| 377 |
+
[(v, u, k) for u, v, k in _multigraph_eulerian_circuit(G, source)]
|
| 378 |
+
)
|
| 379 |
+
else:
|
| 380 |
+
yield from reversed(
|
| 381 |
+
[(v, u) for u, v, k in _multigraph_eulerian_circuit(G, source)]
|
| 382 |
+
)
|
| 383 |
+
else:
|
| 384 |
+
yield from reversed(
|
| 385 |
+
[(v, u) for u, v in _simplegraph_eulerian_circuit(G, source)]
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
@not_implemented_for("directed")
|
| 390 |
+
@nx._dispatchable(returns_graph=True)
|
| 391 |
+
def eulerize(G):
|
| 392 |
+
"""Transforms a graph into an Eulerian graph.
|
| 393 |
+
|
| 394 |
+
If `G` is Eulerian the result is `G` as a MultiGraph, otherwise the result is a smallest
|
| 395 |
+
(in terms of the number of edges) multigraph whose underlying simple graph is `G`.
|
| 396 |
+
|
| 397 |
+
Parameters
|
| 398 |
+
----------
|
| 399 |
+
G : NetworkX graph
|
| 400 |
+
An undirected graph
|
| 401 |
+
|
| 402 |
+
Returns
|
| 403 |
+
-------
|
| 404 |
+
G : NetworkX multigraph
|
| 405 |
+
|
| 406 |
+
Raises
|
| 407 |
+
------
|
| 408 |
+
NetworkXError
|
| 409 |
+
If the graph is not connected.
|
| 410 |
+
|
| 411 |
+
See Also
|
| 412 |
+
--------
|
| 413 |
+
is_eulerian
|
| 414 |
+
eulerian_circuit
|
| 415 |
+
|
| 416 |
+
References
|
| 417 |
+
----------
|
| 418 |
+
.. [1] J. Edmonds, E. L. Johnson.
|
| 419 |
+
Matching, Euler tours and the Chinese postman.
|
| 420 |
+
Mathematical programming, Volume 5, Issue 1 (1973), 111-114.
|
| 421 |
+
.. [2] https://en.wikipedia.org/wiki/Eulerian_path
|
| 422 |
+
.. [3] http://web.math.princeton.edu/math_alive/5/Notes1.pdf
|
| 423 |
+
|
| 424 |
+
Examples
|
| 425 |
+
--------
|
| 426 |
+
>>> G = nx.complete_graph(10)
|
| 427 |
+
>>> H = nx.eulerize(G)
|
| 428 |
+
>>> nx.is_eulerian(H)
|
| 429 |
+
True
|
| 430 |
+
|
| 431 |
+
"""
|
| 432 |
+
if G.order() == 0:
|
| 433 |
+
raise nx.NetworkXPointlessConcept("Cannot Eulerize null graph")
|
| 434 |
+
if not nx.is_connected(G):
|
| 435 |
+
raise nx.NetworkXError("G is not connected")
|
| 436 |
+
odd_degree_nodes = [n for n, d in G.degree() if d % 2 == 1]
|
| 437 |
+
G = nx.MultiGraph(G)
|
| 438 |
+
if len(odd_degree_nodes) == 0:
|
| 439 |
+
return G
|
| 440 |
+
|
| 441 |
+
# get all shortest paths between vertices of odd degree
|
| 442 |
+
odd_deg_pairs_paths = [
|
| 443 |
+
(m, {n: nx.shortest_path(G, source=m, target=n)})
|
| 444 |
+
for m, n in combinations(odd_degree_nodes, 2)
|
| 445 |
+
]
|
| 446 |
+
|
| 447 |
+
# use the number of vertices in a graph + 1 as an upper bound on
|
| 448 |
+
# the maximum length of a path in G
|
| 449 |
+
upper_bound_on_max_path_length = len(G) + 1
|
| 450 |
+
|
| 451 |
+
# use "len(G) + 1 - len(P)",
|
| 452 |
+
# where P is a shortest path between vertices n and m,
|
| 453 |
+
# as edge-weights in a new graph
|
| 454 |
+
# store the paths in the graph for easy indexing later
|
| 455 |
+
Gp = nx.Graph()
|
| 456 |
+
for n, Ps in odd_deg_pairs_paths:
|
| 457 |
+
for m, P in Ps.items():
|
| 458 |
+
if n != m:
|
| 459 |
+
Gp.add_edge(
|
| 460 |
+
m, n, weight=upper_bound_on_max_path_length - len(P), path=P
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
# find the minimum weight matching of edges in the weighted graph
|
| 464 |
+
best_matching = nx.Graph(list(nx.max_weight_matching(Gp)))
|
| 465 |
+
|
| 466 |
+
# duplicate each edge along each path in the set of paths in Gp
|
| 467 |
+
for m, n in best_matching.edges():
|
| 468 |
+
path = Gp[m][n]["path"]
|
| 469 |
+
G.add_edges_from(nx.utils.pairwise(path))
|
| 470 |
+
return G
|
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .maxflow import *
|
| 2 |
+
from .mincost import *
|
| 3 |
+
from .boykovkolmogorov import *
|
| 4 |
+
from .dinitz_alg import *
|
| 5 |
+
from .edmondskarp import *
|
| 6 |
+
from .gomory_hu import *
|
| 7 |
+
from .preflowpush import *
|
| 8 |
+
from .shortestaugmentingpath import *
|
| 9 |
+
from .capacityscaling import *
|
| 10 |
+
from .networksimplex import *
|
| 11 |
+
from .utils import build_flow_dict, build_residual_network
|
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/boykovkolmogorov.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Boykov-Kolmogorov algorithm for maximum flow problems.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from collections import deque
|
| 6 |
+
from operator import itemgetter
|
| 7 |
+
|
| 8 |
+
import networkx as nx
|
| 9 |
+
from networkx.algorithms.flow.utils import build_residual_network
|
| 10 |
+
|
| 11 |
+
__all__ = ["boykov_kolmogorov"]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True)
|
| 15 |
+
def boykov_kolmogorov(
|
| 16 |
+
G, s, t, capacity="capacity", residual=None, value_only=False, cutoff=None
|
| 17 |
+
):
|
| 18 |
+
r"""Find a maximum single-commodity flow using Boykov-Kolmogorov algorithm.
|
| 19 |
+
|
| 20 |
+
This function returns the residual network resulting after computing
|
| 21 |
+
the maximum flow. See below for details about the conventions
|
| 22 |
+
NetworkX uses for defining residual networks.
|
| 23 |
+
|
| 24 |
+
This algorithm has worse case complexity $O(n^2 m |C|)$ for $n$ nodes, $m$
|
| 25 |
+
edges, and $|C|$ the cost of the minimum cut [1]_. This implementation
|
| 26 |
+
uses the marking heuristic defined in [2]_ which improves its running
|
| 27 |
+
time in many practical problems.
|
| 28 |
+
|
| 29 |
+
Parameters
|
| 30 |
+
----------
|
| 31 |
+
G : NetworkX graph
|
| 32 |
+
Edges of the graph are expected to have an attribute called
|
| 33 |
+
'capacity'. If this attribute is not present, the edge is
|
| 34 |
+
considered to have infinite capacity.
|
| 35 |
+
|
| 36 |
+
s : node
|
| 37 |
+
Source node for the flow.
|
| 38 |
+
|
| 39 |
+
t : node
|
| 40 |
+
Sink node for the flow.
|
| 41 |
+
|
| 42 |
+
capacity : string
|
| 43 |
+
Edges of the graph G are expected to have an attribute capacity
|
| 44 |
+
that indicates how much flow the edge can support. If this
|
| 45 |
+
attribute is not present, the edge is considered to have
|
| 46 |
+
infinite capacity. Default value: 'capacity'.
|
| 47 |
+
|
| 48 |
+
residual : NetworkX graph
|
| 49 |
+
Residual network on which the algorithm is to be executed. If None, a
|
| 50 |
+
new residual network is created. Default value: None.
|
| 51 |
+
|
| 52 |
+
value_only : bool
|
| 53 |
+
If True compute only the value of the maximum flow. This parameter
|
| 54 |
+
will be ignored by this algorithm because it is not applicable.
|
| 55 |
+
|
| 56 |
+
cutoff : integer, float
|
| 57 |
+
If specified, the algorithm will terminate when the flow value reaches
|
| 58 |
+
or exceeds the cutoff. In this case, it may be unable to immediately
|
| 59 |
+
determine a minimum cut. Default value: None.
|
| 60 |
+
|
| 61 |
+
Returns
|
| 62 |
+
-------
|
| 63 |
+
R : NetworkX DiGraph
|
| 64 |
+
Residual network after computing the maximum flow.
|
| 65 |
+
|
| 66 |
+
Raises
|
| 67 |
+
------
|
| 68 |
+
NetworkXError
|
| 69 |
+
The algorithm does not support MultiGraph and MultiDiGraph. If
|
| 70 |
+
the input graph is an instance of one of these two classes, a
|
| 71 |
+
NetworkXError is raised.
|
| 72 |
+
|
| 73 |
+
NetworkXUnbounded
|
| 74 |
+
If the graph has a path of infinite capacity, the value of a
|
| 75 |
+
feasible flow on the graph is unbounded above and the function
|
| 76 |
+
raises a NetworkXUnbounded.
|
| 77 |
+
|
| 78 |
+
See also
|
| 79 |
+
--------
|
| 80 |
+
:meth:`maximum_flow`
|
| 81 |
+
:meth:`minimum_cut`
|
| 82 |
+
:meth:`preflow_push`
|
| 83 |
+
:meth:`shortest_augmenting_path`
|
| 84 |
+
|
| 85 |
+
Notes
|
| 86 |
+
-----
|
| 87 |
+
The residual network :samp:`R` from an input graph :samp:`G` has the
|
| 88 |
+
same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
|
| 89 |
+
of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
|
| 90 |
+
self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
|
| 91 |
+
in :samp:`G`.
|
| 92 |
+
|
| 93 |
+
For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
|
| 94 |
+
is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
|
| 95 |
+
in :samp:`G` or zero otherwise. If the capacity is infinite,
|
| 96 |
+
:samp:`R[u][v]['capacity']` will have a high arbitrary finite value
|
| 97 |
+
that does not affect the solution of the problem. This value is stored in
|
| 98 |
+
:samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
|
| 99 |
+
:samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
|
| 100 |
+
satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
|
| 101 |
+
|
| 102 |
+
The flow value, defined as the total flow into :samp:`t`, the sink, is
|
| 103 |
+
stored in :samp:`R.graph['flow_value']`. If :samp:`cutoff` is not
|
| 104 |
+
specified, reachability to :samp:`t` using only edges :samp:`(u, v)` such
|
| 105 |
+
that :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
|
| 106 |
+
:samp:`s`-:samp:`t` cut.
|
| 107 |
+
|
| 108 |
+
Examples
|
| 109 |
+
--------
|
| 110 |
+
>>> from networkx.algorithms.flow import boykov_kolmogorov
|
| 111 |
+
|
| 112 |
+
The functions that implement flow algorithms and output a residual
|
| 113 |
+
network, such as this one, are not imported to the base NetworkX
|
| 114 |
+
namespace, so you have to explicitly import them from the flow package.
|
| 115 |
+
|
| 116 |
+
>>> G = nx.DiGraph()
|
| 117 |
+
>>> G.add_edge("x", "a", capacity=3.0)
|
| 118 |
+
>>> G.add_edge("x", "b", capacity=1.0)
|
| 119 |
+
>>> G.add_edge("a", "c", capacity=3.0)
|
| 120 |
+
>>> G.add_edge("b", "c", capacity=5.0)
|
| 121 |
+
>>> G.add_edge("b", "d", capacity=4.0)
|
| 122 |
+
>>> G.add_edge("d", "e", capacity=2.0)
|
| 123 |
+
>>> G.add_edge("c", "y", capacity=2.0)
|
| 124 |
+
>>> G.add_edge("e", "y", capacity=3.0)
|
| 125 |
+
>>> R = boykov_kolmogorov(G, "x", "y")
|
| 126 |
+
>>> flow_value = nx.maximum_flow_value(G, "x", "y")
|
| 127 |
+
>>> flow_value
|
| 128 |
+
3.0
|
| 129 |
+
>>> flow_value == R.graph["flow_value"]
|
| 130 |
+
True
|
| 131 |
+
|
| 132 |
+
A nice feature of the Boykov-Kolmogorov algorithm is that a partition
|
| 133 |
+
of the nodes that defines a minimum cut can be easily computed based
|
| 134 |
+
on the search trees used during the algorithm. These trees are stored
|
| 135 |
+
in the graph attribute `trees` of the residual network.
|
| 136 |
+
|
| 137 |
+
>>> source_tree, target_tree = R.graph["trees"]
|
| 138 |
+
>>> partition = (set(source_tree), set(G) - set(source_tree))
|
| 139 |
+
|
| 140 |
+
Or equivalently:
|
| 141 |
+
|
| 142 |
+
>>> partition = (set(G) - set(target_tree), set(target_tree))
|
| 143 |
+
|
| 144 |
+
References
|
| 145 |
+
----------
|
| 146 |
+
.. [1] Boykov, Y., & Kolmogorov, V. (2004). An experimental comparison
|
| 147 |
+
of min-cut/max-flow algorithms for energy minimization in vision.
|
| 148 |
+
Pattern Analysis and Machine Intelligence, IEEE Transactions on,
|
| 149 |
+
26(9), 1124-1137.
|
| 150 |
+
https://doi.org/10.1109/TPAMI.2004.60
|
| 151 |
+
|
| 152 |
+
.. [2] Vladimir Kolmogorov. Graph-based Algorithms for Multi-camera
|
| 153 |
+
Reconstruction Problem. PhD thesis, Cornell University, CS Department,
|
| 154 |
+
2003. pp. 109-114.
|
| 155 |
+
https://web.archive.org/web/20170809091249/https://pub.ist.ac.at/~vnk/papers/thesis.pdf
|
| 156 |
+
|
| 157 |
+
"""
|
| 158 |
+
R = boykov_kolmogorov_impl(G, s, t, capacity, residual, cutoff)
|
| 159 |
+
R.graph["algorithm"] = "boykov_kolmogorov"
|
| 160 |
+
nx._clear_cache(R)
|
| 161 |
+
return R
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def boykov_kolmogorov_impl(G, s, t, capacity, residual, cutoff):
|
| 165 |
+
if s not in G:
|
| 166 |
+
raise nx.NetworkXError(f"node {str(s)} not in graph")
|
| 167 |
+
if t not in G:
|
| 168 |
+
raise nx.NetworkXError(f"node {str(t)} not in graph")
|
| 169 |
+
if s == t:
|
| 170 |
+
raise nx.NetworkXError("source and sink are the same node")
|
| 171 |
+
|
| 172 |
+
if residual is None:
|
| 173 |
+
R = build_residual_network(G, capacity)
|
| 174 |
+
else:
|
| 175 |
+
R = residual
|
| 176 |
+
|
| 177 |
+
# Initialize/reset the residual network.
|
| 178 |
+
# This is way too slow
|
| 179 |
+
# nx.set_edge_attributes(R, 0, 'flow')
|
| 180 |
+
for u in R:
|
| 181 |
+
for e in R[u].values():
|
| 182 |
+
e["flow"] = 0
|
| 183 |
+
|
| 184 |
+
# Use an arbitrary high value as infinite. It is computed
|
| 185 |
+
# when building the residual network.
|
| 186 |
+
INF = R.graph["inf"]
|
| 187 |
+
|
| 188 |
+
if cutoff is None:
|
| 189 |
+
cutoff = INF
|
| 190 |
+
|
| 191 |
+
R_succ = R.succ
|
| 192 |
+
R_pred = R.pred
|
| 193 |
+
|
| 194 |
+
def grow():
|
| 195 |
+
"""Bidirectional breadth-first search for the growth stage.
|
| 196 |
+
|
| 197 |
+
Returns a connecting edge, that is and edge that connects
|
| 198 |
+
a node from the source search tree with a node from the
|
| 199 |
+
target search tree.
|
| 200 |
+
The first node in the connecting edge is always from the
|
| 201 |
+
source tree and the last node from the target tree.
|
| 202 |
+
"""
|
| 203 |
+
while active:
|
| 204 |
+
u = active[0]
|
| 205 |
+
if u in source_tree:
|
| 206 |
+
this_tree = source_tree
|
| 207 |
+
other_tree = target_tree
|
| 208 |
+
neighbors = R_succ
|
| 209 |
+
else:
|
| 210 |
+
this_tree = target_tree
|
| 211 |
+
other_tree = source_tree
|
| 212 |
+
neighbors = R_pred
|
| 213 |
+
for v, attr in neighbors[u].items():
|
| 214 |
+
if attr["capacity"] - attr["flow"] > 0:
|
| 215 |
+
if v not in this_tree:
|
| 216 |
+
if v in other_tree:
|
| 217 |
+
return (u, v) if this_tree is source_tree else (v, u)
|
| 218 |
+
this_tree[v] = u
|
| 219 |
+
dist[v] = dist[u] + 1
|
| 220 |
+
timestamp[v] = timestamp[u]
|
| 221 |
+
active.append(v)
|
| 222 |
+
elif v in this_tree and _is_closer(u, v):
|
| 223 |
+
this_tree[v] = u
|
| 224 |
+
dist[v] = dist[u] + 1
|
| 225 |
+
timestamp[v] = timestamp[u]
|
| 226 |
+
_ = active.popleft()
|
| 227 |
+
return None, None
|
| 228 |
+
|
| 229 |
+
def augment(u, v):
|
| 230 |
+
"""Augmentation stage.
|
| 231 |
+
|
| 232 |
+
Reconstruct path and determine its residual capacity.
|
| 233 |
+
We start from a connecting edge, which links a node
|
| 234 |
+
from the source tree to a node from the target tree.
|
| 235 |
+
The connecting edge is the output of the grow function
|
| 236 |
+
and the input of this function.
|
| 237 |
+
"""
|
| 238 |
+
attr = R_succ[u][v]
|
| 239 |
+
flow = min(INF, attr["capacity"] - attr["flow"])
|
| 240 |
+
path = [u]
|
| 241 |
+
# Trace a path from u to s in source_tree.
|
| 242 |
+
w = u
|
| 243 |
+
while w != s:
|
| 244 |
+
n = w
|
| 245 |
+
w = source_tree[n]
|
| 246 |
+
attr = R_pred[n][w]
|
| 247 |
+
flow = min(flow, attr["capacity"] - attr["flow"])
|
| 248 |
+
path.append(w)
|
| 249 |
+
path.reverse()
|
| 250 |
+
# Trace a path from v to t in target_tree.
|
| 251 |
+
path.append(v)
|
| 252 |
+
w = v
|
| 253 |
+
while w != t:
|
| 254 |
+
n = w
|
| 255 |
+
w = target_tree[n]
|
| 256 |
+
attr = R_succ[n][w]
|
| 257 |
+
flow = min(flow, attr["capacity"] - attr["flow"])
|
| 258 |
+
path.append(w)
|
| 259 |
+
# Augment flow along the path and check for saturated edges.
|
| 260 |
+
it = iter(path)
|
| 261 |
+
u = next(it)
|
| 262 |
+
these_orphans = []
|
| 263 |
+
for v in it:
|
| 264 |
+
R_succ[u][v]["flow"] += flow
|
| 265 |
+
R_succ[v][u]["flow"] -= flow
|
| 266 |
+
if R_succ[u][v]["flow"] == R_succ[u][v]["capacity"]:
|
| 267 |
+
if v in source_tree:
|
| 268 |
+
source_tree[v] = None
|
| 269 |
+
these_orphans.append(v)
|
| 270 |
+
if u in target_tree:
|
| 271 |
+
target_tree[u] = None
|
| 272 |
+
these_orphans.append(u)
|
| 273 |
+
u = v
|
| 274 |
+
orphans.extend(sorted(these_orphans, key=dist.get))
|
| 275 |
+
return flow
|
| 276 |
+
|
| 277 |
+
def adopt():
|
| 278 |
+
"""Adoption stage.
|
| 279 |
+
|
| 280 |
+
Reconstruct search trees by adopting or discarding orphans.
|
| 281 |
+
During augmentation stage some edges got saturated and thus
|
| 282 |
+
the source and target search trees broke down to forests, with
|
| 283 |
+
orphans as roots of some of its trees. We have to reconstruct
|
| 284 |
+
the search trees rooted to source and target before we can grow
|
| 285 |
+
them again.
|
| 286 |
+
"""
|
| 287 |
+
while orphans:
|
| 288 |
+
u = orphans.popleft()
|
| 289 |
+
if u in source_tree:
|
| 290 |
+
tree = source_tree
|
| 291 |
+
neighbors = R_pred
|
| 292 |
+
else:
|
| 293 |
+
tree = target_tree
|
| 294 |
+
neighbors = R_succ
|
| 295 |
+
nbrs = ((n, attr, dist[n]) for n, attr in neighbors[u].items() if n in tree)
|
| 296 |
+
for v, attr, d in sorted(nbrs, key=itemgetter(2)):
|
| 297 |
+
if attr["capacity"] - attr["flow"] > 0:
|
| 298 |
+
if _has_valid_root(v, tree):
|
| 299 |
+
tree[u] = v
|
| 300 |
+
dist[u] = dist[v] + 1
|
| 301 |
+
timestamp[u] = time
|
| 302 |
+
break
|
| 303 |
+
else:
|
| 304 |
+
nbrs = (
|
| 305 |
+
(n, attr, dist[n]) for n, attr in neighbors[u].items() if n in tree
|
| 306 |
+
)
|
| 307 |
+
for v, attr, d in sorted(nbrs, key=itemgetter(2)):
|
| 308 |
+
if attr["capacity"] - attr["flow"] > 0:
|
| 309 |
+
if v not in active:
|
| 310 |
+
active.append(v)
|
| 311 |
+
if tree[v] == u:
|
| 312 |
+
tree[v] = None
|
| 313 |
+
orphans.appendleft(v)
|
| 314 |
+
if u in active:
|
| 315 |
+
active.remove(u)
|
| 316 |
+
del tree[u]
|
| 317 |
+
|
| 318 |
+
def _has_valid_root(n, tree):
|
| 319 |
+
path = []
|
| 320 |
+
v = n
|
| 321 |
+
while v is not None:
|
| 322 |
+
path.append(v)
|
| 323 |
+
if v in (s, t):
|
| 324 |
+
base_dist = 0
|
| 325 |
+
break
|
| 326 |
+
elif timestamp[v] == time:
|
| 327 |
+
base_dist = dist[v]
|
| 328 |
+
break
|
| 329 |
+
v = tree[v]
|
| 330 |
+
else:
|
| 331 |
+
return False
|
| 332 |
+
length = len(path)
|
| 333 |
+
for i, u in enumerate(path, 1):
|
| 334 |
+
dist[u] = base_dist + length - i
|
| 335 |
+
timestamp[u] = time
|
| 336 |
+
return True
|
| 337 |
+
|
| 338 |
+
def _is_closer(u, v):
|
| 339 |
+
return timestamp[v] <= timestamp[u] and dist[v] > dist[u] + 1
|
| 340 |
+
|
| 341 |
+
source_tree = {s: None}
|
| 342 |
+
target_tree = {t: None}
|
| 343 |
+
active = deque([s, t])
|
| 344 |
+
orphans = deque()
|
| 345 |
+
flow_value = 0
|
| 346 |
+
# data structures for the marking heuristic
|
| 347 |
+
time = 1
|
| 348 |
+
timestamp = {s: time, t: time}
|
| 349 |
+
dist = {s: 0, t: 0}
|
| 350 |
+
while flow_value < cutoff:
|
| 351 |
+
# Growth stage
|
| 352 |
+
u, v = grow()
|
| 353 |
+
if u is None:
|
| 354 |
+
break
|
| 355 |
+
time += 1
|
| 356 |
+
# Augmentation stage
|
| 357 |
+
flow_value += augment(u, v)
|
| 358 |
+
# Adoption stage
|
| 359 |
+
adopt()
|
| 360 |
+
|
| 361 |
+
if flow_value * 2 > INF:
|
| 362 |
+
raise nx.NetworkXUnbounded("Infinite capacity path, flow unbounded above.")
|
| 363 |
+
|
| 364 |
+
# Add source and target tree in a graph attribute.
|
| 365 |
+
# A partition that defines a minimum cut can be directly
|
| 366 |
+
# computed from the search trees as explained in the docstrings.
|
| 367 |
+
R.graph["trees"] = (source_tree, target_tree)
|
| 368 |
+
# Add the standard flow_value graph attribute.
|
| 369 |
+
R.graph["flow_value"] = flow_value
|
| 370 |
+
return R
|
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/capacityscaling.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Capacity scaling minimum cost flow algorithm.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
__all__ = ["capacity_scaling"]
|
| 6 |
+
|
| 7 |
+
from itertools import chain
|
| 8 |
+
from math import log
|
| 9 |
+
|
| 10 |
+
import networkx as nx
|
| 11 |
+
|
| 12 |
+
from ...utils import BinaryHeap, arbitrary_element, not_implemented_for
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _detect_unboundedness(R):
|
| 16 |
+
"""Detect infinite-capacity negative cycles."""
|
| 17 |
+
G = nx.DiGraph()
|
| 18 |
+
G.add_nodes_from(R)
|
| 19 |
+
|
| 20 |
+
# Value simulating infinity.
|
| 21 |
+
inf = R.graph["inf"]
|
| 22 |
+
# True infinity.
|
| 23 |
+
f_inf = float("inf")
|
| 24 |
+
for u in R:
|
| 25 |
+
for v, e in R[u].items():
|
| 26 |
+
# Compute the minimum weight of infinite-capacity (u, v) edges.
|
| 27 |
+
w = f_inf
|
| 28 |
+
for k, e in e.items():
|
| 29 |
+
if e["capacity"] == inf:
|
| 30 |
+
w = min(w, e["weight"])
|
| 31 |
+
if w != f_inf:
|
| 32 |
+
G.add_edge(u, v, weight=w)
|
| 33 |
+
|
| 34 |
+
if nx.negative_edge_cycle(G):
|
| 35 |
+
raise nx.NetworkXUnbounded(
|
| 36 |
+
"Negative cost cycle of infinite capacity found. "
|
| 37 |
+
"Min cost flow may be unbounded below."
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@not_implemented_for("undirected")
|
| 42 |
+
def _build_residual_network(G, demand, capacity, weight):
|
| 43 |
+
"""Build a residual network and initialize a zero flow."""
|
| 44 |
+
if sum(G.nodes[u].get(demand, 0) for u in G) != 0:
|
| 45 |
+
raise nx.NetworkXUnfeasible("Sum of the demands should be 0.")
|
| 46 |
+
|
| 47 |
+
R = nx.MultiDiGraph()
|
| 48 |
+
R.add_nodes_from(
|
| 49 |
+
(u, {"excess": -G.nodes[u].get(demand, 0), "potential": 0}) for u in G
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
inf = float("inf")
|
| 53 |
+
# Detect selfloops with infinite capacities and negative weights.
|
| 54 |
+
for u, v, e in nx.selfloop_edges(G, data=True):
|
| 55 |
+
if e.get(weight, 0) < 0 and e.get(capacity, inf) == inf:
|
| 56 |
+
raise nx.NetworkXUnbounded(
|
| 57 |
+
"Negative cost cycle of infinite capacity found. "
|
| 58 |
+
"Min cost flow may be unbounded below."
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Extract edges with positive capacities. Self loops excluded.
|
| 62 |
+
if G.is_multigraph():
|
| 63 |
+
edge_list = [
|
| 64 |
+
(u, v, k, e)
|
| 65 |
+
for u, v, k, e in G.edges(data=True, keys=True)
|
| 66 |
+
if u != v and e.get(capacity, inf) > 0
|
| 67 |
+
]
|
| 68 |
+
else:
|
| 69 |
+
edge_list = [
|
| 70 |
+
(u, v, 0, e)
|
| 71 |
+
for u, v, e in G.edges(data=True)
|
| 72 |
+
if u != v and e.get(capacity, inf) > 0
|
| 73 |
+
]
|
| 74 |
+
# Simulate infinity with the larger of the sum of absolute node imbalances
|
| 75 |
+
# the sum of finite edge capacities or any positive value if both sums are
|
| 76 |
+
# zero. This allows the infinite-capacity edges to be distinguished for
|
| 77 |
+
# unboundedness detection and directly participate in residual capacity
|
| 78 |
+
# calculation.
|
| 79 |
+
inf = (
|
| 80 |
+
max(
|
| 81 |
+
sum(abs(R.nodes[u]["excess"]) for u in R),
|
| 82 |
+
2
|
| 83 |
+
* sum(
|
| 84 |
+
e[capacity]
|
| 85 |
+
for u, v, k, e in edge_list
|
| 86 |
+
if capacity in e and e[capacity] != inf
|
| 87 |
+
),
|
| 88 |
+
)
|
| 89 |
+
or 1
|
| 90 |
+
)
|
| 91 |
+
for u, v, k, e in edge_list:
|
| 92 |
+
r = min(e.get(capacity, inf), inf)
|
| 93 |
+
w = e.get(weight, 0)
|
| 94 |
+
# Add both (u, v) and (v, u) into the residual network marked with the
|
| 95 |
+
# original key. (key[1] == True) indicates the (u, v) is in the
|
| 96 |
+
# original network.
|
| 97 |
+
R.add_edge(u, v, key=(k, True), capacity=r, weight=w, flow=0)
|
| 98 |
+
R.add_edge(v, u, key=(k, False), capacity=0, weight=-w, flow=0)
|
| 99 |
+
|
| 100 |
+
# Record the value simulating infinity.
|
| 101 |
+
R.graph["inf"] = inf
|
| 102 |
+
|
| 103 |
+
_detect_unboundedness(R)
|
| 104 |
+
|
| 105 |
+
return R
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _build_flow_dict(G, R, capacity, weight):
|
| 109 |
+
"""Build a flow dictionary from a residual network."""
|
| 110 |
+
inf = float("inf")
|
| 111 |
+
flow_dict = {}
|
| 112 |
+
if G.is_multigraph():
|
| 113 |
+
for u in G:
|
| 114 |
+
flow_dict[u] = {}
|
| 115 |
+
for v, es in G[u].items():
|
| 116 |
+
flow_dict[u][v] = {
|
| 117 |
+
# Always saturate negative selfloops.
|
| 118 |
+
k: (
|
| 119 |
+
0
|
| 120 |
+
if (
|
| 121 |
+
u != v or e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0
|
| 122 |
+
)
|
| 123 |
+
else e[capacity]
|
| 124 |
+
)
|
| 125 |
+
for k, e in es.items()
|
| 126 |
+
}
|
| 127 |
+
for v, es in R[u].items():
|
| 128 |
+
if v in flow_dict[u]:
|
| 129 |
+
flow_dict[u][v].update(
|
| 130 |
+
(k[0], e["flow"]) for k, e in es.items() if e["flow"] > 0
|
| 131 |
+
)
|
| 132 |
+
else:
|
| 133 |
+
for u in G:
|
| 134 |
+
flow_dict[u] = {
|
| 135 |
+
# Always saturate negative selfloops.
|
| 136 |
+
v: (
|
| 137 |
+
0
|
| 138 |
+
if (u != v or e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0)
|
| 139 |
+
else e[capacity]
|
| 140 |
+
)
|
| 141 |
+
for v, e in G[u].items()
|
| 142 |
+
}
|
| 143 |
+
flow_dict[u].update(
|
| 144 |
+
(v, e["flow"])
|
| 145 |
+
for v, es in R[u].items()
|
| 146 |
+
for e in es.values()
|
| 147 |
+
if e["flow"] > 0
|
| 148 |
+
)
|
| 149 |
+
return flow_dict
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
@nx._dispatchable(
|
| 153 |
+
node_attrs="demand", edge_attrs={"capacity": float("inf"), "weight": 0}
|
| 154 |
+
)
|
| 155 |
+
def capacity_scaling(
|
| 156 |
+
G, demand="demand", capacity="capacity", weight="weight", heap=BinaryHeap
|
| 157 |
+
):
|
| 158 |
+
r"""Find a minimum cost flow satisfying all demands in digraph G.
|
| 159 |
+
|
| 160 |
+
This is a capacity scaling successive shortest augmenting path algorithm.
|
| 161 |
+
|
| 162 |
+
G is a digraph with edge costs and capacities and in which nodes
|
| 163 |
+
have demand, i.e., they want to send or receive some amount of
|
| 164 |
+
flow. A negative demand means that the node wants to send flow, a
|
| 165 |
+
positive demand means that the node want to receive flow. A flow on
|
| 166 |
+
the digraph G satisfies all demand if the net flow into each node
|
| 167 |
+
is equal to the demand of that node.
|
| 168 |
+
|
| 169 |
+
Parameters
|
| 170 |
+
----------
|
| 171 |
+
G : NetworkX graph
|
| 172 |
+
DiGraph or MultiDiGraph on which a minimum cost flow satisfying all
|
| 173 |
+
demands is to be found.
|
| 174 |
+
|
| 175 |
+
demand : string
|
| 176 |
+
Nodes of the graph G are expected to have an attribute demand
|
| 177 |
+
that indicates how much flow a node wants to send (negative
|
| 178 |
+
demand) or receive (positive demand). Note that the sum of the
|
| 179 |
+
demands should be 0 otherwise the problem in not feasible. If
|
| 180 |
+
this attribute is not present, a node is considered to have 0
|
| 181 |
+
demand. Default value: 'demand'.
|
| 182 |
+
|
| 183 |
+
capacity : string
|
| 184 |
+
Edges of the graph G are expected to have an attribute capacity
|
| 185 |
+
that indicates how much flow the edge can support. If this
|
| 186 |
+
attribute is not present, the edge is considered to have
|
| 187 |
+
infinite capacity. Default value: 'capacity'.
|
| 188 |
+
|
| 189 |
+
weight : string
|
| 190 |
+
Edges of the graph G are expected to have an attribute weight
|
| 191 |
+
that indicates the cost incurred by sending one unit of flow on
|
| 192 |
+
that edge. If not present, the weight is considered to be 0.
|
| 193 |
+
Default value: 'weight'.
|
| 194 |
+
|
| 195 |
+
heap : class
|
| 196 |
+
Type of heap to be used in the algorithm. It should be a subclass of
|
| 197 |
+
:class:`MinHeap` or implement a compatible interface.
|
| 198 |
+
|
| 199 |
+
If a stock heap implementation is to be used, :class:`BinaryHeap` is
|
| 200 |
+
recommended over :class:`PairingHeap` for Python implementations without
|
| 201 |
+
optimized attribute accesses (e.g., CPython) despite a slower
|
| 202 |
+
asymptotic running time. For Python implementations with optimized
|
| 203 |
+
attribute accesses (e.g., PyPy), :class:`PairingHeap` provides better
|
| 204 |
+
performance. Default value: :class:`BinaryHeap`.
|
| 205 |
+
|
| 206 |
+
Returns
|
| 207 |
+
-------
|
| 208 |
+
flowCost : integer
|
| 209 |
+
Cost of a minimum cost flow satisfying all demands.
|
| 210 |
+
|
| 211 |
+
flowDict : dictionary
|
| 212 |
+
If G is a digraph, a dict-of-dicts keyed by nodes such that
|
| 213 |
+
flowDict[u][v] is the flow on edge (u, v).
|
| 214 |
+
If G is a MultiDiGraph, a dict-of-dicts-of-dicts keyed by nodes
|
| 215 |
+
so that flowDict[u][v][key] is the flow on edge (u, v, key).
|
| 216 |
+
|
| 217 |
+
Raises
|
| 218 |
+
------
|
| 219 |
+
NetworkXError
|
| 220 |
+
This exception is raised if the input graph is not directed,
|
| 221 |
+
not connected.
|
| 222 |
+
|
| 223 |
+
NetworkXUnfeasible
|
| 224 |
+
This exception is raised in the following situations:
|
| 225 |
+
|
| 226 |
+
* The sum of the demands is not zero. Then, there is no
|
| 227 |
+
flow satisfying all demands.
|
| 228 |
+
* There is no flow satisfying all demand.
|
| 229 |
+
|
| 230 |
+
NetworkXUnbounded
|
| 231 |
+
This exception is raised if the digraph G has a cycle of
|
| 232 |
+
negative cost and infinite capacity. Then, the cost of a flow
|
| 233 |
+
satisfying all demands is unbounded below.
|
| 234 |
+
|
| 235 |
+
Notes
|
| 236 |
+
-----
|
| 237 |
+
This algorithm does not work if edge weights are floating-point numbers.
|
| 238 |
+
|
| 239 |
+
See also
|
| 240 |
+
--------
|
| 241 |
+
:meth:`network_simplex`
|
| 242 |
+
|
| 243 |
+
Examples
|
| 244 |
+
--------
|
| 245 |
+
A simple example of a min cost flow problem.
|
| 246 |
+
|
| 247 |
+
>>> G = nx.DiGraph()
|
| 248 |
+
>>> G.add_node("a", demand=-5)
|
| 249 |
+
>>> G.add_node("d", demand=5)
|
| 250 |
+
>>> G.add_edge("a", "b", weight=3, capacity=4)
|
| 251 |
+
>>> G.add_edge("a", "c", weight=6, capacity=10)
|
| 252 |
+
>>> G.add_edge("b", "d", weight=1, capacity=9)
|
| 253 |
+
>>> G.add_edge("c", "d", weight=2, capacity=5)
|
| 254 |
+
>>> flowCost, flowDict = nx.capacity_scaling(G)
|
| 255 |
+
>>> flowCost
|
| 256 |
+
24
|
| 257 |
+
>>> flowDict
|
| 258 |
+
{'a': {'b': 4, 'c': 1}, 'd': {}, 'b': {'d': 4}, 'c': {'d': 1}}
|
| 259 |
+
|
| 260 |
+
It is possible to change the name of the attributes used for the
|
| 261 |
+
algorithm.
|
| 262 |
+
|
| 263 |
+
>>> G = nx.DiGraph()
|
| 264 |
+
>>> G.add_node("p", spam=-4)
|
| 265 |
+
>>> G.add_node("q", spam=2)
|
| 266 |
+
>>> G.add_node("a", spam=-2)
|
| 267 |
+
>>> G.add_node("d", spam=-1)
|
| 268 |
+
>>> G.add_node("t", spam=2)
|
| 269 |
+
>>> G.add_node("w", spam=3)
|
| 270 |
+
>>> G.add_edge("p", "q", cost=7, vacancies=5)
|
| 271 |
+
>>> G.add_edge("p", "a", cost=1, vacancies=4)
|
| 272 |
+
>>> G.add_edge("q", "d", cost=2, vacancies=3)
|
| 273 |
+
>>> G.add_edge("t", "q", cost=1, vacancies=2)
|
| 274 |
+
>>> G.add_edge("a", "t", cost=2, vacancies=4)
|
| 275 |
+
>>> G.add_edge("d", "w", cost=3, vacancies=4)
|
| 276 |
+
>>> G.add_edge("t", "w", cost=4, vacancies=1)
|
| 277 |
+
>>> flowCost, flowDict = nx.capacity_scaling(
|
| 278 |
+
... G, demand="spam", capacity="vacancies", weight="cost"
|
| 279 |
+
... )
|
| 280 |
+
>>> flowCost
|
| 281 |
+
37
|
| 282 |
+
>>> flowDict
|
| 283 |
+
{'p': {'q': 2, 'a': 2}, 'q': {'d': 1}, 'a': {'t': 4}, 'd': {'w': 2}, 't': {'q': 1, 'w': 1}, 'w': {}}
|
| 284 |
+
"""
|
| 285 |
+
R = _build_residual_network(G, demand, capacity, weight)
|
| 286 |
+
|
| 287 |
+
inf = float("inf")
|
| 288 |
+
# Account cost of negative selfloops.
|
| 289 |
+
flow_cost = sum(
|
| 290 |
+
0
|
| 291 |
+
if e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0
|
| 292 |
+
else e[capacity] * e[weight]
|
| 293 |
+
for u, v, e in nx.selfloop_edges(G, data=True)
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
# Determine the maximum edge capacity.
|
| 297 |
+
wmax = max(chain([-inf], (e["capacity"] for u, v, e in R.edges(data=True))))
|
| 298 |
+
if wmax == -inf:
|
| 299 |
+
# Residual network has no edges.
|
| 300 |
+
return flow_cost, _build_flow_dict(G, R, capacity, weight)
|
| 301 |
+
|
| 302 |
+
R_nodes = R.nodes
|
| 303 |
+
R_succ = R.succ
|
| 304 |
+
|
| 305 |
+
delta = 2 ** int(log(wmax, 2))
|
| 306 |
+
while delta >= 1:
|
| 307 |
+
# Saturate Δ-residual edges with negative reduced costs to achieve
|
| 308 |
+
# Δ-optimality.
|
| 309 |
+
for u in R:
|
| 310 |
+
p_u = R_nodes[u]["potential"]
|
| 311 |
+
for v, es in R_succ[u].items():
|
| 312 |
+
for k, e in es.items():
|
| 313 |
+
flow = e["capacity"] - e["flow"]
|
| 314 |
+
if e["weight"] - p_u + R_nodes[v]["potential"] < 0:
|
| 315 |
+
flow = e["capacity"] - e["flow"]
|
| 316 |
+
if flow >= delta:
|
| 317 |
+
e["flow"] += flow
|
| 318 |
+
R_succ[v][u][(k[0], not k[1])]["flow"] -= flow
|
| 319 |
+
R_nodes[u]["excess"] -= flow
|
| 320 |
+
R_nodes[v]["excess"] += flow
|
| 321 |
+
# Determine the Δ-active nodes.
|
| 322 |
+
S = set()
|
| 323 |
+
T = set()
|
| 324 |
+
S_add = S.add
|
| 325 |
+
S_remove = S.remove
|
| 326 |
+
T_add = T.add
|
| 327 |
+
T_remove = T.remove
|
| 328 |
+
for u in R:
|
| 329 |
+
excess = R_nodes[u]["excess"]
|
| 330 |
+
if excess >= delta:
|
| 331 |
+
S_add(u)
|
| 332 |
+
elif excess <= -delta:
|
| 333 |
+
T_add(u)
|
| 334 |
+
# Repeatedly augment flow from S to T along shortest paths until
|
| 335 |
+
# Δ-feasibility is achieved.
|
| 336 |
+
while S and T:
|
| 337 |
+
s = arbitrary_element(S)
|
| 338 |
+
t = None
|
| 339 |
+
# Search for a shortest path in terms of reduce costs from s to
|
| 340 |
+
# any t in T in the Δ-residual network.
|
| 341 |
+
d = {}
|
| 342 |
+
pred = {s: None}
|
| 343 |
+
h = heap()
|
| 344 |
+
h_insert = h.insert
|
| 345 |
+
h_get = h.get
|
| 346 |
+
h_insert(s, 0)
|
| 347 |
+
while h:
|
| 348 |
+
u, d_u = h.pop()
|
| 349 |
+
d[u] = d_u
|
| 350 |
+
if u in T:
|
| 351 |
+
# Path found.
|
| 352 |
+
t = u
|
| 353 |
+
break
|
| 354 |
+
p_u = R_nodes[u]["potential"]
|
| 355 |
+
for v, es in R_succ[u].items():
|
| 356 |
+
if v in d:
|
| 357 |
+
continue
|
| 358 |
+
wmin = inf
|
| 359 |
+
# Find the minimum-weighted (u, v) Δ-residual edge.
|
| 360 |
+
for k, e in es.items():
|
| 361 |
+
if e["capacity"] - e["flow"] >= delta:
|
| 362 |
+
w = e["weight"]
|
| 363 |
+
if w < wmin:
|
| 364 |
+
wmin = w
|
| 365 |
+
kmin = k
|
| 366 |
+
emin = e
|
| 367 |
+
if wmin == inf:
|
| 368 |
+
continue
|
| 369 |
+
# Update the distance label of v.
|
| 370 |
+
d_v = d_u + wmin - p_u + R_nodes[v]["potential"]
|
| 371 |
+
if h_insert(v, d_v):
|
| 372 |
+
pred[v] = (u, kmin, emin)
|
| 373 |
+
if t is not None:
|
| 374 |
+
# Augment Δ units of flow from s to t.
|
| 375 |
+
while u != s:
|
| 376 |
+
v = u
|
| 377 |
+
u, k, e = pred[v]
|
| 378 |
+
e["flow"] += delta
|
| 379 |
+
R_succ[v][u][(k[0], not k[1])]["flow"] -= delta
|
| 380 |
+
# Account node excess and deficit.
|
| 381 |
+
R_nodes[s]["excess"] -= delta
|
| 382 |
+
R_nodes[t]["excess"] += delta
|
| 383 |
+
if R_nodes[s]["excess"] < delta:
|
| 384 |
+
S_remove(s)
|
| 385 |
+
if R_nodes[t]["excess"] > -delta:
|
| 386 |
+
T_remove(t)
|
| 387 |
+
# Update node potentials.
|
| 388 |
+
d_t = d[t]
|
| 389 |
+
for u, d_u in d.items():
|
| 390 |
+
R_nodes[u]["potential"] -= d_u - d_t
|
| 391 |
+
else:
|
| 392 |
+
# Path not found.
|
| 393 |
+
S_remove(s)
|
| 394 |
+
delta //= 2
|
| 395 |
+
|
| 396 |
+
if any(R.nodes[u]["excess"] != 0 for u in R):
|
| 397 |
+
raise nx.NetworkXUnfeasible("No flow satisfying all demands.")
|
| 398 |
+
|
| 399 |
+
# Calculate the flow cost.
|
| 400 |
+
for u in R:
|
| 401 |
+
for v, es in R_succ[u].items():
|
| 402 |
+
for e in es.values():
|
| 403 |
+
flow = e["flow"]
|
| 404 |
+
if flow > 0:
|
| 405 |
+
flow_cost += flow * e["weight"]
|
| 406 |
+
|
| 407 |
+
return flow_cost, _build_flow_dict(G, R, capacity, weight)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/dinitz_alg.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Dinitz' algorithm for maximum flow problems.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from collections import deque
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
from networkx.algorithms.flow.utils import build_residual_network
|
| 9 |
+
from networkx.utils import pairwise
|
| 10 |
+
|
| 11 |
+
__all__ = ["dinitz"]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True)
|
| 15 |
+
def dinitz(G, s, t, capacity="capacity", residual=None, value_only=False, cutoff=None):
|
| 16 |
+
"""Find a maximum single-commodity flow using Dinitz' algorithm.
|
| 17 |
+
|
| 18 |
+
This function returns the residual network resulting after computing
|
| 19 |
+
the maximum flow. See below for details about the conventions
|
| 20 |
+
NetworkX uses for defining residual networks.
|
| 21 |
+
|
| 22 |
+
This algorithm has a running time of $O(n^2 m)$ for $n$ nodes and $m$
|
| 23 |
+
edges [1]_.
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
Parameters
|
| 27 |
+
----------
|
| 28 |
+
G : NetworkX graph
|
| 29 |
+
Edges of the graph are expected to have an attribute called
|
| 30 |
+
'capacity'. If this attribute is not present, the edge is
|
| 31 |
+
considered to have infinite capacity.
|
| 32 |
+
|
| 33 |
+
s : node
|
| 34 |
+
Source node for the flow.
|
| 35 |
+
|
| 36 |
+
t : node
|
| 37 |
+
Sink node for the flow.
|
| 38 |
+
|
| 39 |
+
capacity : string
|
| 40 |
+
Edges of the graph G are expected to have an attribute capacity
|
| 41 |
+
that indicates how much flow the edge can support. If this
|
| 42 |
+
attribute is not present, the edge is considered to have
|
| 43 |
+
infinite capacity. Default value: 'capacity'.
|
| 44 |
+
|
| 45 |
+
residual : NetworkX graph
|
| 46 |
+
Residual network on which the algorithm is to be executed. If None, a
|
| 47 |
+
new residual network is created. Default value: None.
|
| 48 |
+
|
| 49 |
+
value_only : bool
|
| 50 |
+
If True compute only the value of the maximum flow. This parameter
|
| 51 |
+
will be ignored by this algorithm because it is not applicable.
|
| 52 |
+
|
| 53 |
+
cutoff : integer, float
|
| 54 |
+
If specified, the algorithm will terminate when the flow value reaches
|
| 55 |
+
or exceeds the cutoff. In this case, it may be unable to immediately
|
| 56 |
+
determine a minimum cut. Default value: None.
|
| 57 |
+
|
| 58 |
+
Returns
|
| 59 |
+
-------
|
| 60 |
+
R : NetworkX DiGraph
|
| 61 |
+
Residual network after computing the maximum flow.
|
| 62 |
+
|
| 63 |
+
Raises
|
| 64 |
+
------
|
| 65 |
+
NetworkXError
|
| 66 |
+
The algorithm does not support MultiGraph and MultiDiGraph. If
|
| 67 |
+
the input graph is an instance of one of these two classes, a
|
| 68 |
+
NetworkXError is raised.
|
| 69 |
+
|
| 70 |
+
NetworkXUnbounded
|
| 71 |
+
If the graph has a path of infinite capacity, the value of a
|
| 72 |
+
feasible flow on the graph is unbounded above and the function
|
| 73 |
+
raises a NetworkXUnbounded.
|
| 74 |
+
|
| 75 |
+
See also
|
| 76 |
+
--------
|
| 77 |
+
:meth:`maximum_flow`
|
| 78 |
+
:meth:`minimum_cut`
|
| 79 |
+
:meth:`preflow_push`
|
| 80 |
+
:meth:`shortest_augmenting_path`
|
| 81 |
+
|
| 82 |
+
Notes
|
| 83 |
+
-----
|
| 84 |
+
The residual network :samp:`R` from an input graph :samp:`G` has the
|
| 85 |
+
same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
|
| 86 |
+
of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
|
| 87 |
+
self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
|
| 88 |
+
in :samp:`G`.
|
| 89 |
+
|
| 90 |
+
For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
|
| 91 |
+
is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
|
| 92 |
+
in :samp:`G` or zero otherwise. If the capacity is infinite,
|
| 93 |
+
:samp:`R[u][v]['capacity']` will have a high arbitrary finite value
|
| 94 |
+
that does not affect the solution of the problem. This value is stored in
|
| 95 |
+
:samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
|
| 96 |
+
:samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
|
| 97 |
+
satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
|
| 98 |
+
|
| 99 |
+
The flow value, defined as the total flow into :samp:`t`, the sink, is
|
| 100 |
+
stored in :samp:`R.graph['flow_value']`. If :samp:`cutoff` is not
|
| 101 |
+
specified, reachability to :samp:`t` using only edges :samp:`(u, v)` such
|
| 102 |
+
that :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
|
| 103 |
+
:samp:`s`-:samp:`t` cut.
|
| 104 |
+
|
| 105 |
+
Examples
|
| 106 |
+
--------
|
| 107 |
+
>>> from networkx.algorithms.flow import dinitz
|
| 108 |
+
|
| 109 |
+
The functions that implement flow algorithms and output a residual
|
| 110 |
+
network, such as this one, are not imported to the base NetworkX
|
| 111 |
+
namespace, so you have to explicitly import them from the flow package.
|
| 112 |
+
|
| 113 |
+
>>> G = nx.DiGraph()
|
| 114 |
+
>>> G.add_edge("x", "a", capacity=3.0)
|
| 115 |
+
>>> G.add_edge("x", "b", capacity=1.0)
|
| 116 |
+
>>> G.add_edge("a", "c", capacity=3.0)
|
| 117 |
+
>>> G.add_edge("b", "c", capacity=5.0)
|
| 118 |
+
>>> G.add_edge("b", "d", capacity=4.0)
|
| 119 |
+
>>> G.add_edge("d", "e", capacity=2.0)
|
| 120 |
+
>>> G.add_edge("c", "y", capacity=2.0)
|
| 121 |
+
>>> G.add_edge("e", "y", capacity=3.0)
|
| 122 |
+
>>> R = dinitz(G, "x", "y")
|
| 123 |
+
>>> flow_value = nx.maximum_flow_value(G, "x", "y")
|
| 124 |
+
>>> flow_value
|
| 125 |
+
3.0
|
| 126 |
+
>>> flow_value == R.graph["flow_value"]
|
| 127 |
+
True
|
| 128 |
+
|
| 129 |
+
References
|
| 130 |
+
----------
|
| 131 |
+
.. [1] Dinitz' Algorithm: The Original Version and Even's Version.
|
| 132 |
+
2006. Yefim Dinitz. In Theoretical Computer Science. Lecture
|
| 133 |
+
Notes in Computer Science. Volume 3895. pp 218-240.
|
| 134 |
+
https://doi.org/10.1007/11685654_10
|
| 135 |
+
|
| 136 |
+
"""
|
| 137 |
+
R = dinitz_impl(G, s, t, capacity, residual, cutoff)
|
| 138 |
+
R.graph["algorithm"] = "dinitz"
|
| 139 |
+
nx._clear_cache(R)
|
| 140 |
+
return R
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def dinitz_impl(G, s, t, capacity, residual, cutoff):
|
| 144 |
+
if s not in G:
|
| 145 |
+
raise nx.NetworkXError(f"node {str(s)} not in graph")
|
| 146 |
+
if t not in G:
|
| 147 |
+
raise nx.NetworkXError(f"node {str(t)} not in graph")
|
| 148 |
+
if s == t:
|
| 149 |
+
raise nx.NetworkXError("source and sink are the same node")
|
| 150 |
+
|
| 151 |
+
if residual is None:
|
| 152 |
+
R = build_residual_network(G, capacity)
|
| 153 |
+
else:
|
| 154 |
+
R = residual
|
| 155 |
+
|
| 156 |
+
# Initialize/reset the residual network.
|
| 157 |
+
for u in R:
|
| 158 |
+
for e in R[u].values():
|
| 159 |
+
e["flow"] = 0
|
| 160 |
+
|
| 161 |
+
# Use an arbitrary high value as infinite. It is computed
|
| 162 |
+
# when building the residual network.
|
| 163 |
+
INF = R.graph["inf"]
|
| 164 |
+
|
| 165 |
+
if cutoff is None:
|
| 166 |
+
cutoff = INF
|
| 167 |
+
|
| 168 |
+
R_succ = R.succ
|
| 169 |
+
R_pred = R.pred
|
| 170 |
+
|
| 171 |
+
def breath_first_search():
|
| 172 |
+
parents = {}
|
| 173 |
+
vertex_dist = {s: 0}
|
| 174 |
+
queue = deque([(s, 0)])
|
| 175 |
+
# Record all the potential edges of shortest augmenting paths
|
| 176 |
+
while queue:
|
| 177 |
+
if t in parents:
|
| 178 |
+
break
|
| 179 |
+
u, dist = queue.popleft()
|
| 180 |
+
for v, attr in R_succ[u].items():
|
| 181 |
+
if attr["capacity"] - attr["flow"] > 0:
|
| 182 |
+
if v in parents:
|
| 183 |
+
if vertex_dist[v] == dist + 1:
|
| 184 |
+
parents[v].append(u)
|
| 185 |
+
else:
|
| 186 |
+
parents[v] = deque([u])
|
| 187 |
+
vertex_dist[v] = dist + 1
|
| 188 |
+
queue.append((v, dist + 1))
|
| 189 |
+
return parents
|
| 190 |
+
|
| 191 |
+
def depth_first_search(parents):
|
| 192 |
+
# DFS to find all the shortest augmenting paths
|
| 193 |
+
"""Build a path using DFS starting from the sink"""
|
| 194 |
+
total_flow = 0
|
| 195 |
+
u = t
|
| 196 |
+
# path also functions as a stack
|
| 197 |
+
path = [u]
|
| 198 |
+
# The loop ends with no augmenting path left in the layered graph
|
| 199 |
+
while True:
|
| 200 |
+
if len(parents[u]) > 0:
|
| 201 |
+
v = parents[u][0]
|
| 202 |
+
path.append(v)
|
| 203 |
+
else:
|
| 204 |
+
path.pop()
|
| 205 |
+
if len(path) == 0:
|
| 206 |
+
break
|
| 207 |
+
v = path[-1]
|
| 208 |
+
parents[v].popleft()
|
| 209 |
+
# Augment the flow along the path found
|
| 210 |
+
if v == s:
|
| 211 |
+
flow = INF
|
| 212 |
+
for u, v in pairwise(path):
|
| 213 |
+
flow = min(flow, R_pred[u][v]["capacity"] - R_pred[u][v]["flow"])
|
| 214 |
+
for u, v in pairwise(reversed(path)):
|
| 215 |
+
R_pred[v][u]["flow"] += flow
|
| 216 |
+
R_pred[u][v]["flow"] -= flow
|
| 217 |
+
# Find the proper node to continue the search
|
| 218 |
+
if R_pred[v][u]["capacity"] - R_pred[v][u]["flow"] == 0:
|
| 219 |
+
parents[v].popleft()
|
| 220 |
+
while path[-1] != v:
|
| 221 |
+
path.pop()
|
| 222 |
+
total_flow += flow
|
| 223 |
+
v = path[-1]
|
| 224 |
+
u = v
|
| 225 |
+
return total_flow
|
| 226 |
+
|
| 227 |
+
flow_value = 0
|
| 228 |
+
while flow_value < cutoff:
|
| 229 |
+
parents = breath_first_search()
|
| 230 |
+
if t not in parents:
|
| 231 |
+
break
|
| 232 |
+
this_flow = depth_first_search(parents)
|
| 233 |
+
if this_flow * 2 > INF:
|
| 234 |
+
raise nx.NetworkXUnbounded("Infinite capacity path, flow unbounded above.")
|
| 235 |
+
flow_value += this_flow
|
| 236 |
+
|
| 237 |
+
R.graph["flow_value"] = flow_value
|
| 238 |
+
return R
|
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/gomory_hu.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gomory-Hu tree of undirected Graphs.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.utils import not_implemented_for
|
| 7 |
+
|
| 8 |
+
from .edmondskarp import edmonds_karp
|
| 9 |
+
from .utils import build_residual_network
|
| 10 |
+
|
| 11 |
+
default_flow_func = edmonds_karp
|
| 12 |
+
|
| 13 |
+
__all__ = ["gomory_hu_tree"]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@not_implemented_for("directed")
|
| 17 |
+
@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True)
|
| 18 |
+
def gomory_hu_tree(G, capacity="capacity", flow_func=None):
|
| 19 |
+
r"""Returns the Gomory-Hu tree of an undirected graph G.
|
| 20 |
+
|
| 21 |
+
A Gomory-Hu tree of an undirected graph with capacities is a
|
| 22 |
+
weighted tree that represents the minimum s-t cuts for all s-t
|
| 23 |
+
pairs in the graph.
|
| 24 |
+
|
| 25 |
+
It only requires `n-1` minimum cut computations instead of the
|
| 26 |
+
obvious `n(n-1)/2`. The tree represents all s-t cuts as the
|
| 27 |
+
minimum cut value among any pair of nodes is the minimum edge
|
| 28 |
+
weight in the shortest path between the two nodes in the
|
| 29 |
+
Gomory-Hu tree.
|
| 30 |
+
|
| 31 |
+
The Gomory-Hu tree also has the property that removing the
|
| 32 |
+
edge with the minimum weight in the shortest path between
|
| 33 |
+
any two nodes leaves two connected components that form
|
| 34 |
+
a partition of the nodes in G that defines the minimum s-t
|
| 35 |
+
cut.
|
| 36 |
+
|
| 37 |
+
See Examples section below for details.
|
| 38 |
+
|
| 39 |
+
Parameters
|
| 40 |
+
----------
|
| 41 |
+
G : NetworkX graph
|
| 42 |
+
Undirected graph
|
| 43 |
+
|
| 44 |
+
capacity : string
|
| 45 |
+
Edges of the graph G are expected to have an attribute capacity
|
| 46 |
+
that indicates how much flow the edge can support. If this
|
| 47 |
+
attribute is not present, the edge is considered to have
|
| 48 |
+
infinite capacity. Default value: 'capacity'.
|
| 49 |
+
|
| 50 |
+
flow_func : function
|
| 51 |
+
Function to perform the underlying flow computations. Default value
|
| 52 |
+
:func:`edmonds_karp`. This function performs better in sparse graphs
|
| 53 |
+
with right tailed degree distributions.
|
| 54 |
+
:func:`shortest_augmenting_path` will perform better in denser
|
| 55 |
+
graphs.
|
| 56 |
+
|
| 57 |
+
Returns
|
| 58 |
+
-------
|
| 59 |
+
Tree : NetworkX graph
|
| 60 |
+
A NetworkX graph representing the Gomory-Hu tree of the input graph.
|
| 61 |
+
|
| 62 |
+
Raises
|
| 63 |
+
------
|
| 64 |
+
NetworkXNotImplemented
|
| 65 |
+
Raised if the input graph is directed.
|
| 66 |
+
|
| 67 |
+
NetworkXError
|
| 68 |
+
Raised if the input graph is an empty Graph.
|
| 69 |
+
|
| 70 |
+
Examples
|
| 71 |
+
--------
|
| 72 |
+
>>> G = nx.karate_club_graph()
|
| 73 |
+
>>> nx.set_edge_attributes(G, 1, "capacity")
|
| 74 |
+
>>> T = nx.gomory_hu_tree(G)
|
| 75 |
+
>>> # The value of the minimum cut between any pair
|
| 76 |
+
... # of nodes in G is the minimum edge weight in the
|
| 77 |
+
... # shortest path between the two nodes in the
|
| 78 |
+
... # Gomory-Hu tree.
|
| 79 |
+
... def minimum_edge_weight_in_shortest_path(T, u, v):
|
| 80 |
+
... path = nx.shortest_path(T, u, v, weight="weight")
|
| 81 |
+
... return min((T[u][v]["weight"], (u, v)) for (u, v) in zip(path, path[1:]))
|
| 82 |
+
>>> u, v = 0, 33
|
| 83 |
+
>>> cut_value, edge = minimum_edge_weight_in_shortest_path(T, u, v)
|
| 84 |
+
>>> cut_value
|
| 85 |
+
10
|
| 86 |
+
>>> nx.minimum_cut_value(G, u, v)
|
| 87 |
+
10
|
| 88 |
+
>>> # The Gomory-Hu tree also has the property that removing the
|
| 89 |
+
... # edge with the minimum weight in the shortest path between
|
| 90 |
+
... # any two nodes leaves two connected components that form
|
| 91 |
+
... # a partition of the nodes in G that defines the minimum s-t
|
| 92 |
+
... # cut.
|
| 93 |
+
... cut_value, edge = minimum_edge_weight_in_shortest_path(T, u, v)
|
| 94 |
+
>>> T.remove_edge(*edge)
|
| 95 |
+
>>> U, V = list(nx.connected_components(T))
|
| 96 |
+
>>> # Thus U and V form a partition that defines a minimum cut
|
| 97 |
+
... # between u and v in G. You can compute the edge cut set,
|
| 98 |
+
... # that is, the set of edges that if removed from G will
|
| 99 |
+
... # disconnect u from v in G, with this information:
|
| 100 |
+
... cutset = set()
|
| 101 |
+
>>> for x, nbrs in ((n, G[n]) for n in U):
|
| 102 |
+
... cutset.update((x, y) for y in nbrs if y in V)
|
| 103 |
+
>>> # Because we have set the capacities of all edges to 1
|
| 104 |
+
... # the cutset contains ten edges
|
| 105 |
+
... len(cutset)
|
| 106 |
+
10
|
| 107 |
+
>>> # You can use any maximum flow algorithm for the underlying
|
| 108 |
+
... # flow computations using the argument flow_func
|
| 109 |
+
... from networkx.algorithms import flow
|
| 110 |
+
>>> T = nx.gomory_hu_tree(G, flow_func=flow.boykov_kolmogorov)
|
| 111 |
+
>>> cut_value, edge = minimum_edge_weight_in_shortest_path(T, u, v)
|
| 112 |
+
>>> cut_value
|
| 113 |
+
10
|
| 114 |
+
>>> nx.minimum_cut_value(G, u, v, flow_func=flow.boykov_kolmogorov)
|
| 115 |
+
10
|
| 116 |
+
|
| 117 |
+
Notes
|
| 118 |
+
-----
|
| 119 |
+
This implementation is based on Gusfield approach [1]_ to compute
|
| 120 |
+
Gomory-Hu trees, which does not require node contractions and has
|
| 121 |
+
the same computational complexity than the original method.
|
| 122 |
+
|
| 123 |
+
See also
|
| 124 |
+
--------
|
| 125 |
+
:func:`minimum_cut`
|
| 126 |
+
:func:`maximum_flow`
|
| 127 |
+
|
| 128 |
+
References
|
| 129 |
+
----------
|
| 130 |
+
.. [1] Gusfield D: Very simple methods for all pairs network flow analysis.
|
| 131 |
+
SIAM J Comput 19(1):143-155, 1990.
|
| 132 |
+
|
| 133 |
+
"""
|
| 134 |
+
if flow_func is None:
|
| 135 |
+
flow_func = default_flow_func
|
| 136 |
+
|
| 137 |
+
if len(G) == 0: # empty graph
|
| 138 |
+
msg = "Empty Graph does not have a Gomory-Hu tree representation"
|
| 139 |
+
raise nx.NetworkXError(msg)
|
| 140 |
+
|
| 141 |
+
# Start the tree as a star graph with an arbitrary node at the center
|
| 142 |
+
tree = {}
|
| 143 |
+
labels = {}
|
| 144 |
+
iter_nodes = iter(G)
|
| 145 |
+
root = next(iter_nodes)
|
| 146 |
+
for n in iter_nodes:
|
| 147 |
+
tree[n] = root
|
| 148 |
+
|
| 149 |
+
# Reuse residual network
|
| 150 |
+
R = build_residual_network(G, capacity)
|
| 151 |
+
|
| 152 |
+
# For all the leaves in the star graph tree (that is n-1 nodes).
|
| 153 |
+
for source in tree:
|
| 154 |
+
# Find neighbor in the tree
|
| 155 |
+
target = tree[source]
|
| 156 |
+
# compute minimum cut
|
| 157 |
+
cut_value, partition = nx.minimum_cut(
|
| 158 |
+
G, source, target, capacity=capacity, flow_func=flow_func, residual=R
|
| 159 |
+
)
|
| 160 |
+
labels[(source, target)] = cut_value
|
| 161 |
+
# Update the tree
|
| 162 |
+
# Source will always be in partition[0] and target in partition[1]
|
| 163 |
+
for node in partition[0]:
|
| 164 |
+
if node != source and node in tree and tree[node] == target:
|
| 165 |
+
tree[node] = source
|
| 166 |
+
labels[node, source] = labels.get((node, target), cut_value)
|
| 167 |
+
#
|
| 168 |
+
if target != root and tree[target] in partition[0]:
|
| 169 |
+
labels[source, tree[target]] = labels[target, tree[target]]
|
| 170 |
+
labels[target, source] = cut_value
|
| 171 |
+
tree[source] = tree[target]
|
| 172 |
+
tree[target] = source
|
| 173 |
+
|
| 174 |
+
# Build the tree
|
| 175 |
+
T = nx.Graph()
|
| 176 |
+
T.add_nodes_from(G)
|
| 177 |
+
T.add_weighted_edges_from(((u, v, labels[u, v]) for u, v in tree.items()))
|
| 178 |
+
return T
|
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/maxflow.py
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Maximum flow (and minimum cut) algorithms on capacitated graphs.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
from .boykovkolmogorov import boykov_kolmogorov
|
| 8 |
+
from .dinitz_alg import dinitz
|
| 9 |
+
from .edmondskarp import edmonds_karp
|
| 10 |
+
from .preflowpush import preflow_push
|
| 11 |
+
from .shortestaugmentingpath import shortest_augmenting_path
|
| 12 |
+
from .utils import build_flow_dict
|
| 13 |
+
|
| 14 |
+
# Define the default flow function for computing maximum flow.
|
| 15 |
+
default_flow_func = preflow_push
|
| 16 |
+
|
| 17 |
+
__all__ = ["maximum_flow", "maximum_flow_value", "minimum_cut", "minimum_cut_value"]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")})
|
| 21 |
+
def maximum_flow(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs):
|
| 22 |
+
"""Find a maximum single-commodity flow.
|
| 23 |
+
|
| 24 |
+
Parameters
|
| 25 |
+
----------
|
| 26 |
+
flowG : NetworkX graph
|
| 27 |
+
Edges of the graph are expected to have an attribute called
|
| 28 |
+
'capacity'. If this attribute is not present, the edge is
|
| 29 |
+
considered to have infinite capacity.
|
| 30 |
+
|
| 31 |
+
_s : node
|
| 32 |
+
Source node for the flow.
|
| 33 |
+
|
| 34 |
+
_t : node
|
| 35 |
+
Sink node for the flow.
|
| 36 |
+
|
| 37 |
+
capacity : string
|
| 38 |
+
Edges of the graph G are expected to have an attribute capacity
|
| 39 |
+
that indicates how much flow the edge can support. If this
|
| 40 |
+
attribute is not present, the edge is considered to have
|
| 41 |
+
infinite capacity. Default value: 'capacity'.
|
| 42 |
+
|
| 43 |
+
flow_func : function
|
| 44 |
+
A function for computing the maximum flow among a pair of nodes
|
| 45 |
+
in a capacitated graph. The function has to accept at least three
|
| 46 |
+
parameters: a Graph or Digraph, a source node, and a target node.
|
| 47 |
+
And return a residual network that follows NetworkX conventions
|
| 48 |
+
(see Notes). If flow_func is None, the default maximum
|
| 49 |
+
flow function (:meth:`preflow_push`) is used. See below for
|
| 50 |
+
alternative algorithms. The choice of the default function may change
|
| 51 |
+
from version to version and should not be relied on. Default value:
|
| 52 |
+
None.
|
| 53 |
+
|
| 54 |
+
kwargs : Any other keyword parameter is passed to the function that
|
| 55 |
+
computes the maximum flow.
|
| 56 |
+
|
| 57 |
+
Returns
|
| 58 |
+
-------
|
| 59 |
+
flow_value : integer, float
|
| 60 |
+
Value of the maximum flow, i.e., net outflow from the source.
|
| 61 |
+
|
| 62 |
+
flow_dict : dict
|
| 63 |
+
A dictionary containing the value of the flow that went through
|
| 64 |
+
each edge.
|
| 65 |
+
|
| 66 |
+
Raises
|
| 67 |
+
------
|
| 68 |
+
NetworkXError
|
| 69 |
+
The algorithm does not support MultiGraph and MultiDiGraph. If
|
| 70 |
+
the input graph is an instance of one of these two classes, a
|
| 71 |
+
NetworkXError is raised.
|
| 72 |
+
|
| 73 |
+
NetworkXUnbounded
|
| 74 |
+
If the graph has a path of infinite capacity, the value of a
|
| 75 |
+
feasible flow on the graph is unbounded above and the function
|
| 76 |
+
raises a NetworkXUnbounded.
|
| 77 |
+
|
| 78 |
+
See also
|
| 79 |
+
--------
|
| 80 |
+
:meth:`maximum_flow_value`
|
| 81 |
+
:meth:`minimum_cut`
|
| 82 |
+
:meth:`minimum_cut_value`
|
| 83 |
+
:meth:`edmonds_karp`
|
| 84 |
+
:meth:`preflow_push`
|
| 85 |
+
:meth:`shortest_augmenting_path`
|
| 86 |
+
|
| 87 |
+
Notes
|
| 88 |
+
-----
|
| 89 |
+
The function used in the flow_func parameter has to return a residual
|
| 90 |
+
network that follows NetworkX conventions:
|
| 91 |
+
|
| 92 |
+
The residual network :samp:`R` from an input graph :samp:`G` has the
|
| 93 |
+
same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
|
| 94 |
+
of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
|
| 95 |
+
self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
|
| 96 |
+
in :samp:`G`.
|
| 97 |
+
|
| 98 |
+
For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
|
| 99 |
+
is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
|
| 100 |
+
in :samp:`G` or zero otherwise. If the capacity is infinite,
|
| 101 |
+
:samp:`R[u][v]['capacity']` will have a high arbitrary finite value
|
| 102 |
+
that does not affect the solution of the problem. This value is stored in
|
| 103 |
+
:samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
|
| 104 |
+
:samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
|
| 105 |
+
satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
|
| 106 |
+
|
| 107 |
+
The flow value, defined as the total flow into :samp:`t`, the sink, is
|
| 108 |
+
stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using
|
| 109 |
+
only edges :samp:`(u, v)` such that
|
| 110 |
+
:samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
|
| 111 |
+
:samp:`s`-:samp:`t` cut.
|
| 112 |
+
|
| 113 |
+
Specific algorithms may store extra data in :samp:`R`.
|
| 114 |
+
|
| 115 |
+
The function should supports an optional boolean parameter value_only. When
|
| 116 |
+
True, it can optionally terminate the algorithm as soon as the maximum flow
|
| 117 |
+
value and the minimum cut can be determined.
|
| 118 |
+
|
| 119 |
+
Examples
|
| 120 |
+
--------
|
| 121 |
+
>>> G = nx.DiGraph()
|
| 122 |
+
>>> G.add_edge("x", "a", capacity=3.0)
|
| 123 |
+
>>> G.add_edge("x", "b", capacity=1.0)
|
| 124 |
+
>>> G.add_edge("a", "c", capacity=3.0)
|
| 125 |
+
>>> G.add_edge("b", "c", capacity=5.0)
|
| 126 |
+
>>> G.add_edge("b", "d", capacity=4.0)
|
| 127 |
+
>>> G.add_edge("d", "e", capacity=2.0)
|
| 128 |
+
>>> G.add_edge("c", "y", capacity=2.0)
|
| 129 |
+
>>> G.add_edge("e", "y", capacity=3.0)
|
| 130 |
+
|
| 131 |
+
maximum_flow returns both the value of the maximum flow and a
|
| 132 |
+
dictionary with all flows.
|
| 133 |
+
|
| 134 |
+
>>> flow_value, flow_dict = nx.maximum_flow(G, "x", "y")
|
| 135 |
+
>>> flow_value
|
| 136 |
+
3.0
|
| 137 |
+
>>> print(flow_dict["x"]["b"])
|
| 138 |
+
1.0
|
| 139 |
+
|
| 140 |
+
You can also use alternative algorithms for computing the
|
| 141 |
+
maximum flow by using the flow_func parameter.
|
| 142 |
+
|
| 143 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 144 |
+
>>> flow_value == nx.maximum_flow(G, "x", "y", flow_func=shortest_augmenting_path)[
|
| 145 |
+
... 0
|
| 146 |
+
... ]
|
| 147 |
+
True
|
| 148 |
+
|
| 149 |
+
"""
|
| 150 |
+
if flow_func is None:
|
| 151 |
+
if kwargs:
|
| 152 |
+
raise nx.NetworkXError(
|
| 153 |
+
"You have to explicitly set a flow_func if"
|
| 154 |
+
" you need to pass parameters via kwargs."
|
| 155 |
+
)
|
| 156 |
+
flow_func = default_flow_func
|
| 157 |
+
|
| 158 |
+
if not callable(flow_func):
|
| 159 |
+
raise nx.NetworkXError("flow_func has to be callable.")
|
| 160 |
+
|
| 161 |
+
R = flow_func(flowG, _s, _t, capacity=capacity, value_only=False, **kwargs)
|
| 162 |
+
flow_dict = build_flow_dict(flowG, R)
|
| 163 |
+
|
| 164 |
+
return (R.graph["flow_value"], flow_dict)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
@nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")})
|
| 168 |
+
def maximum_flow_value(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs):
|
| 169 |
+
"""Find the value of maximum single-commodity flow.
|
| 170 |
+
|
| 171 |
+
Parameters
|
| 172 |
+
----------
|
| 173 |
+
flowG : NetworkX graph
|
| 174 |
+
Edges of the graph are expected to have an attribute called
|
| 175 |
+
'capacity'. If this attribute is not present, the edge is
|
| 176 |
+
considered to have infinite capacity.
|
| 177 |
+
|
| 178 |
+
_s : node
|
| 179 |
+
Source node for the flow.
|
| 180 |
+
|
| 181 |
+
_t : node
|
| 182 |
+
Sink node for the flow.
|
| 183 |
+
|
| 184 |
+
capacity : string
|
| 185 |
+
Edges of the graph G are expected to have an attribute capacity
|
| 186 |
+
that indicates how much flow the edge can support. If this
|
| 187 |
+
attribute is not present, the edge is considered to have
|
| 188 |
+
infinite capacity. Default value: 'capacity'.
|
| 189 |
+
|
| 190 |
+
flow_func : function
|
| 191 |
+
A function for computing the maximum flow among a pair of nodes
|
| 192 |
+
in a capacitated graph. The function has to accept at least three
|
| 193 |
+
parameters: a Graph or Digraph, a source node, and a target node.
|
| 194 |
+
And return a residual network that follows NetworkX conventions
|
| 195 |
+
(see Notes). If flow_func is None, the default maximum
|
| 196 |
+
flow function (:meth:`preflow_push`) is used. See below for
|
| 197 |
+
alternative algorithms. The choice of the default function may change
|
| 198 |
+
from version to version and should not be relied on. Default value:
|
| 199 |
+
None.
|
| 200 |
+
|
| 201 |
+
kwargs : Any other keyword parameter is passed to the function that
|
| 202 |
+
computes the maximum flow.
|
| 203 |
+
|
| 204 |
+
Returns
|
| 205 |
+
-------
|
| 206 |
+
flow_value : integer, float
|
| 207 |
+
Value of the maximum flow, i.e., net outflow from the source.
|
| 208 |
+
|
| 209 |
+
Raises
|
| 210 |
+
------
|
| 211 |
+
NetworkXError
|
| 212 |
+
The algorithm does not support MultiGraph and MultiDiGraph. If
|
| 213 |
+
the input graph is an instance of one of these two classes, a
|
| 214 |
+
NetworkXError is raised.
|
| 215 |
+
|
| 216 |
+
NetworkXUnbounded
|
| 217 |
+
If the graph has a path of infinite capacity, the value of a
|
| 218 |
+
feasible flow on the graph is unbounded above and the function
|
| 219 |
+
raises a NetworkXUnbounded.
|
| 220 |
+
|
| 221 |
+
See also
|
| 222 |
+
--------
|
| 223 |
+
:meth:`maximum_flow`
|
| 224 |
+
:meth:`minimum_cut`
|
| 225 |
+
:meth:`minimum_cut_value`
|
| 226 |
+
:meth:`edmonds_karp`
|
| 227 |
+
:meth:`preflow_push`
|
| 228 |
+
:meth:`shortest_augmenting_path`
|
| 229 |
+
|
| 230 |
+
Notes
|
| 231 |
+
-----
|
| 232 |
+
The function used in the flow_func parameter has to return a residual
|
| 233 |
+
network that follows NetworkX conventions:
|
| 234 |
+
|
| 235 |
+
The residual network :samp:`R` from an input graph :samp:`G` has the
|
| 236 |
+
same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
|
| 237 |
+
of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
|
| 238 |
+
self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
|
| 239 |
+
in :samp:`G`.
|
| 240 |
+
|
| 241 |
+
For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
|
| 242 |
+
is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
|
| 243 |
+
in :samp:`G` or zero otherwise. If the capacity is infinite,
|
| 244 |
+
:samp:`R[u][v]['capacity']` will have a high arbitrary finite value
|
| 245 |
+
that does not affect the solution of the problem. This value is stored in
|
| 246 |
+
:samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
|
| 247 |
+
:samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
|
| 248 |
+
satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
|
| 249 |
+
|
| 250 |
+
The flow value, defined as the total flow into :samp:`t`, the sink, is
|
| 251 |
+
stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using
|
| 252 |
+
only edges :samp:`(u, v)` such that
|
| 253 |
+
:samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
|
| 254 |
+
:samp:`s`-:samp:`t` cut.
|
| 255 |
+
|
| 256 |
+
Specific algorithms may store extra data in :samp:`R`.
|
| 257 |
+
|
| 258 |
+
The function should supports an optional boolean parameter value_only. When
|
| 259 |
+
True, it can optionally terminate the algorithm as soon as the maximum flow
|
| 260 |
+
value and the minimum cut can be determined.
|
| 261 |
+
|
| 262 |
+
Examples
|
| 263 |
+
--------
|
| 264 |
+
>>> G = nx.DiGraph()
|
| 265 |
+
>>> G.add_edge("x", "a", capacity=3.0)
|
| 266 |
+
>>> G.add_edge("x", "b", capacity=1.0)
|
| 267 |
+
>>> G.add_edge("a", "c", capacity=3.0)
|
| 268 |
+
>>> G.add_edge("b", "c", capacity=5.0)
|
| 269 |
+
>>> G.add_edge("b", "d", capacity=4.0)
|
| 270 |
+
>>> G.add_edge("d", "e", capacity=2.0)
|
| 271 |
+
>>> G.add_edge("c", "y", capacity=2.0)
|
| 272 |
+
>>> G.add_edge("e", "y", capacity=3.0)
|
| 273 |
+
|
| 274 |
+
maximum_flow_value computes only the value of the
|
| 275 |
+
maximum flow:
|
| 276 |
+
|
| 277 |
+
>>> flow_value = nx.maximum_flow_value(G, "x", "y")
|
| 278 |
+
>>> flow_value
|
| 279 |
+
3.0
|
| 280 |
+
|
| 281 |
+
You can also use alternative algorithms for computing the
|
| 282 |
+
maximum flow by using the flow_func parameter.
|
| 283 |
+
|
| 284 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 285 |
+
>>> flow_value == nx.maximum_flow_value(
|
| 286 |
+
... G, "x", "y", flow_func=shortest_augmenting_path
|
| 287 |
+
... )
|
| 288 |
+
True
|
| 289 |
+
|
| 290 |
+
"""
|
| 291 |
+
if flow_func is None:
|
| 292 |
+
if kwargs:
|
| 293 |
+
raise nx.NetworkXError(
|
| 294 |
+
"You have to explicitly set a flow_func if"
|
| 295 |
+
" you need to pass parameters via kwargs."
|
| 296 |
+
)
|
| 297 |
+
flow_func = default_flow_func
|
| 298 |
+
|
| 299 |
+
if not callable(flow_func):
|
| 300 |
+
raise nx.NetworkXError("flow_func has to be callable.")
|
| 301 |
+
|
| 302 |
+
R = flow_func(flowG, _s, _t, capacity=capacity, value_only=True, **kwargs)
|
| 303 |
+
|
| 304 |
+
return R.graph["flow_value"]
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
@nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")})
|
| 308 |
+
def minimum_cut(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs):
|
| 309 |
+
"""Compute the value and the node partition of a minimum (s, t)-cut.
|
| 310 |
+
|
| 311 |
+
Use the max-flow min-cut theorem, i.e., the capacity of a minimum
|
| 312 |
+
capacity cut is equal to the flow value of a maximum flow.
|
| 313 |
+
|
| 314 |
+
Parameters
|
| 315 |
+
----------
|
| 316 |
+
flowG : NetworkX graph
|
| 317 |
+
Edges of the graph are expected to have an attribute called
|
| 318 |
+
'capacity'. If this attribute is not present, the edge is
|
| 319 |
+
considered to have infinite capacity.
|
| 320 |
+
|
| 321 |
+
_s : node
|
| 322 |
+
Source node for the flow.
|
| 323 |
+
|
| 324 |
+
_t : node
|
| 325 |
+
Sink node for the flow.
|
| 326 |
+
|
| 327 |
+
capacity : string
|
| 328 |
+
Edges of the graph G are expected to have an attribute capacity
|
| 329 |
+
that indicates how much flow the edge can support. If this
|
| 330 |
+
attribute is not present, the edge is considered to have
|
| 331 |
+
infinite capacity. Default value: 'capacity'.
|
| 332 |
+
|
| 333 |
+
flow_func : function
|
| 334 |
+
A function for computing the maximum flow among a pair of nodes
|
| 335 |
+
in a capacitated graph. The function has to accept at least three
|
| 336 |
+
parameters: a Graph or Digraph, a source node, and a target node.
|
| 337 |
+
And return a residual network that follows NetworkX conventions
|
| 338 |
+
(see Notes). If flow_func is None, the default maximum
|
| 339 |
+
flow function (:meth:`preflow_push`) is used. See below for
|
| 340 |
+
alternative algorithms. The choice of the default function may change
|
| 341 |
+
from version to version and should not be relied on. Default value:
|
| 342 |
+
None.
|
| 343 |
+
|
| 344 |
+
kwargs : Any other keyword parameter is passed to the function that
|
| 345 |
+
computes the maximum flow.
|
| 346 |
+
|
| 347 |
+
Returns
|
| 348 |
+
-------
|
| 349 |
+
cut_value : integer, float
|
| 350 |
+
Value of the minimum cut.
|
| 351 |
+
|
| 352 |
+
partition : pair of node sets
|
| 353 |
+
A partitioning of the nodes that defines a minimum cut.
|
| 354 |
+
|
| 355 |
+
Raises
|
| 356 |
+
------
|
| 357 |
+
NetworkXUnbounded
|
| 358 |
+
If the graph has a path of infinite capacity, all cuts have
|
| 359 |
+
infinite capacity and the function raises a NetworkXError.
|
| 360 |
+
|
| 361 |
+
See also
|
| 362 |
+
--------
|
| 363 |
+
:meth:`maximum_flow`
|
| 364 |
+
:meth:`maximum_flow_value`
|
| 365 |
+
:meth:`minimum_cut_value`
|
| 366 |
+
:meth:`edmonds_karp`
|
| 367 |
+
:meth:`preflow_push`
|
| 368 |
+
:meth:`shortest_augmenting_path`
|
| 369 |
+
|
| 370 |
+
Notes
|
| 371 |
+
-----
|
| 372 |
+
The function used in the flow_func parameter has to return a residual
|
| 373 |
+
network that follows NetworkX conventions:
|
| 374 |
+
|
| 375 |
+
The residual network :samp:`R` from an input graph :samp:`G` has the
|
| 376 |
+
same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
|
| 377 |
+
of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
|
| 378 |
+
self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
|
| 379 |
+
in :samp:`G`.
|
| 380 |
+
|
| 381 |
+
For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
|
| 382 |
+
is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
|
| 383 |
+
in :samp:`G` or zero otherwise. If the capacity is infinite,
|
| 384 |
+
:samp:`R[u][v]['capacity']` will have a high arbitrary finite value
|
| 385 |
+
that does not affect the solution of the problem. This value is stored in
|
| 386 |
+
:samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
|
| 387 |
+
:samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
|
| 388 |
+
satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
|
| 389 |
+
|
| 390 |
+
The flow value, defined as the total flow into :samp:`t`, the sink, is
|
| 391 |
+
stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using
|
| 392 |
+
only edges :samp:`(u, v)` such that
|
| 393 |
+
:samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
|
| 394 |
+
:samp:`s`-:samp:`t` cut.
|
| 395 |
+
|
| 396 |
+
Specific algorithms may store extra data in :samp:`R`.
|
| 397 |
+
|
| 398 |
+
The function should supports an optional boolean parameter value_only. When
|
| 399 |
+
True, it can optionally terminate the algorithm as soon as the maximum flow
|
| 400 |
+
value and the minimum cut can be determined.
|
| 401 |
+
|
| 402 |
+
Examples
|
| 403 |
+
--------
|
| 404 |
+
>>> G = nx.DiGraph()
|
| 405 |
+
>>> G.add_edge("x", "a", capacity=3.0)
|
| 406 |
+
>>> G.add_edge("x", "b", capacity=1.0)
|
| 407 |
+
>>> G.add_edge("a", "c", capacity=3.0)
|
| 408 |
+
>>> G.add_edge("b", "c", capacity=5.0)
|
| 409 |
+
>>> G.add_edge("b", "d", capacity=4.0)
|
| 410 |
+
>>> G.add_edge("d", "e", capacity=2.0)
|
| 411 |
+
>>> G.add_edge("c", "y", capacity=2.0)
|
| 412 |
+
>>> G.add_edge("e", "y", capacity=3.0)
|
| 413 |
+
|
| 414 |
+
minimum_cut computes both the value of the
|
| 415 |
+
minimum cut and the node partition:
|
| 416 |
+
|
| 417 |
+
>>> cut_value, partition = nx.minimum_cut(G, "x", "y")
|
| 418 |
+
>>> reachable, non_reachable = partition
|
| 419 |
+
|
| 420 |
+
'partition' here is a tuple with the two sets of nodes that define
|
| 421 |
+
the minimum cut. You can compute the cut set of edges that induce
|
| 422 |
+
the minimum cut as follows:
|
| 423 |
+
|
| 424 |
+
>>> cutset = set()
|
| 425 |
+
>>> for u, nbrs in ((n, G[n]) for n in reachable):
|
| 426 |
+
... cutset.update((u, v) for v in nbrs if v in non_reachable)
|
| 427 |
+
>>> print(sorted(cutset))
|
| 428 |
+
[('c', 'y'), ('x', 'b')]
|
| 429 |
+
>>> cut_value == sum(G.edges[u, v]["capacity"] for (u, v) in cutset)
|
| 430 |
+
True
|
| 431 |
+
|
| 432 |
+
You can also use alternative algorithms for computing the
|
| 433 |
+
minimum cut by using the flow_func parameter.
|
| 434 |
+
|
| 435 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 436 |
+
>>> cut_value == nx.minimum_cut(G, "x", "y", flow_func=shortest_augmenting_path)[0]
|
| 437 |
+
True
|
| 438 |
+
|
| 439 |
+
"""
|
| 440 |
+
if flow_func is None:
|
| 441 |
+
if kwargs:
|
| 442 |
+
raise nx.NetworkXError(
|
| 443 |
+
"You have to explicitly set a flow_func if"
|
| 444 |
+
" you need to pass parameters via kwargs."
|
| 445 |
+
)
|
| 446 |
+
flow_func = default_flow_func
|
| 447 |
+
|
| 448 |
+
if not callable(flow_func):
|
| 449 |
+
raise nx.NetworkXError("flow_func has to be callable.")
|
| 450 |
+
|
| 451 |
+
if kwargs.get("cutoff") is not None and flow_func is preflow_push:
|
| 452 |
+
raise nx.NetworkXError("cutoff should not be specified.")
|
| 453 |
+
|
| 454 |
+
R = flow_func(flowG, _s, _t, capacity=capacity, value_only=True, **kwargs)
|
| 455 |
+
# Remove saturated edges from the residual network
|
| 456 |
+
cutset = [(u, v, d) for u, v, d in R.edges(data=True) if d["flow"] == d["capacity"]]
|
| 457 |
+
R.remove_edges_from(cutset)
|
| 458 |
+
|
| 459 |
+
# Then, reachable and non reachable nodes from source in the
|
| 460 |
+
# residual network form the node partition that defines
|
| 461 |
+
# the minimum cut.
|
| 462 |
+
non_reachable = set(dict(nx.shortest_path_length(R, target=_t)))
|
| 463 |
+
partition = (set(flowG) - non_reachable, non_reachable)
|
| 464 |
+
# Finally add again cutset edges to the residual network to make
|
| 465 |
+
# sure that it is reusable.
|
| 466 |
+
R.add_edges_from(cutset)
|
| 467 |
+
return (R.graph["flow_value"], partition)
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
@nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")})
|
| 471 |
+
def minimum_cut_value(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs):
|
| 472 |
+
"""Compute the value of a minimum (s, t)-cut.
|
| 473 |
+
|
| 474 |
+
Use the max-flow min-cut theorem, i.e., the capacity of a minimum
|
| 475 |
+
capacity cut is equal to the flow value of a maximum flow.
|
| 476 |
+
|
| 477 |
+
Parameters
|
| 478 |
+
----------
|
| 479 |
+
flowG : NetworkX graph
|
| 480 |
+
Edges of the graph are expected to have an attribute called
|
| 481 |
+
'capacity'. If this attribute is not present, the edge is
|
| 482 |
+
considered to have infinite capacity.
|
| 483 |
+
|
| 484 |
+
_s : node
|
| 485 |
+
Source node for the flow.
|
| 486 |
+
|
| 487 |
+
_t : node
|
| 488 |
+
Sink node for the flow.
|
| 489 |
+
|
| 490 |
+
capacity : string
|
| 491 |
+
Edges of the graph G are expected to have an attribute capacity
|
| 492 |
+
that indicates how much flow the edge can support. If this
|
| 493 |
+
attribute is not present, the edge is considered to have
|
| 494 |
+
infinite capacity. Default value: 'capacity'.
|
| 495 |
+
|
| 496 |
+
flow_func : function
|
| 497 |
+
A function for computing the maximum flow among a pair of nodes
|
| 498 |
+
in a capacitated graph. The function has to accept at least three
|
| 499 |
+
parameters: a Graph or Digraph, a source node, and a target node.
|
| 500 |
+
And return a residual network that follows NetworkX conventions
|
| 501 |
+
(see Notes). If flow_func is None, the default maximum
|
| 502 |
+
flow function (:meth:`preflow_push`) is used. See below for
|
| 503 |
+
alternative algorithms. The choice of the default function may change
|
| 504 |
+
from version to version and should not be relied on. Default value:
|
| 505 |
+
None.
|
| 506 |
+
|
| 507 |
+
kwargs : Any other keyword parameter is passed to the function that
|
| 508 |
+
computes the maximum flow.
|
| 509 |
+
|
| 510 |
+
Returns
|
| 511 |
+
-------
|
| 512 |
+
cut_value : integer, float
|
| 513 |
+
Value of the minimum cut.
|
| 514 |
+
|
| 515 |
+
Raises
|
| 516 |
+
------
|
| 517 |
+
NetworkXUnbounded
|
| 518 |
+
If the graph has a path of infinite capacity, all cuts have
|
| 519 |
+
infinite capacity and the function raises a NetworkXError.
|
| 520 |
+
|
| 521 |
+
See also
|
| 522 |
+
--------
|
| 523 |
+
:meth:`maximum_flow`
|
| 524 |
+
:meth:`maximum_flow_value`
|
| 525 |
+
:meth:`minimum_cut`
|
| 526 |
+
:meth:`edmonds_karp`
|
| 527 |
+
:meth:`preflow_push`
|
| 528 |
+
:meth:`shortest_augmenting_path`
|
| 529 |
+
|
| 530 |
+
Notes
|
| 531 |
+
-----
|
| 532 |
+
The function used in the flow_func parameter has to return a residual
|
| 533 |
+
network that follows NetworkX conventions:
|
| 534 |
+
|
| 535 |
+
The residual network :samp:`R` from an input graph :samp:`G` has the
|
| 536 |
+
same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
|
| 537 |
+
of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
|
| 538 |
+
self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
|
| 539 |
+
in :samp:`G`.
|
| 540 |
+
|
| 541 |
+
For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
|
| 542 |
+
is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
|
| 543 |
+
in :samp:`G` or zero otherwise. If the capacity is infinite,
|
| 544 |
+
:samp:`R[u][v]['capacity']` will have a high arbitrary finite value
|
| 545 |
+
that does not affect the solution of the problem. This value is stored in
|
| 546 |
+
:samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
|
| 547 |
+
:samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
|
| 548 |
+
satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
|
| 549 |
+
|
| 550 |
+
The flow value, defined as the total flow into :samp:`t`, the sink, is
|
| 551 |
+
stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using
|
| 552 |
+
only edges :samp:`(u, v)` such that
|
| 553 |
+
:samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
|
| 554 |
+
:samp:`s`-:samp:`t` cut.
|
| 555 |
+
|
| 556 |
+
Specific algorithms may store extra data in :samp:`R`.
|
| 557 |
+
|
| 558 |
+
The function should supports an optional boolean parameter value_only. When
|
| 559 |
+
True, it can optionally terminate the algorithm as soon as the maximum flow
|
| 560 |
+
value and the minimum cut can be determined.
|
| 561 |
+
|
| 562 |
+
Examples
|
| 563 |
+
--------
|
| 564 |
+
>>> G = nx.DiGraph()
|
| 565 |
+
>>> G.add_edge("x", "a", capacity=3.0)
|
| 566 |
+
>>> G.add_edge("x", "b", capacity=1.0)
|
| 567 |
+
>>> G.add_edge("a", "c", capacity=3.0)
|
| 568 |
+
>>> G.add_edge("b", "c", capacity=5.0)
|
| 569 |
+
>>> G.add_edge("b", "d", capacity=4.0)
|
| 570 |
+
>>> G.add_edge("d", "e", capacity=2.0)
|
| 571 |
+
>>> G.add_edge("c", "y", capacity=2.0)
|
| 572 |
+
>>> G.add_edge("e", "y", capacity=3.0)
|
| 573 |
+
|
| 574 |
+
minimum_cut_value computes only the value of the
|
| 575 |
+
minimum cut:
|
| 576 |
+
|
| 577 |
+
>>> cut_value = nx.minimum_cut_value(G, "x", "y")
|
| 578 |
+
>>> cut_value
|
| 579 |
+
3.0
|
| 580 |
+
|
| 581 |
+
You can also use alternative algorithms for computing the
|
| 582 |
+
minimum cut by using the flow_func parameter.
|
| 583 |
+
|
| 584 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 585 |
+
>>> cut_value == nx.minimum_cut_value(
|
| 586 |
+
... G, "x", "y", flow_func=shortest_augmenting_path
|
| 587 |
+
... )
|
| 588 |
+
True
|
| 589 |
+
|
| 590 |
+
"""
|
| 591 |
+
if flow_func is None:
|
| 592 |
+
if kwargs:
|
| 593 |
+
raise nx.NetworkXError(
|
| 594 |
+
"You have to explicitly set a flow_func if"
|
| 595 |
+
" you need to pass parameters via kwargs."
|
| 596 |
+
)
|
| 597 |
+
flow_func = default_flow_func
|
| 598 |
+
|
| 599 |
+
if not callable(flow_func):
|
| 600 |
+
raise nx.NetworkXError("flow_func has to be callable.")
|
| 601 |
+
|
| 602 |
+
if kwargs.get("cutoff") is not None and flow_func is preflow_push:
|
| 603 |
+
raise nx.NetworkXError("cutoff should not be specified.")
|
| 604 |
+
|
| 605 |
+
R = flow_func(flowG, _s, _t, capacity=capacity, value_only=True, **kwargs)
|
| 606 |
+
|
| 607 |
+
return R.graph["flow_value"]
|
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/networksimplex.py
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Minimum cost flow algorithms on directed connected graphs.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
__all__ = ["network_simplex"]
|
| 6 |
+
|
| 7 |
+
from itertools import chain, islice, repeat
|
| 8 |
+
from math import ceil, sqrt
|
| 9 |
+
|
| 10 |
+
import networkx as nx
|
| 11 |
+
from networkx.utils import not_implemented_for
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class _DataEssentialsAndFunctions:
|
| 15 |
+
def __init__(
|
| 16 |
+
self, G, multigraph, demand="demand", capacity="capacity", weight="weight"
|
| 17 |
+
):
|
| 18 |
+
# Number all nodes and edges and hereafter reference them using ONLY their numbers
|
| 19 |
+
self.node_list = list(G) # nodes
|
| 20 |
+
self.node_indices = {u: i for i, u in enumerate(self.node_list)} # node indices
|
| 21 |
+
self.node_demands = [
|
| 22 |
+
G.nodes[u].get(demand, 0) for u in self.node_list
|
| 23 |
+
] # node demands
|
| 24 |
+
|
| 25 |
+
self.edge_sources = [] # edge sources
|
| 26 |
+
self.edge_targets = [] # edge targets
|
| 27 |
+
if multigraph:
|
| 28 |
+
self.edge_keys = [] # edge keys
|
| 29 |
+
self.edge_indices = {} # edge indices
|
| 30 |
+
self.edge_capacities = [] # edge capacities
|
| 31 |
+
self.edge_weights = [] # edge weights
|
| 32 |
+
|
| 33 |
+
if not multigraph:
|
| 34 |
+
edges = G.edges(data=True)
|
| 35 |
+
else:
|
| 36 |
+
edges = G.edges(data=True, keys=True)
|
| 37 |
+
|
| 38 |
+
inf = float("inf")
|
| 39 |
+
edges = (e for e in edges if e[0] != e[1] and e[-1].get(capacity, inf) != 0)
|
| 40 |
+
for i, e in enumerate(edges):
|
| 41 |
+
self.edge_sources.append(self.node_indices[e[0]])
|
| 42 |
+
self.edge_targets.append(self.node_indices[e[1]])
|
| 43 |
+
if multigraph:
|
| 44 |
+
self.edge_keys.append(e[2])
|
| 45 |
+
self.edge_indices[e[:-1]] = i
|
| 46 |
+
self.edge_capacities.append(e[-1].get(capacity, inf))
|
| 47 |
+
self.edge_weights.append(e[-1].get(weight, 0))
|
| 48 |
+
|
| 49 |
+
# spanning tree specific data to be initialized
|
| 50 |
+
|
| 51 |
+
self.edge_count = None # number of edges
|
| 52 |
+
self.edge_flow = None # edge flows
|
| 53 |
+
self.node_potentials = None # node potentials
|
| 54 |
+
self.parent = None # parent nodes
|
| 55 |
+
self.parent_edge = None # edges to parents
|
| 56 |
+
self.subtree_size = None # subtree sizes
|
| 57 |
+
self.next_node_dft = None # next nodes in depth-first thread
|
| 58 |
+
self.prev_node_dft = None # previous nodes in depth-first thread
|
| 59 |
+
self.last_descendent_dft = None # last descendants in depth-first thread
|
| 60 |
+
self._spanning_tree_initialized = (
|
| 61 |
+
False # False until initialize_spanning_tree() is called
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
def initialize_spanning_tree(self, n, faux_inf):
|
| 65 |
+
self.edge_count = len(self.edge_indices) # number of edges
|
| 66 |
+
self.edge_flow = list(
|
| 67 |
+
chain(repeat(0, self.edge_count), (abs(d) for d in self.node_demands))
|
| 68 |
+
) # edge flows
|
| 69 |
+
self.node_potentials = [
|
| 70 |
+
faux_inf if d <= 0 else -faux_inf for d in self.node_demands
|
| 71 |
+
] # node potentials
|
| 72 |
+
self.parent = list(chain(repeat(-1, n), [None])) # parent nodes
|
| 73 |
+
self.parent_edge = list(
|
| 74 |
+
range(self.edge_count, self.edge_count + n)
|
| 75 |
+
) # edges to parents
|
| 76 |
+
self.subtree_size = list(chain(repeat(1, n), [n + 1])) # subtree sizes
|
| 77 |
+
self.next_node_dft = list(
|
| 78 |
+
chain(range(1, n), [-1, 0])
|
| 79 |
+
) # next nodes in depth-first thread
|
| 80 |
+
self.prev_node_dft = list(range(-1, n)) # previous nodes in depth-first thread
|
| 81 |
+
self.last_descendent_dft = list(
|
| 82 |
+
chain(range(n), [n - 1])
|
| 83 |
+
) # last descendants in depth-first thread
|
| 84 |
+
self._spanning_tree_initialized = True # True only if all the assignments pass
|
| 85 |
+
|
| 86 |
+
def find_apex(self, p, q):
|
| 87 |
+
"""
|
| 88 |
+
Find the lowest common ancestor of nodes p and q in the spanning tree.
|
| 89 |
+
"""
|
| 90 |
+
size_p = self.subtree_size[p]
|
| 91 |
+
size_q = self.subtree_size[q]
|
| 92 |
+
while True:
|
| 93 |
+
while size_p < size_q:
|
| 94 |
+
p = self.parent[p]
|
| 95 |
+
size_p = self.subtree_size[p]
|
| 96 |
+
while size_p > size_q:
|
| 97 |
+
q = self.parent[q]
|
| 98 |
+
size_q = self.subtree_size[q]
|
| 99 |
+
if size_p == size_q:
|
| 100 |
+
if p != q:
|
| 101 |
+
p = self.parent[p]
|
| 102 |
+
size_p = self.subtree_size[p]
|
| 103 |
+
q = self.parent[q]
|
| 104 |
+
size_q = self.subtree_size[q]
|
| 105 |
+
else:
|
| 106 |
+
return p
|
| 107 |
+
|
| 108 |
+
def trace_path(self, p, w):
|
| 109 |
+
"""
|
| 110 |
+
Returns the nodes and edges on the path from node p to its ancestor w.
|
| 111 |
+
"""
|
| 112 |
+
Wn = [p]
|
| 113 |
+
We = []
|
| 114 |
+
while p != w:
|
| 115 |
+
We.append(self.parent_edge[p])
|
| 116 |
+
p = self.parent[p]
|
| 117 |
+
Wn.append(p)
|
| 118 |
+
return Wn, We
|
| 119 |
+
|
| 120 |
+
def find_cycle(self, i, p, q):
|
| 121 |
+
"""
|
| 122 |
+
Returns the nodes and edges on the cycle containing edge i == (p, q)
|
| 123 |
+
when the latter is added to the spanning tree.
|
| 124 |
+
|
| 125 |
+
The cycle is oriented in the direction from p to q.
|
| 126 |
+
"""
|
| 127 |
+
w = self.find_apex(p, q)
|
| 128 |
+
Wn, We = self.trace_path(p, w)
|
| 129 |
+
Wn.reverse()
|
| 130 |
+
We.reverse()
|
| 131 |
+
if We != [i]:
|
| 132 |
+
We.append(i)
|
| 133 |
+
WnR, WeR = self.trace_path(q, w)
|
| 134 |
+
del WnR[-1]
|
| 135 |
+
Wn += WnR
|
| 136 |
+
We += WeR
|
| 137 |
+
return Wn, We
|
| 138 |
+
|
| 139 |
+
def augment_flow(self, Wn, We, f):
|
| 140 |
+
"""
|
| 141 |
+
Augment f units of flow along a cycle represented by Wn and We.
|
| 142 |
+
"""
|
| 143 |
+
for i, p in zip(We, Wn):
|
| 144 |
+
if self.edge_sources[i] == p:
|
| 145 |
+
self.edge_flow[i] += f
|
| 146 |
+
else:
|
| 147 |
+
self.edge_flow[i] -= f
|
| 148 |
+
|
| 149 |
+
def trace_subtree(self, p):
|
| 150 |
+
"""
|
| 151 |
+
Yield the nodes in the subtree rooted at a node p.
|
| 152 |
+
"""
|
| 153 |
+
yield p
|
| 154 |
+
l = self.last_descendent_dft[p]
|
| 155 |
+
while p != l:
|
| 156 |
+
p = self.next_node_dft[p]
|
| 157 |
+
yield p
|
| 158 |
+
|
| 159 |
+
def remove_edge(self, s, t):
|
| 160 |
+
"""
|
| 161 |
+
Remove an edge (s, t) where parent[t] == s from the spanning tree.
|
| 162 |
+
"""
|
| 163 |
+
size_t = self.subtree_size[t]
|
| 164 |
+
prev_t = self.prev_node_dft[t]
|
| 165 |
+
last_t = self.last_descendent_dft[t]
|
| 166 |
+
next_last_t = self.next_node_dft[last_t]
|
| 167 |
+
# Remove (s, t).
|
| 168 |
+
self.parent[t] = None
|
| 169 |
+
self.parent_edge[t] = None
|
| 170 |
+
# Remove the subtree rooted at t from the depth-first thread.
|
| 171 |
+
self.next_node_dft[prev_t] = next_last_t
|
| 172 |
+
self.prev_node_dft[next_last_t] = prev_t
|
| 173 |
+
self.next_node_dft[last_t] = t
|
| 174 |
+
self.prev_node_dft[t] = last_t
|
| 175 |
+
# Update the subtree sizes and last descendants of the (old) ancestors
|
| 176 |
+
# of t.
|
| 177 |
+
while s is not None:
|
| 178 |
+
self.subtree_size[s] -= size_t
|
| 179 |
+
if self.last_descendent_dft[s] == last_t:
|
| 180 |
+
self.last_descendent_dft[s] = prev_t
|
| 181 |
+
s = self.parent[s]
|
| 182 |
+
|
| 183 |
+
def make_root(self, q):
|
| 184 |
+
"""
|
| 185 |
+
Make a node q the root of its containing subtree.
|
| 186 |
+
"""
|
| 187 |
+
ancestors = []
|
| 188 |
+
while q is not None:
|
| 189 |
+
ancestors.append(q)
|
| 190 |
+
q = self.parent[q]
|
| 191 |
+
ancestors.reverse()
|
| 192 |
+
for p, q in zip(ancestors, islice(ancestors, 1, None)):
|
| 193 |
+
size_p = self.subtree_size[p]
|
| 194 |
+
last_p = self.last_descendent_dft[p]
|
| 195 |
+
prev_q = self.prev_node_dft[q]
|
| 196 |
+
last_q = self.last_descendent_dft[q]
|
| 197 |
+
next_last_q = self.next_node_dft[last_q]
|
| 198 |
+
# Make p a child of q.
|
| 199 |
+
self.parent[p] = q
|
| 200 |
+
self.parent[q] = None
|
| 201 |
+
self.parent_edge[p] = self.parent_edge[q]
|
| 202 |
+
self.parent_edge[q] = None
|
| 203 |
+
self.subtree_size[p] = size_p - self.subtree_size[q]
|
| 204 |
+
self.subtree_size[q] = size_p
|
| 205 |
+
# Remove the subtree rooted at q from the depth-first thread.
|
| 206 |
+
self.next_node_dft[prev_q] = next_last_q
|
| 207 |
+
self.prev_node_dft[next_last_q] = prev_q
|
| 208 |
+
self.next_node_dft[last_q] = q
|
| 209 |
+
self.prev_node_dft[q] = last_q
|
| 210 |
+
if last_p == last_q:
|
| 211 |
+
self.last_descendent_dft[p] = prev_q
|
| 212 |
+
last_p = prev_q
|
| 213 |
+
# Add the remaining parts of the subtree rooted at p as a subtree
|
| 214 |
+
# of q in the depth-first thread.
|
| 215 |
+
self.prev_node_dft[p] = last_q
|
| 216 |
+
self.next_node_dft[last_q] = p
|
| 217 |
+
self.next_node_dft[last_p] = q
|
| 218 |
+
self.prev_node_dft[q] = last_p
|
| 219 |
+
self.last_descendent_dft[q] = last_p
|
| 220 |
+
|
| 221 |
+
def add_edge(self, i, p, q):
|
| 222 |
+
"""
|
| 223 |
+
Add an edge (p, q) to the spanning tree where q is the root of a subtree.
|
| 224 |
+
"""
|
| 225 |
+
last_p = self.last_descendent_dft[p]
|
| 226 |
+
next_last_p = self.next_node_dft[last_p]
|
| 227 |
+
size_q = self.subtree_size[q]
|
| 228 |
+
last_q = self.last_descendent_dft[q]
|
| 229 |
+
# Make q a child of p.
|
| 230 |
+
self.parent[q] = p
|
| 231 |
+
self.parent_edge[q] = i
|
| 232 |
+
# Insert the subtree rooted at q into the depth-first thread.
|
| 233 |
+
self.next_node_dft[last_p] = q
|
| 234 |
+
self.prev_node_dft[q] = last_p
|
| 235 |
+
self.prev_node_dft[next_last_p] = last_q
|
| 236 |
+
self.next_node_dft[last_q] = next_last_p
|
| 237 |
+
# Update the subtree sizes and last descendants of the (new) ancestors
|
| 238 |
+
# of q.
|
| 239 |
+
while p is not None:
|
| 240 |
+
self.subtree_size[p] += size_q
|
| 241 |
+
if self.last_descendent_dft[p] == last_p:
|
| 242 |
+
self.last_descendent_dft[p] = last_q
|
| 243 |
+
p = self.parent[p]
|
| 244 |
+
|
| 245 |
+
def update_potentials(self, i, p, q):
|
| 246 |
+
"""
|
| 247 |
+
Update the potentials of the nodes in the subtree rooted at a node
|
| 248 |
+
q connected to its parent p by an edge i.
|
| 249 |
+
"""
|
| 250 |
+
if q == self.edge_targets[i]:
|
| 251 |
+
d = self.node_potentials[p] - self.edge_weights[i] - self.node_potentials[q]
|
| 252 |
+
else:
|
| 253 |
+
d = self.node_potentials[p] + self.edge_weights[i] - self.node_potentials[q]
|
| 254 |
+
for q in self.trace_subtree(q):
|
| 255 |
+
self.node_potentials[q] += d
|
| 256 |
+
|
| 257 |
+
def reduced_cost(self, i):
|
| 258 |
+
"""Returns the reduced cost of an edge i."""
|
| 259 |
+
c = (
|
| 260 |
+
self.edge_weights[i]
|
| 261 |
+
- self.node_potentials[self.edge_sources[i]]
|
| 262 |
+
+ self.node_potentials[self.edge_targets[i]]
|
| 263 |
+
)
|
| 264 |
+
return c if self.edge_flow[i] == 0 else -c
|
| 265 |
+
|
| 266 |
+
def find_entering_edges(self):
|
| 267 |
+
"""Yield entering edges until none can be found."""
|
| 268 |
+
if self.edge_count == 0:
|
| 269 |
+
return
|
| 270 |
+
|
| 271 |
+
# Entering edges are found by combining Dantzig's rule and Bland's
|
| 272 |
+
# rule. The edges are cyclically grouped into blocks of size B. Within
|
| 273 |
+
# each block, Dantzig's rule is applied to find an entering edge. The
|
| 274 |
+
# blocks to search is determined following Bland's rule.
|
| 275 |
+
B = int(ceil(sqrt(self.edge_count))) # pivot block size
|
| 276 |
+
M = (self.edge_count + B - 1) // B # number of blocks needed to cover all edges
|
| 277 |
+
m = 0 # number of consecutive blocks without eligible
|
| 278 |
+
# entering edges
|
| 279 |
+
f = 0 # first edge in block
|
| 280 |
+
while m < M:
|
| 281 |
+
# Determine the next block of edges.
|
| 282 |
+
l = f + B
|
| 283 |
+
if l <= self.edge_count:
|
| 284 |
+
edges = range(f, l)
|
| 285 |
+
else:
|
| 286 |
+
l -= self.edge_count
|
| 287 |
+
edges = chain(range(f, self.edge_count), range(l))
|
| 288 |
+
f = l
|
| 289 |
+
# Find the first edge with the lowest reduced cost.
|
| 290 |
+
i = min(edges, key=self.reduced_cost)
|
| 291 |
+
c = self.reduced_cost(i)
|
| 292 |
+
if c >= 0:
|
| 293 |
+
# No entering edge found in the current block.
|
| 294 |
+
m += 1
|
| 295 |
+
else:
|
| 296 |
+
# Entering edge found.
|
| 297 |
+
if self.edge_flow[i] == 0:
|
| 298 |
+
p = self.edge_sources[i]
|
| 299 |
+
q = self.edge_targets[i]
|
| 300 |
+
else:
|
| 301 |
+
p = self.edge_targets[i]
|
| 302 |
+
q = self.edge_sources[i]
|
| 303 |
+
yield i, p, q
|
| 304 |
+
m = 0
|
| 305 |
+
# All edges have nonnegative reduced costs. The current flow is
|
| 306 |
+
# optimal.
|
| 307 |
+
|
| 308 |
+
def residual_capacity(self, i, p):
|
| 309 |
+
"""Returns the residual capacity of an edge i in the direction away
|
| 310 |
+
from its endpoint p.
|
| 311 |
+
"""
|
| 312 |
+
return (
|
| 313 |
+
self.edge_capacities[i] - self.edge_flow[i]
|
| 314 |
+
if self.edge_sources[i] == p
|
| 315 |
+
else self.edge_flow[i]
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
def find_leaving_edge(self, Wn, We):
|
| 319 |
+
"""Returns the leaving edge in a cycle represented by Wn and We."""
|
| 320 |
+
j, s = min(
|
| 321 |
+
zip(reversed(We), reversed(Wn)),
|
| 322 |
+
key=lambda i_p: self.residual_capacity(*i_p),
|
| 323 |
+
)
|
| 324 |
+
t = self.edge_targets[j] if self.edge_sources[j] == s else self.edge_sources[j]
|
| 325 |
+
return j, s, t
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
@not_implemented_for("undirected")
|
| 329 |
+
@nx._dispatchable(
|
| 330 |
+
node_attrs="demand", edge_attrs={"capacity": float("inf"), "weight": 0}
|
| 331 |
+
)
|
| 332 |
+
def network_simplex(G, demand="demand", capacity="capacity", weight="weight"):
|
| 333 |
+
r"""Find a minimum cost flow satisfying all demands in digraph G.
|
| 334 |
+
|
| 335 |
+
This is a primal network simplex algorithm that uses the leaving
|
| 336 |
+
arc rule to prevent cycling.
|
| 337 |
+
|
| 338 |
+
G is a digraph with edge costs and capacities and in which nodes
|
| 339 |
+
have demand, i.e., they want to send or receive some amount of
|
| 340 |
+
flow. A negative demand means that the node wants to send flow, a
|
| 341 |
+
positive demand means that the node want to receive flow. A flow on
|
| 342 |
+
the digraph G satisfies all demand if the net flow into each node
|
| 343 |
+
is equal to the demand of that node.
|
| 344 |
+
|
| 345 |
+
Parameters
|
| 346 |
+
----------
|
| 347 |
+
G : NetworkX graph
|
| 348 |
+
DiGraph on which a minimum cost flow satisfying all demands is
|
| 349 |
+
to be found.
|
| 350 |
+
|
| 351 |
+
demand : string
|
| 352 |
+
Nodes of the graph G are expected to have an attribute demand
|
| 353 |
+
that indicates how much flow a node wants to send (negative
|
| 354 |
+
demand) or receive (positive demand). Note that the sum of the
|
| 355 |
+
demands should be 0 otherwise the problem in not feasible. If
|
| 356 |
+
this attribute is not present, a node is considered to have 0
|
| 357 |
+
demand. Default value: 'demand'.
|
| 358 |
+
|
| 359 |
+
capacity : string
|
| 360 |
+
Edges of the graph G are expected to have an attribute capacity
|
| 361 |
+
that indicates how much flow the edge can support. If this
|
| 362 |
+
attribute is not present, the edge is considered to have
|
| 363 |
+
infinite capacity. Default value: 'capacity'.
|
| 364 |
+
|
| 365 |
+
weight : string
|
| 366 |
+
Edges of the graph G are expected to have an attribute weight
|
| 367 |
+
that indicates the cost incurred by sending one unit of flow on
|
| 368 |
+
that edge. If not present, the weight is considered to be 0.
|
| 369 |
+
Default value: 'weight'.
|
| 370 |
+
|
| 371 |
+
Returns
|
| 372 |
+
-------
|
| 373 |
+
flowCost : integer, float
|
| 374 |
+
Cost of a minimum cost flow satisfying all demands.
|
| 375 |
+
|
| 376 |
+
flowDict : dictionary
|
| 377 |
+
Dictionary of dictionaries keyed by nodes such that
|
| 378 |
+
flowDict[u][v] is the flow edge (u, v).
|
| 379 |
+
|
| 380 |
+
Raises
|
| 381 |
+
------
|
| 382 |
+
NetworkXError
|
| 383 |
+
This exception is raised if the input graph is not directed or
|
| 384 |
+
not connected.
|
| 385 |
+
|
| 386 |
+
NetworkXUnfeasible
|
| 387 |
+
This exception is raised in the following situations:
|
| 388 |
+
|
| 389 |
+
* The sum of the demands is not zero. Then, there is no
|
| 390 |
+
flow satisfying all demands.
|
| 391 |
+
* There is no flow satisfying all demand.
|
| 392 |
+
|
| 393 |
+
NetworkXUnbounded
|
| 394 |
+
This exception is raised if the digraph G has a cycle of
|
| 395 |
+
negative cost and infinite capacity. Then, the cost of a flow
|
| 396 |
+
satisfying all demands is unbounded below.
|
| 397 |
+
|
| 398 |
+
Notes
|
| 399 |
+
-----
|
| 400 |
+
This algorithm is not guaranteed to work if edge weights or demands
|
| 401 |
+
are floating point numbers (overflows and roundoff errors can
|
| 402 |
+
cause problems). As a workaround you can use integer numbers by
|
| 403 |
+
multiplying the relevant edge attributes by a convenient
|
| 404 |
+
constant factor (eg 100).
|
| 405 |
+
|
| 406 |
+
See also
|
| 407 |
+
--------
|
| 408 |
+
cost_of_flow, max_flow_min_cost, min_cost_flow, min_cost_flow_cost
|
| 409 |
+
|
| 410 |
+
Examples
|
| 411 |
+
--------
|
| 412 |
+
A simple example of a min cost flow problem.
|
| 413 |
+
|
| 414 |
+
>>> G = nx.DiGraph()
|
| 415 |
+
>>> G.add_node("a", demand=-5)
|
| 416 |
+
>>> G.add_node("d", demand=5)
|
| 417 |
+
>>> G.add_edge("a", "b", weight=3, capacity=4)
|
| 418 |
+
>>> G.add_edge("a", "c", weight=6, capacity=10)
|
| 419 |
+
>>> G.add_edge("b", "d", weight=1, capacity=9)
|
| 420 |
+
>>> G.add_edge("c", "d", weight=2, capacity=5)
|
| 421 |
+
>>> flowCost, flowDict = nx.network_simplex(G)
|
| 422 |
+
>>> flowCost
|
| 423 |
+
24
|
| 424 |
+
>>> flowDict
|
| 425 |
+
{'a': {'b': 4, 'c': 1}, 'd': {}, 'b': {'d': 4}, 'c': {'d': 1}}
|
| 426 |
+
|
| 427 |
+
The mincost flow algorithm can also be used to solve shortest path
|
| 428 |
+
problems. To find the shortest path between two nodes u and v,
|
| 429 |
+
give all edges an infinite capacity, give node u a demand of -1 and
|
| 430 |
+
node v a demand a 1. Then run the network simplex. The value of a
|
| 431 |
+
min cost flow will be the distance between u and v and edges
|
| 432 |
+
carrying positive flow will indicate the path.
|
| 433 |
+
|
| 434 |
+
>>> G = nx.DiGraph()
|
| 435 |
+
>>> G.add_weighted_edges_from(
|
| 436 |
+
... [
|
| 437 |
+
... ("s", "u", 10),
|
| 438 |
+
... ("s", "x", 5),
|
| 439 |
+
... ("u", "v", 1),
|
| 440 |
+
... ("u", "x", 2),
|
| 441 |
+
... ("v", "y", 1),
|
| 442 |
+
... ("x", "u", 3),
|
| 443 |
+
... ("x", "v", 5),
|
| 444 |
+
... ("x", "y", 2),
|
| 445 |
+
... ("y", "s", 7),
|
| 446 |
+
... ("y", "v", 6),
|
| 447 |
+
... ]
|
| 448 |
+
... )
|
| 449 |
+
>>> G.add_node("s", demand=-1)
|
| 450 |
+
>>> G.add_node("v", demand=1)
|
| 451 |
+
>>> flowCost, flowDict = nx.network_simplex(G)
|
| 452 |
+
>>> flowCost == nx.shortest_path_length(G, "s", "v", weight="weight")
|
| 453 |
+
True
|
| 454 |
+
>>> sorted([(u, v) for u in flowDict for v in flowDict[u] if flowDict[u][v] > 0])
|
| 455 |
+
[('s', 'x'), ('u', 'v'), ('x', 'u')]
|
| 456 |
+
>>> nx.shortest_path(G, "s", "v", weight="weight")
|
| 457 |
+
['s', 'x', 'u', 'v']
|
| 458 |
+
|
| 459 |
+
It is possible to change the name of the attributes used for the
|
| 460 |
+
algorithm.
|
| 461 |
+
|
| 462 |
+
>>> G = nx.DiGraph()
|
| 463 |
+
>>> G.add_node("p", spam=-4)
|
| 464 |
+
>>> G.add_node("q", spam=2)
|
| 465 |
+
>>> G.add_node("a", spam=-2)
|
| 466 |
+
>>> G.add_node("d", spam=-1)
|
| 467 |
+
>>> G.add_node("t", spam=2)
|
| 468 |
+
>>> G.add_node("w", spam=3)
|
| 469 |
+
>>> G.add_edge("p", "q", cost=7, vacancies=5)
|
| 470 |
+
>>> G.add_edge("p", "a", cost=1, vacancies=4)
|
| 471 |
+
>>> G.add_edge("q", "d", cost=2, vacancies=3)
|
| 472 |
+
>>> G.add_edge("t", "q", cost=1, vacancies=2)
|
| 473 |
+
>>> G.add_edge("a", "t", cost=2, vacancies=4)
|
| 474 |
+
>>> G.add_edge("d", "w", cost=3, vacancies=4)
|
| 475 |
+
>>> G.add_edge("t", "w", cost=4, vacancies=1)
|
| 476 |
+
>>> flowCost, flowDict = nx.network_simplex(
|
| 477 |
+
... G, demand="spam", capacity="vacancies", weight="cost"
|
| 478 |
+
... )
|
| 479 |
+
>>> flowCost
|
| 480 |
+
37
|
| 481 |
+
>>> flowDict
|
| 482 |
+
{'p': {'q': 2, 'a': 2}, 'q': {'d': 1}, 'a': {'t': 4}, 'd': {'w': 2}, 't': {'q': 1, 'w': 1}, 'w': {}}
|
| 483 |
+
|
| 484 |
+
References
|
| 485 |
+
----------
|
| 486 |
+
.. [1] Z. Kiraly, P. Kovacs.
|
| 487 |
+
Efficient implementation of minimum-cost flow algorithms.
|
| 488 |
+
Acta Universitatis Sapientiae, Informatica 4(1):67--118. 2012.
|
| 489 |
+
.. [2] R. Barr, F. Glover, D. Klingman.
|
| 490 |
+
Enhancement of spanning tree labeling procedures for network
|
| 491 |
+
optimization.
|
| 492 |
+
INFOR 17(1):16--34. 1979.
|
| 493 |
+
"""
|
| 494 |
+
###########################################################################
|
| 495 |
+
# Problem essentials extraction and sanity check
|
| 496 |
+
###########################################################################
|
| 497 |
+
|
| 498 |
+
if len(G) == 0:
|
| 499 |
+
raise nx.NetworkXError("graph has no nodes")
|
| 500 |
+
|
| 501 |
+
multigraph = G.is_multigraph()
|
| 502 |
+
|
| 503 |
+
# extracting data essential to problem
|
| 504 |
+
DEAF = _DataEssentialsAndFunctions(
|
| 505 |
+
G, multigraph, demand=demand, capacity=capacity, weight=weight
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
###########################################################################
|
| 509 |
+
# Quick Error Detection
|
| 510 |
+
###########################################################################
|
| 511 |
+
|
| 512 |
+
inf = float("inf")
|
| 513 |
+
for u, d in zip(DEAF.node_list, DEAF.node_demands):
|
| 514 |
+
if abs(d) == inf:
|
| 515 |
+
raise nx.NetworkXError(f"node {u!r} has infinite demand")
|
| 516 |
+
for e, w in zip(DEAF.edge_indices, DEAF.edge_weights):
|
| 517 |
+
if abs(w) == inf:
|
| 518 |
+
raise nx.NetworkXError(f"edge {e!r} has infinite weight")
|
| 519 |
+
if not multigraph:
|
| 520 |
+
edges = nx.selfloop_edges(G, data=True)
|
| 521 |
+
else:
|
| 522 |
+
edges = nx.selfloop_edges(G, data=True, keys=True)
|
| 523 |
+
for e in edges:
|
| 524 |
+
if abs(e[-1].get(weight, 0)) == inf:
|
| 525 |
+
raise nx.NetworkXError(f"edge {e[:-1]!r} has infinite weight")
|
| 526 |
+
|
| 527 |
+
###########################################################################
|
| 528 |
+
# Quick Infeasibility Detection
|
| 529 |
+
###########################################################################
|
| 530 |
+
|
| 531 |
+
if sum(DEAF.node_demands) != 0:
|
| 532 |
+
raise nx.NetworkXUnfeasible("total node demand is not zero")
|
| 533 |
+
for e, c in zip(DEAF.edge_indices, DEAF.edge_capacities):
|
| 534 |
+
if c < 0:
|
| 535 |
+
raise nx.NetworkXUnfeasible(f"edge {e!r} has negative capacity")
|
| 536 |
+
if not multigraph:
|
| 537 |
+
edges = nx.selfloop_edges(G, data=True)
|
| 538 |
+
else:
|
| 539 |
+
edges = nx.selfloop_edges(G, data=True, keys=True)
|
| 540 |
+
for e in edges:
|
| 541 |
+
if e[-1].get(capacity, inf) < 0:
|
| 542 |
+
raise nx.NetworkXUnfeasible(f"edge {e[:-1]!r} has negative capacity")
|
| 543 |
+
|
| 544 |
+
###########################################################################
|
| 545 |
+
# Initialization
|
| 546 |
+
###########################################################################
|
| 547 |
+
|
| 548 |
+
# Add a dummy node -1 and connect all existing nodes to it with infinite-
|
| 549 |
+
# capacity dummy edges. Node -1 will serve as the root of the
|
| 550 |
+
# spanning tree of the network simplex method. The new edges will used to
|
| 551 |
+
# trivially satisfy the node demands and create an initial strongly
|
| 552 |
+
# feasible spanning tree.
|
| 553 |
+
for i, d in enumerate(DEAF.node_demands):
|
| 554 |
+
# Must be greater-than here. Zero-demand nodes must have
|
| 555 |
+
# edges pointing towards the root to ensure strong feasibility.
|
| 556 |
+
if d > 0:
|
| 557 |
+
DEAF.edge_sources.append(-1)
|
| 558 |
+
DEAF.edge_targets.append(i)
|
| 559 |
+
else:
|
| 560 |
+
DEAF.edge_sources.append(i)
|
| 561 |
+
DEAF.edge_targets.append(-1)
|
| 562 |
+
faux_inf = (
|
| 563 |
+
3
|
| 564 |
+
* max(
|
| 565 |
+
chain(
|
| 566 |
+
[
|
| 567 |
+
sum(c for c in DEAF.edge_capacities if c < inf),
|
| 568 |
+
sum(abs(w) for w in DEAF.edge_weights),
|
| 569 |
+
],
|
| 570 |
+
(abs(d) for d in DEAF.node_demands),
|
| 571 |
+
)
|
| 572 |
+
)
|
| 573 |
+
or 1
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
n = len(DEAF.node_list) # number of nodes
|
| 577 |
+
DEAF.edge_weights.extend(repeat(faux_inf, n))
|
| 578 |
+
DEAF.edge_capacities.extend(repeat(faux_inf, n))
|
| 579 |
+
|
| 580 |
+
# Construct the initial spanning tree.
|
| 581 |
+
DEAF.initialize_spanning_tree(n, faux_inf)
|
| 582 |
+
|
| 583 |
+
###########################################################################
|
| 584 |
+
# Pivot loop
|
| 585 |
+
###########################################################################
|
| 586 |
+
|
| 587 |
+
for i, p, q in DEAF.find_entering_edges():
|
| 588 |
+
Wn, We = DEAF.find_cycle(i, p, q)
|
| 589 |
+
j, s, t = DEAF.find_leaving_edge(Wn, We)
|
| 590 |
+
DEAF.augment_flow(Wn, We, DEAF.residual_capacity(j, s))
|
| 591 |
+
# Do nothing more if the entering edge is the same as the leaving edge.
|
| 592 |
+
if i != j:
|
| 593 |
+
if DEAF.parent[t] != s:
|
| 594 |
+
# Ensure that s is the parent of t.
|
| 595 |
+
s, t = t, s
|
| 596 |
+
if We.index(i) > We.index(j):
|
| 597 |
+
# Ensure that q is in the subtree rooted at t.
|
| 598 |
+
p, q = q, p
|
| 599 |
+
DEAF.remove_edge(s, t)
|
| 600 |
+
DEAF.make_root(q)
|
| 601 |
+
DEAF.add_edge(i, p, q)
|
| 602 |
+
DEAF.update_potentials(i, p, q)
|
| 603 |
+
|
| 604 |
+
###########################################################################
|
| 605 |
+
# Infeasibility and unboundedness detection
|
| 606 |
+
###########################################################################
|
| 607 |
+
|
| 608 |
+
if any(DEAF.edge_flow[i] != 0 for i in range(-n, 0)):
|
| 609 |
+
raise nx.NetworkXUnfeasible("no flow satisfies all node demands")
|
| 610 |
+
|
| 611 |
+
if any(DEAF.edge_flow[i] * 2 >= faux_inf for i in range(DEAF.edge_count)) or any(
|
| 612 |
+
e[-1].get(capacity, inf) == inf and e[-1].get(weight, 0) < 0
|
| 613 |
+
for e in nx.selfloop_edges(G, data=True)
|
| 614 |
+
):
|
| 615 |
+
raise nx.NetworkXUnbounded("negative cycle with infinite capacity found")
|
| 616 |
+
|
| 617 |
+
###########################################################################
|
| 618 |
+
# Flow cost calculation and flow dict construction
|
| 619 |
+
###########################################################################
|
| 620 |
+
|
| 621 |
+
del DEAF.edge_flow[DEAF.edge_count :]
|
| 622 |
+
flow_cost = sum(w * x for w, x in zip(DEAF.edge_weights, DEAF.edge_flow))
|
| 623 |
+
flow_dict = {n: {} for n in DEAF.node_list}
|
| 624 |
+
|
| 625 |
+
def add_entry(e):
|
| 626 |
+
"""Add a flow dict entry."""
|
| 627 |
+
d = flow_dict[e[0]]
|
| 628 |
+
for k in e[1:-2]:
|
| 629 |
+
try:
|
| 630 |
+
d = d[k]
|
| 631 |
+
except KeyError:
|
| 632 |
+
t = {}
|
| 633 |
+
d[k] = t
|
| 634 |
+
d = t
|
| 635 |
+
d[e[-2]] = e[-1]
|
| 636 |
+
|
| 637 |
+
DEAF.edge_sources = (
|
| 638 |
+
DEAF.node_list[s] for s in DEAF.edge_sources
|
| 639 |
+
) # Use original nodes.
|
| 640 |
+
DEAF.edge_targets = (
|
| 641 |
+
DEAF.node_list[t] for t in DEAF.edge_targets
|
| 642 |
+
) # Use original nodes.
|
| 643 |
+
if not multigraph:
|
| 644 |
+
for e in zip(DEAF.edge_sources, DEAF.edge_targets, DEAF.edge_flow):
|
| 645 |
+
add_entry(e)
|
| 646 |
+
edges = G.edges(data=True)
|
| 647 |
+
else:
|
| 648 |
+
for e in zip(
|
| 649 |
+
DEAF.edge_sources, DEAF.edge_targets, DEAF.edge_keys, DEAF.edge_flow
|
| 650 |
+
):
|
| 651 |
+
add_entry(e)
|
| 652 |
+
edges = G.edges(data=True, keys=True)
|
| 653 |
+
for e in edges:
|
| 654 |
+
if e[0] != e[1]:
|
| 655 |
+
if e[-1].get(capacity, inf) == 0:
|
| 656 |
+
add_entry(e[:-1] + (0,))
|
| 657 |
+
else:
|
| 658 |
+
w = e[-1].get(weight, 0)
|
| 659 |
+
if w >= 0:
|
| 660 |
+
add_entry(e[:-1] + (0,))
|
| 661 |
+
else:
|
| 662 |
+
c = e[-1][capacity]
|
| 663 |
+
flow_cost += w * c
|
| 664 |
+
add_entry(e[:-1] + (c,))
|
| 665 |
+
|
| 666 |
+
return flow_cost, flow_dict
|
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/preflowpush.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Highest-label preflow-push algorithm for maximum flow problems.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from collections import deque
|
| 6 |
+
from itertools import islice
|
| 7 |
+
|
| 8 |
+
import networkx as nx
|
| 9 |
+
|
| 10 |
+
from ...utils import arbitrary_element
|
| 11 |
+
from .utils import (
|
| 12 |
+
CurrentEdge,
|
| 13 |
+
GlobalRelabelThreshold,
|
| 14 |
+
Level,
|
| 15 |
+
build_residual_network,
|
| 16 |
+
detect_unboundedness,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
__all__ = ["preflow_push"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def preflow_push_impl(G, s, t, capacity, residual, global_relabel_freq, value_only):
|
| 23 |
+
"""Implementation of the highest-label preflow-push algorithm."""
|
| 24 |
+
if s not in G:
|
| 25 |
+
raise nx.NetworkXError(f"node {str(s)} not in graph")
|
| 26 |
+
if t not in G:
|
| 27 |
+
raise nx.NetworkXError(f"node {str(t)} not in graph")
|
| 28 |
+
if s == t:
|
| 29 |
+
raise nx.NetworkXError("source and sink are the same node")
|
| 30 |
+
|
| 31 |
+
if global_relabel_freq is None:
|
| 32 |
+
global_relabel_freq = 0
|
| 33 |
+
if global_relabel_freq < 0:
|
| 34 |
+
raise nx.NetworkXError("global_relabel_freq must be nonnegative.")
|
| 35 |
+
|
| 36 |
+
if residual is None:
|
| 37 |
+
R = build_residual_network(G, capacity)
|
| 38 |
+
else:
|
| 39 |
+
R = residual
|
| 40 |
+
|
| 41 |
+
detect_unboundedness(R, s, t)
|
| 42 |
+
|
| 43 |
+
R_nodes = R.nodes
|
| 44 |
+
R_pred = R.pred
|
| 45 |
+
R_succ = R.succ
|
| 46 |
+
|
| 47 |
+
# Initialize/reset the residual network.
|
| 48 |
+
for u in R:
|
| 49 |
+
R_nodes[u]["excess"] = 0
|
| 50 |
+
for e in R_succ[u].values():
|
| 51 |
+
e["flow"] = 0
|
| 52 |
+
|
| 53 |
+
def reverse_bfs(src):
|
| 54 |
+
"""Perform a reverse breadth-first search from src in the residual
|
| 55 |
+
network.
|
| 56 |
+
"""
|
| 57 |
+
heights = {src: 0}
|
| 58 |
+
q = deque([(src, 0)])
|
| 59 |
+
while q:
|
| 60 |
+
u, height = q.popleft()
|
| 61 |
+
height += 1
|
| 62 |
+
for v, attr in R_pred[u].items():
|
| 63 |
+
if v not in heights and attr["flow"] < attr["capacity"]:
|
| 64 |
+
heights[v] = height
|
| 65 |
+
q.append((v, height))
|
| 66 |
+
return heights
|
| 67 |
+
|
| 68 |
+
# Initialize heights of the nodes.
|
| 69 |
+
heights = reverse_bfs(t)
|
| 70 |
+
|
| 71 |
+
if s not in heights:
|
| 72 |
+
# t is not reachable from s in the residual network. The maximum flow
|
| 73 |
+
# must be zero.
|
| 74 |
+
R.graph["flow_value"] = 0
|
| 75 |
+
return R
|
| 76 |
+
|
| 77 |
+
n = len(R)
|
| 78 |
+
# max_height represents the height of the highest level below level n with
|
| 79 |
+
# at least one active node.
|
| 80 |
+
max_height = max(heights[u] for u in heights if u != s)
|
| 81 |
+
heights[s] = n
|
| 82 |
+
|
| 83 |
+
grt = GlobalRelabelThreshold(n, R.size(), global_relabel_freq)
|
| 84 |
+
|
| 85 |
+
# Initialize heights and 'current edge' data structures of the nodes.
|
| 86 |
+
for u in R:
|
| 87 |
+
R_nodes[u]["height"] = heights[u] if u in heights else n + 1
|
| 88 |
+
R_nodes[u]["curr_edge"] = CurrentEdge(R_succ[u])
|
| 89 |
+
|
| 90 |
+
def push(u, v, flow):
|
| 91 |
+
"""Push flow units of flow from u to v."""
|
| 92 |
+
R_succ[u][v]["flow"] += flow
|
| 93 |
+
R_succ[v][u]["flow"] -= flow
|
| 94 |
+
R_nodes[u]["excess"] -= flow
|
| 95 |
+
R_nodes[v]["excess"] += flow
|
| 96 |
+
|
| 97 |
+
# The maximum flow must be nonzero now. Initialize the preflow by
|
| 98 |
+
# saturating all edges emanating from s.
|
| 99 |
+
for u, attr in R_succ[s].items():
|
| 100 |
+
flow = attr["capacity"]
|
| 101 |
+
if flow > 0:
|
| 102 |
+
push(s, u, flow)
|
| 103 |
+
|
| 104 |
+
# Partition nodes into levels.
|
| 105 |
+
levels = [Level() for i in range(2 * n)]
|
| 106 |
+
for u in R:
|
| 107 |
+
if u != s and u != t:
|
| 108 |
+
level = levels[R_nodes[u]["height"]]
|
| 109 |
+
if R_nodes[u]["excess"] > 0:
|
| 110 |
+
level.active.add(u)
|
| 111 |
+
else:
|
| 112 |
+
level.inactive.add(u)
|
| 113 |
+
|
| 114 |
+
def activate(v):
|
| 115 |
+
"""Move a node from the inactive set to the active set of its level."""
|
| 116 |
+
if v != s and v != t:
|
| 117 |
+
level = levels[R_nodes[v]["height"]]
|
| 118 |
+
if v in level.inactive:
|
| 119 |
+
level.inactive.remove(v)
|
| 120 |
+
level.active.add(v)
|
| 121 |
+
|
| 122 |
+
def relabel(u):
|
| 123 |
+
"""Relabel a node to create an admissible edge."""
|
| 124 |
+
grt.add_work(len(R_succ[u]))
|
| 125 |
+
return (
|
| 126 |
+
min(
|
| 127 |
+
R_nodes[v]["height"]
|
| 128 |
+
for v, attr in R_succ[u].items()
|
| 129 |
+
if attr["flow"] < attr["capacity"]
|
| 130 |
+
)
|
| 131 |
+
+ 1
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
def discharge(u, is_phase1):
|
| 135 |
+
"""Discharge a node until it becomes inactive or, during phase 1 (see
|
| 136 |
+
below), its height reaches at least n. The node is known to have the
|
| 137 |
+
largest height among active nodes.
|
| 138 |
+
"""
|
| 139 |
+
height = R_nodes[u]["height"]
|
| 140 |
+
curr_edge = R_nodes[u]["curr_edge"]
|
| 141 |
+
# next_height represents the next height to examine after discharging
|
| 142 |
+
# the current node. During phase 1, it is capped to below n.
|
| 143 |
+
next_height = height
|
| 144 |
+
levels[height].active.remove(u)
|
| 145 |
+
while True:
|
| 146 |
+
v, attr = curr_edge.get()
|
| 147 |
+
if height == R_nodes[v]["height"] + 1 and attr["flow"] < attr["capacity"]:
|
| 148 |
+
flow = min(R_nodes[u]["excess"], attr["capacity"] - attr["flow"])
|
| 149 |
+
push(u, v, flow)
|
| 150 |
+
activate(v)
|
| 151 |
+
if R_nodes[u]["excess"] == 0:
|
| 152 |
+
# The node has become inactive.
|
| 153 |
+
levels[height].inactive.add(u)
|
| 154 |
+
break
|
| 155 |
+
try:
|
| 156 |
+
curr_edge.move_to_next()
|
| 157 |
+
except StopIteration:
|
| 158 |
+
# We have run off the end of the adjacency list, and there can
|
| 159 |
+
# be no more admissible edges. Relabel the node to create one.
|
| 160 |
+
height = relabel(u)
|
| 161 |
+
if is_phase1 and height >= n - 1:
|
| 162 |
+
# Although the node is still active, with a height at least
|
| 163 |
+
# n - 1, it is now known to be on the s side of the minimum
|
| 164 |
+
# s-t cut. Stop processing it until phase 2.
|
| 165 |
+
levels[height].active.add(u)
|
| 166 |
+
break
|
| 167 |
+
# The first relabel operation after global relabeling may not
|
| 168 |
+
# increase the height of the node since the 'current edge' data
|
| 169 |
+
# structure is not rewound. Use height instead of (height - 1)
|
| 170 |
+
# in case other active nodes at the same level are missed.
|
| 171 |
+
next_height = height
|
| 172 |
+
R_nodes[u]["height"] = height
|
| 173 |
+
return next_height
|
| 174 |
+
|
| 175 |
+
def gap_heuristic(height):
|
| 176 |
+
"""Apply the gap heuristic."""
|
| 177 |
+
# Move all nodes at levels (height + 1) to max_height to level n + 1.
|
| 178 |
+
for level in islice(levels, height + 1, max_height + 1):
|
| 179 |
+
for u in level.active:
|
| 180 |
+
R_nodes[u]["height"] = n + 1
|
| 181 |
+
for u in level.inactive:
|
| 182 |
+
R_nodes[u]["height"] = n + 1
|
| 183 |
+
levels[n + 1].active.update(level.active)
|
| 184 |
+
level.active.clear()
|
| 185 |
+
levels[n + 1].inactive.update(level.inactive)
|
| 186 |
+
level.inactive.clear()
|
| 187 |
+
|
| 188 |
+
def global_relabel(from_sink):
|
| 189 |
+
"""Apply the global relabeling heuristic."""
|
| 190 |
+
src = t if from_sink else s
|
| 191 |
+
heights = reverse_bfs(src)
|
| 192 |
+
if not from_sink:
|
| 193 |
+
# s must be reachable from t. Remove t explicitly.
|
| 194 |
+
del heights[t]
|
| 195 |
+
max_height = max(heights.values())
|
| 196 |
+
if from_sink:
|
| 197 |
+
# Also mark nodes from which t is unreachable for relabeling. This
|
| 198 |
+
# serves the same purpose as the gap heuristic.
|
| 199 |
+
for u in R:
|
| 200 |
+
if u not in heights and R_nodes[u]["height"] < n:
|
| 201 |
+
heights[u] = n + 1
|
| 202 |
+
else:
|
| 203 |
+
# Shift the computed heights because the height of s is n.
|
| 204 |
+
for u in heights:
|
| 205 |
+
heights[u] += n
|
| 206 |
+
max_height += n
|
| 207 |
+
del heights[src]
|
| 208 |
+
for u, new_height in heights.items():
|
| 209 |
+
old_height = R_nodes[u]["height"]
|
| 210 |
+
if new_height != old_height:
|
| 211 |
+
if u in levels[old_height].active:
|
| 212 |
+
levels[old_height].active.remove(u)
|
| 213 |
+
levels[new_height].active.add(u)
|
| 214 |
+
else:
|
| 215 |
+
levels[old_height].inactive.remove(u)
|
| 216 |
+
levels[new_height].inactive.add(u)
|
| 217 |
+
R_nodes[u]["height"] = new_height
|
| 218 |
+
return max_height
|
| 219 |
+
|
| 220 |
+
# Phase 1: Find the maximum preflow by pushing as much flow as possible to
|
| 221 |
+
# t.
|
| 222 |
+
|
| 223 |
+
height = max_height
|
| 224 |
+
while height > 0:
|
| 225 |
+
# Discharge active nodes in the current level.
|
| 226 |
+
while True:
|
| 227 |
+
level = levels[height]
|
| 228 |
+
if not level.active:
|
| 229 |
+
# All active nodes in the current level have been discharged.
|
| 230 |
+
# Move to the next lower level.
|
| 231 |
+
height -= 1
|
| 232 |
+
break
|
| 233 |
+
# Record the old height and level for the gap heuristic.
|
| 234 |
+
old_height = height
|
| 235 |
+
old_level = level
|
| 236 |
+
u = arbitrary_element(level.active)
|
| 237 |
+
height = discharge(u, True)
|
| 238 |
+
if grt.is_reached():
|
| 239 |
+
# Global relabeling heuristic: Recompute the exact heights of
|
| 240 |
+
# all nodes.
|
| 241 |
+
height = global_relabel(True)
|
| 242 |
+
max_height = height
|
| 243 |
+
grt.clear_work()
|
| 244 |
+
elif not old_level.active and not old_level.inactive:
|
| 245 |
+
# Gap heuristic: If the level at old_height is empty (a 'gap'),
|
| 246 |
+
# a minimum cut has been identified. All nodes with heights
|
| 247 |
+
# above old_height can have their heights set to n + 1 and not
|
| 248 |
+
# be further processed before a maximum preflow is found.
|
| 249 |
+
gap_heuristic(old_height)
|
| 250 |
+
height = old_height - 1
|
| 251 |
+
max_height = height
|
| 252 |
+
else:
|
| 253 |
+
# Update the height of the highest level with at least one
|
| 254 |
+
# active node.
|
| 255 |
+
max_height = max(max_height, height)
|
| 256 |
+
|
| 257 |
+
# A maximum preflow has been found. The excess at t is the maximum flow
|
| 258 |
+
# value.
|
| 259 |
+
if value_only:
|
| 260 |
+
R.graph["flow_value"] = R_nodes[t]["excess"]
|
| 261 |
+
return R
|
| 262 |
+
|
| 263 |
+
# Phase 2: Convert the maximum preflow into a maximum flow by returning the
|
| 264 |
+
# excess to s.
|
| 265 |
+
|
| 266 |
+
# Relabel all nodes so that they have accurate heights.
|
| 267 |
+
height = global_relabel(False)
|
| 268 |
+
grt.clear_work()
|
| 269 |
+
|
| 270 |
+
# Continue to discharge the active nodes.
|
| 271 |
+
while height > n:
|
| 272 |
+
# Discharge active nodes in the current level.
|
| 273 |
+
while True:
|
| 274 |
+
level = levels[height]
|
| 275 |
+
if not level.active:
|
| 276 |
+
# All active nodes in the current level have been discharged.
|
| 277 |
+
# Move to the next lower level.
|
| 278 |
+
height -= 1
|
| 279 |
+
break
|
| 280 |
+
u = arbitrary_element(level.active)
|
| 281 |
+
height = discharge(u, False)
|
| 282 |
+
if grt.is_reached():
|
| 283 |
+
# Global relabeling heuristic.
|
| 284 |
+
height = global_relabel(False)
|
| 285 |
+
grt.clear_work()
|
| 286 |
+
|
| 287 |
+
R.graph["flow_value"] = R_nodes[t]["excess"]
|
| 288 |
+
return R
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True)
|
| 292 |
+
def preflow_push(
|
| 293 |
+
G, s, t, capacity="capacity", residual=None, global_relabel_freq=1, value_only=False
|
| 294 |
+
):
|
| 295 |
+
r"""Find a maximum single-commodity flow using the highest-label
|
| 296 |
+
preflow-push algorithm.
|
| 297 |
+
|
| 298 |
+
This function returns the residual network resulting after computing
|
| 299 |
+
the maximum flow. See below for details about the conventions
|
| 300 |
+
NetworkX uses for defining residual networks.
|
| 301 |
+
|
| 302 |
+
This algorithm has a running time of $O(n^2 \sqrt{m})$ for $n$ nodes and
|
| 303 |
+
$m$ edges.
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
Parameters
|
| 307 |
+
----------
|
| 308 |
+
G : NetworkX graph
|
| 309 |
+
Edges of the graph are expected to have an attribute called
|
| 310 |
+
'capacity'. If this attribute is not present, the edge is
|
| 311 |
+
considered to have infinite capacity.
|
| 312 |
+
|
| 313 |
+
s : node
|
| 314 |
+
Source node for the flow.
|
| 315 |
+
|
| 316 |
+
t : node
|
| 317 |
+
Sink node for the flow.
|
| 318 |
+
|
| 319 |
+
capacity : string
|
| 320 |
+
Edges of the graph G are expected to have an attribute capacity
|
| 321 |
+
that indicates how much flow the edge can support. If this
|
| 322 |
+
attribute is not present, the edge is considered to have
|
| 323 |
+
infinite capacity. Default value: 'capacity'.
|
| 324 |
+
|
| 325 |
+
residual : NetworkX graph
|
| 326 |
+
Residual network on which the algorithm is to be executed. If None, a
|
| 327 |
+
new residual network is created. Default value: None.
|
| 328 |
+
|
| 329 |
+
global_relabel_freq : integer, float
|
| 330 |
+
Relative frequency of applying the global relabeling heuristic to speed
|
| 331 |
+
up the algorithm. If it is None, the heuristic is disabled. Default
|
| 332 |
+
value: 1.
|
| 333 |
+
|
| 334 |
+
value_only : bool
|
| 335 |
+
If False, compute a maximum flow; otherwise, compute a maximum preflow
|
| 336 |
+
which is enough for computing the maximum flow value. Default value:
|
| 337 |
+
False.
|
| 338 |
+
|
| 339 |
+
Returns
|
| 340 |
+
-------
|
| 341 |
+
R : NetworkX DiGraph
|
| 342 |
+
Residual network after computing the maximum flow.
|
| 343 |
+
|
| 344 |
+
Raises
|
| 345 |
+
------
|
| 346 |
+
NetworkXError
|
| 347 |
+
The algorithm does not support MultiGraph and MultiDiGraph. If
|
| 348 |
+
the input graph is an instance of one of these two classes, a
|
| 349 |
+
NetworkXError is raised.
|
| 350 |
+
|
| 351 |
+
NetworkXUnbounded
|
| 352 |
+
If the graph has a path of infinite capacity, the value of a
|
| 353 |
+
feasible flow on the graph is unbounded above and the function
|
| 354 |
+
raises a NetworkXUnbounded.
|
| 355 |
+
|
| 356 |
+
See also
|
| 357 |
+
--------
|
| 358 |
+
:meth:`maximum_flow`
|
| 359 |
+
:meth:`minimum_cut`
|
| 360 |
+
:meth:`edmonds_karp`
|
| 361 |
+
:meth:`shortest_augmenting_path`
|
| 362 |
+
|
| 363 |
+
Notes
|
| 364 |
+
-----
|
| 365 |
+
The residual network :samp:`R` from an input graph :samp:`G` has the
|
| 366 |
+
same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
|
| 367 |
+
of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
|
| 368 |
+
self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
|
| 369 |
+
in :samp:`G`. For each node :samp:`u` in :samp:`R`,
|
| 370 |
+
:samp:`R.nodes[u]['excess']` represents the difference between flow into
|
| 371 |
+
:samp:`u` and flow out of :samp:`u`.
|
| 372 |
+
|
| 373 |
+
For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
|
| 374 |
+
is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
|
| 375 |
+
in :samp:`G` or zero otherwise. If the capacity is infinite,
|
| 376 |
+
:samp:`R[u][v]['capacity']` will have a high arbitrary finite value
|
| 377 |
+
that does not affect the solution of the problem. This value is stored in
|
| 378 |
+
:samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
|
| 379 |
+
:samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
|
| 380 |
+
satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
|
| 381 |
+
|
| 382 |
+
The flow value, defined as the total flow into :samp:`t`, the sink, is
|
| 383 |
+
stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using
|
| 384 |
+
only edges :samp:`(u, v)` such that
|
| 385 |
+
:samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
|
| 386 |
+
:samp:`s`-:samp:`t` cut.
|
| 387 |
+
|
| 388 |
+
Examples
|
| 389 |
+
--------
|
| 390 |
+
>>> from networkx.algorithms.flow import preflow_push
|
| 391 |
+
|
| 392 |
+
The functions that implement flow algorithms and output a residual
|
| 393 |
+
network, such as this one, are not imported to the base NetworkX
|
| 394 |
+
namespace, so you have to explicitly import them from the flow package.
|
| 395 |
+
|
| 396 |
+
>>> G = nx.DiGraph()
|
| 397 |
+
>>> G.add_edge("x", "a", capacity=3.0)
|
| 398 |
+
>>> G.add_edge("x", "b", capacity=1.0)
|
| 399 |
+
>>> G.add_edge("a", "c", capacity=3.0)
|
| 400 |
+
>>> G.add_edge("b", "c", capacity=5.0)
|
| 401 |
+
>>> G.add_edge("b", "d", capacity=4.0)
|
| 402 |
+
>>> G.add_edge("d", "e", capacity=2.0)
|
| 403 |
+
>>> G.add_edge("c", "y", capacity=2.0)
|
| 404 |
+
>>> G.add_edge("e", "y", capacity=3.0)
|
| 405 |
+
>>> R = preflow_push(G, "x", "y")
|
| 406 |
+
>>> flow_value = nx.maximum_flow_value(G, "x", "y")
|
| 407 |
+
>>> flow_value == R.graph["flow_value"]
|
| 408 |
+
True
|
| 409 |
+
>>> # preflow_push also stores the maximum flow value
|
| 410 |
+
>>> # in the excess attribute of the sink node t
|
| 411 |
+
>>> flow_value == R.nodes["y"]["excess"]
|
| 412 |
+
True
|
| 413 |
+
>>> # For some problems, you might only want to compute a
|
| 414 |
+
>>> # maximum preflow.
|
| 415 |
+
>>> R = preflow_push(G, "x", "y", value_only=True)
|
| 416 |
+
>>> flow_value == R.graph["flow_value"]
|
| 417 |
+
True
|
| 418 |
+
>>> flow_value == R.nodes["y"]["excess"]
|
| 419 |
+
True
|
| 420 |
+
|
| 421 |
+
"""
|
| 422 |
+
R = preflow_push_impl(G, s, t, capacity, residual, global_relabel_freq, value_only)
|
| 423 |
+
R.graph["algorithm"] = "preflow_push"
|
| 424 |
+
nx._clear_cache(R)
|
| 425 |
+
return R
|
.venv/lib/python3.11/site-packages/networkx/algorithms/graph_hashing.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Functions for hashing graphs to strings.
|
| 3 |
+
Isomorphic graphs should be assigned identical hashes.
|
| 4 |
+
For now, only Weisfeiler-Lehman hashing is implemented.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from collections import Counter, defaultdict
|
| 8 |
+
from hashlib import blake2b
|
| 9 |
+
|
| 10 |
+
import networkx as nx
|
| 11 |
+
|
| 12 |
+
__all__ = ["weisfeiler_lehman_graph_hash", "weisfeiler_lehman_subgraph_hashes"]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _hash_label(label, digest_size):
|
| 16 |
+
return blake2b(label.encode("ascii"), digest_size=digest_size).hexdigest()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _init_node_labels(G, edge_attr, node_attr):
|
| 20 |
+
if node_attr:
|
| 21 |
+
return {u: str(dd[node_attr]) for u, dd in G.nodes(data=True)}
|
| 22 |
+
elif edge_attr:
|
| 23 |
+
return {u: "" for u in G}
|
| 24 |
+
else:
|
| 25 |
+
return {u: str(deg) for u, deg in G.degree()}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _neighborhood_aggregate(G, node, node_labels, edge_attr=None):
|
| 29 |
+
"""
|
| 30 |
+
Compute new labels for given node by aggregating
|
| 31 |
+
the labels of each node's neighbors.
|
| 32 |
+
"""
|
| 33 |
+
label_list = []
|
| 34 |
+
for nbr in G.neighbors(node):
|
| 35 |
+
prefix = "" if edge_attr is None else str(G[node][nbr][edge_attr])
|
| 36 |
+
label_list.append(prefix + node_labels[nbr])
|
| 37 |
+
return node_labels[node] + "".join(sorted(label_list))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@nx.utils.not_implemented_for("multigraph")
|
| 41 |
+
@nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr")
|
| 42 |
+
def weisfeiler_lehman_graph_hash(
|
| 43 |
+
G, edge_attr=None, node_attr=None, iterations=3, digest_size=16
|
| 44 |
+
):
|
| 45 |
+
"""Return Weisfeiler Lehman (WL) graph hash.
|
| 46 |
+
|
| 47 |
+
The function iteratively aggregates and hashes neighborhoods of each node.
|
| 48 |
+
After each node's neighbors are hashed to obtain updated node labels,
|
| 49 |
+
a hashed histogram of resulting labels is returned as the final hash.
|
| 50 |
+
|
| 51 |
+
Hashes are identical for isomorphic graphs and strong guarantees that
|
| 52 |
+
non-isomorphic graphs will get different hashes. See [1]_ for details.
|
| 53 |
+
|
| 54 |
+
If no node or edge attributes are provided, the degree of each node
|
| 55 |
+
is used as its initial label.
|
| 56 |
+
Otherwise, node and/or edge labels are used to compute the hash.
|
| 57 |
+
|
| 58 |
+
Parameters
|
| 59 |
+
----------
|
| 60 |
+
G : graph
|
| 61 |
+
The graph to be hashed.
|
| 62 |
+
Can have node and/or edge attributes. Can also have no attributes.
|
| 63 |
+
edge_attr : string, optional (default=None)
|
| 64 |
+
The key in edge attribute dictionary to be used for hashing.
|
| 65 |
+
If None, edge labels are ignored.
|
| 66 |
+
node_attr: string, optional (default=None)
|
| 67 |
+
The key in node attribute dictionary to be used for hashing.
|
| 68 |
+
If None, and no edge_attr given, use the degrees of the nodes as labels.
|
| 69 |
+
iterations: int, optional (default=3)
|
| 70 |
+
Number of neighbor aggregations to perform.
|
| 71 |
+
Should be larger for larger graphs.
|
| 72 |
+
digest_size: int, optional (default=16)
|
| 73 |
+
Size (in bits) of blake2b hash digest to use for hashing node labels.
|
| 74 |
+
|
| 75 |
+
Returns
|
| 76 |
+
-------
|
| 77 |
+
h : string
|
| 78 |
+
Hexadecimal string corresponding to hash of the input graph.
|
| 79 |
+
|
| 80 |
+
Examples
|
| 81 |
+
--------
|
| 82 |
+
Two graphs with edge attributes that are isomorphic, except for
|
| 83 |
+
differences in the edge labels.
|
| 84 |
+
|
| 85 |
+
>>> G1 = nx.Graph()
|
| 86 |
+
>>> G1.add_edges_from(
|
| 87 |
+
... [
|
| 88 |
+
... (1, 2, {"label": "A"}),
|
| 89 |
+
... (2, 3, {"label": "A"}),
|
| 90 |
+
... (3, 1, {"label": "A"}),
|
| 91 |
+
... (1, 4, {"label": "B"}),
|
| 92 |
+
... ]
|
| 93 |
+
... )
|
| 94 |
+
>>> G2 = nx.Graph()
|
| 95 |
+
>>> G2.add_edges_from(
|
| 96 |
+
... [
|
| 97 |
+
... (5, 6, {"label": "B"}),
|
| 98 |
+
... (6, 7, {"label": "A"}),
|
| 99 |
+
... (7, 5, {"label": "A"}),
|
| 100 |
+
... (7, 8, {"label": "A"}),
|
| 101 |
+
... ]
|
| 102 |
+
... )
|
| 103 |
+
|
| 104 |
+
Omitting the `edge_attr` option, results in identical hashes.
|
| 105 |
+
|
| 106 |
+
>>> nx.weisfeiler_lehman_graph_hash(G1)
|
| 107 |
+
'7bc4dde9a09d0b94c5097b219891d81a'
|
| 108 |
+
>>> nx.weisfeiler_lehman_graph_hash(G2)
|
| 109 |
+
'7bc4dde9a09d0b94c5097b219891d81a'
|
| 110 |
+
|
| 111 |
+
With edge labels, the graphs are no longer assigned
|
| 112 |
+
the same hash digest.
|
| 113 |
+
|
| 114 |
+
>>> nx.weisfeiler_lehman_graph_hash(G1, edge_attr="label")
|
| 115 |
+
'c653d85538bcf041d88c011f4f905f10'
|
| 116 |
+
>>> nx.weisfeiler_lehman_graph_hash(G2, edge_attr="label")
|
| 117 |
+
'3dcd84af1ca855d0eff3c978d88e7ec7'
|
| 118 |
+
|
| 119 |
+
Notes
|
| 120 |
+
-----
|
| 121 |
+
To return the WL hashes of each subgraph of a graph, use
|
| 122 |
+
`weisfeiler_lehman_subgraph_hashes`
|
| 123 |
+
|
| 124 |
+
Similarity between hashes does not imply similarity between graphs.
|
| 125 |
+
|
| 126 |
+
References
|
| 127 |
+
----------
|
| 128 |
+
.. [1] Shervashidze, Nino, Pascal Schweitzer, Erik Jan Van Leeuwen,
|
| 129 |
+
Kurt Mehlhorn, and Karsten M. Borgwardt. Weisfeiler Lehman
|
| 130 |
+
Graph Kernels. Journal of Machine Learning Research. 2011.
|
| 131 |
+
http://www.jmlr.org/papers/volume12/shervashidze11a/shervashidze11a.pdf
|
| 132 |
+
|
| 133 |
+
See also
|
| 134 |
+
--------
|
| 135 |
+
weisfeiler_lehman_subgraph_hashes
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
def weisfeiler_lehman_step(G, labels, edge_attr=None):
|
| 139 |
+
"""
|
| 140 |
+
Apply neighborhood aggregation to each node
|
| 141 |
+
in the graph.
|
| 142 |
+
Computes a dictionary with labels for each node.
|
| 143 |
+
"""
|
| 144 |
+
new_labels = {}
|
| 145 |
+
for node in G.nodes():
|
| 146 |
+
label = _neighborhood_aggregate(G, node, labels, edge_attr=edge_attr)
|
| 147 |
+
new_labels[node] = _hash_label(label, digest_size)
|
| 148 |
+
return new_labels
|
| 149 |
+
|
| 150 |
+
# set initial node labels
|
| 151 |
+
node_labels = _init_node_labels(G, edge_attr, node_attr)
|
| 152 |
+
|
| 153 |
+
subgraph_hash_counts = []
|
| 154 |
+
for _ in range(iterations):
|
| 155 |
+
node_labels = weisfeiler_lehman_step(G, node_labels, edge_attr=edge_attr)
|
| 156 |
+
counter = Counter(node_labels.values())
|
| 157 |
+
# sort the counter, extend total counts
|
| 158 |
+
subgraph_hash_counts.extend(sorted(counter.items(), key=lambda x: x[0]))
|
| 159 |
+
|
| 160 |
+
# hash the final counter
|
| 161 |
+
return _hash_label(str(tuple(subgraph_hash_counts)), digest_size)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
@nx.utils.not_implemented_for("multigraph")
|
| 165 |
+
@nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr")
|
| 166 |
+
def weisfeiler_lehman_subgraph_hashes(
|
| 167 |
+
G,
|
| 168 |
+
edge_attr=None,
|
| 169 |
+
node_attr=None,
|
| 170 |
+
iterations=3,
|
| 171 |
+
digest_size=16,
|
| 172 |
+
include_initial_labels=False,
|
| 173 |
+
):
|
| 174 |
+
"""
|
| 175 |
+
Return a dictionary of subgraph hashes by node.
|
| 176 |
+
|
| 177 |
+
Dictionary keys are nodes in `G`, and values are a list of hashes.
|
| 178 |
+
Each hash corresponds to a subgraph rooted at a given node u in `G`.
|
| 179 |
+
Lists of subgraph hashes are sorted in increasing order of depth from
|
| 180 |
+
their root node, with the hash at index i corresponding to a subgraph
|
| 181 |
+
of nodes at most i edges distance from u. Thus, each list will contain
|
| 182 |
+
`iterations` elements - a hash for a subgraph at each depth. If
|
| 183 |
+
`include_initial_labels` is set to `True`, each list will additionally
|
| 184 |
+
have contain a hash of the initial node label (or equivalently a
|
| 185 |
+
subgraph of depth 0) prepended, totalling ``iterations + 1`` elements.
|
| 186 |
+
|
| 187 |
+
The function iteratively aggregates and hashes neighborhoods of each node.
|
| 188 |
+
This is achieved for each step by replacing for each node its label from
|
| 189 |
+
the previous iteration with its hashed 1-hop neighborhood aggregate.
|
| 190 |
+
The new node label is then appended to a list of node labels for each
|
| 191 |
+
node.
|
| 192 |
+
|
| 193 |
+
To aggregate neighborhoods for a node $u$ at each step, all labels of
|
| 194 |
+
nodes adjacent to $u$ are concatenated. If the `edge_attr` parameter is set,
|
| 195 |
+
labels for each neighboring node are prefixed with the value of this attribute
|
| 196 |
+
along the connecting edge from this neighbor to node $u$. The resulting string
|
| 197 |
+
is then hashed to compress this information into a fixed digest size.
|
| 198 |
+
|
| 199 |
+
Thus, at the $i$-th iteration, nodes within $i$ hops influence any given
|
| 200 |
+
hashed node label. We can therefore say that at depth $i$ for node $u$
|
| 201 |
+
we have a hash for a subgraph induced by the $i$-hop neighborhood of $u$.
|
| 202 |
+
|
| 203 |
+
The output can be used to create general Weisfeiler-Lehman graph kernels,
|
| 204 |
+
or generate features for graphs or nodes - for example to generate 'words' in
|
| 205 |
+
a graph as seen in the 'graph2vec' algorithm.
|
| 206 |
+
See [1]_ & [2]_ respectively for details.
|
| 207 |
+
|
| 208 |
+
Hashes are identical for isomorphic subgraphs and there exist strong
|
| 209 |
+
guarantees that non-isomorphic graphs will get different hashes.
|
| 210 |
+
See [1]_ for details.
|
| 211 |
+
|
| 212 |
+
If no node or edge attributes are provided, the degree of each node
|
| 213 |
+
is used as its initial label.
|
| 214 |
+
Otherwise, node and/or edge labels are used to compute the hash.
|
| 215 |
+
|
| 216 |
+
Parameters
|
| 217 |
+
----------
|
| 218 |
+
G : graph
|
| 219 |
+
The graph to be hashed.
|
| 220 |
+
Can have node and/or edge attributes. Can also have no attributes.
|
| 221 |
+
edge_attr : string, optional (default=None)
|
| 222 |
+
The key in edge attribute dictionary to be used for hashing.
|
| 223 |
+
If None, edge labels are ignored.
|
| 224 |
+
node_attr : string, optional (default=None)
|
| 225 |
+
The key in node attribute dictionary to be used for hashing.
|
| 226 |
+
If None, and no edge_attr given, use the degrees of the nodes as labels.
|
| 227 |
+
If None, and edge_attr is given, each node starts with an identical label.
|
| 228 |
+
iterations : int, optional (default=3)
|
| 229 |
+
Number of neighbor aggregations to perform.
|
| 230 |
+
Should be larger for larger graphs.
|
| 231 |
+
digest_size : int, optional (default=16)
|
| 232 |
+
Size (in bits) of blake2b hash digest to use for hashing node labels.
|
| 233 |
+
The default size is 16 bits.
|
| 234 |
+
include_initial_labels : bool, optional (default=False)
|
| 235 |
+
If True, include the hashed initial node label as the first subgraph
|
| 236 |
+
hash for each node.
|
| 237 |
+
|
| 238 |
+
Returns
|
| 239 |
+
-------
|
| 240 |
+
node_subgraph_hashes : dict
|
| 241 |
+
A dictionary with each key given by a node in G, and each value given
|
| 242 |
+
by the subgraph hashes in order of depth from the key node.
|
| 243 |
+
|
| 244 |
+
Examples
|
| 245 |
+
--------
|
| 246 |
+
Finding similar nodes in different graphs:
|
| 247 |
+
|
| 248 |
+
>>> G1 = nx.Graph()
|
| 249 |
+
>>> G1.add_edges_from([(1, 2), (2, 3), (2, 4), (3, 5), (4, 6), (5, 7), (6, 7)])
|
| 250 |
+
>>> G2 = nx.Graph()
|
| 251 |
+
>>> G2.add_edges_from([(1, 3), (2, 3), (1, 6), (1, 5), (4, 6)])
|
| 252 |
+
>>> g1_hashes = nx.weisfeiler_lehman_subgraph_hashes(
|
| 253 |
+
... G1, iterations=3, digest_size=8
|
| 254 |
+
... )
|
| 255 |
+
>>> g2_hashes = nx.weisfeiler_lehman_subgraph_hashes(
|
| 256 |
+
... G2, iterations=3, digest_size=8
|
| 257 |
+
... )
|
| 258 |
+
|
| 259 |
+
Even though G1 and G2 are not isomorphic (they have different numbers of edges),
|
| 260 |
+
the hash sequence of depth 3 for node 1 in G1 and node 5 in G2 are similar:
|
| 261 |
+
|
| 262 |
+
>>> g1_hashes[1]
|
| 263 |
+
['a93b64973cfc8897', 'db1b43ae35a1878f', '57872a7d2059c1c0']
|
| 264 |
+
>>> g2_hashes[5]
|
| 265 |
+
['a93b64973cfc8897', 'db1b43ae35a1878f', '1716d2a4012fa4bc']
|
| 266 |
+
|
| 267 |
+
The first 2 WL subgraph hashes match. From this we can conclude that it's very
|
| 268 |
+
likely the neighborhood of 2 hops around these nodes are isomorphic.
|
| 269 |
+
|
| 270 |
+
However the 3-hop neighborhoods of ``G1`` and ``G2`` are not isomorphic since the
|
| 271 |
+
3rd hashes in the lists above are not equal.
|
| 272 |
+
|
| 273 |
+
These nodes may be candidates to be classified together since their local topology
|
| 274 |
+
is similar.
|
| 275 |
+
|
| 276 |
+
Notes
|
| 277 |
+
-----
|
| 278 |
+
To hash the full graph when subgraph hashes are not needed, use
|
| 279 |
+
`weisfeiler_lehman_graph_hash` for efficiency.
|
| 280 |
+
|
| 281 |
+
Similarity between hashes does not imply similarity between graphs.
|
| 282 |
+
|
| 283 |
+
References
|
| 284 |
+
----------
|
| 285 |
+
.. [1] Shervashidze, Nino, Pascal Schweitzer, Erik Jan Van Leeuwen,
|
| 286 |
+
Kurt Mehlhorn, and Karsten M. Borgwardt. Weisfeiler Lehman
|
| 287 |
+
Graph Kernels. Journal of Machine Learning Research. 2011.
|
| 288 |
+
http://www.jmlr.org/papers/volume12/shervashidze11a/shervashidze11a.pdf
|
| 289 |
+
.. [2] Annamalai Narayanan, Mahinthan Chandramohan, Rajasekar Venkatesan,
|
| 290 |
+
Lihui Chen, Yang Liu and Shantanu Jaiswa. graph2vec: Learning
|
| 291 |
+
Distributed Representations of Graphs. arXiv. 2017
|
| 292 |
+
https://arxiv.org/pdf/1707.05005.pdf
|
| 293 |
+
|
| 294 |
+
See also
|
| 295 |
+
--------
|
| 296 |
+
weisfeiler_lehman_graph_hash
|
| 297 |
+
"""
|
| 298 |
+
|
| 299 |
+
def weisfeiler_lehman_step(G, labels, node_subgraph_hashes, edge_attr=None):
|
| 300 |
+
"""
|
| 301 |
+
Apply neighborhood aggregation to each node
|
| 302 |
+
in the graph.
|
| 303 |
+
Computes a dictionary with labels for each node.
|
| 304 |
+
Appends the new hashed label to the dictionary of subgraph hashes
|
| 305 |
+
originating from and indexed by each node in G
|
| 306 |
+
"""
|
| 307 |
+
new_labels = {}
|
| 308 |
+
for node in G.nodes():
|
| 309 |
+
label = _neighborhood_aggregate(G, node, labels, edge_attr=edge_attr)
|
| 310 |
+
hashed_label = _hash_label(label, digest_size)
|
| 311 |
+
new_labels[node] = hashed_label
|
| 312 |
+
node_subgraph_hashes[node].append(hashed_label)
|
| 313 |
+
return new_labels
|
| 314 |
+
|
| 315 |
+
node_labels = _init_node_labels(G, edge_attr, node_attr)
|
| 316 |
+
if include_initial_labels:
|
| 317 |
+
node_subgraph_hashes = {
|
| 318 |
+
k: [_hash_label(v, digest_size)] for k, v in node_labels.items()
|
| 319 |
+
}
|
| 320 |
+
else:
|
| 321 |
+
node_subgraph_hashes = defaultdict(list)
|
| 322 |
+
|
| 323 |
+
for _ in range(iterations):
|
| 324 |
+
node_labels = weisfeiler_lehman_step(
|
| 325 |
+
G, node_labels, node_subgraph_hashes, edge_attr
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
return dict(node_subgraph_hashes)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/graphical.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test sequences for graphiness."""
|
| 2 |
+
|
| 3 |
+
import heapq
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"is_graphical",
|
| 9 |
+
"is_multigraphical",
|
| 10 |
+
"is_pseudographical",
|
| 11 |
+
"is_digraphical",
|
| 12 |
+
"is_valid_degree_sequence_erdos_gallai",
|
| 13 |
+
"is_valid_degree_sequence_havel_hakimi",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@nx._dispatchable(graphs=None)
|
| 18 |
+
def is_graphical(sequence, method="eg"):
|
| 19 |
+
"""Returns True if sequence is a valid degree sequence.
|
| 20 |
+
|
| 21 |
+
A degree sequence is valid if some graph can realize it.
|
| 22 |
+
|
| 23 |
+
Parameters
|
| 24 |
+
----------
|
| 25 |
+
sequence : list or iterable container
|
| 26 |
+
A sequence of integer node degrees
|
| 27 |
+
|
| 28 |
+
method : "eg" | "hh" (default: 'eg')
|
| 29 |
+
The method used to validate the degree sequence.
|
| 30 |
+
"eg" corresponds to the Erdős-Gallai algorithm
|
| 31 |
+
[EG1960]_, [choudum1986]_, and
|
| 32 |
+
"hh" to the Havel-Hakimi algorithm
|
| 33 |
+
[havel1955]_, [hakimi1962]_, [CL1996]_.
|
| 34 |
+
|
| 35 |
+
Returns
|
| 36 |
+
-------
|
| 37 |
+
valid : bool
|
| 38 |
+
True if the sequence is a valid degree sequence and False if not.
|
| 39 |
+
|
| 40 |
+
Examples
|
| 41 |
+
--------
|
| 42 |
+
>>> G = nx.path_graph(4)
|
| 43 |
+
>>> sequence = (d for n, d in G.degree())
|
| 44 |
+
>>> nx.is_graphical(sequence)
|
| 45 |
+
True
|
| 46 |
+
|
| 47 |
+
To test a non-graphical sequence:
|
| 48 |
+
>>> sequence_list = [d for n, d in G.degree()]
|
| 49 |
+
>>> sequence_list[-1] += 1
|
| 50 |
+
>>> nx.is_graphical(sequence_list)
|
| 51 |
+
False
|
| 52 |
+
|
| 53 |
+
References
|
| 54 |
+
----------
|
| 55 |
+
.. [EG1960] Erdős and Gallai, Mat. Lapok 11 264, 1960.
|
| 56 |
+
.. [choudum1986] S.A. Choudum. "A simple proof of the Erdős-Gallai theorem on
|
| 57 |
+
graph sequences." Bulletin of the Australian Mathematical Society, 33,
|
| 58 |
+
pp 67-70, 1986. https://doi.org/10.1017/S0004972700002872
|
| 59 |
+
.. [havel1955] Havel, V. "A Remark on the Existence of Finite Graphs"
|
| 60 |
+
Casopis Pest. Mat. 80, 477-480, 1955.
|
| 61 |
+
.. [hakimi1962] Hakimi, S. "On the Realizability of a Set of Integers as
|
| 62 |
+
Degrees of the Vertices of a Graph." SIAM J. Appl. Math. 10, 496-506, 1962.
|
| 63 |
+
.. [CL1996] G. Chartrand and L. Lesniak, "Graphs and Digraphs",
|
| 64 |
+
Chapman and Hall/CRC, 1996.
|
| 65 |
+
"""
|
| 66 |
+
if method == "eg":
|
| 67 |
+
valid = is_valid_degree_sequence_erdos_gallai(list(sequence))
|
| 68 |
+
elif method == "hh":
|
| 69 |
+
valid = is_valid_degree_sequence_havel_hakimi(list(sequence))
|
| 70 |
+
else:
|
| 71 |
+
msg = "`method` must be 'eg' or 'hh'"
|
| 72 |
+
raise nx.NetworkXException(msg)
|
| 73 |
+
return valid
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _basic_graphical_tests(deg_sequence):
|
| 77 |
+
# Sort and perform some simple tests on the sequence
|
| 78 |
+
deg_sequence = nx.utils.make_list_of_ints(deg_sequence)
|
| 79 |
+
p = len(deg_sequence)
|
| 80 |
+
num_degs = [0] * p
|
| 81 |
+
dmax, dmin, dsum, n = 0, p, 0, 0
|
| 82 |
+
for d in deg_sequence:
|
| 83 |
+
# Reject if degree is negative or larger than the sequence length
|
| 84 |
+
if d < 0 or d >= p:
|
| 85 |
+
raise nx.NetworkXUnfeasible
|
| 86 |
+
# Process only the non-zero integers
|
| 87 |
+
elif d > 0:
|
| 88 |
+
dmax, dmin, dsum, n = max(dmax, d), min(dmin, d), dsum + d, n + 1
|
| 89 |
+
num_degs[d] += 1
|
| 90 |
+
# Reject sequence if it has odd sum or is oversaturated
|
| 91 |
+
if dsum % 2 or dsum > n * (n - 1):
|
| 92 |
+
raise nx.NetworkXUnfeasible
|
| 93 |
+
return dmax, dmin, dsum, n, num_degs
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@nx._dispatchable(graphs=None)
|
| 97 |
+
def is_valid_degree_sequence_havel_hakimi(deg_sequence):
|
| 98 |
+
r"""Returns True if deg_sequence can be realized by a simple graph.
|
| 99 |
+
|
| 100 |
+
The validation proceeds using the Havel-Hakimi theorem
|
| 101 |
+
[havel1955]_, [hakimi1962]_, [CL1996]_.
|
| 102 |
+
Worst-case run time is $O(s)$ where $s$ is the sum of the sequence.
|
| 103 |
+
|
| 104 |
+
Parameters
|
| 105 |
+
----------
|
| 106 |
+
deg_sequence : list
|
| 107 |
+
A list of integers where each element specifies the degree of a node
|
| 108 |
+
in a graph.
|
| 109 |
+
|
| 110 |
+
Returns
|
| 111 |
+
-------
|
| 112 |
+
valid : bool
|
| 113 |
+
True if deg_sequence is graphical and False if not.
|
| 114 |
+
|
| 115 |
+
Examples
|
| 116 |
+
--------
|
| 117 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
|
| 118 |
+
>>> sequence = (d for _, d in G.degree())
|
| 119 |
+
>>> nx.is_valid_degree_sequence_havel_hakimi(sequence)
|
| 120 |
+
True
|
| 121 |
+
|
| 122 |
+
To test a non-valid sequence:
|
| 123 |
+
>>> sequence_list = [d for _, d in G.degree()]
|
| 124 |
+
>>> sequence_list[-1] += 1
|
| 125 |
+
>>> nx.is_valid_degree_sequence_havel_hakimi(sequence_list)
|
| 126 |
+
False
|
| 127 |
+
|
| 128 |
+
Notes
|
| 129 |
+
-----
|
| 130 |
+
The ZZ condition says that for the sequence d if
|
| 131 |
+
|
| 132 |
+
.. math::
|
| 133 |
+
|d| >= \frac{(\max(d) + \min(d) + 1)^2}{4*\min(d)}
|
| 134 |
+
|
| 135 |
+
then d is graphical. This was shown in Theorem 6 in [1]_.
|
| 136 |
+
|
| 137 |
+
References
|
| 138 |
+
----------
|
| 139 |
+
.. [1] I.E. Zverovich and V.E. Zverovich. "Contributions to the theory
|
| 140 |
+
of graphic sequences", Discrete Mathematics, 105, pp. 292-303 (1992).
|
| 141 |
+
.. [havel1955] Havel, V. "A Remark on the Existence of Finite Graphs"
|
| 142 |
+
Casopis Pest. Mat. 80, 477-480, 1955.
|
| 143 |
+
.. [hakimi1962] Hakimi, S. "On the Realizability of a Set of Integers as
|
| 144 |
+
Degrees of the Vertices of a Graph." SIAM J. Appl. Math. 10, 496-506, 1962.
|
| 145 |
+
.. [CL1996] G. Chartrand and L. Lesniak, "Graphs and Digraphs",
|
| 146 |
+
Chapman and Hall/CRC, 1996.
|
| 147 |
+
"""
|
| 148 |
+
try:
|
| 149 |
+
dmax, dmin, dsum, n, num_degs = _basic_graphical_tests(deg_sequence)
|
| 150 |
+
except nx.NetworkXUnfeasible:
|
| 151 |
+
return False
|
| 152 |
+
# Accept if sequence has no non-zero degrees or passes the ZZ condition
|
| 153 |
+
if n == 0 or 4 * dmin * n >= (dmax + dmin + 1) * (dmax + dmin + 1):
|
| 154 |
+
return True
|
| 155 |
+
|
| 156 |
+
modstubs = [0] * (dmax + 1)
|
| 157 |
+
# Successively reduce degree sequence by removing the maximum degree
|
| 158 |
+
while n > 0:
|
| 159 |
+
# Retrieve the maximum degree in the sequence
|
| 160 |
+
while num_degs[dmax] == 0:
|
| 161 |
+
dmax -= 1
|
| 162 |
+
# If there are not enough stubs to connect to, then the sequence is
|
| 163 |
+
# not graphical
|
| 164 |
+
if dmax > n - 1:
|
| 165 |
+
return False
|
| 166 |
+
|
| 167 |
+
# Remove largest stub in list
|
| 168 |
+
num_degs[dmax], n = num_degs[dmax] - 1, n - 1
|
| 169 |
+
# Reduce the next dmax largest stubs
|
| 170 |
+
mslen = 0
|
| 171 |
+
k = dmax
|
| 172 |
+
for i in range(dmax):
|
| 173 |
+
while num_degs[k] == 0:
|
| 174 |
+
k -= 1
|
| 175 |
+
num_degs[k], n = num_degs[k] - 1, n - 1
|
| 176 |
+
if k > 1:
|
| 177 |
+
modstubs[mslen] = k - 1
|
| 178 |
+
mslen += 1
|
| 179 |
+
# Add back to the list any non-zero stubs that were removed
|
| 180 |
+
for i in range(mslen):
|
| 181 |
+
stub = modstubs[i]
|
| 182 |
+
num_degs[stub], n = num_degs[stub] + 1, n + 1
|
| 183 |
+
return True
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
@nx._dispatchable(graphs=None)
|
| 187 |
+
def is_valid_degree_sequence_erdos_gallai(deg_sequence):
|
| 188 |
+
r"""Returns True if deg_sequence can be realized by a simple graph.
|
| 189 |
+
|
| 190 |
+
The validation is done using the Erdős-Gallai theorem [EG1960]_.
|
| 191 |
+
|
| 192 |
+
Parameters
|
| 193 |
+
----------
|
| 194 |
+
deg_sequence : list
|
| 195 |
+
A list of integers
|
| 196 |
+
|
| 197 |
+
Returns
|
| 198 |
+
-------
|
| 199 |
+
valid : bool
|
| 200 |
+
True if deg_sequence is graphical and False if not.
|
| 201 |
+
|
| 202 |
+
Examples
|
| 203 |
+
--------
|
| 204 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
|
| 205 |
+
>>> sequence = (d for _, d in G.degree())
|
| 206 |
+
>>> nx.is_valid_degree_sequence_erdos_gallai(sequence)
|
| 207 |
+
True
|
| 208 |
+
|
| 209 |
+
To test a non-valid sequence:
|
| 210 |
+
>>> sequence_list = [d for _, d in G.degree()]
|
| 211 |
+
>>> sequence_list[-1] += 1
|
| 212 |
+
>>> nx.is_valid_degree_sequence_erdos_gallai(sequence_list)
|
| 213 |
+
False
|
| 214 |
+
|
| 215 |
+
Notes
|
| 216 |
+
-----
|
| 217 |
+
|
| 218 |
+
This implementation uses an equivalent form of the Erdős-Gallai criterion.
|
| 219 |
+
Worst-case run time is $O(n)$ where $n$ is the length of the sequence.
|
| 220 |
+
|
| 221 |
+
Specifically, a sequence d is graphical if and only if the
|
| 222 |
+
sum of the sequence is even and for all strong indices k in the sequence,
|
| 223 |
+
|
| 224 |
+
.. math::
|
| 225 |
+
|
| 226 |
+
\sum_{i=1}^{k} d_i \leq k(k-1) + \sum_{j=k+1}^{n} \min(d_i,k)
|
| 227 |
+
= k(n-1) - ( k \sum_{j=0}^{k-1} n_j - \sum_{j=0}^{k-1} j n_j )
|
| 228 |
+
|
| 229 |
+
A strong index k is any index where d_k >= k and the value n_j is the
|
| 230 |
+
number of occurrences of j in d. The maximal strong index is called the
|
| 231 |
+
Durfee index.
|
| 232 |
+
|
| 233 |
+
This particular rearrangement comes from the proof of Theorem 3 in [2]_.
|
| 234 |
+
|
| 235 |
+
The ZZ condition says that for the sequence d if
|
| 236 |
+
|
| 237 |
+
.. math::
|
| 238 |
+
|d| >= \frac{(\max(d) + \min(d) + 1)^2}{4*\min(d)}
|
| 239 |
+
|
| 240 |
+
then d is graphical. This was shown in Theorem 6 in [2]_.
|
| 241 |
+
|
| 242 |
+
References
|
| 243 |
+
----------
|
| 244 |
+
.. [1] A. Tripathi and S. Vijay. "A note on a theorem of Erdős & Gallai",
|
| 245 |
+
Discrete Mathematics, 265, pp. 417-420 (2003).
|
| 246 |
+
.. [2] I.E. Zverovich and V.E. Zverovich. "Contributions to the theory
|
| 247 |
+
of graphic sequences", Discrete Mathematics, 105, pp. 292-303 (1992).
|
| 248 |
+
.. [EG1960] Erdős and Gallai, Mat. Lapok 11 264, 1960.
|
| 249 |
+
"""
|
| 250 |
+
try:
|
| 251 |
+
dmax, dmin, dsum, n, num_degs = _basic_graphical_tests(deg_sequence)
|
| 252 |
+
except nx.NetworkXUnfeasible:
|
| 253 |
+
return False
|
| 254 |
+
# Accept if sequence has no non-zero degrees or passes the ZZ condition
|
| 255 |
+
if n == 0 or 4 * dmin * n >= (dmax + dmin + 1) * (dmax + dmin + 1):
|
| 256 |
+
return True
|
| 257 |
+
|
| 258 |
+
# Perform the EG checks using the reformulation of Zverovich and Zverovich
|
| 259 |
+
k, sum_deg, sum_nj, sum_jnj = 0, 0, 0, 0
|
| 260 |
+
for dk in range(dmax, dmin - 1, -1):
|
| 261 |
+
if dk < k + 1: # Check if already past Durfee index
|
| 262 |
+
return True
|
| 263 |
+
if num_degs[dk] > 0:
|
| 264 |
+
run_size = num_degs[dk] # Process a run of identical-valued degrees
|
| 265 |
+
if dk < k + run_size: # Check if end of run is past Durfee index
|
| 266 |
+
run_size = dk - k # Adjust back to Durfee index
|
| 267 |
+
sum_deg += run_size * dk
|
| 268 |
+
for v in range(run_size):
|
| 269 |
+
sum_nj += num_degs[k + v]
|
| 270 |
+
sum_jnj += (k + v) * num_degs[k + v]
|
| 271 |
+
k += run_size
|
| 272 |
+
if sum_deg > k * (n - 1) - k * sum_nj + sum_jnj:
|
| 273 |
+
return False
|
| 274 |
+
return True
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
@nx._dispatchable(graphs=None)
|
| 278 |
+
def is_multigraphical(sequence):
|
| 279 |
+
"""Returns True if some multigraph can realize the sequence.
|
| 280 |
+
|
| 281 |
+
Parameters
|
| 282 |
+
----------
|
| 283 |
+
sequence : list
|
| 284 |
+
A list of integers
|
| 285 |
+
|
| 286 |
+
Returns
|
| 287 |
+
-------
|
| 288 |
+
valid : bool
|
| 289 |
+
True if deg_sequence is a multigraphic degree sequence and False if not.
|
| 290 |
+
|
| 291 |
+
Examples
|
| 292 |
+
--------
|
| 293 |
+
>>> G = nx.MultiGraph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
|
| 294 |
+
>>> sequence = (d for _, d in G.degree())
|
| 295 |
+
>>> nx.is_multigraphical(sequence)
|
| 296 |
+
True
|
| 297 |
+
|
| 298 |
+
To test a non-multigraphical sequence:
|
| 299 |
+
>>> sequence_list = [d for _, d in G.degree()]
|
| 300 |
+
>>> sequence_list[-1] += 1
|
| 301 |
+
>>> nx.is_multigraphical(sequence_list)
|
| 302 |
+
False
|
| 303 |
+
|
| 304 |
+
Notes
|
| 305 |
+
-----
|
| 306 |
+
The worst-case run time is $O(n)$ where $n$ is the length of the sequence.
|
| 307 |
+
|
| 308 |
+
References
|
| 309 |
+
----------
|
| 310 |
+
.. [1] S. L. Hakimi. "On the realizability of a set of integers as
|
| 311 |
+
degrees of the vertices of a linear graph", J. SIAM, 10, pp. 496-506
|
| 312 |
+
(1962).
|
| 313 |
+
"""
|
| 314 |
+
try:
|
| 315 |
+
deg_sequence = nx.utils.make_list_of_ints(sequence)
|
| 316 |
+
except nx.NetworkXError:
|
| 317 |
+
return False
|
| 318 |
+
dsum, dmax = 0, 0
|
| 319 |
+
for d in deg_sequence:
|
| 320 |
+
if d < 0:
|
| 321 |
+
return False
|
| 322 |
+
dsum, dmax = dsum + d, max(dmax, d)
|
| 323 |
+
if dsum % 2 or dsum < 2 * dmax:
|
| 324 |
+
return False
|
| 325 |
+
return True
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
@nx._dispatchable(graphs=None)
|
| 329 |
+
def is_pseudographical(sequence):
|
| 330 |
+
"""Returns True if some pseudograph can realize the sequence.
|
| 331 |
+
|
| 332 |
+
Every nonnegative integer sequence with an even sum is pseudographical
|
| 333 |
+
(see [1]_).
|
| 334 |
+
|
| 335 |
+
Parameters
|
| 336 |
+
----------
|
| 337 |
+
sequence : list or iterable container
|
| 338 |
+
A sequence of integer node degrees
|
| 339 |
+
|
| 340 |
+
Returns
|
| 341 |
+
-------
|
| 342 |
+
valid : bool
|
| 343 |
+
True if the sequence is a pseudographic degree sequence and False if not.
|
| 344 |
+
|
| 345 |
+
Examples
|
| 346 |
+
--------
|
| 347 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
|
| 348 |
+
>>> sequence = (d for _, d in G.degree())
|
| 349 |
+
>>> nx.is_pseudographical(sequence)
|
| 350 |
+
True
|
| 351 |
+
|
| 352 |
+
To test a non-pseudographical sequence:
|
| 353 |
+
>>> sequence_list = [d for _, d in G.degree()]
|
| 354 |
+
>>> sequence_list[-1] += 1
|
| 355 |
+
>>> nx.is_pseudographical(sequence_list)
|
| 356 |
+
False
|
| 357 |
+
|
| 358 |
+
Notes
|
| 359 |
+
-----
|
| 360 |
+
The worst-case run time is $O(n)$ where n is the length of the sequence.
|
| 361 |
+
|
| 362 |
+
References
|
| 363 |
+
----------
|
| 364 |
+
.. [1] F. Boesch and F. Harary. "Line removal algorithms for graphs
|
| 365 |
+
and their degree lists", IEEE Trans. Circuits and Systems, CAS-23(12),
|
| 366 |
+
pp. 778-782 (1976).
|
| 367 |
+
"""
|
| 368 |
+
try:
|
| 369 |
+
deg_sequence = nx.utils.make_list_of_ints(sequence)
|
| 370 |
+
except nx.NetworkXError:
|
| 371 |
+
return False
|
| 372 |
+
return sum(deg_sequence) % 2 == 0 and min(deg_sequence) >= 0
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
@nx._dispatchable(graphs=None)
|
| 376 |
+
def is_digraphical(in_sequence, out_sequence):
|
| 377 |
+
r"""Returns True if some directed graph can realize the in- and out-degree
|
| 378 |
+
sequences.
|
| 379 |
+
|
| 380 |
+
Parameters
|
| 381 |
+
----------
|
| 382 |
+
in_sequence : list or iterable container
|
| 383 |
+
A sequence of integer node in-degrees
|
| 384 |
+
|
| 385 |
+
out_sequence : list or iterable container
|
| 386 |
+
A sequence of integer node out-degrees
|
| 387 |
+
|
| 388 |
+
Returns
|
| 389 |
+
-------
|
| 390 |
+
valid : bool
|
| 391 |
+
True if in and out-sequences are digraphic False if not.
|
| 392 |
+
|
| 393 |
+
Examples
|
| 394 |
+
--------
|
| 395 |
+
>>> G = nx.DiGraph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
|
| 396 |
+
>>> in_seq = (d for n, d in G.in_degree())
|
| 397 |
+
>>> out_seq = (d for n, d in G.out_degree())
|
| 398 |
+
>>> nx.is_digraphical(in_seq, out_seq)
|
| 399 |
+
True
|
| 400 |
+
|
| 401 |
+
To test a non-digraphical scenario:
|
| 402 |
+
>>> in_seq_list = [d for n, d in G.in_degree()]
|
| 403 |
+
>>> in_seq_list[-1] += 1
|
| 404 |
+
>>> nx.is_digraphical(in_seq_list, out_seq)
|
| 405 |
+
False
|
| 406 |
+
|
| 407 |
+
Notes
|
| 408 |
+
-----
|
| 409 |
+
This algorithm is from Kleitman and Wang [1]_.
|
| 410 |
+
The worst case runtime is $O(s \times \log n)$ where $s$ and $n$ are the
|
| 411 |
+
sum and length of the sequences respectively.
|
| 412 |
+
|
| 413 |
+
References
|
| 414 |
+
----------
|
| 415 |
+
.. [1] D.J. Kleitman and D.L. Wang
|
| 416 |
+
Algorithms for Constructing Graphs and Digraphs with Given Valences
|
| 417 |
+
and Factors, Discrete Mathematics, 6(1), pp. 79-88 (1973)
|
| 418 |
+
"""
|
| 419 |
+
try:
|
| 420 |
+
in_deg_sequence = nx.utils.make_list_of_ints(in_sequence)
|
| 421 |
+
out_deg_sequence = nx.utils.make_list_of_ints(out_sequence)
|
| 422 |
+
except nx.NetworkXError:
|
| 423 |
+
return False
|
| 424 |
+
# Process the sequences and form two heaps to store degree pairs with
|
| 425 |
+
# either zero or non-zero out degrees
|
| 426 |
+
sumin, sumout, nin, nout = 0, 0, len(in_deg_sequence), len(out_deg_sequence)
|
| 427 |
+
maxn = max(nin, nout)
|
| 428 |
+
maxin = 0
|
| 429 |
+
if maxn == 0:
|
| 430 |
+
return True
|
| 431 |
+
stubheap, zeroheap = [], []
|
| 432 |
+
for n in range(maxn):
|
| 433 |
+
in_deg, out_deg = 0, 0
|
| 434 |
+
if n < nout:
|
| 435 |
+
out_deg = out_deg_sequence[n]
|
| 436 |
+
if n < nin:
|
| 437 |
+
in_deg = in_deg_sequence[n]
|
| 438 |
+
if in_deg < 0 or out_deg < 0:
|
| 439 |
+
return False
|
| 440 |
+
sumin, sumout, maxin = sumin + in_deg, sumout + out_deg, max(maxin, in_deg)
|
| 441 |
+
if in_deg > 0:
|
| 442 |
+
stubheap.append((-1 * out_deg, -1 * in_deg))
|
| 443 |
+
elif out_deg > 0:
|
| 444 |
+
zeroheap.append(-1 * out_deg)
|
| 445 |
+
if sumin != sumout:
|
| 446 |
+
return False
|
| 447 |
+
heapq.heapify(stubheap)
|
| 448 |
+
heapq.heapify(zeroheap)
|
| 449 |
+
|
| 450 |
+
modstubs = [(0, 0)] * (maxin + 1)
|
| 451 |
+
# Successively reduce degree sequence by removing the maximum out degree
|
| 452 |
+
while stubheap:
|
| 453 |
+
# Take the first value in the sequence with non-zero in degree
|
| 454 |
+
(freeout, freein) = heapq.heappop(stubheap)
|
| 455 |
+
freein *= -1
|
| 456 |
+
if freein > len(stubheap) + len(zeroheap):
|
| 457 |
+
return False
|
| 458 |
+
|
| 459 |
+
# Attach out stubs to the nodes with the most in stubs
|
| 460 |
+
mslen = 0
|
| 461 |
+
for i in range(freein):
|
| 462 |
+
if zeroheap and (not stubheap or stubheap[0][0] > zeroheap[0]):
|
| 463 |
+
stubout = heapq.heappop(zeroheap)
|
| 464 |
+
stubin = 0
|
| 465 |
+
else:
|
| 466 |
+
(stubout, stubin) = heapq.heappop(stubheap)
|
| 467 |
+
if stubout == 0:
|
| 468 |
+
return False
|
| 469 |
+
# Check if target is now totally connected
|
| 470 |
+
if stubout + 1 < 0 or stubin < 0:
|
| 471 |
+
modstubs[mslen] = (stubout + 1, stubin)
|
| 472 |
+
mslen += 1
|
| 473 |
+
|
| 474 |
+
# Add back the nodes to the heap that still have available stubs
|
| 475 |
+
for i in range(mslen):
|
| 476 |
+
stub = modstubs[i]
|
| 477 |
+
if stub[1] < 0:
|
| 478 |
+
heapq.heappush(stubheap, stub)
|
| 479 |
+
else:
|
| 480 |
+
heapq.heappush(zeroheap, stub[0])
|
| 481 |
+
if freeout < 0:
|
| 482 |
+
heapq.heappush(zeroheap, freeout)
|
| 483 |
+
return True
|
.venv/lib/python3.11/site-packages/networkx/algorithms/hierarchy.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Flow Hierarchy.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
__all__ = ["flow_hierarchy"]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 11 |
+
def flow_hierarchy(G, weight=None):
|
| 12 |
+
"""Returns the flow hierarchy of a directed network.
|
| 13 |
+
|
| 14 |
+
Flow hierarchy is defined as the fraction of edges not participating
|
| 15 |
+
in cycles in a directed graph [1]_.
|
| 16 |
+
|
| 17 |
+
Parameters
|
| 18 |
+
----------
|
| 19 |
+
G : DiGraph or MultiDiGraph
|
| 20 |
+
A directed graph
|
| 21 |
+
|
| 22 |
+
weight : string, optional (default=None)
|
| 23 |
+
Attribute to use for edge weights. If None the weight defaults to 1.
|
| 24 |
+
|
| 25 |
+
Returns
|
| 26 |
+
-------
|
| 27 |
+
h : float
|
| 28 |
+
Flow hierarchy value
|
| 29 |
+
|
| 30 |
+
Raises
|
| 31 |
+
------
|
| 32 |
+
NetworkXError
|
| 33 |
+
If `G` is not a directed graph or if `G` has no edges.
|
| 34 |
+
|
| 35 |
+
Notes
|
| 36 |
+
-----
|
| 37 |
+
The algorithm described in [1]_ computes the flow hierarchy through
|
| 38 |
+
exponentiation of the adjacency matrix. This function implements an
|
| 39 |
+
alternative approach that finds strongly connected components.
|
| 40 |
+
An edge is in a cycle if and only if it is in a strongly connected
|
| 41 |
+
component, which can be found in $O(m)$ time using Tarjan's algorithm.
|
| 42 |
+
|
| 43 |
+
References
|
| 44 |
+
----------
|
| 45 |
+
.. [1] Luo, J.; Magee, C.L. (2011),
|
| 46 |
+
Detecting evolving patterns of self-organizing networks by flow
|
| 47 |
+
hierarchy measurement, Complexity, Volume 16 Issue 6 53-61.
|
| 48 |
+
DOI: 10.1002/cplx.20368
|
| 49 |
+
http://web.mit.edu/~cmagee/www/documents/28-DetectingEvolvingPatterns_FlowHierarchy.pdf
|
| 50 |
+
"""
|
| 51 |
+
# corner case: G has no edges
|
| 52 |
+
if nx.is_empty(G):
|
| 53 |
+
raise nx.NetworkXError("flow_hierarchy not applicable to empty graphs")
|
| 54 |
+
if not G.is_directed():
|
| 55 |
+
raise nx.NetworkXError("G must be a digraph in flow_hierarchy")
|
| 56 |
+
scc = nx.strongly_connected_components(G)
|
| 57 |
+
return 1 - sum(G.subgraph(c).size(weight) for c in scc) / G.size(weight)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/hybrid.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Provides functions for finding and testing for locally `(k, l)`-connected
|
| 3 |
+
graphs.
|
| 4 |
+
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import copy
|
| 8 |
+
|
| 9 |
+
import networkx as nx
|
| 10 |
+
|
| 11 |
+
__all__ = ["kl_connected_subgraph", "is_kl_connected"]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@nx._dispatchable(returns_graph=True)
|
| 15 |
+
def kl_connected_subgraph(G, k, l, low_memory=False, same_as_graph=False):
|
| 16 |
+
"""Returns the maximum locally `(k, l)`-connected subgraph of `G`.
|
| 17 |
+
|
| 18 |
+
A graph is locally `(k, l)`-connected if for each edge `(u, v)` in the
|
| 19 |
+
graph there are at least `l` edge-disjoint paths of length at most `k`
|
| 20 |
+
joining `u` to `v`.
|
| 21 |
+
|
| 22 |
+
Parameters
|
| 23 |
+
----------
|
| 24 |
+
G : NetworkX graph
|
| 25 |
+
The graph in which to find a maximum locally `(k, l)`-connected
|
| 26 |
+
subgraph.
|
| 27 |
+
|
| 28 |
+
k : integer
|
| 29 |
+
The maximum length of paths to consider. A higher number means a looser
|
| 30 |
+
connectivity requirement.
|
| 31 |
+
|
| 32 |
+
l : integer
|
| 33 |
+
The number of edge-disjoint paths. A higher number means a stricter
|
| 34 |
+
connectivity requirement.
|
| 35 |
+
|
| 36 |
+
low_memory : bool
|
| 37 |
+
If this is True, this function uses an algorithm that uses slightly
|
| 38 |
+
more time but less memory.
|
| 39 |
+
|
| 40 |
+
same_as_graph : bool
|
| 41 |
+
If True then return a tuple of the form `(H, is_same)`,
|
| 42 |
+
where `H` is the maximum locally `(k, l)`-connected subgraph and
|
| 43 |
+
`is_same` is a Boolean representing whether `G` is locally `(k,
|
| 44 |
+
l)`-connected (and hence, whether `H` is simply a copy of the input
|
| 45 |
+
graph `G`).
|
| 46 |
+
|
| 47 |
+
Returns
|
| 48 |
+
-------
|
| 49 |
+
NetworkX graph or two-tuple
|
| 50 |
+
If `same_as_graph` is True, then this function returns a
|
| 51 |
+
two-tuple as described above. Otherwise, it returns only the maximum
|
| 52 |
+
locally `(k, l)`-connected subgraph.
|
| 53 |
+
|
| 54 |
+
See also
|
| 55 |
+
--------
|
| 56 |
+
is_kl_connected
|
| 57 |
+
|
| 58 |
+
References
|
| 59 |
+
----------
|
| 60 |
+
.. [1] Chung, Fan and Linyuan Lu. "The Small World Phenomenon in Hybrid
|
| 61 |
+
Power Law Graphs." *Complex Networks*. Springer Berlin Heidelberg,
|
| 62 |
+
2004. 89--104.
|
| 63 |
+
|
| 64 |
+
"""
|
| 65 |
+
H = copy.deepcopy(G) # subgraph we construct by removing from G
|
| 66 |
+
|
| 67 |
+
graphOK = True
|
| 68 |
+
deleted_some = True # hack to start off the while loop
|
| 69 |
+
while deleted_some:
|
| 70 |
+
deleted_some = False
|
| 71 |
+
# We use `for edge in list(H.edges()):` instead of
|
| 72 |
+
# `for edge in H.edges():` because we edit the graph `H` in
|
| 73 |
+
# the loop. Hence using an iterator will result in
|
| 74 |
+
# `RuntimeError: dictionary changed size during iteration`
|
| 75 |
+
for edge in list(H.edges()):
|
| 76 |
+
(u, v) = edge
|
| 77 |
+
# Get copy of graph needed for this search
|
| 78 |
+
if low_memory:
|
| 79 |
+
verts = {u, v}
|
| 80 |
+
for i in range(k):
|
| 81 |
+
for w in verts.copy():
|
| 82 |
+
verts.update(G[w])
|
| 83 |
+
G2 = G.subgraph(verts).copy()
|
| 84 |
+
else:
|
| 85 |
+
G2 = copy.deepcopy(G)
|
| 86 |
+
###
|
| 87 |
+
path = [u, v]
|
| 88 |
+
cnt = 0
|
| 89 |
+
accept = 0
|
| 90 |
+
while path:
|
| 91 |
+
cnt += 1 # Found a path
|
| 92 |
+
if cnt >= l:
|
| 93 |
+
accept = 1
|
| 94 |
+
break
|
| 95 |
+
# record edges along this graph
|
| 96 |
+
prev = u
|
| 97 |
+
for w in path:
|
| 98 |
+
if prev != w:
|
| 99 |
+
G2.remove_edge(prev, w)
|
| 100 |
+
prev = w
|
| 101 |
+
# path = shortest_path(G2, u, v, k) # ??? should "Cutoff" be k+1?
|
| 102 |
+
try:
|
| 103 |
+
path = nx.shortest_path(G2, u, v) # ??? should "Cutoff" be k+1?
|
| 104 |
+
except nx.NetworkXNoPath:
|
| 105 |
+
path = False
|
| 106 |
+
# No Other Paths
|
| 107 |
+
if accept == 0:
|
| 108 |
+
H.remove_edge(u, v)
|
| 109 |
+
deleted_some = True
|
| 110 |
+
if graphOK:
|
| 111 |
+
graphOK = False
|
| 112 |
+
# We looked through all edges and removed none of them.
|
| 113 |
+
# So, H is the maximal (k,l)-connected subgraph of G
|
| 114 |
+
if same_as_graph:
|
| 115 |
+
return (H, graphOK)
|
| 116 |
+
return H
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@nx._dispatchable
|
| 120 |
+
def is_kl_connected(G, k, l, low_memory=False):
|
| 121 |
+
"""Returns True if and only if `G` is locally `(k, l)`-connected.
|
| 122 |
+
|
| 123 |
+
A graph is locally `(k, l)`-connected if for each edge `(u, v)` in the
|
| 124 |
+
graph there are at least `l` edge-disjoint paths of length at most `k`
|
| 125 |
+
joining `u` to `v`.
|
| 126 |
+
|
| 127 |
+
Parameters
|
| 128 |
+
----------
|
| 129 |
+
G : NetworkX graph
|
| 130 |
+
The graph to test for local `(k, l)`-connectedness.
|
| 131 |
+
|
| 132 |
+
k : integer
|
| 133 |
+
The maximum length of paths to consider. A higher number means a looser
|
| 134 |
+
connectivity requirement.
|
| 135 |
+
|
| 136 |
+
l : integer
|
| 137 |
+
The number of edge-disjoint paths. A higher number means a stricter
|
| 138 |
+
connectivity requirement.
|
| 139 |
+
|
| 140 |
+
low_memory : bool
|
| 141 |
+
If this is True, this function uses an algorithm that uses slightly
|
| 142 |
+
more time but less memory.
|
| 143 |
+
|
| 144 |
+
Returns
|
| 145 |
+
-------
|
| 146 |
+
bool
|
| 147 |
+
Whether the graph is locally `(k, l)`-connected subgraph.
|
| 148 |
+
|
| 149 |
+
See also
|
| 150 |
+
--------
|
| 151 |
+
kl_connected_subgraph
|
| 152 |
+
|
| 153 |
+
References
|
| 154 |
+
----------
|
| 155 |
+
.. [1] Chung, Fan and Linyuan Lu. "The Small World Phenomenon in Hybrid
|
| 156 |
+
Power Law Graphs." *Complex Networks*. Springer Berlin Heidelberg,
|
| 157 |
+
2004. 89--104.
|
| 158 |
+
|
| 159 |
+
"""
|
| 160 |
+
graphOK = True
|
| 161 |
+
for edge in G.edges():
|
| 162 |
+
(u, v) = edge
|
| 163 |
+
# Get copy of graph needed for this search
|
| 164 |
+
if low_memory:
|
| 165 |
+
verts = {u, v}
|
| 166 |
+
for i in range(k):
|
| 167 |
+
[verts.update(G.neighbors(w)) for w in verts.copy()]
|
| 168 |
+
G2 = G.subgraph(verts)
|
| 169 |
+
else:
|
| 170 |
+
G2 = copy.deepcopy(G)
|
| 171 |
+
###
|
| 172 |
+
path = [u, v]
|
| 173 |
+
cnt = 0
|
| 174 |
+
accept = 0
|
| 175 |
+
while path:
|
| 176 |
+
cnt += 1 # Found a path
|
| 177 |
+
if cnt >= l:
|
| 178 |
+
accept = 1
|
| 179 |
+
break
|
| 180 |
+
# record edges along this graph
|
| 181 |
+
prev = u
|
| 182 |
+
for w in path:
|
| 183 |
+
if w != prev:
|
| 184 |
+
G2.remove_edge(prev, w)
|
| 185 |
+
prev = w
|
| 186 |
+
# path = shortest_path(G2, u, v, k) # ??? should "Cutoff" be k+1?
|
| 187 |
+
try:
|
| 188 |
+
path = nx.shortest_path(G2, u, v) # ??? should "Cutoff" be k+1?
|
| 189 |
+
except nx.NetworkXNoPath:
|
| 190 |
+
path = False
|
| 191 |
+
# No Other Paths
|
| 192 |
+
if accept == 0:
|
| 193 |
+
graphOK = False
|
| 194 |
+
break
|
| 195 |
+
# return status
|
| 196 |
+
return graphOK
|
.venv/lib/python3.11/site-packages/networkx/algorithms/isolate.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Functions for identifying isolate (degree zero) nodes.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
__all__ = ["is_isolate", "isolates", "number_of_isolates"]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@nx._dispatchable
|
| 11 |
+
def is_isolate(G, n):
|
| 12 |
+
"""Determines whether a node is an isolate.
|
| 13 |
+
|
| 14 |
+
An *isolate* is a node with no neighbors (that is, with degree
|
| 15 |
+
zero). For directed graphs, this means no in-neighbors and no
|
| 16 |
+
out-neighbors.
|
| 17 |
+
|
| 18 |
+
Parameters
|
| 19 |
+
----------
|
| 20 |
+
G : NetworkX graph
|
| 21 |
+
|
| 22 |
+
n : node
|
| 23 |
+
A node in `G`.
|
| 24 |
+
|
| 25 |
+
Returns
|
| 26 |
+
-------
|
| 27 |
+
is_isolate : bool
|
| 28 |
+
True if and only if `n` has no neighbors.
|
| 29 |
+
|
| 30 |
+
Examples
|
| 31 |
+
--------
|
| 32 |
+
>>> G = nx.Graph()
|
| 33 |
+
>>> G.add_edge(1, 2)
|
| 34 |
+
>>> G.add_node(3)
|
| 35 |
+
>>> nx.is_isolate(G, 2)
|
| 36 |
+
False
|
| 37 |
+
>>> nx.is_isolate(G, 3)
|
| 38 |
+
True
|
| 39 |
+
"""
|
| 40 |
+
return G.degree(n) == 0
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@nx._dispatchable
|
| 44 |
+
def isolates(G):
|
| 45 |
+
"""Iterator over isolates in the graph.
|
| 46 |
+
|
| 47 |
+
An *isolate* is a node with no neighbors (that is, with degree
|
| 48 |
+
zero). For directed graphs, this means no in-neighbors and no
|
| 49 |
+
out-neighbors.
|
| 50 |
+
|
| 51 |
+
Parameters
|
| 52 |
+
----------
|
| 53 |
+
G : NetworkX graph
|
| 54 |
+
|
| 55 |
+
Returns
|
| 56 |
+
-------
|
| 57 |
+
iterator
|
| 58 |
+
An iterator over the isolates of `G`.
|
| 59 |
+
|
| 60 |
+
Examples
|
| 61 |
+
--------
|
| 62 |
+
To get a list of all isolates of a graph, use the :class:`list`
|
| 63 |
+
constructor::
|
| 64 |
+
|
| 65 |
+
>>> G = nx.Graph()
|
| 66 |
+
>>> G.add_edge(1, 2)
|
| 67 |
+
>>> G.add_node(3)
|
| 68 |
+
>>> list(nx.isolates(G))
|
| 69 |
+
[3]
|
| 70 |
+
|
| 71 |
+
To remove all isolates in the graph, first create a list of the
|
| 72 |
+
isolates, then use :meth:`Graph.remove_nodes_from`::
|
| 73 |
+
|
| 74 |
+
>>> G.remove_nodes_from(list(nx.isolates(G)))
|
| 75 |
+
>>> list(G)
|
| 76 |
+
[1, 2]
|
| 77 |
+
|
| 78 |
+
For digraphs, isolates have zero in-degree and zero out_degre::
|
| 79 |
+
|
| 80 |
+
>>> G = nx.DiGraph([(0, 1), (1, 2)])
|
| 81 |
+
>>> G.add_node(3)
|
| 82 |
+
>>> list(nx.isolates(G))
|
| 83 |
+
[3]
|
| 84 |
+
|
| 85 |
+
"""
|
| 86 |
+
return (n for n, d in G.degree() if d == 0)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
@nx._dispatchable
|
| 90 |
+
def number_of_isolates(G):
|
| 91 |
+
"""Returns the number of isolates in the graph.
|
| 92 |
+
|
| 93 |
+
An *isolate* is a node with no neighbors (that is, with degree
|
| 94 |
+
zero). For directed graphs, this means no in-neighbors and no
|
| 95 |
+
out-neighbors.
|
| 96 |
+
|
| 97 |
+
Parameters
|
| 98 |
+
----------
|
| 99 |
+
G : NetworkX graph
|
| 100 |
+
|
| 101 |
+
Returns
|
| 102 |
+
-------
|
| 103 |
+
int
|
| 104 |
+
The number of degree zero nodes in the graph `G`.
|
| 105 |
+
|
| 106 |
+
"""
|
| 107 |
+
return sum(1 for v in isolates(G))
|
.venv/lib/python3.11/site-packages/networkx/algorithms/link_prediction.py
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Link prediction algorithms.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from math import log
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
from networkx.utils import not_implemented_for
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
"resource_allocation_index",
|
| 12 |
+
"jaccard_coefficient",
|
| 13 |
+
"adamic_adar_index",
|
| 14 |
+
"preferential_attachment",
|
| 15 |
+
"cn_soundarajan_hopcroft",
|
| 16 |
+
"ra_index_soundarajan_hopcroft",
|
| 17 |
+
"within_inter_cluster",
|
| 18 |
+
"common_neighbor_centrality",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _apply_prediction(G, func, ebunch=None):
|
| 23 |
+
"""Applies the given function to each edge in the specified iterable
|
| 24 |
+
of edges.
|
| 25 |
+
|
| 26 |
+
`G` is an instance of :class:`networkx.Graph`.
|
| 27 |
+
|
| 28 |
+
`func` is a function on two inputs, each of which is a node in the
|
| 29 |
+
graph. The function can return anything, but it should return a
|
| 30 |
+
value representing a prediction of the likelihood of a "link"
|
| 31 |
+
joining the two nodes.
|
| 32 |
+
|
| 33 |
+
`ebunch` is an iterable of pairs of nodes. If not specified, all
|
| 34 |
+
non-edges in the graph `G` will be used.
|
| 35 |
+
|
| 36 |
+
"""
|
| 37 |
+
if ebunch is None:
|
| 38 |
+
ebunch = nx.non_edges(G)
|
| 39 |
+
else:
|
| 40 |
+
for u, v in ebunch:
|
| 41 |
+
if u not in G:
|
| 42 |
+
raise nx.NodeNotFound(f"Node {u} not in G.")
|
| 43 |
+
if v not in G:
|
| 44 |
+
raise nx.NodeNotFound(f"Node {v} not in G.")
|
| 45 |
+
return ((u, v, func(u, v)) for u, v in ebunch)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@not_implemented_for("directed")
|
| 49 |
+
@not_implemented_for("multigraph")
|
| 50 |
+
@nx._dispatchable
|
| 51 |
+
def resource_allocation_index(G, ebunch=None):
|
| 52 |
+
r"""Compute the resource allocation index of all node pairs in ebunch.
|
| 53 |
+
|
| 54 |
+
Resource allocation index of `u` and `v` is defined as
|
| 55 |
+
|
| 56 |
+
.. math::
|
| 57 |
+
|
| 58 |
+
\sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{1}{|\Gamma(w)|}
|
| 59 |
+
|
| 60 |
+
where $\Gamma(u)$ denotes the set of neighbors of $u$.
|
| 61 |
+
|
| 62 |
+
Parameters
|
| 63 |
+
----------
|
| 64 |
+
G : graph
|
| 65 |
+
A NetworkX undirected graph.
|
| 66 |
+
|
| 67 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 68 |
+
Resource allocation index will be computed for each pair of
|
| 69 |
+
nodes given in the iterable. The pairs must be given as
|
| 70 |
+
2-tuples (u, v) where u and v are nodes in the graph. If ebunch
|
| 71 |
+
is None then all nonexistent edges in the graph will be used.
|
| 72 |
+
Default value: None.
|
| 73 |
+
|
| 74 |
+
Returns
|
| 75 |
+
-------
|
| 76 |
+
piter : iterator
|
| 77 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 78 |
+
pair of nodes and p is their resource allocation index.
|
| 79 |
+
|
| 80 |
+
Raises
|
| 81 |
+
------
|
| 82 |
+
NetworkXNotImplemented
|
| 83 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 84 |
+
|
| 85 |
+
NodeNotFound
|
| 86 |
+
If `ebunch` has a node that is not in `G`.
|
| 87 |
+
|
| 88 |
+
Examples
|
| 89 |
+
--------
|
| 90 |
+
>>> G = nx.complete_graph(5)
|
| 91 |
+
>>> preds = nx.resource_allocation_index(G, [(0, 1), (2, 3)])
|
| 92 |
+
>>> for u, v, p in preds:
|
| 93 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 94 |
+
(0, 1) -> 0.75000000
|
| 95 |
+
(2, 3) -> 0.75000000
|
| 96 |
+
|
| 97 |
+
References
|
| 98 |
+
----------
|
| 99 |
+
.. [1] T. Zhou, L. Lu, Y.-C. Zhang.
|
| 100 |
+
Predicting missing links via local information.
|
| 101 |
+
Eur. Phys. J. B 71 (2009) 623.
|
| 102 |
+
https://arxiv.org/pdf/0901.0553.pdf
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
def predict(u, v):
|
| 106 |
+
return sum(1 / G.degree(w) for w in nx.common_neighbors(G, u, v))
|
| 107 |
+
|
| 108 |
+
return _apply_prediction(G, predict, ebunch)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@not_implemented_for("directed")
|
| 112 |
+
@not_implemented_for("multigraph")
|
| 113 |
+
@nx._dispatchable
|
| 114 |
+
def jaccard_coefficient(G, ebunch=None):
|
| 115 |
+
r"""Compute the Jaccard coefficient of all node pairs in ebunch.
|
| 116 |
+
|
| 117 |
+
Jaccard coefficient of nodes `u` and `v` is defined as
|
| 118 |
+
|
| 119 |
+
.. math::
|
| 120 |
+
|
| 121 |
+
\frac{|\Gamma(u) \cap \Gamma(v)|}{|\Gamma(u) \cup \Gamma(v)|}
|
| 122 |
+
|
| 123 |
+
where $\Gamma(u)$ denotes the set of neighbors of $u$.
|
| 124 |
+
|
| 125 |
+
Parameters
|
| 126 |
+
----------
|
| 127 |
+
G : graph
|
| 128 |
+
A NetworkX undirected graph.
|
| 129 |
+
|
| 130 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 131 |
+
Jaccard coefficient will be computed for each pair of nodes
|
| 132 |
+
given in the iterable. The pairs must be given as 2-tuples
|
| 133 |
+
(u, v) where u and v are nodes in the graph. If ebunch is None
|
| 134 |
+
then all nonexistent edges in the graph will be used.
|
| 135 |
+
Default value: None.
|
| 136 |
+
|
| 137 |
+
Returns
|
| 138 |
+
-------
|
| 139 |
+
piter : iterator
|
| 140 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 141 |
+
pair of nodes and p is their Jaccard coefficient.
|
| 142 |
+
|
| 143 |
+
Raises
|
| 144 |
+
------
|
| 145 |
+
NetworkXNotImplemented
|
| 146 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 147 |
+
|
| 148 |
+
NodeNotFound
|
| 149 |
+
If `ebunch` has a node that is not in `G`.
|
| 150 |
+
|
| 151 |
+
Examples
|
| 152 |
+
--------
|
| 153 |
+
>>> G = nx.complete_graph(5)
|
| 154 |
+
>>> preds = nx.jaccard_coefficient(G, [(0, 1), (2, 3)])
|
| 155 |
+
>>> for u, v, p in preds:
|
| 156 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 157 |
+
(0, 1) -> 0.60000000
|
| 158 |
+
(2, 3) -> 0.60000000
|
| 159 |
+
|
| 160 |
+
References
|
| 161 |
+
----------
|
| 162 |
+
.. [1] D. Liben-Nowell, J. Kleinberg.
|
| 163 |
+
The Link Prediction Problem for Social Networks (2004).
|
| 164 |
+
http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
def predict(u, v):
|
| 168 |
+
union_size = len(set(G[u]) | set(G[v]))
|
| 169 |
+
if union_size == 0:
|
| 170 |
+
return 0
|
| 171 |
+
return len(nx.common_neighbors(G, u, v)) / union_size
|
| 172 |
+
|
| 173 |
+
return _apply_prediction(G, predict, ebunch)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
@not_implemented_for("directed")
|
| 177 |
+
@not_implemented_for("multigraph")
|
| 178 |
+
@nx._dispatchable
|
| 179 |
+
def adamic_adar_index(G, ebunch=None):
|
| 180 |
+
r"""Compute the Adamic-Adar index of all node pairs in ebunch.
|
| 181 |
+
|
| 182 |
+
Adamic-Adar index of `u` and `v` is defined as
|
| 183 |
+
|
| 184 |
+
.. math::
|
| 185 |
+
|
| 186 |
+
\sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{1}{\log |\Gamma(w)|}
|
| 187 |
+
|
| 188 |
+
where $\Gamma(u)$ denotes the set of neighbors of $u$.
|
| 189 |
+
This index leads to zero-division for nodes only connected via self-loops.
|
| 190 |
+
It is intended to be used when no self-loops are present.
|
| 191 |
+
|
| 192 |
+
Parameters
|
| 193 |
+
----------
|
| 194 |
+
G : graph
|
| 195 |
+
NetworkX undirected graph.
|
| 196 |
+
|
| 197 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 198 |
+
Adamic-Adar index will be computed for each pair of nodes given
|
| 199 |
+
in the iterable. The pairs must be given as 2-tuples (u, v)
|
| 200 |
+
where u and v are nodes in the graph. If ebunch is None then all
|
| 201 |
+
nonexistent edges in the graph will be used.
|
| 202 |
+
Default value: None.
|
| 203 |
+
|
| 204 |
+
Returns
|
| 205 |
+
-------
|
| 206 |
+
piter : iterator
|
| 207 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 208 |
+
pair of nodes and p is their Adamic-Adar index.
|
| 209 |
+
|
| 210 |
+
Raises
|
| 211 |
+
------
|
| 212 |
+
NetworkXNotImplemented
|
| 213 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 214 |
+
|
| 215 |
+
NodeNotFound
|
| 216 |
+
If `ebunch` has a node that is not in `G`.
|
| 217 |
+
|
| 218 |
+
Examples
|
| 219 |
+
--------
|
| 220 |
+
>>> G = nx.complete_graph(5)
|
| 221 |
+
>>> preds = nx.adamic_adar_index(G, [(0, 1), (2, 3)])
|
| 222 |
+
>>> for u, v, p in preds:
|
| 223 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 224 |
+
(0, 1) -> 2.16404256
|
| 225 |
+
(2, 3) -> 2.16404256
|
| 226 |
+
|
| 227 |
+
References
|
| 228 |
+
----------
|
| 229 |
+
.. [1] D. Liben-Nowell, J. Kleinberg.
|
| 230 |
+
The Link Prediction Problem for Social Networks (2004).
|
| 231 |
+
http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
|
| 232 |
+
"""
|
| 233 |
+
|
| 234 |
+
def predict(u, v):
|
| 235 |
+
return sum(1 / log(G.degree(w)) for w in nx.common_neighbors(G, u, v))
|
| 236 |
+
|
| 237 |
+
return _apply_prediction(G, predict, ebunch)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
@not_implemented_for("directed")
|
| 241 |
+
@not_implemented_for("multigraph")
|
| 242 |
+
@nx._dispatchable
|
| 243 |
+
def common_neighbor_centrality(G, ebunch=None, alpha=0.8):
|
| 244 |
+
r"""Return the CCPA score for each pair of nodes.
|
| 245 |
+
|
| 246 |
+
Compute the Common Neighbor and Centrality based Parameterized Algorithm(CCPA)
|
| 247 |
+
score of all node pairs in ebunch.
|
| 248 |
+
|
| 249 |
+
CCPA score of `u` and `v` is defined as
|
| 250 |
+
|
| 251 |
+
.. math::
|
| 252 |
+
|
| 253 |
+
\alpha \cdot (|\Gamma (u){\cap }^{}\Gamma (v)|)+(1-\alpha )\cdot \frac{N}{{d}_{uv}}
|
| 254 |
+
|
| 255 |
+
where $\Gamma(u)$ denotes the set of neighbors of $u$, $\Gamma(v)$ denotes the
|
| 256 |
+
set of neighbors of $v$, $\alpha$ is parameter varies between [0,1], $N$ denotes
|
| 257 |
+
total number of nodes in the Graph and ${d}_{uv}$ denotes shortest distance
|
| 258 |
+
between $u$ and $v$.
|
| 259 |
+
|
| 260 |
+
This algorithm is based on two vital properties of nodes, namely the number
|
| 261 |
+
of common neighbors and their centrality. Common neighbor refers to the common
|
| 262 |
+
nodes between two nodes. Centrality refers to the prestige that a node enjoys
|
| 263 |
+
in a network.
|
| 264 |
+
|
| 265 |
+
.. seealso::
|
| 266 |
+
|
| 267 |
+
:func:`common_neighbors`
|
| 268 |
+
|
| 269 |
+
Parameters
|
| 270 |
+
----------
|
| 271 |
+
G : graph
|
| 272 |
+
NetworkX undirected graph.
|
| 273 |
+
|
| 274 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 275 |
+
Preferential attachment score will be computed for each pair of
|
| 276 |
+
nodes given in the iterable. The pairs must be given as
|
| 277 |
+
2-tuples (u, v) where u and v are nodes in the graph. If ebunch
|
| 278 |
+
is None then all nonexistent edges in the graph will be used.
|
| 279 |
+
Default value: None.
|
| 280 |
+
|
| 281 |
+
alpha : Parameter defined for participation of Common Neighbor
|
| 282 |
+
and Centrality Algorithm share. Values for alpha should
|
| 283 |
+
normally be between 0 and 1. Default value set to 0.8
|
| 284 |
+
because author found better performance at 0.8 for all the
|
| 285 |
+
dataset.
|
| 286 |
+
Default value: 0.8
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
Returns
|
| 290 |
+
-------
|
| 291 |
+
piter : iterator
|
| 292 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 293 |
+
pair of nodes and p is their Common Neighbor and Centrality based
|
| 294 |
+
Parameterized Algorithm(CCPA) score.
|
| 295 |
+
|
| 296 |
+
Raises
|
| 297 |
+
------
|
| 298 |
+
NetworkXNotImplemented
|
| 299 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 300 |
+
|
| 301 |
+
NetworkXAlgorithmError
|
| 302 |
+
If self loops exist in `ebunch` or in `G` (if `ebunch` is `None`).
|
| 303 |
+
|
| 304 |
+
NodeNotFound
|
| 305 |
+
If `ebunch` has a node that is not in `G`.
|
| 306 |
+
|
| 307 |
+
Examples
|
| 308 |
+
--------
|
| 309 |
+
>>> G = nx.complete_graph(5)
|
| 310 |
+
>>> preds = nx.common_neighbor_centrality(G, [(0, 1), (2, 3)])
|
| 311 |
+
>>> for u, v, p in preds:
|
| 312 |
+
... print(f"({u}, {v}) -> {p}")
|
| 313 |
+
(0, 1) -> 3.4000000000000004
|
| 314 |
+
(2, 3) -> 3.4000000000000004
|
| 315 |
+
|
| 316 |
+
References
|
| 317 |
+
----------
|
| 318 |
+
.. [1] Ahmad, I., Akhtar, M.U., Noor, S. et al.
|
| 319 |
+
Missing Link Prediction using Common Neighbor and Centrality based Parameterized Algorithm.
|
| 320 |
+
Sci Rep 10, 364 (2020).
|
| 321 |
+
https://doi.org/10.1038/s41598-019-57304-y
|
| 322 |
+
"""
|
| 323 |
+
|
| 324 |
+
# When alpha == 1, the CCPA score simplifies to the number of common neighbors.
|
| 325 |
+
if alpha == 1:
|
| 326 |
+
|
| 327 |
+
def predict(u, v):
|
| 328 |
+
if u == v:
|
| 329 |
+
raise nx.NetworkXAlgorithmError("Self loops are not supported")
|
| 330 |
+
|
| 331 |
+
return len(nx.common_neighbors(G, u, v))
|
| 332 |
+
|
| 333 |
+
else:
|
| 334 |
+
spl = dict(nx.shortest_path_length(G))
|
| 335 |
+
inf = float("inf")
|
| 336 |
+
|
| 337 |
+
def predict(u, v):
|
| 338 |
+
if u == v:
|
| 339 |
+
raise nx.NetworkXAlgorithmError("Self loops are not supported")
|
| 340 |
+
path_len = spl[u].get(v, inf)
|
| 341 |
+
|
| 342 |
+
n_nbrs = len(nx.common_neighbors(G, u, v))
|
| 343 |
+
return alpha * n_nbrs + (1 - alpha) * len(G) / path_len
|
| 344 |
+
|
| 345 |
+
return _apply_prediction(G, predict, ebunch)
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
@not_implemented_for("directed")
|
| 349 |
+
@not_implemented_for("multigraph")
|
| 350 |
+
@nx._dispatchable
|
| 351 |
+
def preferential_attachment(G, ebunch=None):
|
| 352 |
+
r"""Compute the preferential attachment score of all node pairs in ebunch.
|
| 353 |
+
|
| 354 |
+
Preferential attachment score of `u` and `v` is defined as
|
| 355 |
+
|
| 356 |
+
.. math::
|
| 357 |
+
|
| 358 |
+
|\Gamma(u)| |\Gamma(v)|
|
| 359 |
+
|
| 360 |
+
where $\Gamma(u)$ denotes the set of neighbors of $u$.
|
| 361 |
+
|
| 362 |
+
Parameters
|
| 363 |
+
----------
|
| 364 |
+
G : graph
|
| 365 |
+
NetworkX undirected graph.
|
| 366 |
+
|
| 367 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 368 |
+
Preferential attachment score will be computed for each pair of
|
| 369 |
+
nodes given in the iterable. The pairs must be given as
|
| 370 |
+
2-tuples (u, v) where u and v are nodes in the graph. If ebunch
|
| 371 |
+
is None then all nonexistent edges in the graph will be used.
|
| 372 |
+
Default value: None.
|
| 373 |
+
|
| 374 |
+
Returns
|
| 375 |
+
-------
|
| 376 |
+
piter : iterator
|
| 377 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 378 |
+
pair of nodes and p is their preferential attachment score.
|
| 379 |
+
|
| 380 |
+
Raises
|
| 381 |
+
------
|
| 382 |
+
NetworkXNotImplemented
|
| 383 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 384 |
+
|
| 385 |
+
NodeNotFound
|
| 386 |
+
If `ebunch` has a node that is not in `G`.
|
| 387 |
+
|
| 388 |
+
Examples
|
| 389 |
+
--------
|
| 390 |
+
>>> G = nx.complete_graph(5)
|
| 391 |
+
>>> preds = nx.preferential_attachment(G, [(0, 1), (2, 3)])
|
| 392 |
+
>>> for u, v, p in preds:
|
| 393 |
+
... print(f"({u}, {v}) -> {p}")
|
| 394 |
+
(0, 1) -> 16
|
| 395 |
+
(2, 3) -> 16
|
| 396 |
+
|
| 397 |
+
References
|
| 398 |
+
----------
|
| 399 |
+
.. [1] D. Liben-Nowell, J. Kleinberg.
|
| 400 |
+
The Link Prediction Problem for Social Networks (2004).
|
| 401 |
+
http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
|
| 402 |
+
"""
|
| 403 |
+
|
| 404 |
+
def predict(u, v):
|
| 405 |
+
return G.degree(u) * G.degree(v)
|
| 406 |
+
|
| 407 |
+
return _apply_prediction(G, predict, ebunch)
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
@not_implemented_for("directed")
|
| 411 |
+
@not_implemented_for("multigraph")
|
| 412 |
+
@nx._dispatchable(node_attrs="community")
|
| 413 |
+
def cn_soundarajan_hopcroft(G, ebunch=None, community="community"):
|
| 414 |
+
r"""Count the number of common neighbors of all node pairs in ebunch
|
| 415 |
+
using community information.
|
| 416 |
+
|
| 417 |
+
For two nodes $u$ and $v$, this function computes the number of
|
| 418 |
+
common neighbors and bonus one for each common neighbor belonging to
|
| 419 |
+
the same community as $u$ and $v$. Mathematically,
|
| 420 |
+
|
| 421 |
+
.. math::
|
| 422 |
+
|
| 423 |
+
|\Gamma(u) \cap \Gamma(v)| + \sum_{w \in \Gamma(u) \cap \Gamma(v)} f(w)
|
| 424 |
+
|
| 425 |
+
where $f(w)$ equals 1 if $w$ belongs to the same community as $u$
|
| 426 |
+
and $v$ or 0 otherwise and $\Gamma(u)$ denotes the set of
|
| 427 |
+
neighbors of $u$.
|
| 428 |
+
|
| 429 |
+
Parameters
|
| 430 |
+
----------
|
| 431 |
+
G : graph
|
| 432 |
+
A NetworkX undirected graph.
|
| 433 |
+
|
| 434 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 435 |
+
The score will be computed for each pair of nodes given in the
|
| 436 |
+
iterable. The pairs must be given as 2-tuples (u, v) where u
|
| 437 |
+
and v are nodes in the graph. If ebunch is None then all
|
| 438 |
+
nonexistent edges in the graph will be used.
|
| 439 |
+
Default value: None.
|
| 440 |
+
|
| 441 |
+
community : string, optional (default = 'community')
|
| 442 |
+
Nodes attribute name containing the community information.
|
| 443 |
+
G[u][community] identifies which community u belongs to. Each
|
| 444 |
+
node belongs to at most one community. Default value: 'community'.
|
| 445 |
+
|
| 446 |
+
Returns
|
| 447 |
+
-------
|
| 448 |
+
piter : iterator
|
| 449 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 450 |
+
pair of nodes and p is their score.
|
| 451 |
+
|
| 452 |
+
Raises
|
| 453 |
+
------
|
| 454 |
+
NetworkXNotImplemented
|
| 455 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 456 |
+
|
| 457 |
+
NetworkXAlgorithmError
|
| 458 |
+
If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
|
| 459 |
+
|
| 460 |
+
NodeNotFound
|
| 461 |
+
If `ebunch` has a node that is not in `G`.
|
| 462 |
+
|
| 463 |
+
Examples
|
| 464 |
+
--------
|
| 465 |
+
>>> G = nx.path_graph(3)
|
| 466 |
+
>>> G.nodes[0]["community"] = 0
|
| 467 |
+
>>> G.nodes[1]["community"] = 0
|
| 468 |
+
>>> G.nodes[2]["community"] = 0
|
| 469 |
+
>>> preds = nx.cn_soundarajan_hopcroft(G, [(0, 2)])
|
| 470 |
+
>>> for u, v, p in preds:
|
| 471 |
+
... print(f"({u}, {v}) -> {p}")
|
| 472 |
+
(0, 2) -> 2
|
| 473 |
+
|
| 474 |
+
References
|
| 475 |
+
----------
|
| 476 |
+
.. [1] Sucheta Soundarajan and John Hopcroft.
|
| 477 |
+
Using community information to improve the precision of link
|
| 478 |
+
prediction methods.
|
| 479 |
+
In Proceedings of the 21st international conference companion on
|
| 480 |
+
World Wide Web (WWW '12 Companion). ACM, New York, NY, USA, 607-608.
|
| 481 |
+
http://doi.acm.org/10.1145/2187980.2188150
|
| 482 |
+
"""
|
| 483 |
+
|
| 484 |
+
def predict(u, v):
|
| 485 |
+
Cu = _community(G, u, community)
|
| 486 |
+
Cv = _community(G, v, community)
|
| 487 |
+
cnbors = nx.common_neighbors(G, u, v)
|
| 488 |
+
neighbors = (
|
| 489 |
+
sum(_community(G, w, community) == Cu for w in cnbors) if Cu == Cv else 0
|
| 490 |
+
)
|
| 491 |
+
return len(cnbors) + neighbors
|
| 492 |
+
|
| 493 |
+
return _apply_prediction(G, predict, ebunch)
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
@not_implemented_for("directed")
|
| 497 |
+
@not_implemented_for("multigraph")
|
| 498 |
+
@nx._dispatchable(node_attrs="community")
|
| 499 |
+
def ra_index_soundarajan_hopcroft(G, ebunch=None, community="community"):
|
| 500 |
+
r"""Compute the resource allocation index of all node pairs in
|
| 501 |
+
ebunch using community information.
|
| 502 |
+
|
| 503 |
+
For two nodes $u$ and $v$, this function computes the resource
|
| 504 |
+
allocation index considering only common neighbors belonging to the
|
| 505 |
+
same community as $u$ and $v$. Mathematically,
|
| 506 |
+
|
| 507 |
+
.. math::
|
| 508 |
+
|
| 509 |
+
\sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{f(w)}{|\Gamma(w)|}
|
| 510 |
+
|
| 511 |
+
where $f(w)$ equals 1 if $w$ belongs to the same community as $u$
|
| 512 |
+
and $v$ or 0 otherwise and $\Gamma(u)$ denotes the set of
|
| 513 |
+
neighbors of $u$.
|
| 514 |
+
|
| 515 |
+
Parameters
|
| 516 |
+
----------
|
| 517 |
+
G : graph
|
| 518 |
+
A NetworkX undirected graph.
|
| 519 |
+
|
| 520 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 521 |
+
The score will be computed for each pair of nodes given in the
|
| 522 |
+
iterable. The pairs must be given as 2-tuples (u, v) where u
|
| 523 |
+
and v are nodes in the graph. If ebunch is None then all
|
| 524 |
+
nonexistent edges in the graph will be used.
|
| 525 |
+
Default value: None.
|
| 526 |
+
|
| 527 |
+
community : string, optional (default = 'community')
|
| 528 |
+
Nodes attribute name containing the community information.
|
| 529 |
+
G[u][community] identifies which community u belongs to. Each
|
| 530 |
+
node belongs to at most one community. Default value: 'community'.
|
| 531 |
+
|
| 532 |
+
Returns
|
| 533 |
+
-------
|
| 534 |
+
piter : iterator
|
| 535 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 536 |
+
pair of nodes and p is their score.
|
| 537 |
+
|
| 538 |
+
Raises
|
| 539 |
+
------
|
| 540 |
+
NetworkXNotImplemented
|
| 541 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 542 |
+
|
| 543 |
+
NetworkXAlgorithmError
|
| 544 |
+
If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
|
| 545 |
+
|
| 546 |
+
NodeNotFound
|
| 547 |
+
If `ebunch` has a node that is not in `G`.
|
| 548 |
+
|
| 549 |
+
Examples
|
| 550 |
+
--------
|
| 551 |
+
>>> G = nx.Graph()
|
| 552 |
+
>>> G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 553 |
+
>>> G.nodes[0]["community"] = 0
|
| 554 |
+
>>> G.nodes[1]["community"] = 0
|
| 555 |
+
>>> G.nodes[2]["community"] = 1
|
| 556 |
+
>>> G.nodes[3]["community"] = 0
|
| 557 |
+
>>> preds = nx.ra_index_soundarajan_hopcroft(G, [(0, 3)])
|
| 558 |
+
>>> for u, v, p in preds:
|
| 559 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 560 |
+
(0, 3) -> 0.50000000
|
| 561 |
+
|
| 562 |
+
References
|
| 563 |
+
----------
|
| 564 |
+
.. [1] Sucheta Soundarajan and John Hopcroft.
|
| 565 |
+
Using community information to improve the precision of link
|
| 566 |
+
prediction methods.
|
| 567 |
+
In Proceedings of the 21st international conference companion on
|
| 568 |
+
World Wide Web (WWW '12 Companion). ACM, New York, NY, USA, 607-608.
|
| 569 |
+
http://doi.acm.org/10.1145/2187980.2188150
|
| 570 |
+
"""
|
| 571 |
+
|
| 572 |
+
def predict(u, v):
|
| 573 |
+
Cu = _community(G, u, community)
|
| 574 |
+
Cv = _community(G, v, community)
|
| 575 |
+
if Cu != Cv:
|
| 576 |
+
return 0
|
| 577 |
+
cnbors = nx.common_neighbors(G, u, v)
|
| 578 |
+
return sum(1 / G.degree(w) for w in cnbors if _community(G, w, community) == Cu)
|
| 579 |
+
|
| 580 |
+
return _apply_prediction(G, predict, ebunch)
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
@not_implemented_for("directed")
|
| 584 |
+
@not_implemented_for("multigraph")
|
| 585 |
+
@nx._dispatchable(node_attrs="community")
|
| 586 |
+
def within_inter_cluster(G, ebunch=None, delta=0.001, community="community"):
|
| 587 |
+
"""Compute the ratio of within- and inter-cluster common neighbors
|
| 588 |
+
of all node pairs in ebunch.
|
| 589 |
+
|
| 590 |
+
For two nodes `u` and `v`, if a common neighbor `w` belongs to the
|
| 591 |
+
same community as them, `w` is considered as within-cluster common
|
| 592 |
+
neighbor of `u` and `v`. Otherwise, it is considered as
|
| 593 |
+
inter-cluster common neighbor of `u` and `v`. The ratio between the
|
| 594 |
+
size of the set of within- and inter-cluster common neighbors is
|
| 595 |
+
defined as the WIC measure. [1]_
|
| 596 |
+
|
| 597 |
+
Parameters
|
| 598 |
+
----------
|
| 599 |
+
G : graph
|
| 600 |
+
A NetworkX undirected graph.
|
| 601 |
+
|
| 602 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 603 |
+
The WIC measure will be computed for each pair of nodes given in
|
| 604 |
+
the iterable. The pairs must be given as 2-tuples (u, v) where
|
| 605 |
+
u and v are nodes in the graph. If ebunch is None then all
|
| 606 |
+
nonexistent edges in the graph will be used.
|
| 607 |
+
Default value: None.
|
| 608 |
+
|
| 609 |
+
delta : float, optional (default = 0.001)
|
| 610 |
+
Value to prevent division by zero in case there is no
|
| 611 |
+
inter-cluster common neighbor between two nodes. See [1]_ for
|
| 612 |
+
details. Default value: 0.001.
|
| 613 |
+
|
| 614 |
+
community : string, optional (default = 'community')
|
| 615 |
+
Nodes attribute name containing the community information.
|
| 616 |
+
G[u][community] identifies which community u belongs to. Each
|
| 617 |
+
node belongs to at most one community. Default value: 'community'.
|
| 618 |
+
|
| 619 |
+
Returns
|
| 620 |
+
-------
|
| 621 |
+
piter : iterator
|
| 622 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 623 |
+
pair of nodes and p is their WIC measure.
|
| 624 |
+
|
| 625 |
+
Raises
|
| 626 |
+
------
|
| 627 |
+
NetworkXNotImplemented
|
| 628 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 629 |
+
|
| 630 |
+
NetworkXAlgorithmError
|
| 631 |
+
- If `delta` is less than or equal to zero.
|
| 632 |
+
- If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
|
| 633 |
+
|
| 634 |
+
NodeNotFound
|
| 635 |
+
If `ebunch` has a node that is not in `G`.
|
| 636 |
+
|
| 637 |
+
Examples
|
| 638 |
+
--------
|
| 639 |
+
>>> G = nx.Graph()
|
| 640 |
+
>>> G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)])
|
| 641 |
+
>>> G.nodes[0]["community"] = 0
|
| 642 |
+
>>> G.nodes[1]["community"] = 1
|
| 643 |
+
>>> G.nodes[2]["community"] = 0
|
| 644 |
+
>>> G.nodes[3]["community"] = 0
|
| 645 |
+
>>> G.nodes[4]["community"] = 0
|
| 646 |
+
>>> preds = nx.within_inter_cluster(G, [(0, 4)])
|
| 647 |
+
>>> for u, v, p in preds:
|
| 648 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 649 |
+
(0, 4) -> 1.99800200
|
| 650 |
+
>>> preds = nx.within_inter_cluster(G, [(0, 4)], delta=0.5)
|
| 651 |
+
>>> for u, v, p in preds:
|
| 652 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 653 |
+
(0, 4) -> 1.33333333
|
| 654 |
+
|
| 655 |
+
References
|
| 656 |
+
----------
|
| 657 |
+
.. [1] Jorge Carlos Valverde-Rebaza and Alneu de Andrade Lopes.
|
| 658 |
+
Link prediction in complex networks based on cluster information.
|
| 659 |
+
In Proceedings of the 21st Brazilian conference on Advances in
|
| 660 |
+
Artificial Intelligence (SBIA'12)
|
| 661 |
+
https://doi.org/10.1007/978-3-642-34459-6_10
|
| 662 |
+
"""
|
| 663 |
+
if delta <= 0:
|
| 664 |
+
raise nx.NetworkXAlgorithmError("Delta must be greater than zero")
|
| 665 |
+
|
| 666 |
+
def predict(u, v):
|
| 667 |
+
Cu = _community(G, u, community)
|
| 668 |
+
Cv = _community(G, v, community)
|
| 669 |
+
if Cu != Cv:
|
| 670 |
+
return 0
|
| 671 |
+
cnbors = nx.common_neighbors(G, u, v)
|
| 672 |
+
within = {w for w in cnbors if _community(G, w, community) == Cu}
|
| 673 |
+
inter = cnbors - within
|
| 674 |
+
return len(within) / (len(inter) + delta)
|
| 675 |
+
|
| 676 |
+
return _apply_prediction(G, predict, ebunch)
|
| 677 |
+
|
| 678 |
+
|
| 679 |
+
def _community(G, u, community):
|
| 680 |
+
"""Get the community of the given node."""
|
| 681 |
+
node_u = G.nodes[u]
|
| 682 |
+
try:
|
| 683 |
+
return node_u[community]
|
| 684 |
+
except KeyError as err:
|
| 685 |
+
raise nx.NetworkXAlgorithmError(
|
| 686 |
+
f"No community information available for Node {u}"
|
| 687 |
+
) from err
|
.venv/lib/python3.11/site-packages/networkx/algorithms/lowest_common_ancestors.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Algorithms for finding the lowest common ancestor of trees and DAGs."""
|
| 2 |
+
|
| 3 |
+
from collections import defaultdict
|
| 4 |
+
from collections.abc import Mapping, Set
|
| 5 |
+
from itertools import combinations_with_replacement
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
from networkx.utils import UnionFind, arbitrary_element, not_implemented_for
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
"all_pairs_lowest_common_ancestor",
|
| 12 |
+
"tree_all_pairs_lowest_common_ancestor",
|
| 13 |
+
"lowest_common_ancestor",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@not_implemented_for("undirected")
|
| 18 |
+
@nx._dispatchable
|
| 19 |
+
def all_pairs_lowest_common_ancestor(G, pairs=None):
|
| 20 |
+
"""Return the lowest common ancestor of all pairs or the provided pairs
|
| 21 |
+
|
| 22 |
+
Parameters
|
| 23 |
+
----------
|
| 24 |
+
G : NetworkX directed graph
|
| 25 |
+
|
| 26 |
+
pairs : iterable of pairs of nodes, optional (default: all pairs)
|
| 27 |
+
The pairs of nodes of interest.
|
| 28 |
+
If None, will find the LCA of all pairs of nodes.
|
| 29 |
+
|
| 30 |
+
Yields
|
| 31 |
+
------
|
| 32 |
+
((node1, node2), lca) : 2-tuple
|
| 33 |
+
Where lca is least common ancestor of node1 and node2.
|
| 34 |
+
Note that for the default case, the order of the node pair is not considered,
|
| 35 |
+
e.g. you will not get both ``(a, b)`` and ``(b, a)``
|
| 36 |
+
|
| 37 |
+
Raises
|
| 38 |
+
------
|
| 39 |
+
NetworkXPointlessConcept
|
| 40 |
+
If `G` is null.
|
| 41 |
+
NetworkXError
|
| 42 |
+
If `G` is not a DAG.
|
| 43 |
+
|
| 44 |
+
Examples
|
| 45 |
+
--------
|
| 46 |
+
The default behavior is to yield the lowest common ancestor for all
|
| 47 |
+
possible combinations of nodes in `G`, including self-pairings:
|
| 48 |
+
|
| 49 |
+
>>> G = nx.DiGraph([(0, 1), (0, 3), (1, 2)])
|
| 50 |
+
>>> dict(nx.all_pairs_lowest_common_ancestor(G))
|
| 51 |
+
{(0, 0): 0, (0, 1): 0, (0, 3): 0, (0, 2): 0, (1, 1): 1, (1, 3): 0, (1, 2): 1, (3, 3): 3, (3, 2): 0, (2, 2): 2}
|
| 52 |
+
|
| 53 |
+
The pairs argument can be used to limit the output to only the
|
| 54 |
+
specified node pairings:
|
| 55 |
+
|
| 56 |
+
>>> dict(nx.all_pairs_lowest_common_ancestor(G, pairs=[(1, 2), (2, 3)]))
|
| 57 |
+
{(1, 2): 1, (2, 3): 0}
|
| 58 |
+
|
| 59 |
+
Notes
|
| 60 |
+
-----
|
| 61 |
+
Only defined on non-null directed acyclic graphs.
|
| 62 |
+
|
| 63 |
+
See Also
|
| 64 |
+
--------
|
| 65 |
+
lowest_common_ancestor
|
| 66 |
+
"""
|
| 67 |
+
if not nx.is_directed_acyclic_graph(G):
|
| 68 |
+
raise nx.NetworkXError("LCA only defined on directed acyclic graphs.")
|
| 69 |
+
if len(G) == 0:
|
| 70 |
+
raise nx.NetworkXPointlessConcept("LCA meaningless on null graphs.")
|
| 71 |
+
|
| 72 |
+
if pairs is None:
|
| 73 |
+
pairs = combinations_with_replacement(G, 2)
|
| 74 |
+
else:
|
| 75 |
+
# Convert iterator to iterable, if necessary. Trim duplicates.
|
| 76 |
+
pairs = dict.fromkeys(pairs)
|
| 77 |
+
# Verify that each of the nodes in the provided pairs is in G
|
| 78 |
+
nodeset = set(G)
|
| 79 |
+
for pair in pairs:
|
| 80 |
+
if set(pair) - nodeset:
|
| 81 |
+
raise nx.NodeNotFound(
|
| 82 |
+
f"Node(s) {set(pair) - nodeset} from pair {pair} not in G."
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Once input validation is done, construct the generator
|
| 86 |
+
def generate_lca_from_pairs(G, pairs):
|
| 87 |
+
ancestor_cache = {}
|
| 88 |
+
|
| 89 |
+
for v, w in pairs:
|
| 90 |
+
if v not in ancestor_cache:
|
| 91 |
+
ancestor_cache[v] = nx.ancestors(G, v)
|
| 92 |
+
ancestor_cache[v].add(v)
|
| 93 |
+
if w not in ancestor_cache:
|
| 94 |
+
ancestor_cache[w] = nx.ancestors(G, w)
|
| 95 |
+
ancestor_cache[w].add(w)
|
| 96 |
+
|
| 97 |
+
common_ancestors = ancestor_cache[v] & ancestor_cache[w]
|
| 98 |
+
|
| 99 |
+
if common_ancestors:
|
| 100 |
+
common_ancestor = next(iter(common_ancestors))
|
| 101 |
+
while True:
|
| 102 |
+
successor = None
|
| 103 |
+
for lower_ancestor in G.successors(common_ancestor):
|
| 104 |
+
if lower_ancestor in common_ancestors:
|
| 105 |
+
successor = lower_ancestor
|
| 106 |
+
break
|
| 107 |
+
if successor is None:
|
| 108 |
+
break
|
| 109 |
+
common_ancestor = successor
|
| 110 |
+
yield ((v, w), common_ancestor)
|
| 111 |
+
|
| 112 |
+
return generate_lca_from_pairs(G, pairs)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@not_implemented_for("undirected")
|
| 116 |
+
@nx._dispatchable
|
| 117 |
+
def lowest_common_ancestor(G, node1, node2, default=None):
|
| 118 |
+
"""Compute the lowest common ancestor of the given pair of nodes.
|
| 119 |
+
|
| 120 |
+
Parameters
|
| 121 |
+
----------
|
| 122 |
+
G : NetworkX directed graph
|
| 123 |
+
|
| 124 |
+
node1, node2 : nodes in the graph.
|
| 125 |
+
|
| 126 |
+
default : object
|
| 127 |
+
Returned if no common ancestor between `node1` and `node2`
|
| 128 |
+
|
| 129 |
+
Returns
|
| 130 |
+
-------
|
| 131 |
+
The lowest common ancestor of node1 and node2,
|
| 132 |
+
or default if they have no common ancestors.
|
| 133 |
+
|
| 134 |
+
Examples
|
| 135 |
+
--------
|
| 136 |
+
>>> G = nx.DiGraph()
|
| 137 |
+
>>> nx.add_path(G, (0, 1, 2, 3))
|
| 138 |
+
>>> nx.add_path(G, (0, 4, 3))
|
| 139 |
+
>>> nx.lowest_common_ancestor(G, 2, 4)
|
| 140 |
+
0
|
| 141 |
+
|
| 142 |
+
See Also
|
| 143 |
+
--------
|
| 144 |
+
all_pairs_lowest_common_ancestor"""
|
| 145 |
+
|
| 146 |
+
ans = list(all_pairs_lowest_common_ancestor(G, pairs=[(node1, node2)]))
|
| 147 |
+
if ans:
|
| 148 |
+
assert len(ans) == 1
|
| 149 |
+
return ans[0][1]
|
| 150 |
+
return default
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@not_implemented_for("undirected")
|
| 154 |
+
@nx._dispatchable
|
| 155 |
+
def tree_all_pairs_lowest_common_ancestor(G, root=None, pairs=None):
|
| 156 |
+
r"""Yield the lowest common ancestor for sets of pairs in a tree.
|
| 157 |
+
|
| 158 |
+
Parameters
|
| 159 |
+
----------
|
| 160 |
+
G : NetworkX directed graph (must be a tree)
|
| 161 |
+
|
| 162 |
+
root : node, optional (default: None)
|
| 163 |
+
The root of the subtree to operate on.
|
| 164 |
+
If None, assume the entire graph has exactly one source and use that.
|
| 165 |
+
|
| 166 |
+
pairs : iterable or iterator of pairs of nodes, optional (default: None)
|
| 167 |
+
The pairs of interest. If None, Defaults to all pairs of nodes
|
| 168 |
+
under `root` that have a lowest common ancestor.
|
| 169 |
+
|
| 170 |
+
Returns
|
| 171 |
+
-------
|
| 172 |
+
lcas : generator of tuples `((u, v), lca)` where `u` and `v` are nodes
|
| 173 |
+
in `pairs` and `lca` is their lowest common ancestor.
|
| 174 |
+
|
| 175 |
+
Examples
|
| 176 |
+
--------
|
| 177 |
+
>>> import pprint
|
| 178 |
+
>>> G = nx.DiGraph([(1, 3), (2, 4), (1, 2)])
|
| 179 |
+
>>> pprint.pprint(dict(nx.tree_all_pairs_lowest_common_ancestor(G)))
|
| 180 |
+
{(1, 1): 1,
|
| 181 |
+
(2, 1): 1,
|
| 182 |
+
(2, 2): 2,
|
| 183 |
+
(3, 1): 1,
|
| 184 |
+
(3, 2): 1,
|
| 185 |
+
(3, 3): 3,
|
| 186 |
+
(3, 4): 1,
|
| 187 |
+
(4, 1): 1,
|
| 188 |
+
(4, 2): 2,
|
| 189 |
+
(4, 4): 4}
|
| 190 |
+
|
| 191 |
+
We can also use `pairs` argument to specify the pairs of nodes for which we
|
| 192 |
+
want to compute lowest common ancestors. Here is an example:
|
| 193 |
+
|
| 194 |
+
>>> dict(nx.tree_all_pairs_lowest_common_ancestor(G, pairs=[(1, 4), (2, 3)]))
|
| 195 |
+
{(2, 3): 1, (1, 4): 1}
|
| 196 |
+
|
| 197 |
+
Notes
|
| 198 |
+
-----
|
| 199 |
+
Only defined on non-null trees represented with directed edges from
|
| 200 |
+
parents to children. Uses Tarjan's off-line lowest-common-ancestors
|
| 201 |
+
algorithm. Runs in time $O(4 \times (V + E + P))$ time, where 4 is the largest
|
| 202 |
+
value of the inverse Ackermann function likely to ever come up in actual
|
| 203 |
+
use, and $P$ is the number of pairs requested (or $V^2$ if all are needed).
|
| 204 |
+
|
| 205 |
+
Tarjan, R. E. (1979), "Applications of path compression on balanced trees",
|
| 206 |
+
Journal of the ACM 26 (4): 690-715, doi:10.1145/322154.322161.
|
| 207 |
+
|
| 208 |
+
See Also
|
| 209 |
+
--------
|
| 210 |
+
all_pairs_lowest_common_ancestor: similar routine for general DAGs
|
| 211 |
+
lowest_common_ancestor: just a single pair for general DAGs
|
| 212 |
+
"""
|
| 213 |
+
if len(G) == 0:
|
| 214 |
+
raise nx.NetworkXPointlessConcept("LCA meaningless on null graphs.")
|
| 215 |
+
|
| 216 |
+
# Index pairs of interest for efficient lookup from either side.
|
| 217 |
+
if pairs is not None:
|
| 218 |
+
pair_dict = defaultdict(set)
|
| 219 |
+
# See note on all_pairs_lowest_common_ancestor.
|
| 220 |
+
if not isinstance(pairs, Mapping | Set):
|
| 221 |
+
pairs = set(pairs)
|
| 222 |
+
for u, v in pairs:
|
| 223 |
+
for n in (u, v):
|
| 224 |
+
if n not in G:
|
| 225 |
+
msg = f"The node {str(n)} is not in the digraph."
|
| 226 |
+
raise nx.NodeNotFound(msg)
|
| 227 |
+
pair_dict[u].add(v)
|
| 228 |
+
pair_dict[v].add(u)
|
| 229 |
+
|
| 230 |
+
# If root is not specified, find the exactly one node with in degree 0 and
|
| 231 |
+
# use it. Raise an error if none are found, or more than one is. Also check
|
| 232 |
+
# for any nodes with in degree larger than 1, which would imply G is not a
|
| 233 |
+
# tree.
|
| 234 |
+
if root is None:
|
| 235 |
+
for n, deg in G.in_degree:
|
| 236 |
+
if deg == 0:
|
| 237 |
+
if root is not None:
|
| 238 |
+
msg = "No root specified and tree has multiple sources."
|
| 239 |
+
raise nx.NetworkXError(msg)
|
| 240 |
+
root = n
|
| 241 |
+
# checking deg>1 is not sufficient for MultiDiGraphs
|
| 242 |
+
elif deg > 1 and len(G.pred[n]) > 1:
|
| 243 |
+
msg = "Tree LCA only defined on trees; use DAG routine."
|
| 244 |
+
raise nx.NetworkXError(msg)
|
| 245 |
+
if root is None:
|
| 246 |
+
raise nx.NetworkXError("Graph contains a cycle.")
|
| 247 |
+
|
| 248 |
+
# Iterative implementation of Tarjan's offline lca algorithm
|
| 249 |
+
# as described in CLRS on page 521 (2nd edition)/page 584 (3rd edition)
|
| 250 |
+
uf = UnionFind()
|
| 251 |
+
ancestors = {}
|
| 252 |
+
for node in G:
|
| 253 |
+
ancestors[node] = uf[node]
|
| 254 |
+
|
| 255 |
+
colors = defaultdict(bool)
|
| 256 |
+
for node in nx.dfs_postorder_nodes(G, root):
|
| 257 |
+
colors[node] = True
|
| 258 |
+
for v in pair_dict[node] if pairs is not None else G:
|
| 259 |
+
if colors[v]:
|
| 260 |
+
# If the user requested both directions of a pair, give it.
|
| 261 |
+
# Otherwise, just give one.
|
| 262 |
+
if pairs is not None and (node, v) in pairs:
|
| 263 |
+
yield (node, v), ancestors[uf[v]]
|
| 264 |
+
if pairs is None or (v, node) in pairs:
|
| 265 |
+
yield (v, node), ancestors[uf[v]]
|
| 266 |
+
if node != root:
|
| 267 |
+
parent = arbitrary_element(G.pred[node])
|
| 268 |
+
uf.union(parent, node)
|
| 269 |
+
ancestors[uf[parent]] = parent
|
.venv/lib/python3.11/site-packages/networkx/algorithms/matching.py
ADDED
|
@@ -0,0 +1,1152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions for computing and verifying matchings in a graph."""
|
| 2 |
+
|
| 3 |
+
from collections import Counter
|
| 4 |
+
from itertools import combinations, repeat
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from networkx.utils import not_implemented_for
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"is_matching",
|
| 11 |
+
"is_maximal_matching",
|
| 12 |
+
"is_perfect_matching",
|
| 13 |
+
"max_weight_matching",
|
| 14 |
+
"min_weight_matching",
|
| 15 |
+
"maximal_matching",
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@not_implemented_for("multigraph")
|
| 20 |
+
@not_implemented_for("directed")
|
| 21 |
+
@nx._dispatchable
|
| 22 |
+
def maximal_matching(G):
|
| 23 |
+
r"""Find a maximal matching in the graph.
|
| 24 |
+
|
| 25 |
+
A matching is a subset of edges in which no node occurs more than once.
|
| 26 |
+
A maximal matching cannot add more edges and still be a matching.
|
| 27 |
+
|
| 28 |
+
Parameters
|
| 29 |
+
----------
|
| 30 |
+
G : NetworkX graph
|
| 31 |
+
Undirected graph
|
| 32 |
+
|
| 33 |
+
Returns
|
| 34 |
+
-------
|
| 35 |
+
matching : set
|
| 36 |
+
A maximal matching of the graph.
|
| 37 |
+
|
| 38 |
+
Examples
|
| 39 |
+
--------
|
| 40 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)])
|
| 41 |
+
>>> sorted(nx.maximal_matching(G))
|
| 42 |
+
[(1, 2), (3, 5)]
|
| 43 |
+
|
| 44 |
+
Notes
|
| 45 |
+
-----
|
| 46 |
+
The algorithm greedily selects a maximal matching M of the graph G
|
| 47 |
+
(i.e. no superset of M exists). It runs in $O(|E|)$ time.
|
| 48 |
+
"""
|
| 49 |
+
matching = set()
|
| 50 |
+
nodes = set()
|
| 51 |
+
for edge in G.edges():
|
| 52 |
+
# If the edge isn't covered, add it to the matching
|
| 53 |
+
# then remove neighborhood of u and v from consideration.
|
| 54 |
+
u, v = edge
|
| 55 |
+
if u not in nodes and v not in nodes and u != v:
|
| 56 |
+
matching.add(edge)
|
| 57 |
+
nodes.update(edge)
|
| 58 |
+
return matching
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def matching_dict_to_set(matching):
|
| 62 |
+
"""Converts matching dict format to matching set format
|
| 63 |
+
|
| 64 |
+
Converts a dictionary representing a matching (as returned by
|
| 65 |
+
:func:`max_weight_matching`) to a set representing a matching (as
|
| 66 |
+
returned by :func:`maximal_matching`).
|
| 67 |
+
|
| 68 |
+
In the definition of maximal matching adopted by NetworkX,
|
| 69 |
+
self-loops are not allowed, so the provided dictionary is expected
|
| 70 |
+
to never have any mapping from a key to itself. However, the
|
| 71 |
+
dictionary is expected to have mirrored key/value pairs, for
|
| 72 |
+
example, key ``u`` with value ``v`` and key ``v`` with value ``u``.
|
| 73 |
+
|
| 74 |
+
"""
|
| 75 |
+
edges = set()
|
| 76 |
+
for edge in matching.items():
|
| 77 |
+
u, v = edge
|
| 78 |
+
if (v, u) in edges or edge in edges:
|
| 79 |
+
continue
|
| 80 |
+
if u == v:
|
| 81 |
+
raise nx.NetworkXError(f"Selfloops cannot appear in matchings {edge}")
|
| 82 |
+
edges.add(edge)
|
| 83 |
+
return edges
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@nx._dispatchable
|
| 87 |
+
def is_matching(G, matching):
|
| 88 |
+
"""Return True if ``matching`` is a valid matching of ``G``
|
| 89 |
+
|
| 90 |
+
A *matching* in a graph is a set of edges in which no two distinct
|
| 91 |
+
edges share a common endpoint. Each node is incident to at most one
|
| 92 |
+
edge in the matching. The edges are said to be independent.
|
| 93 |
+
|
| 94 |
+
Parameters
|
| 95 |
+
----------
|
| 96 |
+
G : NetworkX graph
|
| 97 |
+
|
| 98 |
+
matching : dict or set
|
| 99 |
+
A dictionary or set representing a matching. If a dictionary, it
|
| 100 |
+
must have ``matching[u] == v`` and ``matching[v] == u`` for each
|
| 101 |
+
edge ``(u, v)`` in the matching. If a set, it must have elements
|
| 102 |
+
of the form ``(u, v)``, where ``(u, v)`` is an edge in the
|
| 103 |
+
matching.
|
| 104 |
+
|
| 105 |
+
Returns
|
| 106 |
+
-------
|
| 107 |
+
bool
|
| 108 |
+
Whether the given set or dictionary represents a valid matching
|
| 109 |
+
in the graph.
|
| 110 |
+
|
| 111 |
+
Raises
|
| 112 |
+
------
|
| 113 |
+
NetworkXError
|
| 114 |
+
If the proposed matching has an edge to a node not in G.
|
| 115 |
+
Or if the matching is not a collection of 2-tuple edges.
|
| 116 |
+
|
| 117 |
+
Examples
|
| 118 |
+
--------
|
| 119 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)])
|
| 120 |
+
>>> nx.is_maximal_matching(G, {1: 3, 2: 4}) # using dict to represent matching
|
| 121 |
+
True
|
| 122 |
+
|
| 123 |
+
>>> nx.is_matching(G, {(1, 3), (2, 4)}) # using set to represent matching
|
| 124 |
+
True
|
| 125 |
+
|
| 126 |
+
"""
|
| 127 |
+
if isinstance(matching, dict):
|
| 128 |
+
matching = matching_dict_to_set(matching)
|
| 129 |
+
|
| 130 |
+
nodes = set()
|
| 131 |
+
for edge in matching:
|
| 132 |
+
if len(edge) != 2:
|
| 133 |
+
raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
|
| 134 |
+
u, v = edge
|
| 135 |
+
if u not in G or v not in G:
|
| 136 |
+
raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
|
| 137 |
+
if u == v:
|
| 138 |
+
return False
|
| 139 |
+
if not G.has_edge(u, v):
|
| 140 |
+
return False
|
| 141 |
+
if u in nodes or v in nodes:
|
| 142 |
+
return False
|
| 143 |
+
nodes.update(edge)
|
| 144 |
+
return True
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@nx._dispatchable
|
| 148 |
+
def is_maximal_matching(G, matching):
|
| 149 |
+
"""Return True if ``matching`` is a maximal matching of ``G``
|
| 150 |
+
|
| 151 |
+
A *maximal matching* in a graph is a matching in which adding any
|
| 152 |
+
edge would cause the set to no longer be a valid matching.
|
| 153 |
+
|
| 154 |
+
Parameters
|
| 155 |
+
----------
|
| 156 |
+
G : NetworkX graph
|
| 157 |
+
|
| 158 |
+
matching : dict or set
|
| 159 |
+
A dictionary or set representing a matching. If a dictionary, it
|
| 160 |
+
must have ``matching[u] == v`` and ``matching[v] == u`` for each
|
| 161 |
+
edge ``(u, v)`` in the matching. If a set, it must have elements
|
| 162 |
+
of the form ``(u, v)``, where ``(u, v)`` is an edge in the
|
| 163 |
+
matching.
|
| 164 |
+
|
| 165 |
+
Returns
|
| 166 |
+
-------
|
| 167 |
+
bool
|
| 168 |
+
Whether the given set or dictionary represents a valid maximal
|
| 169 |
+
matching in the graph.
|
| 170 |
+
|
| 171 |
+
Examples
|
| 172 |
+
--------
|
| 173 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (3, 5)])
|
| 174 |
+
>>> nx.is_maximal_matching(G, {(1, 2), (3, 4)})
|
| 175 |
+
True
|
| 176 |
+
|
| 177 |
+
"""
|
| 178 |
+
if isinstance(matching, dict):
|
| 179 |
+
matching = matching_dict_to_set(matching)
|
| 180 |
+
# If the given set is not a matching, then it is not a maximal matching.
|
| 181 |
+
edges = set()
|
| 182 |
+
nodes = set()
|
| 183 |
+
for edge in matching:
|
| 184 |
+
if len(edge) != 2:
|
| 185 |
+
raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
|
| 186 |
+
u, v = edge
|
| 187 |
+
if u not in G or v not in G:
|
| 188 |
+
raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
|
| 189 |
+
if u == v:
|
| 190 |
+
return False
|
| 191 |
+
if not G.has_edge(u, v):
|
| 192 |
+
return False
|
| 193 |
+
if u in nodes or v in nodes:
|
| 194 |
+
return False
|
| 195 |
+
nodes.update(edge)
|
| 196 |
+
edges.add(edge)
|
| 197 |
+
edges.add((v, u))
|
| 198 |
+
# A matching is maximal if adding any new edge from G to it
|
| 199 |
+
# causes the resulting set to match some node twice.
|
| 200 |
+
# Be careful to check for adding selfloops
|
| 201 |
+
for u, v in G.edges:
|
| 202 |
+
if (u, v) not in edges:
|
| 203 |
+
# could add edge (u, v) to edges and have a bigger matching
|
| 204 |
+
if u not in nodes and v not in nodes and u != v:
|
| 205 |
+
return False
|
| 206 |
+
return True
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
@nx._dispatchable
|
| 210 |
+
def is_perfect_matching(G, matching):
|
| 211 |
+
"""Return True if ``matching`` is a perfect matching for ``G``
|
| 212 |
+
|
| 213 |
+
A *perfect matching* in a graph is a matching in which exactly one edge
|
| 214 |
+
is incident upon each vertex.
|
| 215 |
+
|
| 216 |
+
Parameters
|
| 217 |
+
----------
|
| 218 |
+
G : NetworkX graph
|
| 219 |
+
|
| 220 |
+
matching : dict or set
|
| 221 |
+
A dictionary or set representing a matching. If a dictionary, it
|
| 222 |
+
must have ``matching[u] == v`` and ``matching[v] == u`` for each
|
| 223 |
+
edge ``(u, v)`` in the matching. If a set, it must have elements
|
| 224 |
+
of the form ``(u, v)``, where ``(u, v)`` is an edge in the
|
| 225 |
+
matching.
|
| 226 |
+
|
| 227 |
+
Returns
|
| 228 |
+
-------
|
| 229 |
+
bool
|
| 230 |
+
Whether the given set or dictionary represents a valid perfect
|
| 231 |
+
matching in the graph.
|
| 232 |
+
|
| 233 |
+
Examples
|
| 234 |
+
--------
|
| 235 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5), (4, 6)])
|
| 236 |
+
>>> my_match = {1: 2, 3: 5, 4: 6}
|
| 237 |
+
>>> nx.is_perfect_matching(G, my_match)
|
| 238 |
+
True
|
| 239 |
+
|
| 240 |
+
"""
|
| 241 |
+
if isinstance(matching, dict):
|
| 242 |
+
matching = matching_dict_to_set(matching)
|
| 243 |
+
|
| 244 |
+
nodes = set()
|
| 245 |
+
for edge in matching:
|
| 246 |
+
if len(edge) != 2:
|
| 247 |
+
raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
|
| 248 |
+
u, v = edge
|
| 249 |
+
if u not in G or v not in G:
|
| 250 |
+
raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
|
| 251 |
+
if u == v:
|
| 252 |
+
return False
|
| 253 |
+
if not G.has_edge(u, v):
|
| 254 |
+
return False
|
| 255 |
+
if u in nodes or v in nodes:
|
| 256 |
+
return False
|
| 257 |
+
nodes.update(edge)
|
| 258 |
+
return len(nodes) == len(G)
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
@not_implemented_for("multigraph")
|
| 262 |
+
@not_implemented_for("directed")
|
| 263 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 264 |
+
def min_weight_matching(G, weight="weight"):
|
| 265 |
+
"""Computing a minimum-weight maximal matching of G.
|
| 266 |
+
|
| 267 |
+
Use the maximum-weight algorithm with edge weights subtracted
|
| 268 |
+
from the maximum weight of all edges.
|
| 269 |
+
|
| 270 |
+
A matching is a subset of edges in which no node occurs more than once.
|
| 271 |
+
The weight of a matching is the sum of the weights of its edges.
|
| 272 |
+
A maximal matching cannot add more edges and still be a matching.
|
| 273 |
+
The cardinality of a matching is the number of matched edges.
|
| 274 |
+
|
| 275 |
+
This method replaces the edge weights with 1 plus the maximum edge weight
|
| 276 |
+
minus the original edge weight.
|
| 277 |
+
|
| 278 |
+
new_weight = (max_weight + 1) - edge_weight
|
| 279 |
+
|
| 280 |
+
then runs :func:`max_weight_matching` with the new weights.
|
| 281 |
+
The max weight matching with these new weights corresponds
|
| 282 |
+
to the min weight matching using the original weights.
|
| 283 |
+
Adding 1 to the max edge weight keeps all edge weights positive
|
| 284 |
+
and as integers if they started as integers.
|
| 285 |
+
|
| 286 |
+
You might worry that adding 1 to each weight would make the algorithm
|
| 287 |
+
favor matchings with more edges. But we use the parameter
|
| 288 |
+
`maxcardinality=True` in `max_weight_matching` to ensure that the
|
| 289 |
+
number of edges in the competing matchings are the same and thus
|
| 290 |
+
the optimum does not change due to changes in the number of edges.
|
| 291 |
+
|
| 292 |
+
Read the documentation of `max_weight_matching` for more information.
|
| 293 |
+
|
| 294 |
+
Parameters
|
| 295 |
+
----------
|
| 296 |
+
G : NetworkX graph
|
| 297 |
+
Undirected graph
|
| 298 |
+
|
| 299 |
+
weight: string, optional (default='weight')
|
| 300 |
+
Edge data key corresponding to the edge weight.
|
| 301 |
+
If key not found, uses 1 as weight.
|
| 302 |
+
|
| 303 |
+
Returns
|
| 304 |
+
-------
|
| 305 |
+
matching : set
|
| 306 |
+
A minimal weight matching of the graph.
|
| 307 |
+
|
| 308 |
+
See Also
|
| 309 |
+
--------
|
| 310 |
+
max_weight_matching
|
| 311 |
+
"""
|
| 312 |
+
if len(G.edges) == 0:
|
| 313 |
+
return max_weight_matching(G, maxcardinality=True, weight=weight)
|
| 314 |
+
G_edges = G.edges(data=weight, default=1)
|
| 315 |
+
max_weight = 1 + max(w for _, _, w in G_edges)
|
| 316 |
+
InvG = nx.Graph()
|
| 317 |
+
edges = ((u, v, max_weight - w) for u, v, w in G_edges)
|
| 318 |
+
InvG.add_weighted_edges_from(edges, weight=weight)
|
| 319 |
+
return max_weight_matching(InvG, maxcardinality=True, weight=weight)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
@not_implemented_for("multigraph")
|
| 323 |
+
@not_implemented_for("directed")
|
| 324 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 325 |
+
def max_weight_matching(G, maxcardinality=False, weight="weight"):
|
| 326 |
+
"""Compute a maximum-weighted matching of G.
|
| 327 |
+
|
| 328 |
+
A matching is a subset of edges in which no node occurs more than once.
|
| 329 |
+
The weight of a matching is the sum of the weights of its edges.
|
| 330 |
+
A maximal matching cannot add more edges and still be a matching.
|
| 331 |
+
The cardinality of a matching is the number of matched edges.
|
| 332 |
+
|
| 333 |
+
Parameters
|
| 334 |
+
----------
|
| 335 |
+
G : NetworkX graph
|
| 336 |
+
Undirected graph
|
| 337 |
+
|
| 338 |
+
maxcardinality: bool, optional (default=False)
|
| 339 |
+
If maxcardinality is True, compute the maximum-cardinality matching
|
| 340 |
+
with maximum weight among all maximum-cardinality matchings.
|
| 341 |
+
|
| 342 |
+
weight: string, optional (default='weight')
|
| 343 |
+
Edge data key corresponding to the edge weight.
|
| 344 |
+
If key not found, uses 1 as weight.
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
Returns
|
| 348 |
+
-------
|
| 349 |
+
matching : set
|
| 350 |
+
A maximal matching of the graph.
|
| 351 |
+
|
| 352 |
+
Examples
|
| 353 |
+
--------
|
| 354 |
+
>>> G = nx.Graph()
|
| 355 |
+
>>> edges = [(1, 2, 6), (1, 3, 2), (2, 3, 1), (2, 4, 7), (3, 5, 9), (4, 5, 3)]
|
| 356 |
+
>>> G.add_weighted_edges_from(edges)
|
| 357 |
+
>>> sorted(nx.max_weight_matching(G))
|
| 358 |
+
[(2, 4), (5, 3)]
|
| 359 |
+
|
| 360 |
+
Notes
|
| 361 |
+
-----
|
| 362 |
+
If G has edges with weight attributes the edge data are used as
|
| 363 |
+
weight values else the weights are assumed to be 1.
|
| 364 |
+
|
| 365 |
+
This function takes time O(number_of_nodes ** 3).
|
| 366 |
+
|
| 367 |
+
If all edge weights are integers, the algorithm uses only integer
|
| 368 |
+
computations. If floating point weights are used, the algorithm
|
| 369 |
+
could return a slightly suboptimal matching due to numeric
|
| 370 |
+
precision errors.
|
| 371 |
+
|
| 372 |
+
This method is based on the "blossom" method for finding augmenting
|
| 373 |
+
paths and the "primal-dual" method for finding a matching of maximum
|
| 374 |
+
weight, both methods invented by Jack Edmonds [1]_.
|
| 375 |
+
|
| 376 |
+
Bipartite graphs can also be matched using the functions present in
|
| 377 |
+
:mod:`networkx.algorithms.bipartite.matching`.
|
| 378 |
+
|
| 379 |
+
References
|
| 380 |
+
----------
|
| 381 |
+
.. [1] "Efficient Algorithms for Finding Maximum Matching in Graphs",
|
| 382 |
+
Zvi Galil, ACM Computing Surveys, 1986.
|
| 383 |
+
"""
|
| 384 |
+
#
|
| 385 |
+
# The algorithm is taken from "Efficient Algorithms for Finding Maximum
|
| 386 |
+
# Matching in Graphs" by Zvi Galil, ACM Computing Surveys, 1986.
|
| 387 |
+
# It is based on the "blossom" method for finding augmenting paths and
|
| 388 |
+
# the "primal-dual" method for finding a matching of maximum weight, both
|
| 389 |
+
# methods invented by Jack Edmonds.
|
| 390 |
+
#
|
| 391 |
+
# A C program for maximum weight matching by Ed Rothberg was used
|
| 392 |
+
# extensively to validate this new code.
|
| 393 |
+
#
|
| 394 |
+
# Many terms used in the code comments are explained in the paper
|
| 395 |
+
# by Galil. You will probably need the paper to make sense of this code.
|
| 396 |
+
#
|
| 397 |
+
|
| 398 |
+
class NoNode:
|
| 399 |
+
"""Dummy value which is different from any node."""
|
| 400 |
+
|
| 401 |
+
class Blossom:
|
| 402 |
+
"""Representation of a non-trivial blossom or sub-blossom."""
|
| 403 |
+
|
| 404 |
+
__slots__ = ["childs", "edges", "mybestedges"]
|
| 405 |
+
|
| 406 |
+
# b.childs is an ordered list of b's sub-blossoms, starting with
|
| 407 |
+
# the base and going round the blossom.
|
| 408 |
+
|
| 409 |
+
# b.edges is the list of b's connecting edges, such that
|
| 410 |
+
# b.edges[i] = (v, w) where v is a vertex in b.childs[i]
|
| 411 |
+
# and w is a vertex in b.childs[wrap(i+1)].
|
| 412 |
+
|
| 413 |
+
# If b is a top-level S-blossom,
|
| 414 |
+
# b.mybestedges is a list of least-slack edges to neighboring
|
| 415 |
+
# S-blossoms, or None if no such list has been computed yet.
|
| 416 |
+
# This is used for efficient computation of delta3.
|
| 417 |
+
|
| 418 |
+
# Generate the blossom's leaf vertices.
|
| 419 |
+
def leaves(self):
|
| 420 |
+
stack = [*self.childs]
|
| 421 |
+
while stack:
|
| 422 |
+
t = stack.pop()
|
| 423 |
+
if isinstance(t, Blossom):
|
| 424 |
+
stack.extend(t.childs)
|
| 425 |
+
else:
|
| 426 |
+
yield t
|
| 427 |
+
|
| 428 |
+
# Get a list of vertices.
|
| 429 |
+
gnodes = list(G)
|
| 430 |
+
if not gnodes:
|
| 431 |
+
return set() # don't bother with empty graphs
|
| 432 |
+
|
| 433 |
+
# Find the maximum edge weight.
|
| 434 |
+
maxweight = 0
|
| 435 |
+
allinteger = True
|
| 436 |
+
for i, j, d in G.edges(data=True):
|
| 437 |
+
wt = d.get(weight, 1)
|
| 438 |
+
if i != j and wt > maxweight:
|
| 439 |
+
maxweight = wt
|
| 440 |
+
allinteger = allinteger and (str(type(wt)).split("'")[1] in ("int", "long"))
|
| 441 |
+
|
| 442 |
+
# If v is a matched vertex, mate[v] is its partner vertex.
|
| 443 |
+
# If v is a single vertex, v does not occur as a key in mate.
|
| 444 |
+
# Initially all vertices are single; updated during augmentation.
|
| 445 |
+
mate = {}
|
| 446 |
+
|
| 447 |
+
# If b is a top-level blossom,
|
| 448 |
+
# label.get(b) is None if b is unlabeled (free),
|
| 449 |
+
# 1 if b is an S-blossom,
|
| 450 |
+
# 2 if b is a T-blossom.
|
| 451 |
+
# The label of a vertex is found by looking at the label of its top-level
|
| 452 |
+
# containing blossom.
|
| 453 |
+
# If v is a vertex inside a T-blossom, label[v] is 2 iff v is reachable
|
| 454 |
+
# from an S-vertex outside the blossom.
|
| 455 |
+
# Labels are assigned during a stage and reset after each augmentation.
|
| 456 |
+
label = {}
|
| 457 |
+
|
| 458 |
+
# If b is a labeled top-level blossom,
|
| 459 |
+
# labeledge[b] = (v, w) is the edge through which b obtained its label
|
| 460 |
+
# such that w is a vertex in b, or None if b's base vertex is single.
|
| 461 |
+
# If w is a vertex inside a T-blossom and label[w] == 2,
|
| 462 |
+
# labeledge[w] = (v, w) is an edge through which w is reachable from
|
| 463 |
+
# outside the blossom.
|
| 464 |
+
labeledge = {}
|
| 465 |
+
|
| 466 |
+
# If v is a vertex, inblossom[v] is the top-level blossom to which v
|
| 467 |
+
# belongs.
|
| 468 |
+
# If v is a top-level vertex, inblossom[v] == v since v is itself
|
| 469 |
+
# a (trivial) top-level blossom.
|
| 470 |
+
# Initially all vertices are top-level trivial blossoms.
|
| 471 |
+
inblossom = dict(zip(gnodes, gnodes))
|
| 472 |
+
|
| 473 |
+
# If b is a sub-blossom,
|
| 474 |
+
# blossomparent[b] is its immediate parent (sub-)blossom.
|
| 475 |
+
# If b is a top-level blossom, blossomparent[b] is None.
|
| 476 |
+
blossomparent = dict(zip(gnodes, repeat(None)))
|
| 477 |
+
|
| 478 |
+
# If b is a (sub-)blossom,
|
| 479 |
+
# blossombase[b] is its base VERTEX (i.e. recursive sub-blossom).
|
| 480 |
+
blossombase = dict(zip(gnodes, gnodes))
|
| 481 |
+
|
| 482 |
+
# If w is a free vertex (or an unreached vertex inside a T-blossom),
|
| 483 |
+
# bestedge[w] = (v, w) is the least-slack edge from an S-vertex,
|
| 484 |
+
# or None if there is no such edge.
|
| 485 |
+
# If b is a (possibly trivial) top-level S-blossom,
|
| 486 |
+
# bestedge[b] = (v, w) is the least-slack edge to a different S-blossom
|
| 487 |
+
# (v inside b), or None if there is no such edge.
|
| 488 |
+
# This is used for efficient computation of delta2 and delta3.
|
| 489 |
+
bestedge = {}
|
| 490 |
+
|
| 491 |
+
# If v is a vertex,
|
| 492 |
+
# dualvar[v] = 2 * u(v) where u(v) is the v's variable in the dual
|
| 493 |
+
# optimization problem (if all edge weights are integers, multiplication
|
| 494 |
+
# by two ensures that all values remain integers throughout the algorithm).
|
| 495 |
+
# Initially, u(v) = maxweight / 2.
|
| 496 |
+
dualvar = dict(zip(gnodes, repeat(maxweight)))
|
| 497 |
+
|
| 498 |
+
# If b is a non-trivial blossom,
|
| 499 |
+
# blossomdual[b] = z(b) where z(b) is b's variable in the dual
|
| 500 |
+
# optimization problem.
|
| 501 |
+
blossomdual = {}
|
| 502 |
+
|
| 503 |
+
# If (v, w) in allowedge or (w, v) in allowedg, then the edge
|
| 504 |
+
# (v, w) is known to have zero slack in the optimization problem;
|
| 505 |
+
# otherwise the edge may or may not have zero slack.
|
| 506 |
+
allowedge = {}
|
| 507 |
+
|
| 508 |
+
# Queue of newly discovered S-vertices.
|
| 509 |
+
queue = []
|
| 510 |
+
|
| 511 |
+
# Return 2 * slack of edge (v, w) (does not work inside blossoms).
|
| 512 |
+
def slack(v, w):
|
| 513 |
+
return dualvar[v] + dualvar[w] - 2 * G[v][w].get(weight, 1)
|
| 514 |
+
|
| 515 |
+
# Assign label t to the top-level blossom containing vertex w,
|
| 516 |
+
# coming through an edge from vertex v.
|
| 517 |
+
def assignLabel(w, t, v):
|
| 518 |
+
b = inblossom[w]
|
| 519 |
+
assert label.get(w) is None and label.get(b) is None
|
| 520 |
+
label[w] = label[b] = t
|
| 521 |
+
if v is not None:
|
| 522 |
+
labeledge[w] = labeledge[b] = (v, w)
|
| 523 |
+
else:
|
| 524 |
+
labeledge[w] = labeledge[b] = None
|
| 525 |
+
bestedge[w] = bestedge[b] = None
|
| 526 |
+
if t == 1:
|
| 527 |
+
# b became an S-vertex/blossom; add it(s vertices) to the queue.
|
| 528 |
+
if isinstance(b, Blossom):
|
| 529 |
+
queue.extend(b.leaves())
|
| 530 |
+
else:
|
| 531 |
+
queue.append(b)
|
| 532 |
+
elif t == 2:
|
| 533 |
+
# b became a T-vertex/blossom; assign label S to its mate.
|
| 534 |
+
# (If b is a non-trivial blossom, its base is the only vertex
|
| 535 |
+
# with an external mate.)
|
| 536 |
+
base = blossombase[b]
|
| 537 |
+
assignLabel(mate[base], 1, base)
|
| 538 |
+
|
| 539 |
+
# Trace back from vertices v and w to discover either a new blossom
|
| 540 |
+
# or an augmenting path. Return the base vertex of the new blossom,
|
| 541 |
+
# or NoNode if an augmenting path was found.
|
| 542 |
+
def scanBlossom(v, w):
|
| 543 |
+
# Trace back from v and w, placing breadcrumbs as we go.
|
| 544 |
+
path = []
|
| 545 |
+
base = NoNode
|
| 546 |
+
while v is not NoNode:
|
| 547 |
+
# Look for a breadcrumb in v's blossom or put a new breadcrumb.
|
| 548 |
+
b = inblossom[v]
|
| 549 |
+
if label[b] & 4:
|
| 550 |
+
base = blossombase[b]
|
| 551 |
+
break
|
| 552 |
+
assert label[b] == 1
|
| 553 |
+
path.append(b)
|
| 554 |
+
label[b] = 5
|
| 555 |
+
# Trace one step back.
|
| 556 |
+
if labeledge[b] is None:
|
| 557 |
+
# The base of blossom b is single; stop tracing this path.
|
| 558 |
+
assert blossombase[b] not in mate
|
| 559 |
+
v = NoNode
|
| 560 |
+
else:
|
| 561 |
+
assert labeledge[b][0] == mate[blossombase[b]]
|
| 562 |
+
v = labeledge[b][0]
|
| 563 |
+
b = inblossom[v]
|
| 564 |
+
assert label[b] == 2
|
| 565 |
+
# b is a T-blossom; trace one more step back.
|
| 566 |
+
v = labeledge[b][0]
|
| 567 |
+
# Swap v and w so that we alternate between both paths.
|
| 568 |
+
if w is not NoNode:
|
| 569 |
+
v, w = w, v
|
| 570 |
+
# Remove breadcrumbs.
|
| 571 |
+
for b in path:
|
| 572 |
+
label[b] = 1
|
| 573 |
+
# Return base vertex, if we found one.
|
| 574 |
+
return base
|
| 575 |
+
|
| 576 |
+
# Construct a new blossom with given base, through S-vertices v and w.
|
| 577 |
+
# Label the new blossom as S; set its dual variable to zero;
|
| 578 |
+
# relabel its T-vertices to S and add them to the queue.
|
| 579 |
+
def addBlossom(base, v, w):
|
| 580 |
+
bb = inblossom[base]
|
| 581 |
+
bv = inblossom[v]
|
| 582 |
+
bw = inblossom[w]
|
| 583 |
+
# Create blossom.
|
| 584 |
+
b = Blossom()
|
| 585 |
+
blossombase[b] = base
|
| 586 |
+
blossomparent[b] = None
|
| 587 |
+
blossomparent[bb] = b
|
| 588 |
+
# Make list of sub-blossoms and their interconnecting edge endpoints.
|
| 589 |
+
b.childs = path = []
|
| 590 |
+
b.edges = edgs = [(v, w)]
|
| 591 |
+
# Trace back from v to base.
|
| 592 |
+
while bv != bb:
|
| 593 |
+
# Add bv to the new blossom.
|
| 594 |
+
blossomparent[bv] = b
|
| 595 |
+
path.append(bv)
|
| 596 |
+
edgs.append(labeledge[bv])
|
| 597 |
+
assert label[bv] == 2 or (
|
| 598 |
+
label[bv] == 1 and labeledge[bv][0] == mate[blossombase[bv]]
|
| 599 |
+
)
|
| 600 |
+
# Trace one step back.
|
| 601 |
+
v = labeledge[bv][0]
|
| 602 |
+
bv = inblossom[v]
|
| 603 |
+
# Add base sub-blossom; reverse lists.
|
| 604 |
+
path.append(bb)
|
| 605 |
+
path.reverse()
|
| 606 |
+
edgs.reverse()
|
| 607 |
+
# Trace back from w to base.
|
| 608 |
+
while bw != bb:
|
| 609 |
+
# Add bw to the new blossom.
|
| 610 |
+
blossomparent[bw] = b
|
| 611 |
+
path.append(bw)
|
| 612 |
+
edgs.append((labeledge[bw][1], labeledge[bw][0]))
|
| 613 |
+
assert label[bw] == 2 or (
|
| 614 |
+
label[bw] == 1 and labeledge[bw][0] == mate[blossombase[bw]]
|
| 615 |
+
)
|
| 616 |
+
# Trace one step back.
|
| 617 |
+
w = labeledge[bw][0]
|
| 618 |
+
bw = inblossom[w]
|
| 619 |
+
# Set label to S.
|
| 620 |
+
assert label[bb] == 1
|
| 621 |
+
label[b] = 1
|
| 622 |
+
labeledge[b] = labeledge[bb]
|
| 623 |
+
# Set dual variable to zero.
|
| 624 |
+
blossomdual[b] = 0
|
| 625 |
+
# Relabel vertices.
|
| 626 |
+
for v in b.leaves():
|
| 627 |
+
if label[inblossom[v]] == 2:
|
| 628 |
+
# This T-vertex now turns into an S-vertex because it becomes
|
| 629 |
+
# part of an S-blossom; add it to the queue.
|
| 630 |
+
queue.append(v)
|
| 631 |
+
inblossom[v] = b
|
| 632 |
+
# Compute b.mybestedges.
|
| 633 |
+
bestedgeto = {}
|
| 634 |
+
for bv in path:
|
| 635 |
+
if isinstance(bv, Blossom):
|
| 636 |
+
if bv.mybestedges is not None:
|
| 637 |
+
# Walk this subblossom's least-slack edges.
|
| 638 |
+
nblist = bv.mybestedges
|
| 639 |
+
# The sub-blossom won't need this data again.
|
| 640 |
+
bv.mybestedges = None
|
| 641 |
+
else:
|
| 642 |
+
# This subblossom does not have a list of least-slack
|
| 643 |
+
# edges; get the information from the vertices.
|
| 644 |
+
nblist = [
|
| 645 |
+
(v, w) for v in bv.leaves() for w in G.neighbors(v) if v != w
|
| 646 |
+
]
|
| 647 |
+
else:
|
| 648 |
+
nblist = [(bv, w) for w in G.neighbors(bv) if bv != w]
|
| 649 |
+
for k in nblist:
|
| 650 |
+
(i, j) = k
|
| 651 |
+
if inblossom[j] == b:
|
| 652 |
+
i, j = j, i
|
| 653 |
+
bj = inblossom[j]
|
| 654 |
+
if (
|
| 655 |
+
bj != b
|
| 656 |
+
and label.get(bj) == 1
|
| 657 |
+
and ((bj not in bestedgeto) or slack(i, j) < slack(*bestedgeto[bj]))
|
| 658 |
+
):
|
| 659 |
+
bestedgeto[bj] = k
|
| 660 |
+
# Forget about least-slack edge of the subblossom.
|
| 661 |
+
bestedge[bv] = None
|
| 662 |
+
b.mybestedges = list(bestedgeto.values())
|
| 663 |
+
# Select bestedge[b].
|
| 664 |
+
mybestedge = None
|
| 665 |
+
bestedge[b] = None
|
| 666 |
+
for k in b.mybestedges:
|
| 667 |
+
kslack = slack(*k)
|
| 668 |
+
if mybestedge is None or kslack < mybestslack:
|
| 669 |
+
mybestedge = k
|
| 670 |
+
mybestslack = kslack
|
| 671 |
+
bestedge[b] = mybestedge
|
| 672 |
+
|
| 673 |
+
# Expand the given top-level blossom.
|
| 674 |
+
def expandBlossom(b, endstage):
|
| 675 |
+
# This is an obnoxiously complicated recursive function for the sake of
|
| 676 |
+
# a stack-transformation. So, we hack around the complexity by using
|
| 677 |
+
# a trampoline pattern. By yielding the arguments to each recursive
|
| 678 |
+
# call, we keep the actual callstack flat.
|
| 679 |
+
|
| 680 |
+
def _recurse(b, endstage):
|
| 681 |
+
# Convert sub-blossoms into top-level blossoms.
|
| 682 |
+
for s in b.childs:
|
| 683 |
+
blossomparent[s] = None
|
| 684 |
+
if isinstance(s, Blossom):
|
| 685 |
+
if endstage and blossomdual[s] == 0:
|
| 686 |
+
# Recursively expand this sub-blossom.
|
| 687 |
+
yield s
|
| 688 |
+
else:
|
| 689 |
+
for v in s.leaves():
|
| 690 |
+
inblossom[v] = s
|
| 691 |
+
else:
|
| 692 |
+
inblossom[s] = s
|
| 693 |
+
# If we expand a T-blossom during a stage, its sub-blossoms must be
|
| 694 |
+
# relabeled.
|
| 695 |
+
if (not endstage) and label.get(b) == 2:
|
| 696 |
+
# Start at the sub-blossom through which the expanding
|
| 697 |
+
# blossom obtained its label, and relabel sub-blossoms untili
|
| 698 |
+
# we reach the base.
|
| 699 |
+
# Figure out through which sub-blossom the expanding blossom
|
| 700 |
+
# obtained its label initially.
|
| 701 |
+
entrychild = inblossom[labeledge[b][1]]
|
| 702 |
+
# Decide in which direction we will go round the blossom.
|
| 703 |
+
j = b.childs.index(entrychild)
|
| 704 |
+
if j & 1:
|
| 705 |
+
# Start index is odd; go forward and wrap.
|
| 706 |
+
j -= len(b.childs)
|
| 707 |
+
jstep = 1
|
| 708 |
+
else:
|
| 709 |
+
# Start index is even; go backward.
|
| 710 |
+
jstep = -1
|
| 711 |
+
# Move along the blossom until we get to the base.
|
| 712 |
+
v, w = labeledge[b]
|
| 713 |
+
while j != 0:
|
| 714 |
+
# Relabel the T-sub-blossom.
|
| 715 |
+
if jstep == 1:
|
| 716 |
+
p, q = b.edges[j]
|
| 717 |
+
else:
|
| 718 |
+
q, p = b.edges[j - 1]
|
| 719 |
+
label[w] = None
|
| 720 |
+
label[q] = None
|
| 721 |
+
assignLabel(w, 2, v)
|
| 722 |
+
# Step to the next S-sub-blossom and note its forward edge.
|
| 723 |
+
allowedge[(p, q)] = allowedge[(q, p)] = True
|
| 724 |
+
j += jstep
|
| 725 |
+
if jstep == 1:
|
| 726 |
+
v, w = b.edges[j]
|
| 727 |
+
else:
|
| 728 |
+
w, v = b.edges[j - 1]
|
| 729 |
+
# Step to the next T-sub-blossom.
|
| 730 |
+
allowedge[(v, w)] = allowedge[(w, v)] = True
|
| 731 |
+
j += jstep
|
| 732 |
+
# Relabel the base T-sub-blossom WITHOUT stepping through to
|
| 733 |
+
# its mate (so don't call assignLabel).
|
| 734 |
+
bw = b.childs[j]
|
| 735 |
+
label[w] = label[bw] = 2
|
| 736 |
+
labeledge[w] = labeledge[bw] = (v, w)
|
| 737 |
+
bestedge[bw] = None
|
| 738 |
+
# Continue along the blossom until we get back to entrychild.
|
| 739 |
+
j += jstep
|
| 740 |
+
while b.childs[j] != entrychild:
|
| 741 |
+
# Examine the vertices of the sub-blossom to see whether
|
| 742 |
+
# it is reachable from a neighboring S-vertex outside the
|
| 743 |
+
# expanding blossom.
|
| 744 |
+
bv = b.childs[j]
|
| 745 |
+
if label.get(bv) == 1:
|
| 746 |
+
# This sub-blossom just got label S through one of its
|
| 747 |
+
# neighbors; leave it be.
|
| 748 |
+
j += jstep
|
| 749 |
+
continue
|
| 750 |
+
if isinstance(bv, Blossom):
|
| 751 |
+
for v in bv.leaves():
|
| 752 |
+
if label.get(v):
|
| 753 |
+
break
|
| 754 |
+
else:
|
| 755 |
+
v = bv
|
| 756 |
+
# If the sub-blossom contains a reachable vertex, assign
|
| 757 |
+
# label T to the sub-blossom.
|
| 758 |
+
if label.get(v):
|
| 759 |
+
assert label[v] == 2
|
| 760 |
+
assert inblossom[v] == bv
|
| 761 |
+
label[v] = None
|
| 762 |
+
label[mate[blossombase[bv]]] = None
|
| 763 |
+
assignLabel(v, 2, labeledge[v][0])
|
| 764 |
+
j += jstep
|
| 765 |
+
# Remove the expanded blossom entirely.
|
| 766 |
+
label.pop(b, None)
|
| 767 |
+
labeledge.pop(b, None)
|
| 768 |
+
bestedge.pop(b, None)
|
| 769 |
+
del blossomparent[b]
|
| 770 |
+
del blossombase[b]
|
| 771 |
+
del blossomdual[b]
|
| 772 |
+
|
| 773 |
+
# Now, we apply the trampoline pattern. We simulate a recursive
|
| 774 |
+
# callstack by maintaining a stack of generators, each yielding a
|
| 775 |
+
# sequence of function arguments. We grow the stack by appending a call
|
| 776 |
+
# to _recurse on each argument tuple, and shrink the stack whenever a
|
| 777 |
+
# generator is exhausted.
|
| 778 |
+
stack = [_recurse(b, endstage)]
|
| 779 |
+
while stack:
|
| 780 |
+
top = stack[-1]
|
| 781 |
+
for s in top:
|
| 782 |
+
stack.append(_recurse(s, endstage))
|
| 783 |
+
break
|
| 784 |
+
else:
|
| 785 |
+
stack.pop()
|
| 786 |
+
|
| 787 |
+
# Swap matched/unmatched edges over an alternating path through blossom b
|
| 788 |
+
# between vertex v and the base vertex. Keep blossom bookkeeping
|
| 789 |
+
# consistent.
|
| 790 |
+
def augmentBlossom(b, v):
|
| 791 |
+
# This is an obnoxiously complicated recursive function for the sake of
|
| 792 |
+
# a stack-transformation. So, we hack around the complexity by using
|
| 793 |
+
# a trampoline pattern. By yielding the arguments to each recursive
|
| 794 |
+
# call, we keep the actual callstack flat.
|
| 795 |
+
|
| 796 |
+
def _recurse(b, v):
|
| 797 |
+
# Bubble up through the blossom tree from vertex v to an immediate
|
| 798 |
+
# sub-blossom of b.
|
| 799 |
+
t = v
|
| 800 |
+
while blossomparent[t] != b:
|
| 801 |
+
t = blossomparent[t]
|
| 802 |
+
# Recursively deal with the first sub-blossom.
|
| 803 |
+
if isinstance(t, Blossom):
|
| 804 |
+
yield (t, v)
|
| 805 |
+
# Decide in which direction we will go round the blossom.
|
| 806 |
+
i = j = b.childs.index(t)
|
| 807 |
+
if i & 1:
|
| 808 |
+
# Start index is odd; go forward and wrap.
|
| 809 |
+
j -= len(b.childs)
|
| 810 |
+
jstep = 1
|
| 811 |
+
else:
|
| 812 |
+
# Start index is even; go backward.
|
| 813 |
+
jstep = -1
|
| 814 |
+
# Move along the blossom until we get to the base.
|
| 815 |
+
while j != 0:
|
| 816 |
+
# Step to the next sub-blossom and augment it recursively.
|
| 817 |
+
j += jstep
|
| 818 |
+
t = b.childs[j]
|
| 819 |
+
if jstep == 1:
|
| 820 |
+
w, x = b.edges[j]
|
| 821 |
+
else:
|
| 822 |
+
x, w = b.edges[j - 1]
|
| 823 |
+
if isinstance(t, Blossom):
|
| 824 |
+
yield (t, w)
|
| 825 |
+
# Step to the next sub-blossom and augment it recursively.
|
| 826 |
+
j += jstep
|
| 827 |
+
t = b.childs[j]
|
| 828 |
+
if isinstance(t, Blossom):
|
| 829 |
+
yield (t, x)
|
| 830 |
+
# Match the edge connecting those sub-blossoms.
|
| 831 |
+
mate[w] = x
|
| 832 |
+
mate[x] = w
|
| 833 |
+
# Rotate the list of sub-blossoms to put the new base at the front.
|
| 834 |
+
b.childs = b.childs[i:] + b.childs[:i]
|
| 835 |
+
b.edges = b.edges[i:] + b.edges[:i]
|
| 836 |
+
blossombase[b] = blossombase[b.childs[0]]
|
| 837 |
+
assert blossombase[b] == v
|
| 838 |
+
|
| 839 |
+
# Now, we apply the trampoline pattern. We simulate a recursive
|
| 840 |
+
# callstack by maintaining a stack of generators, each yielding a
|
| 841 |
+
# sequence of function arguments. We grow the stack by appending a call
|
| 842 |
+
# to _recurse on each argument tuple, and shrink the stack whenever a
|
| 843 |
+
# generator is exhausted.
|
| 844 |
+
stack = [_recurse(b, v)]
|
| 845 |
+
while stack:
|
| 846 |
+
top = stack[-1]
|
| 847 |
+
for args in top:
|
| 848 |
+
stack.append(_recurse(*args))
|
| 849 |
+
break
|
| 850 |
+
else:
|
| 851 |
+
stack.pop()
|
| 852 |
+
|
| 853 |
+
# Swap matched/unmatched edges over an alternating path between two
|
| 854 |
+
# single vertices. The augmenting path runs through S-vertices v and w.
|
| 855 |
+
def augmentMatching(v, w):
|
| 856 |
+
for s, j in ((v, w), (w, v)):
|
| 857 |
+
# Match vertex s to vertex j. Then trace back from s
|
| 858 |
+
# until we find a single vertex, swapping matched and unmatched
|
| 859 |
+
# edges as we go.
|
| 860 |
+
while 1:
|
| 861 |
+
bs = inblossom[s]
|
| 862 |
+
assert label[bs] == 1
|
| 863 |
+
assert (labeledge[bs] is None and blossombase[bs] not in mate) or (
|
| 864 |
+
labeledge[bs][0] == mate[blossombase[bs]]
|
| 865 |
+
)
|
| 866 |
+
# Augment through the S-blossom from s to base.
|
| 867 |
+
if isinstance(bs, Blossom):
|
| 868 |
+
augmentBlossom(bs, s)
|
| 869 |
+
# Update mate[s]
|
| 870 |
+
mate[s] = j
|
| 871 |
+
# Trace one step back.
|
| 872 |
+
if labeledge[bs] is None:
|
| 873 |
+
# Reached single vertex; stop.
|
| 874 |
+
break
|
| 875 |
+
t = labeledge[bs][0]
|
| 876 |
+
bt = inblossom[t]
|
| 877 |
+
assert label[bt] == 2
|
| 878 |
+
# Trace one more step back.
|
| 879 |
+
s, j = labeledge[bt]
|
| 880 |
+
# Augment through the T-blossom from j to base.
|
| 881 |
+
assert blossombase[bt] == t
|
| 882 |
+
if isinstance(bt, Blossom):
|
| 883 |
+
augmentBlossom(bt, j)
|
| 884 |
+
# Update mate[j]
|
| 885 |
+
mate[j] = s
|
| 886 |
+
|
| 887 |
+
# Verify that the optimum solution has been reached.
|
| 888 |
+
def verifyOptimum():
|
| 889 |
+
if maxcardinality:
|
| 890 |
+
# Vertices may have negative dual;
|
| 891 |
+
# find a constant non-negative number to add to all vertex duals.
|
| 892 |
+
vdualoffset = max(0, -min(dualvar.values()))
|
| 893 |
+
else:
|
| 894 |
+
vdualoffset = 0
|
| 895 |
+
# 0. all dual variables are non-negative
|
| 896 |
+
assert min(dualvar.values()) + vdualoffset >= 0
|
| 897 |
+
assert len(blossomdual) == 0 or min(blossomdual.values()) >= 0
|
| 898 |
+
# 0. all edges have non-negative slack and
|
| 899 |
+
# 1. all matched edges have zero slack;
|
| 900 |
+
for i, j, d in G.edges(data=True):
|
| 901 |
+
wt = d.get(weight, 1)
|
| 902 |
+
if i == j:
|
| 903 |
+
continue # ignore self-loops
|
| 904 |
+
s = dualvar[i] + dualvar[j] - 2 * wt
|
| 905 |
+
iblossoms = [i]
|
| 906 |
+
jblossoms = [j]
|
| 907 |
+
while blossomparent[iblossoms[-1]] is not None:
|
| 908 |
+
iblossoms.append(blossomparent[iblossoms[-1]])
|
| 909 |
+
while blossomparent[jblossoms[-1]] is not None:
|
| 910 |
+
jblossoms.append(blossomparent[jblossoms[-1]])
|
| 911 |
+
iblossoms.reverse()
|
| 912 |
+
jblossoms.reverse()
|
| 913 |
+
for bi, bj in zip(iblossoms, jblossoms):
|
| 914 |
+
if bi != bj:
|
| 915 |
+
break
|
| 916 |
+
s += 2 * blossomdual[bi]
|
| 917 |
+
assert s >= 0
|
| 918 |
+
if mate.get(i) == j or mate.get(j) == i:
|
| 919 |
+
assert mate[i] == j and mate[j] == i
|
| 920 |
+
assert s == 0
|
| 921 |
+
# 2. all single vertices have zero dual value;
|
| 922 |
+
for v in gnodes:
|
| 923 |
+
assert (v in mate) or dualvar[v] + vdualoffset == 0
|
| 924 |
+
# 3. all blossoms with positive dual value are full.
|
| 925 |
+
for b in blossomdual:
|
| 926 |
+
if blossomdual[b] > 0:
|
| 927 |
+
assert len(b.edges) % 2 == 1
|
| 928 |
+
for i, j in b.edges[1::2]:
|
| 929 |
+
assert mate[i] == j and mate[j] == i
|
| 930 |
+
# Ok.
|
| 931 |
+
|
| 932 |
+
# Main loop: continue until no further improvement is possible.
|
| 933 |
+
while 1:
|
| 934 |
+
# Each iteration of this loop is a "stage".
|
| 935 |
+
# A stage finds an augmenting path and uses that to improve
|
| 936 |
+
# the matching.
|
| 937 |
+
|
| 938 |
+
# Remove labels from top-level blossoms/vertices.
|
| 939 |
+
label.clear()
|
| 940 |
+
labeledge.clear()
|
| 941 |
+
|
| 942 |
+
# Forget all about least-slack edges.
|
| 943 |
+
bestedge.clear()
|
| 944 |
+
for b in blossomdual:
|
| 945 |
+
b.mybestedges = None
|
| 946 |
+
|
| 947 |
+
# Loss of labeling means that we can not be sure that currently
|
| 948 |
+
# allowable edges remain allowable throughout this stage.
|
| 949 |
+
allowedge.clear()
|
| 950 |
+
|
| 951 |
+
# Make queue empty.
|
| 952 |
+
queue[:] = []
|
| 953 |
+
|
| 954 |
+
# Label single blossoms/vertices with S and put them in the queue.
|
| 955 |
+
for v in gnodes:
|
| 956 |
+
if (v not in mate) and label.get(inblossom[v]) is None:
|
| 957 |
+
assignLabel(v, 1, None)
|
| 958 |
+
|
| 959 |
+
# Loop until we succeed in augmenting the matching.
|
| 960 |
+
augmented = 0
|
| 961 |
+
while 1:
|
| 962 |
+
# Each iteration of this loop is a "substage".
|
| 963 |
+
# A substage tries to find an augmenting path;
|
| 964 |
+
# if found, the path is used to improve the matching and
|
| 965 |
+
# the stage ends. If there is no augmenting path, the
|
| 966 |
+
# primal-dual method is used to pump some slack out of
|
| 967 |
+
# the dual variables.
|
| 968 |
+
|
| 969 |
+
# Continue labeling until all vertices which are reachable
|
| 970 |
+
# through an alternating path have got a label.
|
| 971 |
+
while queue and not augmented:
|
| 972 |
+
# Take an S vertex from the queue.
|
| 973 |
+
v = queue.pop()
|
| 974 |
+
assert label[inblossom[v]] == 1
|
| 975 |
+
|
| 976 |
+
# Scan its neighbors:
|
| 977 |
+
for w in G.neighbors(v):
|
| 978 |
+
if w == v:
|
| 979 |
+
continue # ignore self-loops
|
| 980 |
+
# w is a neighbor to v
|
| 981 |
+
bv = inblossom[v]
|
| 982 |
+
bw = inblossom[w]
|
| 983 |
+
if bv == bw:
|
| 984 |
+
# this edge is internal to a blossom; ignore it
|
| 985 |
+
continue
|
| 986 |
+
if (v, w) not in allowedge:
|
| 987 |
+
kslack = slack(v, w)
|
| 988 |
+
if kslack <= 0:
|
| 989 |
+
# edge k has zero slack => it is allowable
|
| 990 |
+
allowedge[(v, w)] = allowedge[(w, v)] = True
|
| 991 |
+
if (v, w) in allowedge:
|
| 992 |
+
if label.get(bw) is None:
|
| 993 |
+
# (C1) w is a free vertex;
|
| 994 |
+
# label w with T and label its mate with S (R12).
|
| 995 |
+
assignLabel(w, 2, v)
|
| 996 |
+
elif label.get(bw) == 1:
|
| 997 |
+
# (C2) w is an S-vertex (not in the same blossom);
|
| 998 |
+
# follow back-links to discover either an
|
| 999 |
+
# augmenting path or a new blossom.
|
| 1000 |
+
base = scanBlossom(v, w)
|
| 1001 |
+
if base is not NoNode:
|
| 1002 |
+
# Found a new blossom; add it to the blossom
|
| 1003 |
+
# bookkeeping and turn it into an S-blossom.
|
| 1004 |
+
addBlossom(base, v, w)
|
| 1005 |
+
else:
|
| 1006 |
+
# Found an augmenting path; augment the
|
| 1007 |
+
# matching and end this stage.
|
| 1008 |
+
augmentMatching(v, w)
|
| 1009 |
+
augmented = 1
|
| 1010 |
+
break
|
| 1011 |
+
elif label.get(w) is None:
|
| 1012 |
+
# w is inside a T-blossom, but w itself has not
|
| 1013 |
+
# yet been reached from outside the blossom;
|
| 1014 |
+
# mark it as reached (we need this to relabel
|
| 1015 |
+
# during T-blossom expansion).
|
| 1016 |
+
assert label[bw] == 2
|
| 1017 |
+
label[w] = 2
|
| 1018 |
+
labeledge[w] = (v, w)
|
| 1019 |
+
elif label.get(bw) == 1:
|
| 1020 |
+
# keep track of the least-slack non-allowable edge to
|
| 1021 |
+
# a different S-blossom.
|
| 1022 |
+
if bestedge.get(bv) is None or kslack < slack(*bestedge[bv]):
|
| 1023 |
+
bestedge[bv] = (v, w)
|
| 1024 |
+
elif label.get(w) is None:
|
| 1025 |
+
# w is a free vertex (or an unreached vertex inside
|
| 1026 |
+
# a T-blossom) but we can not reach it yet;
|
| 1027 |
+
# keep track of the least-slack edge that reaches w.
|
| 1028 |
+
if bestedge.get(w) is None or kslack < slack(*bestedge[w]):
|
| 1029 |
+
bestedge[w] = (v, w)
|
| 1030 |
+
|
| 1031 |
+
if augmented:
|
| 1032 |
+
break
|
| 1033 |
+
|
| 1034 |
+
# There is no augmenting path under these constraints;
|
| 1035 |
+
# compute delta and reduce slack in the optimization problem.
|
| 1036 |
+
# (Note that our vertex dual variables, edge slacks and delta's
|
| 1037 |
+
# are pre-multiplied by two.)
|
| 1038 |
+
deltatype = -1
|
| 1039 |
+
delta = deltaedge = deltablossom = None
|
| 1040 |
+
|
| 1041 |
+
# Compute delta1: the minimum value of any vertex dual.
|
| 1042 |
+
if not maxcardinality:
|
| 1043 |
+
deltatype = 1
|
| 1044 |
+
delta = min(dualvar.values())
|
| 1045 |
+
|
| 1046 |
+
# Compute delta2: the minimum slack on any edge between
|
| 1047 |
+
# an S-vertex and a free vertex.
|
| 1048 |
+
for v in G.nodes():
|
| 1049 |
+
if label.get(inblossom[v]) is None and bestedge.get(v) is not None:
|
| 1050 |
+
d = slack(*bestedge[v])
|
| 1051 |
+
if deltatype == -1 or d < delta:
|
| 1052 |
+
delta = d
|
| 1053 |
+
deltatype = 2
|
| 1054 |
+
deltaedge = bestedge[v]
|
| 1055 |
+
|
| 1056 |
+
# Compute delta3: half the minimum slack on any edge between
|
| 1057 |
+
# a pair of S-blossoms.
|
| 1058 |
+
for b in blossomparent:
|
| 1059 |
+
if (
|
| 1060 |
+
blossomparent[b] is None
|
| 1061 |
+
and label.get(b) == 1
|
| 1062 |
+
and bestedge.get(b) is not None
|
| 1063 |
+
):
|
| 1064 |
+
kslack = slack(*bestedge[b])
|
| 1065 |
+
if allinteger:
|
| 1066 |
+
assert (kslack % 2) == 0
|
| 1067 |
+
d = kslack // 2
|
| 1068 |
+
else:
|
| 1069 |
+
d = kslack / 2.0
|
| 1070 |
+
if deltatype == -1 or d < delta:
|
| 1071 |
+
delta = d
|
| 1072 |
+
deltatype = 3
|
| 1073 |
+
deltaedge = bestedge[b]
|
| 1074 |
+
|
| 1075 |
+
# Compute delta4: minimum z variable of any T-blossom.
|
| 1076 |
+
for b in blossomdual:
|
| 1077 |
+
if (
|
| 1078 |
+
blossomparent[b] is None
|
| 1079 |
+
and label.get(b) == 2
|
| 1080 |
+
and (deltatype == -1 or blossomdual[b] < delta)
|
| 1081 |
+
):
|
| 1082 |
+
delta = blossomdual[b]
|
| 1083 |
+
deltatype = 4
|
| 1084 |
+
deltablossom = b
|
| 1085 |
+
|
| 1086 |
+
if deltatype == -1:
|
| 1087 |
+
# No further improvement possible; max-cardinality optimum
|
| 1088 |
+
# reached. Do a final delta update to make the optimum
|
| 1089 |
+
# verifiable.
|
| 1090 |
+
assert maxcardinality
|
| 1091 |
+
deltatype = 1
|
| 1092 |
+
delta = max(0, min(dualvar.values()))
|
| 1093 |
+
|
| 1094 |
+
# Update dual variables according to delta.
|
| 1095 |
+
for v in gnodes:
|
| 1096 |
+
if label.get(inblossom[v]) == 1:
|
| 1097 |
+
# S-vertex: 2*u = 2*u - 2*delta
|
| 1098 |
+
dualvar[v] -= delta
|
| 1099 |
+
elif label.get(inblossom[v]) == 2:
|
| 1100 |
+
# T-vertex: 2*u = 2*u + 2*delta
|
| 1101 |
+
dualvar[v] += delta
|
| 1102 |
+
for b in blossomdual:
|
| 1103 |
+
if blossomparent[b] is None:
|
| 1104 |
+
if label.get(b) == 1:
|
| 1105 |
+
# top-level S-blossom: z = z + 2*delta
|
| 1106 |
+
blossomdual[b] += delta
|
| 1107 |
+
elif label.get(b) == 2:
|
| 1108 |
+
# top-level T-blossom: z = z - 2*delta
|
| 1109 |
+
blossomdual[b] -= delta
|
| 1110 |
+
|
| 1111 |
+
# Take action at the point where minimum delta occurred.
|
| 1112 |
+
if deltatype == 1:
|
| 1113 |
+
# No further improvement possible; optimum reached.
|
| 1114 |
+
break
|
| 1115 |
+
elif deltatype == 2:
|
| 1116 |
+
# Use the least-slack edge to continue the search.
|
| 1117 |
+
(v, w) = deltaedge
|
| 1118 |
+
assert label[inblossom[v]] == 1
|
| 1119 |
+
allowedge[(v, w)] = allowedge[(w, v)] = True
|
| 1120 |
+
queue.append(v)
|
| 1121 |
+
elif deltatype == 3:
|
| 1122 |
+
# Use the least-slack edge to continue the search.
|
| 1123 |
+
(v, w) = deltaedge
|
| 1124 |
+
allowedge[(v, w)] = allowedge[(w, v)] = True
|
| 1125 |
+
assert label[inblossom[v]] == 1
|
| 1126 |
+
queue.append(v)
|
| 1127 |
+
elif deltatype == 4:
|
| 1128 |
+
# Expand the least-z blossom.
|
| 1129 |
+
expandBlossom(deltablossom, False)
|
| 1130 |
+
|
| 1131 |
+
# End of a this substage.
|
| 1132 |
+
|
| 1133 |
+
# Paranoia check that the matching is symmetric.
|
| 1134 |
+
for v in mate:
|
| 1135 |
+
assert mate[mate[v]] == v
|
| 1136 |
+
|
| 1137 |
+
# Stop when no more augmenting path can be found.
|
| 1138 |
+
if not augmented:
|
| 1139 |
+
break
|
| 1140 |
+
|
| 1141 |
+
# End of a stage; expand all S-blossoms which have zero dual.
|
| 1142 |
+
for b in list(blossomdual.keys()):
|
| 1143 |
+
if b not in blossomdual:
|
| 1144 |
+
continue # already expanded
|
| 1145 |
+
if blossomparent[b] is None and label.get(b) == 1 and blossomdual[b] == 0:
|
| 1146 |
+
expandBlossom(b, True)
|
| 1147 |
+
|
| 1148 |
+
# Verify that we reached the optimum solution (only for integer weights).
|
| 1149 |
+
if allinteger:
|
| 1150 |
+
verifyOptimum()
|
| 1151 |
+
|
| 1152 |
+
return matching_dict_to_set(mate)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/moral.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r"""Function for computing the moral graph of a directed graph."""
|
| 2 |
+
|
| 3 |
+
import itertools
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.utils import not_implemented_for
|
| 7 |
+
|
| 8 |
+
__all__ = ["moral_graph"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@not_implemented_for("undirected")
|
| 12 |
+
@nx._dispatchable(returns_graph=True)
|
| 13 |
+
def moral_graph(G):
|
| 14 |
+
r"""Return the Moral Graph
|
| 15 |
+
|
| 16 |
+
Returns the moralized graph of a given directed graph.
|
| 17 |
+
|
| 18 |
+
Parameters
|
| 19 |
+
----------
|
| 20 |
+
G : NetworkX graph
|
| 21 |
+
Directed graph
|
| 22 |
+
|
| 23 |
+
Returns
|
| 24 |
+
-------
|
| 25 |
+
H : NetworkX graph
|
| 26 |
+
The undirected moralized graph of G
|
| 27 |
+
|
| 28 |
+
Raises
|
| 29 |
+
------
|
| 30 |
+
NetworkXNotImplemented
|
| 31 |
+
If `G` is undirected.
|
| 32 |
+
|
| 33 |
+
Examples
|
| 34 |
+
--------
|
| 35 |
+
>>> G = nx.DiGraph([(1, 2), (2, 3), (2, 5), (3, 4), (4, 3)])
|
| 36 |
+
>>> G_moral = nx.moral_graph(G)
|
| 37 |
+
>>> G_moral.edges()
|
| 38 |
+
EdgeView([(1, 2), (2, 3), (2, 5), (2, 4), (3, 4)])
|
| 39 |
+
|
| 40 |
+
Notes
|
| 41 |
+
-----
|
| 42 |
+
A moral graph is an undirected graph H = (V, E) generated from a
|
| 43 |
+
directed Graph, where if a node has more than one parent node, edges
|
| 44 |
+
between these parent nodes are inserted and all directed edges become
|
| 45 |
+
undirected.
|
| 46 |
+
|
| 47 |
+
https://en.wikipedia.org/wiki/Moral_graph
|
| 48 |
+
|
| 49 |
+
References
|
| 50 |
+
----------
|
| 51 |
+
.. [1] Wray L. Buntine. 1995. Chain graphs for learning.
|
| 52 |
+
In Proceedings of the Eleventh conference on Uncertainty
|
| 53 |
+
in artificial intelligence (UAI'95)
|
| 54 |
+
"""
|
| 55 |
+
H = G.to_undirected()
|
| 56 |
+
for preds in G.pred.values():
|
| 57 |
+
predecessors_combinations = itertools.combinations(preds, r=2)
|
| 58 |
+
H.add_edges_from(predecessors_combinations)
|
| 59 |
+
return H
|
.venv/lib/python3.11/site-packages/networkx/algorithms/non_randomness.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r"""Computation of graph non-randomness"""
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.utils import not_implemented_for
|
| 7 |
+
|
| 8 |
+
__all__ = ["non_randomness"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@not_implemented_for("directed")
|
| 12 |
+
@not_implemented_for("multigraph")
|
| 13 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 14 |
+
def non_randomness(G, k=None, weight="weight"):
|
| 15 |
+
"""Compute the non-randomness of graph G.
|
| 16 |
+
|
| 17 |
+
The first returned value nr is the sum of non-randomness values of all
|
| 18 |
+
edges within the graph (where the non-randomness of an edge tends to be
|
| 19 |
+
small when the two nodes linked by that edge are from two different
|
| 20 |
+
communities).
|
| 21 |
+
|
| 22 |
+
The second computed value nr_rd is a relative measure that indicates
|
| 23 |
+
to what extent graph G is different from random graphs in terms
|
| 24 |
+
of probability. When it is close to 0, the graph tends to be more
|
| 25 |
+
likely generated by an Erdos Renyi model.
|
| 26 |
+
|
| 27 |
+
Parameters
|
| 28 |
+
----------
|
| 29 |
+
G : NetworkX graph
|
| 30 |
+
Graph must be symmetric, connected, and without self-loops.
|
| 31 |
+
|
| 32 |
+
k : int
|
| 33 |
+
The number of communities in G.
|
| 34 |
+
If k is not set, the function will use a default community
|
| 35 |
+
detection algorithm to set it.
|
| 36 |
+
|
| 37 |
+
weight : string or None, optional (default=None)
|
| 38 |
+
The name of an edge attribute that holds the numerical value used
|
| 39 |
+
as a weight. If None, then each edge has weight 1, i.e., the graph is
|
| 40 |
+
binary.
|
| 41 |
+
|
| 42 |
+
Returns
|
| 43 |
+
-------
|
| 44 |
+
non-randomness : (float, float) tuple
|
| 45 |
+
Non-randomness, Relative non-randomness w.r.t.
|
| 46 |
+
Erdos Renyi random graphs.
|
| 47 |
+
|
| 48 |
+
Raises
|
| 49 |
+
------
|
| 50 |
+
NetworkXException
|
| 51 |
+
if the input graph is not connected.
|
| 52 |
+
NetworkXError
|
| 53 |
+
if the input graph contains self-loops or if graph has no edges.
|
| 54 |
+
|
| 55 |
+
Examples
|
| 56 |
+
--------
|
| 57 |
+
>>> G = nx.karate_club_graph()
|
| 58 |
+
>>> nr, nr_rd = nx.non_randomness(G, 2)
|
| 59 |
+
>>> nr, nr_rd = nx.non_randomness(G, 2, "weight")
|
| 60 |
+
|
| 61 |
+
Notes
|
| 62 |
+
-----
|
| 63 |
+
This computes Eq. (4.4) and (4.5) in Ref. [1]_.
|
| 64 |
+
|
| 65 |
+
If a weight field is passed, this algorithm will use the eigenvalues
|
| 66 |
+
of the weighted adjacency matrix to compute Eq. (4.4) and (4.5).
|
| 67 |
+
|
| 68 |
+
References
|
| 69 |
+
----------
|
| 70 |
+
.. [1] Xiaowei Ying and Xintao Wu,
|
| 71 |
+
On Randomness Measures for Social Networks,
|
| 72 |
+
SIAM International Conference on Data Mining. 2009
|
| 73 |
+
"""
|
| 74 |
+
import numpy as np
|
| 75 |
+
|
| 76 |
+
# corner case: graph has no edges
|
| 77 |
+
if nx.is_empty(G):
|
| 78 |
+
raise nx.NetworkXError("non_randomness not applicable to empty graphs")
|
| 79 |
+
if not nx.is_connected(G):
|
| 80 |
+
raise nx.NetworkXException("Non connected graph.")
|
| 81 |
+
if len(list(nx.selfloop_edges(G))) > 0:
|
| 82 |
+
raise nx.NetworkXError("Graph must not contain self-loops")
|
| 83 |
+
|
| 84 |
+
if k is None:
|
| 85 |
+
k = len(tuple(nx.community.label_propagation_communities(G)))
|
| 86 |
+
|
| 87 |
+
# eq. 4.4
|
| 88 |
+
eigenvalues = np.linalg.eigvals(nx.to_numpy_array(G, weight=weight))
|
| 89 |
+
nr = float(np.real(np.sum(eigenvalues[:k])))
|
| 90 |
+
|
| 91 |
+
n = G.number_of_nodes()
|
| 92 |
+
m = G.number_of_edges()
|
| 93 |
+
p = (2 * k * m) / (n * (n - k))
|
| 94 |
+
|
| 95 |
+
# eq. 4.5
|
| 96 |
+
nr_rd = (nr - ((n - 2 * k) * p + k)) / math.sqrt(2 * k * p * (1 - p))
|
| 97 |
+
|
| 98 |
+
return nr, nr_rd
|
.venv/lib/python3.11/site-packages/networkx/algorithms/planar_drawing.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections import defaultdict
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
__all__ = ["combinatorial_embedding_to_pos"]
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def combinatorial_embedding_to_pos(embedding, fully_triangulate=False):
|
| 9 |
+
"""Assigns every node a (x, y) position based on the given embedding
|
| 10 |
+
|
| 11 |
+
The algorithm iteratively inserts nodes of the input graph in a certain
|
| 12 |
+
order and rearranges previously inserted nodes so that the planar drawing
|
| 13 |
+
stays valid. This is done efficiently by only maintaining relative
|
| 14 |
+
positions during the node placements and calculating the absolute positions
|
| 15 |
+
at the end. For more information see [1]_.
|
| 16 |
+
|
| 17 |
+
Parameters
|
| 18 |
+
----------
|
| 19 |
+
embedding : nx.PlanarEmbedding
|
| 20 |
+
This defines the order of the edges
|
| 21 |
+
|
| 22 |
+
fully_triangulate : bool
|
| 23 |
+
If set to True the algorithm adds edges to a copy of the input
|
| 24 |
+
embedding and makes it chordal.
|
| 25 |
+
|
| 26 |
+
Returns
|
| 27 |
+
-------
|
| 28 |
+
pos : dict
|
| 29 |
+
Maps each node to a tuple that defines the (x, y) position
|
| 30 |
+
|
| 31 |
+
References
|
| 32 |
+
----------
|
| 33 |
+
.. [1] M. Chrobak and T.H. Payne:
|
| 34 |
+
A Linear-time Algorithm for Drawing a Planar Graph on a Grid 1989
|
| 35 |
+
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.6677
|
| 36 |
+
|
| 37 |
+
"""
|
| 38 |
+
if len(embedding.nodes()) < 4:
|
| 39 |
+
# Position the node in any triangle
|
| 40 |
+
default_positions = [(0, 0), (2, 0), (1, 1)]
|
| 41 |
+
pos = {}
|
| 42 |
+
for i, v in enumerate(embedding.nodes()):
|
| 43 |
+
pos[v] = default_positions[i]
|
| 44 |
+
return pos
|
| 45 |
+
|
| 46 |
+
embedding, outer_face = triangulate_embedding(embedding, fully_triangulate)
|
| 47 |
+
|
| 48 |
+
# The following dicts map a node to another node
|
| 49 |
+
# If a node is not in the key set it means that the node is not yet in G_k
|
| 50 |
+
# If a node maps to None then the corresponding subtree does not exist
|
| 51 |
+
left_t_child = {}
|
| 52 |
+
right_t_child = {}
|
| 53 |
+
|
| 54 |
+
# The following dicts map a node to an integer
|
| 55 |
+
delta_x = {}
|
| 56 |
+
y_coordinate = {}
|
| 57 |
+
|
| 58 |
+
node_list = get_canonical_ordering(embedding, outer_face)
|
| 59 |
+
|
| 60 |
+
# 1. Phase: Compute relative positions
|
| 61 |
+
|
| 62 |
+
# Initialization
|
| 63 |
+
v1, v2, v3 = node_list[0][0], node_list[1][0], node_list[2][0]
|
| 64 |
+
|
| 65 |
+
delta_x[v1] = 0
|
| 66 |
+
y_coordinate[v1] = 0
|
| 67 |
+
right_t_child[v1] = v3
|
| 68 |
+
left_t_child[v1] = None
|
| 69 |
+
|
| 70 |
+
delta_x[v2] = 1
|
| 71 |
+
y_coordinate[v2] = 0
|
| 72 |
+
right_t_child[v2] = None
|
| 73 |
+
left_t_child[v2] = None
|
| 74 |
+
|
| 75 |
+
delta_x[v3] = 1
|
| 76 |
+
y_coordinate[v3] = 1
|
| 77 |
+
right_t_child[v3] = v2
|
| 78 |
+
left_t_child[v3] = None
|
| 79 |
+
|
| 80 |
+
for k in range(3, len(node_list)):
|
| 81 |
+
vk, contour_nbrs = node_list[k]
|
| 82 |
+
wp = contour_nbrs[0]
|
| 83 |
+
wp1 = contour_nbrs[1]
|
| 84 |
+
wq = contour_nbrs[-1]
|
| 85 |
+
wq1 = contour_nbrs[-2]
|
| 86 |
+
adds_mult_tri = len(contour_nbrs) > 2
|
| 87 |
+
|
| 88 |
+
# Stretch gaps:
|
| 89 |
+
delta_x[wp1] += 1
|
| 90 |
+
delta_x[wq] += 1
|
| 91 |
+
|
| 92 |
+
delta_x_wp_wq = sum(delta_x[x] for x in contour_nbrs[1:])
|
| 93 |
+
|
| 94 |
+
# Adjust offsets
|
| 95 |
+
delta_x[vk] = (-y_coordinate[wp] + delta_x_wp_wq + y_coordinate[wq]) // 2
|
| 96 |
+
y_coordinate[vk] = (y_coordinate[wp] + delta_x_wp_wq + y_coordinate[wq]) // 2
|
| 97 |
+
delta_x[wq] = delta_x_wp_wq - delta_x[vk]
|
| 98 |
+
if adds_mult_tri:
|
| 99 |
+
delta_x[wp1] -= delta_x[vk]
|
| 100 |
+
|
| 101 |
+
# Install v_k:
|
| 102 |
+
right_t_child[wp] = vk
|
| 103 |
+
right_t_child[vk] = wq
|
| 104 |
+
if adds_mult_tri:
|
| 105 |
+
left_t_child[vk] = wp1
|
| 106 |
+
right_t_child[wq1] = None
|
| 107 |
+
else:
|
| 108 |
+
left_t_child[vk] = None
|
| 109 |
+
|
| 110 |
+
# 2. Phase: Set absolute positions
|
| 111 |
+
pos = {}
|
| 112 |
+
pos[v1] = (0, y_coordinate[v1])
|
| 113 |
+
remaining_nodes = [v1]
|
| 114 |
+
while remaining_nodes:
|
| 115 |
+
parent_node = remaining_nodes.pop()
|
| 116 |
+
|
| 117 |
+
# Calculate position for left child
|
| 118 |
+
set_position(
|
| 119 |
+
parent_node, left_t_child, remaining_nodes, delta_x, y_coordinate, pos
|
| 120 |
+
)
|
| 121 |
+
# Calculate position for right child
|
| 122 |
+
set_position(
|
| 123 |
+
parent_node, right_t_child, remaining_nodes, delta_x, y_coordinate, pos
|
| 124 |
+
)
|
| 125 |
+
return pos
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def set_position(parent, tree, remaining_nodes, delta_x, y_coordinate, pos):
|
| 129 |
+
"""Helper method to calculate the absolute position of nodes."""
|
| 130 |
+
child = tree[parent]
|
| 131 |
+
parent_node_x = pos[parent][0]
|
| 132 |
+
if child is not None:
|
| 133 |
+
# Calculate pos of child
|
| 134 |
+
child_x = parent_node_x + delta_x[child]
|
| 135 |
+
pos[child] = (child_x, y_coordinate[child])
|
| 136 |
+
# Remember to calculate pos of its children
|
| 137 |
+
remaining_nodes.append(child)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def get_canonical_ordering(embedding, outer_face):
|
| 141 |
+
"""Returns a canonical ordering of the nodes
|
| 142 |
+
|
| 143 |
+
The canonical ordering of nodes (v1, ..., vn) must fulfill the following
|
| 144 |
+
conditions:
|
| 145 |
+
(See Lemma 1 in [2]_)
|
| 146 |
+
|
| 147 |
+
- For the subgraph G_k of the input graph induced by v1, ..., vk it holds:
|
| 148 |
+
- 2-connected
|
| 149 |
+
- internally triangulated
|
| 150 |
+
- the edge (v1, v2) is part of the outer face
|
| 151 |
+
- For a node v(k+1) the following holds:
|
| 152 |
+
- The node v(k+1) is part of the outer face of G_k
|
| 153 |
+
- It has at least two neighbors in G_k
|
| 154 |
+
- All neighbors of v(k+1) in G_k lie consecutively on the outer face of
|
| 155 |
+
G_k (excluding the edge (v1, v2)).
|
| 156 |
+
|
| 157 |
+
The algorithm used here starts with G_n (containing all nodes). It first
|
| 158 |
+
selects the nodes v1 and v2. And then tries to find the order of the other
|
| 159 |
+
nodes by checking which node can be removed in order to fulfill the
|
| 160 |
+
conditions mentioned above. This is done by calculating the number of
|
| 161 |
+
chords of nodes on the outer face. For more information see [1]_.
|
| 162 |
+
|
| 163 |
+
Parameters
|
| 164 |
+
----------
|
| 165 |
+
embedding : nx.PlanarEmbedding
|
| 166 |
+
The embedding must be triangulated
|
| 167 |
+
outer_face : list
|
| 168 |
+
The nodes on the outer face of the graph
|
| 169 |
+
|
| 170 |
+
Returns
|
| 171 |
+
-------
|
| 172 |
+
ordering : list
|
| 173 |
+
A list of tuples `(vk, wp_wq)`. Here `vk` is the node at this position
|
| 174 |
+
in the canonical ordering. The element `wp_wq` is a list of nodes that
|
| 175 |
+
make up the outer face of G_k.
|
| 176 |
+
|
| 177 |
+
References
|
| 178 |
+
----------
|
| 179 |
+
.. [1] Steven Chaplick.
|
| 180 |
+
Canonical Orders of Planar Graphs and (some of) Their Applications 2015
|
| 181 |
+
https://wuecampus2.uni-wuerzburg.de/moodle/pluginfile.php/545727/mod_resource/content/0/vg-ss15-vl03-canonical-orders-druckversion.pdf
|
| 182 |
+
.. [2] M. Chrobak and T.H. Payne:
|
| 183 |
+
A Linear-time Algorithm for Drawing a Planar Graph on a Grid 1989
|
| 184 |
+
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.6677
|
| 185 |
+
|
| 186 |
+
"""
|
| 187 |
+
v1 = outer_face[0]
|
| 188 |
+
v2 = outer_face[1]
|
| 189 |
+
chords = defaultdict(int) # Maps nodes to the number of their chords
|
| 190 |
+
marked_nodes = set()
|
| 191 |
+
ready_to_pick = set(outer_face)
|
| 192 |
+
|
| 193 |
+
# Initialize outer_face_ccw_nbr (do not include v1 -> v2)
|
| 194 |
+
outer_face_ccw_nbr = {}
|
| 195 |
+
prev_nbr = v2
|
| 196 |
+
for idx in range(2, len(outer_face)):
|
| 197 |
+
outer_face_ccw_nbr[prev_nbr] = outer_face[idx]
|
| 198 |
+
prev_nbr = outer_face[idx]
|
| 199 |
+
outer_face_ccw_nbr[prev_nbr] = v1
|
| 200 |
+
|
| 201 |
+
# Initialize outer_face_cw_nbr (do not include v2 -> v1)
|
| 202 |
+
outer_face_cw_nbr = {}
|
| 203 |
+
prev_nbr = v1
|
| 204 |
+
for idx in range(len(outer_face) - 1, 0, -1):
|
| 205 |
+
outer_face_cw_nbr[prev_nbr] = outer_face[idx]
|
| 206 |
+
prev_nbr = outer_face[idx]
|
| 207 |
+
|
| 208 |
+
def is_outer_face_nbr(x, y):
|
| 209 |
+
if x not in outer_face_ccw_nbr:
|
| 210 |
+
return outer_face_cw_nbr[x] == y
|
| 211 |
+
if x not in outer_face_cw_nbr:
|
| 212 |
+
return outer_face_ccw_nbr[x] == y
|
| 213 |
+
return outer_face_ccw_nbr[x] == y or outer_face_cw_nbr[x] == y
|
| 214 |
+
|
| 215 |
+
def is_on_outer_face(x):
|
| 216 |
+
return x not in marked_nodes and (x in outer_face_ccw_nbr or x == v1)
|
| 217 |
+
|
| 218 |
+
# Initialize number of chords
|
| 219 |
+
for v in outer_face:
|
| 220 |
+
for nbr in embedding.neighbors_cw_order(v):
|
| 221 |
+
if is_on_outer_face(nbr) and not is_outer_face_nbr(v, nbr):
|
| 222 |
+
chords[v] += 1
|
| 223 |
+
ready_to_pick.discard(v)
|
| 224 |
+
|
| 225 |
+
# Initialize canonical_ordering
|
| 226 |
+
canonical_ordering = [None] * len(embedding.nodes())
|
| 227 |
+
canonical_ordering[0] = (v1, [])
|
| 228 |
+
canonical_ordering[1] = (v2, [])
|
| 229 |
+
ready_to_pick.discard(v1)
|
| 230 |
+
ready_to_pick.discard(v2)
|
| 231 |
+
|
| 232 |
+
for k in range(len(embedding.nodes()) - 1, 1, -1):
|
| 233 |
+
# 1. Pick v from ready_to_pick
|
| 234 |
+
v = ready_to_pick.pop()
|
| 235 |
+
marked_nodes.add(v)
|
| 236 |
+
|
| 237 |
+
# v has exactly two neighbors on the outer face (wp and wq)
|
| 238 |
+
wp = None
|
| 239 |
+
wq = None
|
| 240 |
+
# Iterate over neighbors of v to find wp and wq
|
| 241 |
+
nbr_iterator = iter(embedding.neighbors_cw_order(v))
|
| 242 |
+
while True:
|
| 243 |
+
nbr = next(nbr_iterator)
|
| 244 |
+
if nbr in marked_nodes:
|
| 245 |
+
# Only consider nodes that are not yet removed
|
| 246 |
+
continue
|
| 247 |
+
if is_on_outer_face(nbr):
|
| 248 |
+
# nbr is either wp or wq
|
| 249 |
+
if nbr == v1:
|
| 250 |
+
wp = v1
|
| 251 |
+
elif nbr == v2:
|
| 252 |
+
wq = v2
|
| 253 |
+
else:
|
| 254 |
+
if outer_face_cw_nbr[nbr] == v:
|
| 255 |
+
# nbr is wp
|
| 256 |
+
wp = nbr
|
| 257 |
+
else:
|
| 258 |
+
# nbr is wq
|
| 259 |
+
wq = nbr
|
| 260 |
+
if wp is not None and wq is not None:
|
| 261 |
+
# We don't need to iterate any further
|
| 262 |
+
break
|
| 263 |
+
|
| 264 |
+
# Obtain new nodes on outer face (neighbors of v from wp to wq)
|
| 265 |
+
wp_wq = [wp]
|
| 266 |
+
nbr = wp
|
| 267 |
+
while nbr != wq:
|
| 268 |
+
# Get next neighbor (clockwise on the outer face)
|
| 269 |
+
next_nbr = embedding[v][nbr]["ccw"]
|
| 270 |
+
wp_wq.append(next_nbr)
|
| 271 |
+
# Update outer face
|
| 272 |
+
outer_face_cw_nbr[nbr] = next_nbr
|
| 273 |
+
outer_face_ccw_nbr[next_nbr] = nbr
|
| 274 |
+
# Move to next neighbor of v
|
| 275 |
+
nbr = next_nbr
|
| 276 |
+
|
| 277 |
+
if len(wp_wq) == 2:
|
| 278 |
+
# There was a chord between wp and wq, decrease number of chords
|
| 279 |
+
chords[wp] -= 1
|
| 280 |
+
if chords[wp] == 0:
|
| 281 |
+
ready_to_pick.add(wp)
|
| 282 |
+
chords[wq] -= 1
|
| 283 |
+
if chords[wq] == 0:
|
| 284 |
+
ready_to_pick.add(wq)
|
| 285 |
+
else:
|
| 286 |
+
# Update all chords involving w_(p+1) to w_(q-1)
|
| 287 |
+
new_face_nodes = set(wp_wq[1:-1])
|
| 288 |
+
for w in new_face_nodes:
|
| 289 |
+
# If we do not find a chord for w later we can pick it next
|
| 290 |
+
ready_to_pick.add(w)
|
| 291 |
+
for nbr in embedding.neighbors_cw_order(w):
|
| 292 |
+
if is_on_outer_face(nbr) and not is_outer_face_nbr(w, nbr):
|
| 293 |
+
# There is a chord involving w
|
| 294 |
+
chords[w] += 1
|
| 295 |
+
ready_to_pick.discard(w)
|
| 296 |
+
if nbr not in new_face_nodes:
|
| 297 |
+
# Also increase chord for the neighbor
|
| 298 |
+
# We only iterator over new_face_nodes
|
| 299 |
+
chords[nbr] += 1
|
| 300 |
+
ready_to_pick.discard(nbr)
|
| 301 |
+
# Set the canonical ordering node and the list of contour neighbors
|
| 302 |
+
canonical_ordering[k] = (v, wp_wq)
|
| 303 |
+
|
| 304 |
+
return canonical_ordering
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def triangulate_face(embedding, v1, v2):
|
| 308 |
+
"""Triangulates the face given by half edge (v, w)
|
| 309 |
+
|
| 310 |
+
Parameters
|
| 311 |
+
----------
|
| 312 |
+
embedding : nx.PlanarEmbedding
|
| 313 |
+
v1 : node
|
| 314 |
+
The half-edge (v1, v2) belongs to the face that gets triangulated
|
| 315 |
+
v2 : node
|
| 316 |
+
"""
|
| 317 |
+
_, v3 = embedding.next_face_half_edge(v1, v2)
|
| 318 |
+
_, v4 = embedding.next_face_half_edge(v2, v3)
|
| 319 |
+
if v1 in (v2, v3):
|
| 320 |
+
# The component has less than 3 nodes
|
| 321 |
+
return
|
| 322 |
+
while v1 != v4:
|
| 323 |
+
# Add edge if not already present on other side
|
| 324 |
+
if embedding.has_edge(v1, v3):
|
| 325 |
+
# Cannot triangulate at this position
|
| 326 |
+
v1, v2, v3 = v2, v3, v4
|
| 327 |
+
else:
|
| 328 |
+
# Add edge for triangulation
|
| 329 |
+
embedding.add_half_edge(v1, v3, ccw=v2)
|
| 330 |
+
embedding.add_half_edge(v3, v1, cw=v2)
|
| 331 |
+
v1, v2, v3 = v1, v3, v4
|
| 332 |
+
# Get next node
|
| 333 |
+
_, v4 = embedding.next_face_half_edge(v2, v3)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def triangulate_embedding(embedding, fully_triangulate=True):
|
| 337 |
+
"""Triangulates the embedding.
|
| 338 |
+
|
| 339 |
+
Traverses faces of the embedding and adds edges to a copy of the
|
| 340 |
+
embedding to triangulate it.
|
| 341 |
+
The method also ensures that the resulting graph is 2-connected by adding
|
| 342 |
+
edges if the same vertex is contained twice on a path around a face.
|
| 343 |
+
|
| 344 |
+
Parameters
|
| 345 |
+
----------
|
| 346 |
+
embedding : nx.PlanarEmbedding
|
| 347 |
+
The input graph must contain at least 3 nodes.
|
| 348 |
+
|
| 349 |
+
fully_triangulate : bool
|
| 350 |
+
If set to False the face with the most nodes is chooses as outer face.
|
| 351 |
+
This outer face does not get triangulated.
|
| 352 |
+
|
| 353 |
+
Returns
|
| 354 |
+
-------
|
| 355 |
+
(embedding, outer_face) : (nx.PlanarEmbedding, list) tuple
|
| 356 |
+
The element `embedding` is a new embedding containing all edges from
|
| 357 |
+
the input embedding and the additional edges to triangulate the graph.
|
| 358 |
+
The element `outer_face` is a list of nodes that lie on the outer face.
|
| 359 |
+
If the graph is fully triangulated these are three arbitrary connected
|
| 360 |
+
nodes.
|
| 361 |
+
|
| 362 |
+
"""
|
| 363 |
+
if len(embedding.nodes) <= 1:
|
| 364 |
+
return embedding, list(embedding.nodes)
|
| 365 |
+
embedding = nx.PlanarEmbedding(embedding)
|
| 366 |
+
|
| 367 |
+
# Get a list with a node for each connected component
|
| 368 |
+
component_nodes = [next(iter(x)) for x in nx.connected_components(embedding)]
|
| 369 |
+
|
| 370 |
+
# 1. Make graph a single component (add edge between components)
|
| 371 |
+
for i in range(len(component_nodes) - 1):
|
| 372 |
+
v1 = component_nodes[i]
|
| 373 |
+
v2 = component_nodes[i + 1]
|
| 374 |
+
embedding.connect_components(v1, v2)
|
| 375 |
+
|
| 376 |
+
# 2. Calculate faces, ensure 2-connectedness and determine outer face
|
| 377 |
+
outer_face = [] # A face with the most number of nodes
|
| 378 |
+
face_list = []
|
| 379 |
+
edges_visited = set() # Used to keep track of already visited faces
|
| 380 |
+
for v in embedding.nodes():
|
| 381 |
+
for w in embedding.neighbors_cw_order(v):
|
| 382 |
+
new_face = make_bi_connected(embedding, v, w, edges_visited)
|
| 383 |
+
if new_face:
|
| 384 |
+
# Found a new face
|
| 385 |
+
face_list.append(new_face)
|
| 386 |
+
if len(new_face) > len(outer_face):
|
| 387 |
+
# The face is a candidate to be the outer face
|
| 388 |
+
outer_face = new_face
|
| 389 |
+
|
| 390 |
+
# 3. Triangulate (internal) faces
|
| 391 |
+
for face in face_list:
|
| 392 |
+
if face is not outer_face or fully_triangulate:
|
| 393 |
+
# Triangulate this face
|
| 394 |
+
triangulate_face(embedding, face[0], face[1])
|
| 395 |
+
|
| 396 |
+
if fully_triangulate:
|
| 397 |
+
v1 = outer_face[0]
|
| 398 |
+
v2 = outer_face[1]
|
| 399 |
+
v3 = embedding[v2][v1]["ccw"]
|
| 400 |
+
outer_face = [v1, v2, v3]
|
| 401 |
+
|
| 402 |
+
return embedding, outer_face
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
def make_bi_connected(embedding, starting_node, outgoing_node, edges_counted):
|
| 406 |
+
"""Triangulate a face and make it 2-connected
|
| 407 |
+
|
| 408 |
+
This method also adds all edges on the face to `edges_counted`.
|
| 409 |
+
|
| 410 |
+
Parameters
|
| 411 |
+
----------
|
| 412 |
+
embedding: nx.PlanarEmbedding
|
| 413 |
+
The embedding that defines the faces
|
| 414 |
+
starting_node : node
|
| 415 |
+
A node on the face
|
| 416 |
+
outgoing_node : node
|
| 417 |
+
A node such that the half edge (starting_node, outgoing_node) belongs
|
| 418 |
+
to the face
|
| 419 |
+
edges_counted: set
|
| 420 |
+
Set of all half-edges that belong to a face that have been visited
|
| 421 |
+
|
| 422 |
+
Returns
|
| 423 |
+
-------
|
| 424 |
+
face_nodes: list
|
| 425 |
+
A list of all nodes at the border of this face
|
| 426 |
+
"""
|
| 427 |
+
|
| 428 |
+
# Check if the face has already been calculated
|
| 429 |
+
if (starting_node, outgoing_node) in edges_counted:
|
| 430 |
+
# This face was already counted
|
| 431 |
+
return []
|
| 432 |
+
edges_counted.add((starting_node, outgoing_node))
|
| 433 |
+
|
| 434 |
+
# Add all edges to edges_counted which have this face to their left
|
| 435 |
+
v1 = starting_node
|
| 436 |
+
v2 = outgoing_node
|
| 437 |
+
face_list = [starting_node] # List of nodes around the face
|
| 438 |
+
face_set = set(face_list) # Set for faster queries
|
| 439 |
+
_, v3 = embedding.next_face_half_edge(v1, v2)
|
| 440 |
+
|
| 441 |
+
# Move the nodes v1, v2, v3 around the face:
|
| 442 |
+
while v2 != starting_node or v3 != outgoing_node:
|
| 443 |
+
if v1 == v2:
|
| 444 |
+
raise nx.NetworkXException("Invalid half-edge")
|
| 445 |
+
# cycle is not completed yet
|
| 446 |
+
if v2 in face_set:
|
| 447 |
+
# v2 encountered twice: Add edge to ensure 2-connectedness
|
| 448 |
+
embedding.add_half_edge(v1, v3, ccw=v2)
|
| 449 |
+
embedding.add_half_edge(v3, v1, cw=v2)
|
| 450 |
+
edges_counted.add((v2, v3))
|
| 451 |
+
edges_counted.add((v3, v1))
|
| 452 |
+
v2 = v1
|
| 453 |
+
else:
|
| 454 |
+
face_set.add(v2)
|
| 455 |
+
face_list.append(v2)
|
| 456 |
+
|
| 457 |
+
# set next edge
|
| 458 |
+
v1 = v2
|
| 459 |
+
v2, v3 = embedding.next_face_half_edge(v2, v3)
|
| 460 |
+
|
| 461 |
+
# remember that this edge has been counted
|
| 462 |
+
edges_counted.add((v1, v2))
|
| 463 |
+
|
| 464 |
+
return face_list
|
.venv/lib/python3.11/site-packages/networkx/algorithms/polynomials.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Provides algorithms supporting the computation of graph polynomials.
|
| 2 |
+
|
| 3 |
+
Graph polynomials are polynomial-valued graph invariants that encode a wide
|
| 4 |
+
variety of structural information. Examples include the Tutte polynomial,
|
| 5 |
+
chromatic polynomial, characteristic polynomial, and matching polynomial. An
|
| 6 |
+
extensive treatment is provided in [1]_.
|
| 7 |
+
|
| 8 |
+
For a simple example, the `~sympy.matrices.matrices.MatrixDeterminant.charpoly`
|
| 9 |
+
method can be used to compute the characteristic polynomial from the adjacency
|
| 10 |
+
matrix of a graph. Consider the complete graph ``K_4``:
|
| 11 |
+
|
| 12 |
+
>>> import sympy
|
| 13 |
+
>>> x = sympy.Symbol("x")
|
| 14 |
+
>>> G = nx.complete_graph(4)
|
| 15 |
+
>>> A = nx.to_numpy_array(G, dtype=int)
|
| 16 |
+
>>> M = sympy.SparseMatrix(A)
|
| 17 |
+
>>> M.charpoly(x).as_expr()
|
| 18 |
+
x**4 - 6*x**2 - 8*x - 3
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
.. [1] Y. Shi, M. Dehmer, X. Li, I. Gutman,
|
| 22 |
+
"Graph Polynomials"
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from collections import deque
|
| 26 |
+
|
| 27 |
+
import networkx as nx
|
| 28 |
+
from networkx.utils import not_implemented_for
|
| 29 |
+
|
| 30 |
+
__all__ = ["tutte_polynomial", "chromatic_polynomial"]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@not_implemented_for("directed")
|
| 34 |
+
@nx._dispatchable
|
| 35 |
+
def tutte_polynomial(G):
|
| 36 |
+
r"""Returns the Tutte polynomial of `G`
|
| 37 |
+
|
| 38 |
+
This function computes the Tutte polynomial via an iterative version of
|
| 39 |
+
the deletion-contraction algorithm.
|
| 40 |
+
|
| 41 |
+
The Tutte polynomial `T_G(x, y)` is a fundamental graph polynomial invariant in
|
| 42 |
+
two variables. It encodes a wide array of information related to the
|
| 43 |
+
edge-connectivity of a graph; "Many problems about graphs can be reduced to
|
| 44 |
+
problems of finding and evaluating the Tutte polynomial at certain values" [1]_.
|
| 45 |
+
In fact, every deletion-contraction-expressible feature of a graph is a
|
| 46 |
+
specialization of the Tutte polynomial [2]_ (see Notes for examples).
|
| 47 |
+
|
| 48 |
+
There are several equivalent definitions; here are three:
|
| 49 |
+
|
| 50 |
+
Def 1 (rank-nullity expansion): For `G` an undirected graph, `n(G)` the
|
| 51 |
+
number of vertices of `G`, `E` the edge set of `G`, `V` the vertex set of
|
| 52 |
+
`G`, and `c(A)` the number of connected components of the graph with vertex
|
| 53 |
+
set `V` and edge set `A` [3]_:
|
| 54 |
+
|
| 55 |
+
.. math::
|
| 56 |
+
|
| 57 |
+
T_G(x, y) = \sum_{A \in E} (x-1)^{c(A) - c(E)} (y-1)^{c(A) + |A| - n(G)}
|
| 58 |
+
|
| 59 |
+
Def 2 (spanning tree expansion): Let `G` be an undirected graph, `T` a spanning
|
| 60 |
+
tree of `G`, and `E` the edge set of `G`. Let `E` have an arbitrary strict
|
| 61 |
+
linear order `L`. Let `B_e` be the unique minimal nonempty edge cut of
|
| 62 |
+
$E \setminus T \cup {e}$. An edge `e` is internally active with respect to
|
| 63 |
+
`T` and `L` if `e` is the least edge in `B_e` according to the linear order
|
| 64 |
+
`L`. The internal activity of `T` (denoted `i(T)`) is the number of edges
|
| 65 |
+
in $E \setminus T$ that are internally active with respect to `T` and `L`.
|
| 66 |
+
Let `P_e` be the unique path in $T \cup {e}$ whose source and target vertex
|
| 67 |
+
are the same. An edge `e` is externally active with respect to `T` and `L`
|
| 68 |
+
if `e` is the least edge in `P_e` according to the linear order `L`. The
|
| 69 |
+
external activity of `T` (denoted `e(T)`) is the number of edges in
|
| 70 |
+
$E \setminus T$ that are externally active with respect to `T` and `L`.
|
| 71 |
+
Then [4]_ [5]_:
|
| 72 |
+
|
| 73 |
+
.. math::
|
| 74 |
+
|
| 75 |
+
T_G(x, y) = \sum_{T \text{ a spanning tree of } G} x^{i(T)} y^{e(T)}
|
| 76 |
+
|
| 77 |
+
Def 3 (deletion-contraction recurrence): For `G` an undirected graph, `G-e`
|
| 78 |
+
the graph obtained from `G` by deleting edge `e`, `G/e` the graph obtained
|
| 79 |
+
from `G` by contracting edge `e`, `k(G)` the number of cut-edges of `G`,
|
| 80 |
+
and `l(G)` the number of self-loops of `G`:
|
| 81 |
+
|
| 82 |
+
.. math::
|
| 83 |
+
T_G(x, y) = \begin{cases}
|
| 84 |
+
x^{k(G)} y^{l(G)}, & \text{if all edges are cut-edges or self-loops} \\
|
| 85 |
+
T_{G-e}(x, y) + T_{G/e}(x, y), & \text{otherwise, for an arbitrary edge $e$ not a cut-edge or loop}
|
| 86 |
+
\end{cases}
|
| 87 |
+
|
| 88 |
+
Parameters
|
| 89 |
+
----------
|
| 90 |
+
G : NetworkX graph
|
| 91 |
+
|
| 92 |
+
Returns
|
| 93 |
+
-------
|
| 94 |
+
instance of `sympy.core.add.Add`
|
| 95 |
+
A Sympy expression representing the Tutte polynomial for `G`.
|
| 96 |
+
|
| 97 |
+
Examples
|
| 98 |
+
--------
|
| 99 |
+
>>> C = nx.cycle_graph(5)
|
| 100 |
+
>>> nx.tutte_polynomial(C)
|
| 101 |
+
x**4 + x**3 + x**2 + x + y
|
| 102 |
+
|
| 103 |
+
>>> D = nx.diamond_graph()
|
| 104 |
+
>>> nx.tutte_polynomial(D)
|
| 105 |
+
x**3 + 2*x**2 + 2*x*y + x + y**2 + y
|
| 106 |
+
|
| 107 |
+
Notes
|
| 108 |
+
-----
|
| 109 |
+
Some specializations of the Tutte polynomial:
|
| 110 |
+
|
| 111 |
+
- `T_G(1, 1)` counts the number of spanning trees of `G`
|
| 112 |
+
- `T_G(1, 2)` counts the number of connected spanning subgraphs of `G`
|
| 113 |
+
- `T_G(2, 1)` counts the number of spanning forests in `G`
|
| 114 |
+
- `T_G(0, 2)` counts the number of strong orientations of `G`
|
| 115 |
+
- `T_G(2, 0)` counts the number of acyclic orientations of `G`
|
| 116 |
+
|
| 117 |
+
Edge contraction is defined and deletion-contraction is introduced in [6]_.
|
| 118 |
+
Combinatorial meaning of the coefficients is introduced in [7]_.
|
| 119 |
+
Universality, properties, and applications are discussed in [8]_.
|
| 120 |
+
|
| 121 |
+
Practically, up-front computation of the Tutte polynomial may be useful when
|
| 122 |
+
users wish to repeatedly calculate edge-connectivity-related information
|
| 123 |
+
about one or more graphs.
|
| 124 |
+
|
| 125 |
+
References
|
| 126 |
+
----------
|
| 127 |
+
.. [1] M. Brandt,
|
| 128 |
+
"The Tutte Polynomial."
|
| 129 |
+
Talking About Combinatorial Objects Seminar, 2015
|
| 130 |
+
https://math.berkeley.edu/~brandtm/talks/tutte.pdf
|
| 131 |
+
.. [2] A. Björklund, T. Husfeldt, P. Kaski, M. Koivisto,
|
| 132 |
+
"Computing the Tutte polynomial in vertex-exponential time"
|
| 133 |
+
49th Annual IEEE Symposium on Foundations of Computer Science, 2008
|
| 134 |
+
https://ieeexplore.ieee.org/abstract/document/4691000
|
| 135 |
+
.. [3] Y. Shi, M. Dehmer, X. Li, I. Gutman,
|
| 136 |
+
"Graph Polynomials," p. 14
|
| 137 |
+
.. [4] Y. Shi, M. Dehmer, X. Li, I. Gutman,
|
| 138 |
+
"Graph Polynomials," p. 46
|
| 139 |
+
.. [5] A. Nešetril, J. Goodall,
|
| 140 |
+
"Graph invariants, homomorphisms, and the Tutte polynomial"
|
| 141 |
+
https://iuuk.mff.cuni.cz/~andrew/Tutte.pdf
|
| 142 |
+
.. [6] D. B. West,
|
| 143 |
+
"Introduction to Graph Theory," p. 84
|
| 144 |
+
.. [7] G. Coutinho,
|
| 145 |
+
"A brief introduction to the Tutte polynomial"
|
| 146 |
+
Structural Analysis of Complex Networks, 2011
|
| 147 |
+
https://homepages.dcc.ufmg.br/~gabriel/seminars/coutinho_tuttepolynomial_seminar.pdf
|
| 148 |
+
.. [8] J. A. Ellis-Monaghan, C. Merino,
|
| 149 |
+
"Graph polynomials and their applications I: The Tutte polynomial"
|
| 150 |
+
Structural Analysis of Complex Networks, 2011
|
| 151 |
+
https://arxiv.org/pdf/0803.3079.pdf
|
| 152 |
+
"""
|
| 153 |
+
import sympy
|
| 154 |
+
|
| 155 |
+
x = sympy.Symbol("x")
|
| 156 |
+
y = sympy.Symbol("y")
|
| 157 |
+
stack = deque()
|
| 158 |
+
stack.append(nx.MultiGraph(G))
|
| 159 |
+
|
| 160 |
+
polynomial = 0
|
| 161 |
+
while stack:
|
| 162 |
+
G = stack.pop()
|
| 163 |
+
bridges = set(nx.bridges(G))
|
| 164 |
+
|
| 165 |
+
e = None
|
| 166 |
+
for i in G.edges:
|
| 167 |
+
if (i[0], i[1]) not in bridges and i[0] != i[1]:
|
| 168 |
+
e = i
|
| 169 |
+
break
|
| 170 |
+
if not e:
|
| 171 |
+
loops = list(nx.selfloop_edges(G, keys=True))
|
| 172 |
+
polynomial += x ** len(bridges) * y ** len(loops)
|
| 173 |
+
else:
|
| 174 |
+
# deletion-contraction
|
| 175 |
+
C = nx.contracted_edge(G, e, self_loops=True)
|
| 176 |
+
C.remove_edge(e[0], e[0])
|
| 177 |
+
G.remove_edge(*e)
|
| 178 |
+
stack.append(G)
|
| 179 |
+
stack.append(C)
|
| 180 |
+
return sympy.simplify(polynomial)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@not_implemented_for("directed")
|
| 184 |
+
@nx._dispatchable
|
| 185 |
+
def chromatic_polynomial(G):
|
| 186 |
+
r"""Returns the chromatic polynomial of `G`
|
| 187 |
+
|
| 188 |
+
This function computes the chromatic polynomial via an iterative version of
|
| 189 |
+
the deletion-contraction algorithm.
|
| 190 |
+
|
| 191 |
+
The chromatic polynomial `X_G(x)` is a fundamental graph polynomial
|
| 192 |
+
invariant in one variable. Evaluating `X_G(k)` for an natural number `k`
|
| 193 |
+
enumerates the proper k-colorings of `G`.
|
| 194 |
+
|
| 195 |
+
There are several equivalent definitions; here are three:
|
| 196 |
+
|
| 197 |
+
Def 1 (explicit formula):
|
| 198 |
+
For `G` an undirected graph, `c(G)` the number of connected components of
|
| 199 |
+
`G`, `E` the edge set of `G`, and `G(S)` the spanning subgraph of `G` with
|
| 200 |
+
edge set `S` [1]_:
|
| 201 |
+
|
| 202 |
+
.. math::
|
| 203 |
+
|
| 204 |
+
X_G(x) = \sum_{S \subseteq E} (-1)^{|S|} x^{c(G(S))}
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
Def 2 (interpolating polynomial):
|
| 208 |
+
For `G` an undirected graph, `n(G)` the number of vertices of `G`, `k_0 = 0`,
|
| 209 |
+
and `k_i` the number of distinct ways to color the vertices of `G` with `i`
|
| 210 |
+
unique colors (for `i` a natural number at most `n(G)`), `X_G(x)` is the
|
| 211 |
+
unique Lagrange interpolating polynomial of degree `n(G)` through the points
|
| 212 |
+
`(0, k_0), (1, k_1), \dots, (n(G), k_{n(G)})` [2]_.
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
Def 3 (chromatic recurrence):
|
| 216 |
+
For `G` an undirected graph, `G-e` the graph obtained from `G` by deleting
|
| 217 |
+
edge `e`, `G/e` the graph obtained from `G` by contracting edge `e`, `n(G)`
|
| 218 |
+
the number of vertices of `G`, and `e(G)` the number of edges of `G` [3]_:
|
| 219 |
+
|
| 220 |
+
.. math::
|
| 221 |
+
X_G(x) = \begin{cases}
|
| 222 |
+
x^{n(G)}, & \text{if $e(G)=0$} \\
|
| 223 |
+
X_{G-e}(x) - X_{G/e}(x), & \text{otherwise, for an arbitrary edge $e$}
|
| 224 |
+
\end{cases}
|
| 225 |
+
|
| 226 |
+
This formulation is also known as the Fundamental Reduction Theorem [4]_.
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
Parameters
|
| 230 |
+
----------
|
| 231 |
+
G : NetworkX graph
|
| 232 |
+
|
| 233 |
+
Returns
|
| 234 |
+
-------
|
| 235 |
+
instance of `sympy.core.add.Add`
|
| 236 |
+
A Sympy expression representing the chromatic polynomial for `G`.
|
| 237 |
+
|
| 238 |
+
Examples
|
| 239 |
+
--------
|
| 240 |
+
>>> C = nx.cycle_graph(5)
|
| 241 |
+
>>> nx.chromatic_polynomial(C)
|
| 242 |
+
x**5 - 5*x**4 + 10*x**3 - 10*x**2 + 4*x
|
| 243 |
+
|
| 244 |
+
>>> G = nx.complete_graph(4)
|
| 245 |
+
>>> nx.chromatic_polynomial(G)
|
| 246 |
+
x**4 - 6*x**3 + 11*x**2 - 6*x
|
| 247 |
+
|
| 248 |
+
Notes
|
| 249 |
+
-----
|
| 250 |
+
Interpretation of the coefficients is discussed in [5]_. Several special
|
| 251 |
+
cases are listed in [2]_.
|
| 252 |
+
|
| 253 |
+
The chromatic polynomial is a specialization of the Tutte polynomial; in
|
| 254 |
+
particular, ``X_G(x) = T_G(x, 0)`` [6]_.
|
| 255 |
+
|
| 256 |
+
The chromatic polynomial may take negative arguments, though evaluations
|
| 257 |
+
may not have chromatic interpretations. For instance, ``X_G(-1)`` enumerates
|
| 258 |
+
the acyclic orientations of `G` [7]_.
|
| 259 |
+
|
| 260 |
+
References
|
| 261 |
+
----------
|
| 262 |
+
.. [1] D. B. West,
|
| 263 |
+
"Introduction to Graph Theory," p. 222
|
| 264 |
+
.. [2] E. W. Weisstein
|
| 265 |
+
"Chromatic Polynomial"
|
| 266 |
+
MathWorld--A Wolfram Web Resource
|
| 267 |
+
https://mathworld.wolfram.com/ChromaticPolynomial.html
|
| 268 |
+
.. [3] D. B. West,
|
| 269 |
+
"Introduction to Graph Theory," p. 221
|
| 270 |
+
.. [4] J. Zhang, J. Goodall,
|
| 271 |
+
"An Introduction to Chromatic Polynomials"
|
| 272 |
+
https://math.mit.edu/~apost/courses/18.204_2018/Julie_Zhang_paper.pdf
|
| 273 |
+
.. [5] R. C. Read,
|
| 274 |
+
"An Introduction to Chromatic Polynomials"
|
| 275 |
+
Journal of Combinatorial Theory, 1968
|
| 276 |
+
https://math.berkeley.edu/~mrklug/ReadChromatic.pdf
|
| 277 |
+
.. [6] W. T. Tutte,
|
| 278 |
+
"Graph-polynomials"
|
| 279 |
+
Advances in Applied Mathematics, 2004
|
| 280 |
+
https://www.sciencedirect.com/science/article/pii/S0196885803000411
|
| 281 |
+
.. [7] R. P. Stanley,
|
| 282 |
+
"Acyclic orientations of graphs"
|
| 283 |
+
Discrete Mathematics, 2006
|
| 284 |
+
https://math.mit.edu/~rstan/pubs/pubfiles/18.pdf
|
| 285 |
+
"""
|
| 286 |
+
import sympy
|
| 287 |
+
|
| 288 |
+
x = sympy.Symbol("x")
|
| 289 |
+
stack = deque()
|
| 290 |
+
stack.append(nx.MultiGraph(G, contraction_idx=0))
|
| 291 |
+
|
| 292 |
+
polynomial = 0
|
| 293 |
+
while stack:
|
| 294 |
+
G = stack.pop()
|
| 295 |
+
edges = list(G.edges)
|
| 296 |
+
if not edges:
|
| 297 |
+
polynomial += (-1) ** G.graph["contraction_idx"] * x ** len(G)
|
| 298 |
+
else:
|
| 299 |
+
e = edges[0]
|
| 300 |
+
C = nx.contracted_edge(G, e, self_loops=True)
|
| 301 |
+
C.graph["contraction_idx"] = G.graph["contraction_idx"] + 1
|
| 302 |
+
C.remove_edge(e[0], e[0])
|
| 303 |
+
G.remove_edge(*e)
|
| 304 |
+
stack.append(G)
|
| 305 |
+
stack.append(C)
|
| 306 |
+
return polynomial
|
.venv/lib/python3.11/site-packages/networkx/algorithms/reciprocity.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Algorithms to calculate reciprocity in a directed graph."""
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from networkx import NetworkXError
|
| 5 |
+
|
| 6 |
+
from ..utils import not_implemented_for
|
| 7 |
+
|
| 8 |
+
__all__ = ["reciprocity", "overall_reciprocity"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@not_implemented_for("undirected", "multigraph")
|
| 12 |
+
@nx._dispatchable
|
| 13 |
+
def reciprocity(G, nodes=None):
|
| 14 |
+
r"""Compute the reciprocity in a directed graph.
|
| 15 |
+
|
| 16 |
+
The reciprocity of a directed graph is defined as the ratio
|
| 17 |
+
of the number of edges pointing in both directions to the total
|
| 18 |
+
number of edges in the graph.
|
| 19 |
+
Formally, $r = |{(u,v) \in G|(v,u) \in G}| / |{(u,v) \in G}|$.
|
| 20 |
+
|
| 21 |
+
The reciprocity of a single node u is defined similarly,
|
| 22 |
+
it is the ratio of the number of edges in both directions to
|
| 23 |
+
the total number of edges attached to node u.
|
| 24 |
+
|
| 25 |
+
Parameters
|
| 26 |
+
----------
|
| 27 |
+
G : graph
|
| 28 |
+
A networkx directed graph
|
| 29 |
+
nodes : container of nodes, optional (default=whole graph)
|
| 30 |
+
Compute reciprocity for nodes in this container.
|
| 31 |
+
|
| 32 |
+
Returns
|
| 33 |
+
-------
|
| 34 |
+
out : dictionary
|
| 35 |
+
Reciprocity keyed by node label.
|
| 36 |
+
|
| 37 |
+
Notes
|
| 38 |
+
-----
|
| 39 |
+
The reciprocity is not defined for isolated nodes.
|
| 40 |
+
In such cases this function will return None.
|
| 41 |
+
|
| 42 |
+
"""
|
| 43 |
+
# If `nodes` is not specified, calculate the reciprocity of the graph.
|
| 44 |
+
if nodes is None:
|
| 45 |
+
return overall_reciprocity(G)
|
| 46 |
+
|
| 47 |
+
# If `nodes` represents a single node in the graph, return only its
|
| 48 |
+
# reciprocity.
|
| 49 |
+
if nodes in G:
|
| 50 |
+
reciprocity = next(_reciprocity_iter(G, nodes))[1]
|
| 51 |
+
if reciprocity is None:
|
| 52 |
+
raise NetworkXError("Not defined for isolated nodes.")
|
| 53 |
+
else:
|
| 54 |
+
return reciprocity
|
| 55 |
+
|
| 56 |
+
# Otherwise, `nodes` represents an iterable of nodes, so return a
|
| 57 |
+
# dictionary mapping node to its reciprocity.
|
| 58 |
+
return dict(_reciprocity_iter(G, nodes))
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _reciprocity_iter(G, nodes):
|
| 62 |
+
"""Return an iterator of (node, reciprocity)."""
|
| 63 |
+
n = G.nbunch_iter(nodes)
|
| 64 |
+
for node in n:
|
| 65 |
+
pred = set(G.predecessors(node))
|
| 66 |
+
succ = set(G.successors(node))
|
| 67 |
+
overlap = pred & succ
|
| 68 |
+
n_total = len(pred) + len(succ)
|
| 69 |
+
|
| 70 |
+
# Reciprocity is not defined for isolated nodes.
|
| 71 |
+
# Return None.
|
| 72 |
+
if n_total == 0:
|
| 73 |
+
yield (node, None)
|
| 74 |
+
else:
|
| 75 |
+
reciprocity = 2 * len(overlap) / n_total
|
| 76 |
+
yield (node, reciprocity)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@not_implemented_for("undirected", "multigraph")
|
| 80 |
+
@nx._dispatchable
|
| 81 |
+
def overall_reciprocity(G):
|
| 82 |
+
"""Compute the reciprocity for the whole graph.
|
| 83 |
+
|
| 84 |
+
See the doc of reciprocity for the definition.
|
| 85 |
+
|
| 86 |
+
Parameters
|
| 87 |
+
----------
|
| 88 |
+
G : graph
|
| 89 |
+
A networkx graph
|
| 90 |
+
|
| 91 |
+
"""
|
| 92 |
+
n_all_edge = G.number_of_edges()
|
| 93 |
+
n_overlap_edge = (n_all_edge - G.to_undirected().number_of_edges()) * 2
|
| 94 |
+
|
| 95 |
+
if n_all_edge == 0:
|
| 96 |
+
raise NetworkXError("Not defined for empty graphs")
|
| 97 |
+
|
| 98 |
+
return n_overlap_edge / n_all_edge
|
.venv/lib/python3.11/site-packages/networkx/algorithms/regular.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions for computing and verifying regular graphs."""
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from networkx.utils import not_implemented_for
|
| 5 |
+
|
| 6 |
+
__all__ = ["is_regular", "is_k_regular", "k_factor"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@nx._dispatchable
|
| 10 |
+
def is_regular(G):
|
| 11 |
+
"""Determines whether the graph ``G`` is a regular graph.
|
| 12 |
+
|
| 13 |
+
A regular graph is a graph where each vertex has the same degree. A
|
| 14 |
+
regular digraph is a graph where the indegree and outdegree of each
|
| 15 |
+
vertex are equal.
|
| 16 |
+
|
| 17 |
+
Parameters
|
| 18 |
+
----------
|
| 19 |
+
G : NetworkX graph
|
| 20 |
+
|
| 21 |
+
Returns
|
| 22 |
+
-------
|
| 23 |
+
bool
|
| 24 |
+
Whether the given graph or digraph is regular.
|
| 25 |
+
|
| 26 |
+
Examples
|
| 27 |
+
--------
|
| 28 |
+
>>> G = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 1)])
|
| 29 |
+
>>> nx.is_regular(G)
|
| 30 |
+
True
|
| 31 |
+
|
| 32 |
+
"""
|
| 33 |
+
if len(G) == 0:
|
| 34 |
+
raise nx.NetworkXPointlessConcept("Graph has no nodes.")
|
| 35 |
+
n1 = nx.utils.arbitrary_element(G)
|
| 36 |
+
if not G.is_directed():
|
| 37 |
+
d1 = G.degree(n1)
|
| 38 |
+
return all(d1 == d for _, d in G.degree)
|
| 39 |
+
else:
|
| 40 |
+
d_in = G.in_degree(n1)
|
| 41 |
+
in_regular = all(d_in == d for _, d in G.in_degree)
|
| 42 |
+
d_out = G.out_degree(n1)
|
| 43 |
+
out_regular = all(d_out == d for _, d in G.out_degree)
|
| 44 |
+
return in_regular and out_regular
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@not_implemented_for("directed")
|
| 48 |
+
@nx._dispatchable
|
| 49 |
+
def is_k_regular(G, k):
|
| 50 |
+
"""Determines whether the graph ``G`` is a k-regular graph.
|
| 51 |
+
|
| 52 |
+
A k-regular graph is a graph where each vertex has degree k.
|
| 53 |
+
|
| 54 |
+
Parameters
|
| 55 |
+
----------
|
| 56 |
+
G : NetworkX graph
|
| 57 |
+
|
| 58 |
+
Returns
|
| 59 |
+
-------
|
| 60 |
+
bool
|
| 61 |
+
Whether the given graph is k-regular.
|
| 62 |
+
|
| 63 |
+
Examples
|
| 64 |
+
--------
|
| 65 |
+
>>> G = nx.Graph([(1, 2), (2, 3), (3, 4), (4, 1)])
|
| 66 |
+
>>> nx.is_k_regular(G, k=3)
|
| 67 |
+
False
|
| 68 |
+
|
| 69 |
+
"""
|
| 70 |
+
return all(d == k for n, d in G.degree)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@not_implemented_for("directed")
|
| 74 |
+
@not_implemented_for("multigraph")
|
| 75 |
+
@nx._dispatchable(preserve_edge_attrs=True, returns_graph=True)
|
| 76 |
+
def k_factor(G, k, matching_weight="weight"):
|
| 77 |
+
"""Compute a k-factor of G
|
| 78 |
+
|
| 79 |
+
A k-factor of a graph is a spanning k-regular subgraph.
|
| 80 |
+
A spanning k-regular subgraph of G is a subgraph that contains
|
| 81 |
+
each vertex of G and a subset of the edges of G such that each
|
| 82 |
+
vertex has degree k.
|
| 83 |
+
|
| 84 |
+
Parameters
|
| 85 |
+
----------
|
| 86 |
+
G : NetworkX graph
|
| 87 |
+
Undirected graph
|
| 88 |
+
|
| 89 |
+
matching_weight: string, optional (default='weight')
|
| 90 |
+
Edge data key corresponding to the edge weight.
|
| 91 |
+
Used for finding the max-weighted perfect matching.
|
| 92 |
+
If key not found, uses 1 as weight.
|
| 93 |
+
|
| 94 |
+
Returns
|
| 95 |
+
-------
|
| 96 |
+
G2 : NetworkX graph
|
| 97 |
+
A k-factor of G
|
| 98 |
+
|
| 99 |
+
Examples
|
| 100 |
+
--------
|
| 101 |
+
>>> G = nx.Graph([(1, 2), (2, 3), (3, 4), (4, 1)])
|
| 102 |
+
>>> G2 = nx.k_factor(G, k=1)
|
| 103 |
+
>>> G2.edges()
|
| 104 |
+
EdgeView([(1, 2), (3, 4)])
|
| 105 |
+
|
| 106 |
+
References
|
| 107 |
+
----------
|
| 108 |
+
.. [1] "An algorithm for computing simple k-factors.",
|
| 109 |
+
Meijer, Henk, Yurai Núñez-Rodríguez, and David Rappaport,
|
| 110 |
+
Information processing letters, 2009.
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
from networkx.algorithms.matching import is_perfect_matching, max_weight_matching
|
| 114 |
+
|
| 115 |
+
class LargeKGadget:
|
| 116 |
+
def __init__(self, k, degree, node, g):
|
| 117 |
+
self.original = node
|
| 118 |
+
self.g = g
|
| 119 |
+
self.k = k
|
| 120 |
+
self.degree = degree
|
| 121 |
+
|
| 122 |
+
self.outer_vertices = [(node, x) for x in range(degree)]
|
| 123 |
+
self.core_vertices = [(node, x + degree) for x in range(degree - k)]
|
| 124 |
+
|
| 125 |
+
def replace_node(self):
|
| 126 |
+
adj_view = self.g[self.original]
|
| 127 |
+
neighbors = list(adj_view.keys())
|
| 128 |
+
edge_attrs = list(adj_view.values())
|
| 129 |
+
for outer, neighbor, edge_attrs in zip(
|
| 130 |
+
self.outer_vertices, neighbors, edge_attrs
|
| 131 |
+
):
|
| 132 |
+
self.g.add_edge(outer, neighbor, **edge_attrs)
|
| 133 |
+
for core in self.core_vertices:
|
| 134 |
+
for outer in self.outer_vertices:
|
| 135 |
+
self.g.add_edge(core, outer)
|
| 136 |
+
self.g.remove_node(self.original)
|
| 137 |
+
|
| 138 |
+
def restore_node(self):
|
| 139 |
+
self.g.add_node(self.original)
|
| 140 |
+
for outer in self.outer_vertices:
|
| 141 |
+
adj_view = self.g[outer]
|
| 142 |
+
for neighbor, edge_attrs in list(adj_view.items()):
|
| 143 |
+
if neighbor not in self.core_vertices:
|
| 144 |
+
self.g.add_edge(self.original, neighbor, **edge_attrs)
|
| 145 |
+
break
|
| 146 |
+
g.remove_nodes_from(self.outer_vertices)
|
| 147 |
+
g.remove_nodes_from(self.core_vertices)
|
| 148 |
+
|
| 149 |
+
class SmallKGadget:
|
| 150 |
+
def __init__(self, k, degree, node, g):
|
| 151 |
+
self.original = node
|
| 152 |
+
self.k = k
|
| 153 |
+
self.degree = degree
|
| 154 |
+
self.g = g
|
| 155 |
+
|
| 156 |
+
self.outer_vertices = [(node, x) for x in range(degree)]
|
| 157 |
+
self.inner_vertices = [(node, x + degree) for x in range(degree)]
|
| 158 |
+
self.core_vertices = [(node, x + 2 * degree) for x in range(k)]
|
| 159 |
+
|
| 160 |
+
def replace_node(self):
|
| 161 |
+
adj_view = self.g[self.original]
|
| 162 |
+
for outer, inner, (neighbor, edge_attrs) in zip(
|
| 163 |
+
self.outer_vertices, self.inner_vertices, list(adj_view.items())
|
| 164 |
+
):
|
| 165 |
+
self.g.add_edge(outer, inner)
|
| 166 |
+
self.g.add_edge(outer, neighbor, **edge_attrs)
|
| 167 |
+
for core in self.core_vertices:
|
| 168 |
+
for inner in self.inner_vertices:
|
| 169 |
+
self.g.add_edge(core, inner)
|
| 170 |
+
self.g.remove_node(self.original)
|
| 171 |
+
|
| 172 |
+
def restore_node(self):
|
| 173 |
+
self.g.add_node(self.original)
|
| 174 |
+
for outer in self.outer_vertices:
|
| 175 |
+
adj_view = self.g[outer]
|
| 176 |
+
for neighbor, edge_attrs in adj_view.items():
|
| 177 |
+
if neighbor not in self.core_vertices:
|
| 178 |
+
self.g.add_edge(self.original, neighbor, **edge_attrs)
|
| 179 |
+
break
|
| 180 |
+
self.g.remove_nodes_from(self.outer_vertices)
|
| 181 |
+
self.g.remove_nodes_from(self.inner_vertices)
|
| 182 |
+
self.g.remove_nodes_from(self.core_vertices)
|
| 183 |
+
|
| 184 |
+
# Step 1
|
| 185 |
+
if any(d < k for _, d in G.degree):
|
| 186 |
+
raise nx.NetworkXUnfeasible("Graph contains a vertex with degree less than k")
|
| 187 |
+
g = G.copy()
|
| 188 |
+
|
| 189 |
+
# Step 2
|
| 190 |
+
gadgets = []
|
| 191 |
+
for node, degree in list(g.degree):
|
| 192 |
+
if k < degree / 2.0:
|
| 193 |
+
gadget = SmallKGadget(k, degree, node, g)
|
| 194 |
+
else:
|
| 195 |
+
gadget = LargeKGadget(k, degree, node, g)
|
| 196 |
+
gadget.replace_node()
|
| 197 |
+
gadgets.append(gadget)
|
| 198 |
+
|
| 199 |
+
# Step 3
|
| 200 |
+
matching = max_weight_matching(g, maxcardinality=True, weight=matching_weight)
|
| 201 |
+
|
| 202 |
+
# Step 4
|
| 203 |
+
if not is_perfect_matching(g, matching):
|
| 204 |
+
raise nx.NetworkXUnfeasible(
|
| 205 |
+
"Cannot find k-factor because no perfect matching exists"
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
for edge in g.edges():
|
| 209 |
+
if edge not in matching and (edge[1], edge[0]) not in matching:
|
| 210 |
+
g.remove_edge(edge[0], edge[1])
|
| 211 |
+
|
| 212 |
+
for gadget in gadgets:
|
| 213 |
+
gadget.restore_node()
|
| 214 |
+
|
| 215 |
+
return g
|
.venv/lib/python3.11/site-packages/networkx/algorithms/richclub.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions for computing rich-club coefficients."""
|
| 2 |
+
|
| 3 |
+
from itertools import accumulate
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.utils import not_implemented_for
|
| 7 |
+
|
| 8 |
+
__all__ = ["rich_club_coefficient"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@not_implemented_for("directed")
|
| 12 |
+
@not_implemented_for("multigraph")
|
| 13 |
+
@nx._dispatchable
|
| 14 |
+
def rich_club_coefficient(G, normalized=True, Q=100, seed=None):
|
| 15 |
+
r"""Returns the rich-club coefficient of the graph `G`.
|
| 16 |
+
|
| 17 |
+
For each degree *k*, the *rich-club coefficient* is the ratio of the
|
| 18 |
+
number of actual to the number of potential edges for nodes with
|
| 19 |
+
degree greater than *k*:
|
| 20 |
+
|
| 21 |
+
.. math::
|
| 22 |
+
|
| 23 |
+
\phi(k) = \frac{2 E_k}{N_k (N_k - 1)}
|
| 24 |
+
|
| 25 |
+
where `N_k` is the number of nodes with degree larger than *k*, and
|
| 26 |
+
`E_k` is the number of edges among those nodes.
|
| 27 |
+
|
| 28 |
+
Parameters
|
| 29 |
+
----------
|
| 30 |
+
G : NetworkX graph
|
| 31 |
+
Undirected graph with neither parallel edges nor self-loops.
|
| 32 |
+
normalized : bool (optional)
|
| 33 |
+
Normalize using randomized network as in [1]_
|
| 34 |
+
Q : float (optional, default=100)
|
| 35 |
+
If `normalized` is True, perform `Q * m` double-edge
|
| 36 |
+
swaps, where `m` is the number of edges in `G`, to use as a
|
| 37 |
+
null-model for normalization.
|
| 38 |
+
seed : integer, random_state, or None (default)
|
| 39 |
+
Indicator of random number generation state.
|
| 40 |
+
See :ref:`Randomness<randomness>`.
|
| 41 |
+
|
| 42 |
+
Returns
|
| 43 |
+
-------
|
| 44 |
+
rc : dictionary
|
| 45 |
+
A dictionary, keyed by degree, with rich-club coefficient values.
|
| 46 |
+
|
| 47 |
+
Raises
|
| 48 |
+
------
|
| 49 |
+
NetworkXError
|
| 50 |
+
If `G` has fewer than four nodes and ``normalized=True``.
|
| 51 |
+
A randomly sampled graph for normalization cannot be generated in this case.
|
| 52 |
+
|
| 53 |
+
Examples
|
| 54 |
+
--------
|
| 55 |
+
>>> G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)])
|
| 56 |
+
>>> rc = nx.rich_club_coefficient(G, normalized=False, seed=42)
|
| 57 |
+
>>> rc[0]
|
| 58 |
+
0.4
|
| 59 |
+
|
| 60 |
+
Notes
|
| 61 |
+
-----
|
| 62 |
+
The rich club definition and algorithm are found in [1]_. This
|
| 63 |
+
algorithm ignores any edge weights and is not defined for directed
|
| 64 |
+
graphs or graphs with parallel edges or self loops.
|
| 65 |
+
|
| 66 |
+
Normalization is done by computing the rich club coefficient for a randomly
|
| 67 |
+
sampled graph with the same degree distribution as `G` by
|
| 68 |
+
repeatedly swapping the endpoints of existing edges. For graphs with fewer than 4
|
| 69 |
+
nodes, it is not possible to generate a random graph with a prescribed
|
| 70 |
+
degree distribution, as the degree distribution fully determines the graph
|
| 71 |
+
(hence making the coefficients trivially normalized to 1).
|
| 72 |
+
This function raises an exception in this case.
|
| 73 |
+
|
| 74 |
+
Estimates for appropriate values of `Q` are found in [2]_.
|
| 75 |
+
|
| 76 |
+
References
|
| 77 |
+
----------
|
| 78 |
+
.. [1] Julian J. McAuley, Luciano da Fontoura Costa,
|
| 79 |
+
and Tibério S. Caetano,
|
| 80 |
+
"The rich-club phenomenon across complex network hierarchies",
|
| 81 |
+
Applied Physics Letters Vol 91 Issue 8, August 2007.
|
| 82 |
+
https://arxiv.org/abs/physics/0701290
|
| 83 |
+
.. [2] R. Milo, N. Kashtan, S. Itzkovitz, M. E. J. Newman, U. Alon,
|
| 84 |
+
"Uniform generation of random graphs with arbitrary degree
|
| 85 |
+
sequences", 2006. https://arxiv.org/abs/cond-mat/0312028
|
| 86 |
+
"""
|
| 87 |
+
if nx.number_of_selfloops(G) > 0:
|
| 88 |
+
raise Exception(
|
| 89 |
+
"rich_club_coefficient is not implemented for graphs with self loops."
|
| 90 |
+
)
|
| 91 |
+
rc = _compute_rc(G)
|
| 92 |
+
if normalized:
|
| 93 |
+
# make R a copy of G, randomize with Q*|E| double edge swaps
|
| 94 |
+
# and use rich_club coefficient of R to normalize
|
| 95 |
+
R = G.copy()
|
| 96 |
+
E = R.number_of_edges()
|
| 97 |
+
nx.double_edge_swap(R, Q * E, max_tries=Q * E * 10, seed=seed)
|
| 98 |
+
rcran = _compute_rc(R)
|
| 99 |
+
rc = {k: v / rcran[k] for k, v in rc.items()}
|
| 100 |
+
return rc
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _compute_rc(G):
|
| 104 |
+
"""Returns the rich-club coefficient for each degree in the graph
|
| 105 |
+
`G`.
|
| 106 |
+
|
| 107 |
+
`G` is an undirected graph without multiedges.
|
| 108 |
+
|
| 109 |
+
Returns a dictionary mapping degree to rich-club coefficient for
|
| 110 |
+
that degree.
|
| 111 |
+
|
| 112 |
+
"""
|
| 113 |
+
deghist = nx.degree_histogram(G)
|
| 114 |
+
total = sum(deghist)
|
| 115 |
+
# Compute the number of nodes with degree greater than `k`, for each
|
| 116 |
+
# degree `k` (omitting the last entry, which is zero).
|
| 117 |
+
nks = (total - cs for cs in accumulate(deghist) if total - cs > 1)
|
| 118 |
+
# Create a sorted list of pairs of edge endpoint degrees.
|
| 119 |
+
#
|
| 120 |
+
# The list is sorted in reverse order so that we can pop from the
|
| 121 |
+
# right side of the list later, instead of popping from the left
|
| 122 |
+
# side of the list, which would have a linear time cost.
|
| 123 |
+
edge_degrees = sorted((sorted(map(G.degree, e)) for e in G.edges()), reverse=True)
|
| 124 |
+
ek = G.number_of_edges()
|
| 125 |
+
if ek == 0:
|
| 126 |
+
return {}
|
| 127 |
+
|
| 128 |
+
k1, k2 = edge_degrees.pop()
|
| 129 |
+
rc = {}
|
| 130 |
+
for d, nk in enumerate(nks):
|
| 131 |
+
while k1 <= d:
|
| 132 |
+
if len(edge_degrees) == 0:
|
| 133 |
+
ek = 0
|
| 134 |
+
break
|
| 135 |
+
k1, k2 = edge_degrees.pop()
|
| 136 |
+
ek -= 1
|
| 137 |
+
rc[d] = 2 * ek / (nk * (nk - 1))
|
| 138 |
+
return rc
|
.venv/lib/python3.11/site-packages/networkx/algorithms/similarity.py
ADDED
|
@@ -0,0 +1,1780 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions measuring similarity using graph edit distance.
|
| 2 |
+
|
| 3 |
+
The graph edit distance is the number of edge/node changes needed
|
| 4 |
+
to make two graphs isomorphic.
|
| 5 |
+
|
| 6 |
+
The default algorithm/implementation is sub-optimal for some graphs.
|
| 7 |
+
The problem of finding the exact Graph Edit Distance (GED) is NP-hard
|
| 8 |
+
so it is often slow. If the simple interface `graph_edit_distance`
|
| 9 |
+
takes too long for your graph, try `optimize_graph_edit_distance`
|
| 10 |
+
and/or `optimize_edit_paths`.
|
| 11 |
+
|
| 12 |
+
At the same time, I encourage capable people to investigate
|
| 13 |
+
alternative GED algorithms, in order to improve the choices available.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import math
|
| 17 |
+
import time
|
| 18 |
+
import warnings
|
| 19 |
+
from dataclasses import dataclass
|
| 20 |
+
from itertools import product
|
| 21 |
+
|
| 22 |
+
import networkx as nx
|
| 23 |
+
from networkx.utils import np_random_state
|
| 24 |
+
|
| 25 |
+
__all__ = [
|
| 26 |
+
"graph_edit_distance",
|
| 27 |
+
"optimal_edit_paths",
|
| 28 |
+
"optimize_graph_edit_distance",
|
| 29 |
+
"optimize_edit_paths",
|
| 30 |
+
"simrank_similarity",
|
| 31 |
+
"panther_similarity",
|
| 32 |
+
"generate_random_paths",
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def debug_print(*args, **kwargs):
|
| 37 |
+
print(*args, **kwargs)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@nx._dispatchable(
|
| 41 |
+
graphs={"G1": 0, "G2": 1}, preserve_edge_attrs=True, preserve_node_attrs=True
|
| 42 |
+
)
|
| 43 |
+
def graph_edit_distance(
|
| 44 |
+
G1,
|
| 45 |
+
G2,
|
| 46 |
+
node_match=None,
|
| 47 |
+
edge_match=None,
|
| 48 |
+
node_subst_cost=None,
|
| 49 |
+
node_del_cost=None,
|
| 50 |
+
node_ins_cost=None,
|
| 51 |
+
edge_subst_cost=None,
|
| 52 |
+
edge_del_cost=None,
|
| 53 |
+
edge_ins_cost=None,
|
| 54 |
+
roots=None,
|
| 55 |
+
upper_bound=None,
|
| 56 |
+
timeout=None,
|
| 57 |
+
):
|
| 58 |
+
"""Returns GED (graph edit distance) between graphs G1 and G2.
|
| 59 |
+
|
| 60 |
+
Graph edit distance is a graph similarity measure analogous to
|
| 61 |
+
Levenshtein distance for strings. It is defined as minimum cost
|
| 62 |
+
of edit path (sequence of node and edge edit operations)
|
| 63 |
+
transforming graph G1 to graph isomorphic to G2.
|
| 64 |
+
|
| 65 |
+
Parameters
|
| 66 |
+
----------
|
| 67 |
+
G1, G2: graphs
|
| 68 |
+
The two graphs G1 and G2 must be of the same type.
|
| 69 |
+
|
| 70 |
+
node_match : callable
|
| 71 |
+
A function that returns True if node n1 in G1 and n2 in G2
|
| 72 |
+
should be considered equal during matching.
|
| 73 |
+
|
| 74 |
+
The function will be called like
|
| 75 |
+
|
| 76 |
+
node_match(G1.nodes[n1], G2.nodes[n2]).
|
| 77 |
+
|
| 78 |
+
That is, the function will receive the node attribute
|
| 79 |
+
dictionaries for n1 and n2 as inputs.
|
| 80 |
+
|
| 81 |
+
Ignored if node_subst_cost is specified. If neither
|
| 82 |
+
node_match nor node_subst_cost are specified then node
|
| 83 |
+
attributes are not considered.
|
| 84 |
+
|
| 85 |
+
edge_match : callable
|
| 86 |
+
A function that returns True if the edge attribute dictionaries
|
| 87 |
+
for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
|
| 88 |
+
be considered equal during matching.
|
| 89 |
+
|
| 90 |
+
The function will be called like
|
| 91 |
+
|
| 92 |
+
edge_match(G1[u1][v1], G2[u2][v2]).
|
| 93 |
+
|
| 94 |
+
That is, the function will receive the edge attribute
|
| 95 |
+
dictionaries of the edges under consideration.
|
| 96 |
+
|
| 97 |
+
Ignored if edge_subst_cost is specified. If neither
|
| 98 |
+
edge_match nor edge_subst_cost are specified then edge
|
| 99 |
+
attributes are not considered.
|
| 100 |
+
|
| 101 |
+
node_subst_cost, node_del_cost, node_ins_cost : callable
|
| 102 |
+
Functions that return the costs of node substitution, node
|
| 103 |
+
deletion, and node insertion, respectively.
|
| 104 |
+
|
| 105 |
+
The functions will be called like
|
| 106 |
+
|
| 107 |
+
node_subst_cost(G1.nodes[n1], G2.nodes[n2]),
|
| 108 |
+
node_del_cost(G1.nodes[n1]),
|
| 109 |
+
node_ins_cost(G2.nodes[n2]).
|
| 110 |
+
|
| 111 |
+
That is, the functions will receive the node attribute
|
| 112 |
+
dictionaries as inputs. The functions are expected to return
|
| 113 |
+
positive numeric values.
|
| 114 |
+
|
| 115 |
+
Function node_subst_cost overrides node_match if specified.
|
| 116 |
+
If neither node_match nor node_subst_cost are specified then
|
| 117 |
+
default node substitution cost of 0 is used (node attributes
|
| 118 |
+
are not considered during matching).
|
| 119 |
+
|
| 120 |
+
If node_del_cost is not specified then default node deletion
|
| 121 |
+
cost of 1 is used. If node_ins_cost is not specified then
|
| 122 |
+
default node insertion cost of 1 is used.
|
| 123 |
+
|
| 124 |
+
edge_subst_cost, edge_del_cost, edge_ins_cost : callable
|
| 125 |
+
Functions that return the costs of edge substitution, edge
|
| 126 |
+
deletion, and edge insertion, respectively.
|
| 127 |
+
|
| 128 |
+
The functions will be called like
|
| 129 |
+
|
| 130 |
+
edge_subst_cost(G1[u1][v1], G2[u2][v2]),
|
| 131 |
+
edge_del_cost(G1[u1][v1]),
|
| 132 |
+
edge_ins_cost(G2[u2][v2]).
|
| 133 |
+
|
| 134 |
+
That is, the functions will receive the edge attribute
|
| 135 |
+
dictionaries as inputs. The functions are expected to return
|
| 136 |
+
positive numeric values.
|
| 137 |
+
|
| 138 |
+
Function edge_subst_cost overrides edge_match if specified.
|
| 139 |
+
If neither edge_match nor edge_subst_cost are specified then
|
| 140 |
+
default edge substitution cost of 0 is used (edge attributes
|
| 141 |
+
are not considered during matching).
|
| 142 |
+
|
| 143 |
+
If edge_del_cost is not specified then default edge deletion
|
| 144 |
+
cost of 1 is used. If edge_ins_cost is not specified then
|
| 145 |
+
default edge insertion cost of 1 is used.
|
| 146 |
+
|
| 147 |
+
roots : 2-tuple
|
| 148 |
+
Tuple where first element is a node in G1 and the second
|
| 149 |
+
is a node in G2.
|
| 150 |
+
These nodes are forced to be matched in the comparison to
|
| 151 |
+
allow comparison between rooted graphs.
|
| 152 |
+
|
| 153 |
+
upper_bound : numeric
|
| 154 |
+
Maximum edit distance to consider. Return None if no edit
|
| 155 |
+
distance under or equal to upper_bound exists.
|
| 156 |
+
|
| 157 |
+
timeout : numeric
|
| 158 |
+
Maximum number of seconds to execute.
|
| 159 |
+
After timeout is met, the current best GED is returned.
|
| 160 |
+
|
| 161 |
+
Examples
|
| 162 |
+
--------
|
| 163 |
+
>>> G1 = nx.cycle_graph(6)
|
| 164 |
+
>>> G2 = nx.wheel_graph(7)
|
| 165 |
+
>>> nx.graph_edit_distance(G1, G2)
|
| 166 |
+
7.0
|
| 167 |
+
|
| 168 |
+
>>> G1 = nx.star_graph(5)
|
| 169 |
+
>>> G2 = nx.star_graph(5)
|
| 170 |
+
>>> nx.graph_edit_distance(G1, G2, roots=(0, 0))
|
| 171 |
+
0.0
|
| 172 |
+
>>> nx.graph_edit_distance(G1, G2, roots=(1, 0))
|
| 173 |
+
8.0
|
| 174 |
+
|
| 175 |
+
See Also
|
| 176 |
+
--------
|
| 177 |
+
optimal_edit_paths, optimize_graph_edit_distance,
|
| 178 |
+
|
| 179 |
+
is_isomorphic: test for graph edit distance of 0
|
| 180 |
+
|
| 181 |
+
References
|
| 182 |
+
----------
|
| 183 |
+
.. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick
|
| 184 |
+
Martineau. An Exact Graph Edit Distance Algorithm for Solving
|
| 185 |
+
Pattern Recognition Problems. 4th International Conference on
|
| 186 |
+
Pattern Recognition Applications and Methods 2015, Jan 2015,
|
| 187 |
+
Lisbon, Portugal. 2015,
|
| 188 |
+
<10.5220/0005209202710278>. <hal-01168816>
|
| 189 |
+
https://hal.archives-ouvertes.fr/hal-01168816
|
| 190 |
+
|
| 191 |
+
"""
|
| 192 |
+
bestcost = None
|
| 193 |
+
for _, _, cost in optimize_edit_paths(
|
| 194 |
+
G1,
|
| 195 |
+
G2,
|
| 196 |
+
node_match,
|
| 197 |
+
edge_match,
|
| 198 |
+
node_subst_cost,
|
| 199 |
+
node_del_cost,
|
| 200 |
+
node_ins_cost,
|
| 201 |
+
edge_subst_cost,
|
| 202 |
+
edge_del_cost,
|
| 203 |
+
edge_ins_cost,
|
| 204 |
+
upper_bound,
|
| 205 |
+
True,
|
| 206 |
+
roots,
|
| 207 |
+
timeout,
|
| 208 |
+
):
|
| 209 |
+
# assert bestcost is None or cost < bestcost
|
| 210 |
+
bestcost = cost
|
| 211 |
+
return bestcost
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
@nx._dispatchable(graphs={"G1": 0, "G2": 1})
|
| 215 |
+
def optimal_edit_paths(
|
| 216 |
+
G1,
|
| 217 |
+
G2,
|
| 218 |
+
node_match=None,
|
| 219 |
+
edge_match=None,
|
| 220 |
+
node_subst_cost=None,
|
| 221 |
+
node_del_cost=None,
|
| 222 |
+
node_ins_cost=None,
|
| 223 |
+
edge_subst_cost=None,
|
| 224 |
+
edge_del_cost=None,
|
| 225 |
+
edge_ins_cost=None,
|
| 226 |
+
upper_bound=None,
|
| 227 |
+
):
|
| 228 |
+
"""Returns all minimum-cost edit paths transforming G1 to G2.
|
| 229 |
+
|
| 230 |
+
Graph edit path is a sequence of node and edge edit operations
|
| 231 |
+
transforming graph G1 to graph isomorphic to G2. Edit operations
|
| 232 |
+
include substitutions, deletions, and insertions.
|
| 233 |
+
|
| 234 |
+
Parameters
|
| 235 |
+
----------
|
| 236 |
+
G1, G2: graphs
|
| 237 |
+
The two graphs G1 and G2 must be of the same type.
|
| 238 |
+
|
| 239 |
+
node_match : callable
|
| 240 |
+
A function that returns True if node n1 in G1 and n2 in G2
|
| 241 |
+
should be considered equal during matching.
|
| 242 |
+
|
| 243 |
+
The function will be called like
|
| 244 |
+
|
| 245 |
+
node_match(G1.nodes[n1], G2.nodes[n2]).
|
| 246 |
+
|
| 247 |
+
That is, the function will receive the node attribute
|
| 248 |
+
dictionaries for n1 and n2 as inputs.
|
| 249 |
+
|
| 250 |
+
Ignored if node_subst_cost is specified. If neither
|
| 251 |
+
node_match nor node_subst_cost are specified then node
|
| 252 |
+
attributes are not considered.
|
| 253 |
+
|
| 254 |
+
edge_match : callable
|
| 255 |
+
A function that returns True if the edge attribute dictionaries
|
| 256 |
+
for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
|
| 257 |
+
be considered equal during matching.
|
| 258 |
+
|
| 259 |
+
The function will be called like
|
| 260 |
+
|
| 261 |
+
edge_match(G1[u1][v1], G2[u2][v2]).
|
| 262 |
+
|
| 263 |
+
That is, the function will receive the edge attribute
|
| 264 |
+
dictionaries of the edges under consideration.
|
| 265 |
+
|
| 266 |
+
Ignored if edge_subst_cost is specified. If neither
|
| 267 |
+
edge_match nor edge_subst_cost are specified then edge
|
| 268 |
+
attributes are not considered.
|
| 269 |
+
|
| 270 |
+
node_subst_cost, node_del_cost, node_ins_cost : callable
|
| 271 |
+
Functions that return the costs of node substitution, node
|
| 272 |
+
deletion, and node insertion, respectively.
|
| 273 |
+
|
| 274 |
+
The functions will be called like
|
| 275 |
+
|
| 276 |
+
node_subst_cost(G1.nodes[n1], G2.nodes[n2]),
|
| 277 |
+
node_del_cost(G1.nodes[n1]),
|
| 278 |
+
node_ins_cost(G2.nodes[n2]).
|
| 279 |
+
|
| 280 |
+
That is, the functions will receive the node attribute
|
| 281 |
+
dictionaries as inputs. The functions are expected to return
|
| 282 |
+
positive numeric values.
|
| 283 |
+
|
| 284 |
+
Function node_subst_cost overrides node_match if specified.
|
| 285 |
+
If neither node_match nor node_subst_cost are specified then
|
| 286 |
+
default node substitution cost of 0 is used (node attributes
|
| 287 |
+
are not considered during matching).
|
| 288 |
+
|
| 289 |
+
If node_del_cost is not specified then default node deletion
|
| 290 |
+
cost of 1 is used. If node_ins_cost is not specified then
|
| 291 |
+
default node insertion cost of 1 is used.
|
| 292 |
+
|
| 293 |
+
edge_subst_cost, edge_del_cost, edge_ins_cost : callable
|
| 294 |
+
Functions that return the costs of edge substitution, edge
|
| 295 |
+
deletion, and edge insertion, respectively.
|
| 296 |
+
|
| 297 |
+
The functions will be called like
|
| 298 |
+
|
| 299 |
+
edge_subst_cost(G1[u1][v1], G2[u2][v2]),
|
| 300 |
+
edge_del_cost(G1[u1][v1]),
|
| 301 |
+
edge_ins_cost(G2[u2][v2]).
|
| 302 |
+
|
| 303 |
+
That is, the functions will receive the edge attribute
|
| 304 |
+
dictionaries as inputs. The functions are expected to return
|
| 305 |
+
positive numeric values.
|
| 306 |
+
|
| 307 |
+
Function edge_subst_cost overrides edge_match if specified.
|
| 308 |
+
If neither edge_match nor edge_subst_cost are specified then
|
| 309 |
+
default edge substitution cost of 0 is used (edge attributes
|
| 310 |
+
are not considered during matching).
|
| 311 |
+
|
| 312 |
+
If edge_del_cost is not specified then default edge deletion
|
| 313 |
+
cost of 1 is used. If edge_ins_cost is not specified then
|
| 314 |
+
default edge insertion cost of 1 is used.
|
| 315 |
+
|
| 316 |
+
upper_bound : numeric
|
| 317 |
+
Maximum edit distance to consider.
|
| 318 |
+
|
| 319 |
+
Returns
|
| 320 |
+
-------
|
| 321 |
+
edit_paths : list of tuples (node_edit_path, edge_edit_path)
|
| 322 |
+
- node_edit_path : list of tuples ``(u, v)`` indicating node transformations
|
| 323 |
+
between `G1` and `G2`. ``u`` is `None` for insertion, ``v`` is `None`
|
| 324 |
+
for deletion.
|
| 325 |
+
- edge_edit_path : list of tuples ``((u1, v1), (u2, v2))`` indicating edge
|
| 326 |
+
transformations between `G1` and `G2`. ``(None, (u2,v2))`` for insertion
|
| 327 |
+
and ``((u1,v1), None)`` for deletion.
|
| 328 |
+
|
| 329 |
+
cost : numeric
|
| 330 |
+
Optimal edit path cost (graph edit distance). When the cost
|
| 331 |
+
is zero, it indicates that `G1` and `G2` are isomorphic.
|
| 332 |
+
|
| 333 |
+
Examples
|
| 334 |
+
--------
|
| 335 |
+
>>> G1 = nx.cycle_graph(4)
|
| 336 |
+
>>> G2 = nx.wheel_graph(5)
|
| 337 |
+
>>> paths, cost = nx.optimal_edit_paths(G1, G2)
|
| 338 |
+
>>> len(paths)
|
| 339 |
+
40
|
| 340 |
+
>>> cost
|
| 341 |
+
5.0
|
| 342 |
+
|
| 343 |
+
Notes
|
| 344 |
+
-----
|
| 345 |
+
To transform `G1` into a graph isomorphic to `G2`, apply the node
|
| 346 |
+
and edge edits in the returned ``edit_paths``.
|
| 347 |
+
In the case of isomorphic graphs, the cost is zero, and the paths
|
| 348 |
+
represent different isomorphic mappings (isomorphisms). That is, the
|
| 349 |
+
edits involve renaming nodes and edges to match the structure of `G2`.
|
| 350 |
+
|
| 351 |
+
See Also
|
| 352 |
+
--------
|
| 353 |
+
graph_edit_distance, optimize_edit_paths
|
| 354 |
+
|
| 355 |
+
References
|
| 356 |
+
----------
|
| 357 |
+
.. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick
|
| 358 |
+
Martineau. An Exact Graph Edit Distance Algorithm for Solving
|
| 359 |
+
Pattern Recognition Problems. 4th International Conference on
|
| 360 |
+
Pattern Recognition Applications and Methods 2015, Jan 2015,
|
| 361 |
+
Lisbon, Portugal. 2015,
|
| 362 |
+
<10.5220/0005209202710278>. <hal-01168816>
|
| 363 |
+
https://hal.archives-ouvertes.fr/hal-01168816
|
| 364 |
+
|
| 365 |
+
"""
|
| 366 |
+
paths = []
|
| 367 |
+
bestcost = None
|
| 368 |
+
for vertex_path, edge_path, cost in optimize_edit_paths(
|
| 369 |
+
G1,
|
| 370 |
+
G2,
|
| 371 |
+
node_match,
|
| 372 |
+
edge_match,
|
| 373 |
+
node_subst_cost,
|
| 374 |
+
node_del_cost,
|
| 375 |
+
node_ins_cost,
|
| 376 |
+
edge_subst_cost,
|
| 377 |
+
edge_del_cost,
|
| 378 |
+
edge_ins_cost,
|
| 379 |
+
upper_bound,
|
| 380 |
+
False,
|
| 381 |
+
):
|
| 382 |
+
# assert bestcost is None or cost <= bestcost
|
| 383 |
+
if bestcost is not None and cost < bestcost:
|
| 384 |
+
paths = []
|
| 385 |
+
paths.append((vertex_path, edge_path))
|
| 386 |
+
bestcost = cost
|
| 387 |
+
return paths, bestcost
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
@nx._dispatchable(graphs={"G1": 0, "G2": 1})
|
| 391 |
+
def optimize_graph_edit_distance(
|
| 392 |
+
G1,
|
| 393 |
+
G2,
|
| 394 |
+
node_match=None,
|
| 395 |
+
edge_match=None,
|
| 396 |
+
node_subst_cost=None,
|
| 397 |
+
node_del_cost=None,
|
| 398 |
+
node_ins_cost=None,
|
| 399 |
+
edge_subst_cost=None,
|
| 400 |
+
edge_del_cost=None,
|
| 401 |
+
edge_ins_cost=None,
|
| 402 |
+
upper_bound=None,
|
| 403 |
+
):
|
| 404 |
+
"""Returns consecutive approximations of GED (graph edit distance)
|
| 405 |
+
between graphs G1 and G2.
|
| 406 |
+
|
| 407 |
+
Graph edit distance is a graph similarity measure analogous to
|
| 408 |
+
Levenshtein distance for strings. It is defined as minimum cost
|
| 409 |
+
of edit path (sequence of node and edge edit operations)
|
| 410 |
+
transforming graph G1 to graph isomorphic to G2.
|
| 411 |
+
|
| 412 |
+
Parameters
|
| 413 |
+
----------
|
| 414 |
+
G1, G2: graphs
|
| 415 |
+
The two graphs G1 and G2 must be of the same type.
|
| 416 |
+
|
| 417 |
+
node_match : callable
|
| 418 |
+
A function that returns True if node n1 in G1 and n2 in G2
|
| 419 |
+
should be considered equal during matching.
|
| 420 |
+
|
| 421 |
+
The function will be called like
|
| 422 |
+
|
| 423 |
+
node_match(G1.nodes[n1], G2.nodes[n2]).
|
| 424 |
+
|
| 425 |
+
That is, the function will receive the node attribute
|
| 426 |
+
dictionaries for n1 and n2 as inputs.
|
| 427 |
+
|
| 428 |
+
Ignored if node_subst_cost is specified. If neither
|
| 429 |
+
node_match nor node_subst_cost are specified then node
|
| 430 |
+
attributes are not considered.
|
| 431 |
+
|
| 432 |
+
edge_match : callable
|
| 433 |
+
A function that returns True if the edge attribute dictionaries
|
| 434 |
+
for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
|
| 435 |
+
be considered equal during matching.
|
| 436 |
+
|
| 437 |
+
The function will be called like
|
| 438 |
+
|
| 439 |
+
edge_match(G1[u1][v1], G2[u2][v2]).
|
| 440 |
+
|
| 441 |
+
That is, the function will receive the edge attribute
|
| 442 |
+
dictionaries of the edges under consideration.
|
| 443 |
+
|
| 444 |
+
Ignored if edge_subst_cost is specified. If neither
|
| 445 |
+
edge_match nor edge_subst_cost are specified then edge
|
| 446 |
+
attributes are not considered.
|
| 447 |
+
|
| 448 |
+
node_subst_cost, node_del_cost, node_ins_cost : callable
|
| 449 |
+
Functions that return the costs of node substitution, node
|
| 450 |
+
deletion, and node insertion, respectively.
|
| 451 |
+
|
| 452 |
+
The functions will be called like
|
| 453 |
+
|
| 454 |
+
node_subst_cost(G1.nodes[n1], G2.nodes[n2]),
|
| 455 |
+
node_del_cost(G1.nodes[n1]),
|
| 456 |
+
node_ins_cost(G2.nodes[n2]).
|
| 457 |
+
|
| 458 |
+
That is, the functions will receive the node attribute
|
| 459 |
+
dictionaries as inputs. The functions are expected to return
|
| 460 |
+
positive numeric values.
|
| 461 |
+
|
| 462 |
+
Function node_subst_cost overrides node_match if specified.
|
| 463 |
+
If neither node_match nor node_subst_cost are specified then
|
| 464 |
+
default node substitution cost of 0 is used (node attributes
|
| 465 |
+
are not considered during matching).
|
| 466 |
+
|
| 467 |
+
If node_del_cost is not specified then default node deletion
|
| 468 |
+
cost of 1 is used. If node_ins_cost is not specified then
|
| 469 |
+
default node insertion cost of 1 is used.
|
| 470 |
+
|
| 471 |
+
edge_subst_cost, edge_del_cost, edge_ins_cost : callable
|
| 472 |
+
Functions that return the costs of edge substitution, edge
|
| 473 |
+
deletion, and edge insertion, respectively.
|
| 474 |
+
|
| 475 |
+
The functions will be called like
|
| 476 |
+
|
| 477 |
+
edge_subst_cost(G1[u1][v1], G2[u2][v2]),
|
| 478 |
+
edge_del_cost(G1[u1][v1]),
|
| 479 |
+
edge_ins_cost(G2[u2][v2]).
|
| 480 |
+
|
| 481 |
+
That is, the functions will receive the edge attribute
|
| 482 |
+
dictionaries as inputs. The functions are expected to return
|
| 483 |
+
positive numeric values.
|
| 484 |
+
|
| 485 |
+
Function edge_subst_cost overrides edge_match if specified.
|
| 486 |
+
If neither edge_match nor edge_subst_cost are specified then
|
| 487 |
+
default edge substitution cost of 0 is used (edge attributes
|
| 488 |
+
are not considered during matching).
|
| 489 |
+
|
| 490 |
+
If edge_del_cost is not specified then default edge deletion
|
| 491 |
+
cost of 1 is used. If edge_ins_cost is not specified then
|
| 492 |
+
default edge insertion cost of 1 is used.
|
| 493 |
+
|
| 494 |
+
upper_bound : numeric
|
| 495 |
+
Maximum edit distance to consider.
|
| 496 |
+
|
| 497 |
+
Returns
|
| 498 |
+
-------
|
| 499 |
+
Generator of consecutive approximations of graph edit distance.
|
| 500 |
+
|
| 501 |
+
Examples
|
| 502 |
+
--------
|
| 503 |
+
>>> G1 = nx.cycle_graph(6)
|
| 504 |
+
>>> G2 = nx.wheel_graph(7)
|
| 505 |
+
>>> for v in nx.optimize_graph_edit_distance(G1, G2):
|
| 506 |
+
... minv = v
|
| 507 |
+
>>> minv
|
| 508 |
+
7.0
|
| 509 |
+
|
| 510 |
+
See Also
|
| 511 |
+
--------
|
| 512 |
+
graph_edit_distance, optimize_edit_paths
|
| 513 |
+
|
| 514 |
+
References
|
| 515 |
+
----------
|
| 516 |
+
.. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick
|
| 517 |
+
Martineau. An Exact Graph Edit Distance Algorithm for Solving
|
| 518 |
+
Pattern Recognition Problems. 4th International Conference on
|
| 519 |
+
Pattern Recognition Applications and Methods 2015, Jan 2015,
|
| 520 |
+
Lisbon, Portugal. 2015,
|
| 521 |
+
<10.5220/0005209202710278>. <hal-01168816>
|
| 522 |
+
https://hal.archives-ouvertes.fr/hal-01168816
|
| 523 |
+
"""
|
| 524 |
+
for _, _, cost in optimize_edit_paths(
|
| 525 |
+
G1,
|
| 526 |
+
G2,
|
| 527 |
+
node_match,
|
| 528 |
+
edge_match,
|
| 529 |
+
node_subst_cost,
|
| 530 |
+
node_del_cost,
|
| 531 |
+
node_ins_cost,
|
| 532 |
+
edge_subst_cost,
|
| 533 |
+
edge_del_cost,
|
| 534 |
+
edge_ins_cost,
|
| 535 |
+
upper_bound,
|
| 536 |
+
True,
|
| 537 |
+
):
|
| 538 |
+
yield cost
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
@nx._dispatchable(
|
| 542 |
+
graphs={"G1": 0, "G2": 1}, preserve_edge_attrs=True, preserve_node_attrs=True
|
| 543 |
+
)
|
| 544 |
+
def optimize_edit_paths(
|
| 545 |
+
G1,
|
| 546 |
+
G2,
|
| 547 |
+
node_match=None,
|
| 548 |
+
edge_match=None,
|
| 549 |
+
node_subst_cost=None,
|
| 550 |
+
node_del_cost=None,
|
| 551 |
+
node_ins_cost=None,
|
| 552 |
+
edge_subst_cost=None,
|
| 553 |
+
edge_del_cost=None,
|
| 554 |
+
edge_ins_cost=None,
|
| 555 |
+
upper_bound=None,
|
| 556 |
+
strictly_decreasing=True,
|
| 557 |
+
roots=None,
|
| 558 |
+
timeout=None,
|
| 559 |
+
):
|
| 560 |
+
"""GED (graph edit distance) calculation: advanced interface.
|
| 561 |
+
|
| 562 |
+
Graph edit path is a sequence of node and edge edit operations
|
| 563 |
+
transforming graph G1 to graph isomorphic to G2. Edit operations
|
| 564 |
+
include substitutions, deletions, and insertions.
|
| 565 |
+
|
| 566 |
+
Graph edit distance is defined as minimum cost of edit path.
|
| 567 |
+
|
| 568 |
+
Parameters
|
| 569 |
+
----------
|
| 570 |
+
G1, G2: graphs
|
| 571 |
+
The two graphs G1 and G2 must be of the same type.
|
| 572 |
+
|
| 573 |
+
node_match : callable
|
| 574 |
+
A function that returns True if node n1 in G1 and n2 in G2
|
| 575 |
+
should be considered equal during matching.
|
| 576 |
+
|
| 577 |
+
The function will be called like
|
| 578 |
+
|
| 579 |
+
node_match(G1.nodes[n1], G2.nodes[n2]).
|
| 580 |
+
|
| 581 |
+
That is, the function will receive the node attribute
|
| 582 |
+
dictionaries for n1 and n2 as inputs.
|
| 583 |
+
|
| 584 |
+
Ignored if node_subst_cost is specified. If neither
|
| 585 |
+
node_match nor node_subst_cost are specified then node
|
| 586 |
+
attributes are not considered.
|
| 587 |
+
|
| 588 |
+
edge_match : callable
|
| 589 |
+
A function that returns True if the edge attribute dictionaries
|
| 590 |
+
for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
|
| 591 |
+
be considered equal during matching.
|
| 592 |
+
|
| 593 |
+
The function will be called like
|
| 594 |
+
|
| 595 |
+
edge_match(G1[u1][v1], G2[u2][v2]).
|
| 596 |
+
|
| 597 |
+
That is, the function will receive the edge attribute
|
| 598 |
+
dictionaries of the edges under consideration.
|
| 599 |
+
|
| 600 |
+
Ignored if edge_subst_cost is specified. If neither
|
| 601 |
+
edge_match nor edge_subst_cost are specified then edge
|
| 602 |
+
attributes are not considered.
|
| 603 |
+
|
| 604 |
+
node_subst_cost, node_del_cost, node_ins_cost : callable
|
| 605 |
+
Functions that return the costs of node substitution, node
|
| 606 |
+
deletion, and node insertion, respectively.
|
| 607 |
+
|
| 608 |
+
The functions will be called like
|
| 609 |
+
|
| 610 |
+
node_subst_cost(G1.nodes[n1], G2.nodes[n2]),
|
| 611 |
+
node_del_cost(G1.nodes[n1]),
|
| 612 |
+
node_ins_cost(G2.nodes[n2]).
|
| 613 |
+
|
| 614 |
+
That is, the functions will receive the node attribute
|
| 615 |
+
dictionaries as inputs. The functions are expected to return
|
| 616 |
+
positive numeric values.
|
| 617 |
+
|
| 618 |
+
Function node_subst_cost overrides node_match if specified.
|
| 619 |
+
If neither node_match nor node_subst_cost are specified then
|
| 620 |
+
default node substitution cost of 0 is used (node attributes
|
| 621 |
+
are not considered during matching).
|
| 622 |
+
|
| 623 |
+
If node_del_cost is not specified then default node deletion
|
| 624 |
+
cost of 1 is used. If node_ins_cost is not specified then
|
| 625 |
+
default node insertion cost of 1 is used.
|
| 626 |
+
|
| 627 |
+
edge_subst_cost, edge_del_cost, edge_ins_cost : callable
|
| 628 |
+
Functions that return the costs of edge substitution, edge
|
| 629 |
+
deletion, and edge insertion, respectively.
|
| 630 |
+
|
| 631 |
+
The functions will be called like
|
| 632 |
+
|
| 633 |
+
edge_subst_cost(G1[u1][v1], G2[u2][v2]),
|
| 634 |
+
edge_del_cost(G1[u1][v1]),
|
| 635 |
+
edge_ins_cost(G2[u2][v2]).
|
| 636 |
+
|
| 637 |
+
That is, the functions will receive the edge attribute
|
| 638 |
+
dictionaries as inputs. The functions are expected to return
|
| 639 |
+
positive numeric values.
|
| 640 |
+
|
| 641 |
+
Function edge_subst_cost overrides edge_match if specified.
|
| 642 |
+
If neither edge_match nor edge_subst_cost are specified then
|
| 643 |
+
default edge substitution cost of 0 is used (edge attributes
|
| 644 |
+
are not considered during matching).
|
| 645 |
+
|
| 646 |
+
If edge_del_cost is not specified then default edge deletion
|
| 647 |
+
cost of 1 is used. If edge_ins_cost is not specified then
|
| 648 |
+
default edge insertion cost of 1 is used.
|
| 649 |
+
|
| 650 |
+
upper_bound : numeric
|
| 651 |
+
Maximum edit distance to consider.
|
| 652 |
+
|
| 653 |
+
strictly_decreasing : bool
|
| 654 |
+
If True, return consecutive approximations of strictly
|
| 655 |
+
decreasing cost. Otherwise, return all edit paths of cost
|
| 656 |
+
less than or equal to the previous minimum cost.
|
| 657 |
+
|
| 658 |
+
roots : 2-tuple
|
| 659 |
+
Tuple where first element is a node in G1 and the second
|
| 660 |
+
is a node in G2.
|
| 661 |
+
These nodes are forced to be matched in the comparison to
|
| 662 |
+
allow comparison between rooted graphs.
|
| 663 |
+
|
| 664 |
+
timeout : numeric
|
| 665 |
+
Maximum number of seconds to execute.
|
| 666 |
+
After timeout is met, the current best GED is returned.
|
| 667 |
+
|
| 668 |
+
Returns
|
| 669 |
+
-------
|
| 670 |
+
Generator of tuples (node_edit_path, edge_edit_path, cost)
|
| 671 |
+
node_edit_path : list of tuples (u, v)
|
| 672 |
+
edge_edit_path : list of tuples ((u1, v1), (u2, v2))
|
| 673 |
+
cost : numeric
|
| 674 |
+
|
| 675 |
+
See Also
|
| 676 |
+
--------
|
| 677 |
+
graph_edit_distance, optimize_graph_edit_distance, optimal_edit_paths
|
| 678 |
+
|
| 679 |
+
References
|
| 680 |
+
----------
|
| 681 |
+
.. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick
|
| 682 |
+
Martineau. An Exact Graph Edit Distance Algorithm for Solving
|
| 683 |
+
Pattern Recognition Problems. 4th International Conference on
|
| 684 |
+
Pattern Recognition Applications and Methods 2015, Jan 2015,
|
| 685 |
+
Lisbon, Portugal. 2015,
|
| 686 |
+
<10.5220/0005209202710278>. <hal-01168816>
|
| 687 |
+
https://hal.archives-ouvertes.fr/hal-01168816
|
| 688 |
+
|
| 689 |
+
"""
|
| 690 |
+
# TODO: support DiGraph
|
| 691 |
+
|
| 692 |
+
import numpy as np
|
| 693 |
+
import scipy as sp
|
| 694 |
+
|
| 695 |
+
@dataclass
|
| 696 |
+
class CostMatrix:
|
| 697 |
+
C: ...
|
| 698 |
+
lsa_row_ind: ...
|
| 699 |
+
lsa_col_ind: ...
|
| 700 |
+
ls: ...
|
| 701 |
+
|
| 702 |
+
def make_CostMatrix(C, m, n):
|
| 703 |
+
# assert(C.shape == (m + n, m + n))
|
| 704 |
+
lsa_row_ind, lsa_col_ind = sp.optimize.linear_sum_assignment(C)
|
| 705 |
+
|
| 706 |
+
# Fixup dummy assignments:
|
| 707 |
+
# each substitution i<->j should have dummy assignment m+j<->n+i
|
| 708 |
+
# NOTE: fast reduce of Cv relies on it
|
| 709 |
+
# Create masks for substitution and dummy indices
|
| 710 |
+
is_subst = (lsa_row_ind < m) & (lsa_col_ind < n)
|
| 711 |
+
is_dummy = (lsa_row_ind >= m) & (lsa_col_ind >= n)
|
| 712 |
+
|
| 713 |
+
# Map dummy assignments to the correct indices
|
| 714 |
+
lsa_row_ind[is_dummy] = lsa_col_ind[is_subst] + m
|
| 715 |
+
lsa_col_ind[is_dummy] = lsa_row_ind[is_subst] + n
|
| 716 |
+
|
| 717 |
+
return CostMatrix(
|
| 718 |
+
C, lsa_row_ind, lsa_col_ind, C[lsa_row_ind, lsa_col_ind].sum()
|
| 719 |
+
)
|
| 720 |
+
|
| 721 |
+
def extract_C(C, i, j, m, n):
|
| 722 |
+
# assert(C.shape == (m + n, m + n))
|
| 723 |
+
row_ind = [k in i or k - m in j for k in range(m + n)]
|
| 724 |
+
col_ind = [k in j or k - n in i for k in range(m + n)]
|
| 725 |
+
return C[row_ind, :][:, col_ind]
|
| 726 |
+
|
| 727 |
+
def reduce_C(C, i, j, m, n):
|
| 728 |
+
# assert(C.shape == (m + n, m + n))
|
| 729 |
+
row_ind = [k not in i and k - m not in j for k in range(m + n)]
|
| 730 |
+
col_ind = [k not in j and k - n not in i for k in range(m + n)]
|
| 731 |
+
return C[row_ind, :][:, col_ind]
|
| 732 |
+
|
| 733 |
+
def reduce_ind(ind, i):
|
| 734 |
+
# assert set(ind) == set(range(len(ind)))
|
| 735 |
+
rind = ind[[k not in i for k in ind]]
|
| 736 |
+
for k in set(i):
|
| 737 |
+
rind[rind >= k] -= 1
|
| 738 |
+
return rind
|
| 739 |
+
|
| 740 |
+
def match_edges(u, v, pending_g, pending_h, Ce, matched_uv=None):
|
| 741 |
+
"""
|
| 742 |
+
Parameters:
|
| 743 |
+
u, v: matched vertices, u=None or v=None for
|
| 744 |
+
deletion/insertion
|
| 745 |
+
pending_g, pending_h: lists of edges not yet mapped
|
| 746 |
+
Ce: CostMatrix of pending edge mappings
|
| 747 |
+
matched_uv: partial vertex edit path
|
| 748 |
+
list of tuples (u, v) of previously matched vertex
|
| 749 |
+
mappings u<->v, u=None or v=None for
|
| 750 |
+
deletion/insertion
|
| 751 |
+
|
| 752 |
+
Returns:
|
| 753 |
+
list of (i, j): indices of edge mappings g<->h
|
| 754 |
+
localCe: local CostMatrix of edge mappings
|
| 755 |
+
(basically submatrix of Ce at cross of rows i, cols j)
|
| 756 |
+
"""
|
| 757 |
+
M = len(pending_g)
|
| 758 |
+
N = len(pending_h)
|
| 759 |
+
# assert Ce.C.shape == (M + N, M + N)
|
| 760 |
+
|
| 761 |
+
# only attempt to match edges after one node match has been made
|
| 762 |
+
# this will stop self-edges on the first node being automatically deleted
|
| 763 |
+
# even when a substitution is the better option
|
| 764 |
+
if matched_uv is None or len(matched_uv) == 0:
|
| 765 |
+
g_ind = []
|
| 766 |
+
h_ind = []
|
| 767 |
+
else:
|
| 768 |
+
g_ind = [
|
| 769 |
+
i
|
| 770 |
+
for i in range(M)
|
| 771 |
+
if pending_g[i][:2] == (u, u)
|
| 772 |
+
or any(
|
| 773 |
+
pending_g[i][:2] in ((p, u), (u, p), (p, p)) for p, q in matched_uv
|
| 774 |
+
)
|
| 775 |
+
]
|
| 776 |
+
h_ind = [
|
| 777 |
+
j
|
| 778 |
+
for j in range(N)
|
| 779 |
+
if pending_h[j][:2] == (v, v)
|
| 780 |
+
or any(
|
| 781 |
+
pending_h[j][:2] in ((q, v), (v, q), (q, q)) for p, q in matched_uv
|
| 782 |
+
)
|
| 783 |
+
]
|
| 784 |
+
|
| 785 |
+
m = len(g_ind)
|
| 786 |
+
n = len(h_ind)
|
| 787 |
+
|
| 788 |
+
if m or n:
|
| 789 |
+
C = extract_C(Ce.C, g_ind, h_ind, M, N)
|
| 790 |
+
# assert C.shape == (m + n, m + n)
|
| 791 |
+
|
| 792 |
+
# Forbid structurally invalid matches
|
| 793 |
+
# NOTE: inf remembered from Ce construction
|
| 794 |
+
for k, i in enumerate(g_ind):
|
| 795 |
+
g = pending_g[i][:2]
|
| 796 |
+
for l, j in enumerate(h_ind):
|
| 797 |
+
h = pending_h[j][:2]
|
| 798 |
+
if nx.is_directed(G1) or nx.is_directed(G2):
|
| 799 |
+
if any(
|
| 800 |
+
g == (p, u) and h == (q, v) or g == (u, p) and h == (v, q)
|
| 801 |
+
for p, q in matched_uv
|
| 802 |
+
):
|
| 803 |
+
continue
|
| 804 |
+
else:
|
| 805 |
+
if any(
|
| 806 |
+
g in ((p, u), (u, p)) and h in ((q, v), (v, q))
|
| 807 |
+
for p, q in matched_uv
|
| 808 |
+
):
|
| 809 |
+
continue
|
| 810 |
+
if g == (u, u) or any(g == (p, p) for p, q in matched_uv):
|
| 811 |
+
continue
|
| 812 |
+
if h == (v, v) or any(h == (q, q) for p, q in matched_uv):
|
| 813 |
+
continue
|
| 814 |
+
C[k, l] = inf
|
| 815 |
+
|
| 816 |
+
localCe = make_CostMatrix(C, m, n)
|
| 817 |
+
ij = [
|
| 818 |
+
(
|
| 819 |
+
g_ind[k] if k < m else M + h_ind[l],
|
| 820 |
+
h_ind[l] if l < n else N + g_ind[k],
|
| 821 |
+
)
|
| 822 |
+
for k, l in zip(localCe.lsa_row_ind, localCe.lsa_col_ind)
|
| 823 |
+
if k < m or l < n
|
| 824 |
+
]
|
| 825 |
+
|
| 826 |
+
else:
|
| 827 |
+
ij = []
|
| 828 |
+
localCe = CostMatrix(np.empty((0, 0)), [], [], 0)
|
| 829 |
+
|
| 830 |
+
return ij, localCe
|
| 831 |
+
|
| 832 |
+
def reduce_Ce(Ce, ij, m, n):
|
| 833 |
+
if len(ij):
|
| 834 |
+
i, j = zip(*ij)
|
| 835 |
+
m_i = m - sum(1 for t in i if t < m)
|
| 836 |
+
n_j = n - sum(1 for t in j if t < n)
|
| 837 |
+
return make_CostMatrix(reduce_C(Ce.C, i, j, m, n), m_i, n_j)
|
| 838 |
+
return Ce
|
| 839 |
+
|
| 840 |
+
def get_edit_ops(
|
| 841 |
+
matched_uv, pending_u, pending_v, Cv, pending_g, pending_h, Ce, matched_cost
|
| 842 |
+
):
|
| 843 |
+
"""
|
| 844 |
+
Parameters:
|
| 845 |
+
matched_uv: partial vertex edit path
|
| 846 |
+
list of tuples (u, v) of vertex mappings u<->v,
|
| 847 |
+
u=None or v=None for deletion/insertion
|
| 848 |
+
pending_u, pending_v: lists of vertices not yet mapped
|
| 849 |
+
Cv: CostMatrix of pending vertex mappings
|
| 850 |
+
pending_g, pending_h: lists of edges not yet mapped
|
| 851 |
+
Ce: CostMatrix of pending edge mappings
|
| 852 |
+
matched_cost: cost of partial edit path
|
| 853 |
+
|
| 854 |
+
Returns:
|
| 855 |
+
sequence of
|
| 856 |
+
(i, j): indices of vertex mapping u<->v
|
| 857 |
+
Cv_ij: reduced CostMatrix of pending vertex mappings
|
| 858 |
+
(basically Cv with row i, col j removed)
|
| 859 |
+
list of (x, y): indices of edge mappings g<->h
|
| 860 |
+
Ce_xy: reduced CostMatrix of pending edge mappings
|
| 861 |
+
(basically Ce with rows x, cols y removed)
|
| 862 |
+
cost: total cost of edit operation
|
| 863 |
+
NOTE: most promising ops first
|
| 864 |
+
"""
|
| 865 |
+
m = len(pending_u)
|
| 866 |
+
n = len(pending_v)
|
| 867 |
+
# assert Cv.C.shape == (m + n, m + n)
|
| 868 |
+
|
| 869 |
+
# 1) a vertex mapping from optimal linear sum assignment
|
| 870 |
+
i, j = min(
|
| 871 |
+
(k, l) for k, l in zip(Cv.lsa_row_ind, Cv.lsa_col_ind) if k < m or l < n
|
| 872 |
+
)
|
| 873 |
+
xy, localCe = match_edges(
|
| 874 |
+
pending_u[i] if i < m else None,
|
| 875 |
+
pending_v[j] if j < n else None,
|
| 876 |
+
pending_g,
|
| 877 |
+
pending_h,
|
| 878 |
+
Ce,
|
| 879 |
+
matched_uv,
|
| 880 |
+
)
|
| 881 |
+
Ce_xy = reduce_Ce(Ce, xy, len(pending_g), len(pending_h))
|
| 882 |
+
# assert Ce.ls <= localCe.ls + Ce_xy.ls
|
| 883 |
+
if prune(matched_cost + Cv.ls + localCe.ls + Ce_xy.ls):
|
| 884 |
+
pass
|
| 885 |
+
else:
|
| 886 |
+
# get reduced Cv efficiently
|
| 887 |
+
Cv_ij = CostMatrix(
|
| 888 |
+
reduce_C(Cv.C, (i,), (j,), m, n),
|
| 889 |
+
reduce_ind(Cv.lsa_row_ind, (i, m + j)),
|
| 890 |
+
reduce_ind(Cv.lsa_col_ind, (j, n + i)),
|
| 891 |
+
Cv.ls - Cv.C[i, j],
|
| 892 |
+
)
|
| 893 |
+
yield (i, j), Cv_ij, xy, Ce_xy, Cv.C[i, j] + localCe.ls
|
| 894 |
+
|
| 895 |
+
# 2) other candidates, sorted by lower-bound cost estimate
|
| 896 |
+
other = []
|
| 897 |
+
fixed_i, fixed_j = i, j
|
| 898 |
+
if m <= n:
|
| 899 |
+
candidates = (
|
| 900 |
+
(t, fixed_j)
|
| 901 |
+
for t in range(m + n)
|
| 902 |
+
if t != fixed_i and (t < m or t == m + fixed_j)
|
| 903 |
+
)
|
| 904 |
+
else:
|
| 905 |
+
candidates = (
|
| 906 |
+
(fixed_i, t)
|
| 907 |
+
for t in range(m + n)
|
| 908 |
+
if t != fixed_j and (t < n or t == n + fixed_i)
|
| 909 |
+
)
|
| 910 |
+
for i, j in candidates:
|
| 911 |
+
if prune(matched_cost + Cv.C[i, j] + Ce.ls):
|
| 912 |
+
continue
|
| 913 |
+
Cv_ij = make_CostMatrix(
|
| 914 |
+
reduce_C(Cv.C, (i,), (j,), m, n),
|
| 915 |
+
m - 1 if i < m else m,
|
| 916 |
+
n - 1 if j < n else n,
|
| 917 |
+
)
|
| 918 |
+
# assert Cv.ls <= Cv.C[i, j] + Cv_ij.ls
|
| 919 |
+
if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + Ce.ls):
|
| 920 |
+
continue
|
| 921 |
+
xy, localCe = match_edges(
|
| 922 |
+
pending_u[i] if i < m else None,
|
| 923 |
+
pending_v[j] if j < n else None,
|
| 924 |
+
pending_g,
|
| 925 |
+
pending_h,
|
| 926 |
+
Ce,
|
| 927 |
+
matched_uv,
|
| 928 |
+
)
|
| 929 |
+
if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + localCe.ls):
|
| 930 |
+
continue
|
| 931 |
+
Ce_xy = reduce_Ce(Ce, xy, len(pending_g), len(pending_h))
|
| 932 |
+
# assert Ce.ls <= localCe.ls + Ce_xy.ls
|
| 933 |
+
if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + localCe.ls + Ce_xy.ls):
|
| 934 |
+
continue
|
| 935 |
+
other.append(((i, j), Cv_ij, xy, Ce_xy, Cv.C[i, j] + localCe.ls))
|
| 936 |
+
|
| 937 |
+
yield from sorted(other, key=lambda t: t[4] + t[1].ls + t[3].ls)
|
| 938 |
+
|
| 939 |
+
def get_edit_paths(
|
| 940 |
+
matched_uv,
|
| 941 |
+
pending_u,
|
| 942 |
+
pending_v,
|
| 943 |
+
Cv,
|
| 944 |
+
matched_gh,
|
| 945 |
+
pending_g,
|
| 946 |
+
pending_h,
|
| 947 |
+
Ce,
|
| 948 |
+
matched_cost,
|
| 949 |
+
):
|
| 950 |
+
"""
|
| 951 |
+
Parameters:
|
| 952 |
+
matched_uv: partial vertex edit path
|
| 953 |
+
list of tuples (u, v) of vertex mappings u<->v,
|
| 954 |
+
u=None or v=None for deletion/insertion
|
| 955 |
+
pending_u, pending_v: lists of vertices not yet mapped
|
| 956 |
+
Cv: CostMatrix of pending vertex mappings
|
| 957 |
+
matched_gh: partial edge edit path
|
| 958 |
+
list of tuples (g, h) of edge mappings g<->h,
|
| 959 |
+
g=None or h=None for deletion/insertion
|
| 960 |
+
pending_g, pending_h: lists of edges not yet mapped
|
| 961 |
+
Ce: CostMatrix of pending edge mappings
|
| 962 |
+
matched_cost: cost of partial edit path
|
| 963 |
+
|
| 964 |
+
Returns:
|
| 965 |
+
sequence of (vertex_path, edge_path, cost)
|
| 966 |
+
vertex_path: complete vertex edit path
|
| 967 |
+
list of tuples (u, v) of vertex mappings u<->v,
|
| 968 |
+
u=None or v=None for deletion/insertion
|
| 969 |
+
edge_path: complete edge edit path
|
| 970 |
+
list of tuples (g, h) of edge mappings g<->h,
|
| 971 |
+
g=None or h=None for deletion/insertion
|
| 972 |
+
cost: total cost of edit path
|
| 973 |
+
NOTE: path costs are non-increasing
|
| 974 |
+
"""
|
| 975 |
+
# debug_print('matched-uv:', matched_uv)
|
| 976 |
+
# debug_print('matched-gh:', matched_gh)
|
| 977 |
+
# debug_print('matched-cost:', matched_cost)
|
| 978 |
+
# debug_print('pending-u:', pending_u)
|
| 979 |
+
# debug_print('pending-v:', pending_v)
|
| 980 |
+
# debug_print(Cv.C)
|
| 981 |
+
# assert list(sorted(G1.nodes)) == list(sorted(list(u for u, v in matched_uv if u is not None) + pending_u))
|
| 982 |
+
# assert list(sorted(G2.nodes)) == list(sorted(list(v for u, v in matched_uv if v is not None) + pending_v))
|
| 983 |
+
# debug_print('pending-g:', pending_g)
|
| 984 |
+
# debug_print('pending-h:', pending_h)
|
| 985 |
+
# debug_print(Ce.C)
|
| 986 |
+
# assert list(sorted(G1.edges)) == list(sorted(list(g for g, h in matched_gh if g is not None) + pending_g))
|
| 987 |
+
# assert list(sorted(G2.edges)) == list(sorted(list(h for g, h in matched_gh if h is not None) + pending_h))
|
| 988 |
+
# debug_print()
|
| 989 |
+
|
| 990 |
+
if prune(matched_cost + Cv.ls + Ce.ls):
|
| 991 |
+
return
|
| 992 |
+
|
| 993 |
+
if not max(len(pending_u), len(pending_v)):
|
| 994 |
+
# assert not len(pending_g)
|
| 995 |
+
# assert not len(pending_h)
|
| 996 |
+
# path completed!
|
| 997 |
+
# assert matched_cost <= maxcost_value
|
| 998 |
+
nonlocal maxcost_value
|
| 999 |
+
maxcost_value = min(maxcost_value, matched_cost)
|
| 1000 |
+
yield matched_uv, matched_gh, matched_cost
|
| 1001 |
+
|
| 1002 |
+
else:
|
| 1003 |
+
edit_ops = get_edit_ops(
|
| 1004 |
+
matched_uv,
|
| 1005 |
+
pending_u,
|
| 1006 |
+
pending_v,
|
| 1007 |
+
Cv,
|
| 1008 |
+
pending_g,
|
| 1009 |
+
pending_h,
|
| 1010 |
+
Ce,
|
| 1011 |
+
matched_cost,
|
| 1012 |
+
)
|
| 1013 |
+
for ij, Cv_ij, xy, Ce_xy, edit_cost in edit_ops:
|
| 1014 |
+
i, j = ij
|
| 1015 |
+
# assert Cv.C[i, j] + sum(Ce.C[t] for t in xy) == edit_cost
|
| 1016 |
+
if prune(matched_cost + edit_cost + Cv_ij.ls + Ce_xy.ls):
|
| 1017 |
+
continue
|
| 1018 |
+
|
| 1019 |
+
# dive deeper
|
| 1020 |
+
u = pending_u.pop(i) if i < len(pending_u) else None
|
| 1021 |
+
v = pending_v.pop(j) if j < len(pending_v) else None
|
| 1022 |
+
matched_uv.append((u, v))
|
| 1023 |
+
for x, y in xy:
|
| 1024 |
+
len_g = len(pending_g)
|
| 1025 |
+
len_h = len(pending_h)
|
| 1026 |
+
matched_gh.append(
|
| 1027 |
+
(
|
| 1028 |
+
pending_g[x] if x < len_g else None,
|
| 1029 |
+
pending_h[y] if y < len_h else None,
|
| 1030 |
+
)
|
| 1031 |
+
)
|
| 1032 |
+
sortedx = sorted(x for x, y in xy)
|
| 1033 |
+
sortedy = sorted(y for x, y in xy)
|
| 1034 |
+
G = [
|
| 1035 |
+
(pending_g.pop(x) if x < len(pending_g) else None)
|
| 1036 |
+
for x in reversed(sortedx)
|
| 1037 |
+
]
|
| 1038 |
+
H = [
|
| 1039 |
+
(pending_h.pop(y) if y < len(pending_h) else None)
|
| 1040 |
+
for y in reversed(sortedy)
|
| 1041 |
+
]
|
| 1042 |
+
|
| 1043 |
+
yield from get_edit_paths(
|
| 1044 |
+
matched_uv,
|
| 1045 |
+
pending_u,
|
| 1046 |
+
pending_v,
|
| 1047 |
+
Cv_ij,
|
| 1048 |
+
matched_gh,
|
| 1049 |
+
pending_g,
|
| 1050 |
+
pending_h,
|
| 1051 |
+
Ce_xy,
|
| 1052 |
+
matched_cost + edit_cost,
|
| 1053 |
+
)
|
| 1054 |
+
|
| 1055 |
+
# backtrack
|
| 1056 |
+
if u is not None:
|
| 1057 |
+
pending_u.insert(i, u)
|
| 1058 |
+
if v is not None:
|
| 1059 |
+
pending_v.insert(j, v)
|
| 1060 |
+
matched_uv.pop()
|
| 1061 |
+
for x, g in zip(sortedx, reversed(G)):
|
| 1062 |
+
if g is not None:
|
| 1063 |
+
pending_g.insert(x, g)
|
| 1064 |
+
for y, h in zip(sortedy, reversed(H)):
|
| 1065 |
+
if h is not None:
|
| 1066 |
+
pending_h.insert(y, h)
|
| 1067 |
+
for _ in xy:
|
| 1068 |
+
matched_gh.pop()
|
| 1069 |
+
|
| 1070 |
+
# Initialization
|
| 1071 |
+
|
| 1072 |
+
pending_u = list(G1.nodes)
|
| 1073 |
+
pending_v = list(G2.nodes)
|
| 1074 |
+
|
| 1075 |
+
initial_cost = 0
|
| 1076 |
+
if roots:
|
| 1077 |
+
root_u, root_v = roots
|
| 1078 |
+
if root_u not in pending_u or root_v not in pending_v:
|
| 1079 |
+
raise nx.NodeNotFound("Root node not in graph.")
|
| 1080 |
+
|
| 1081 |
+
# remove roots from pending
|
| 1082 |
+
pending_u.remove(root_u)
|
| 1083 |
+
pending_v.remove(root_v)
|
| 1084 |
+
|
| 1085 |
+
# cost matrix of vertex mappings
|
| 1086 |
+
m = len(pending_u)
|
| 1087 |
+
n = len(pending_v)
|
| 1088 |
+
C = np.zeros((m + n, m + n))
|
| 1089 |
+
if node_subst_cost:
|
| 1090 |
+
C[0:m, 0:n] = np.array(
|
| 1091 |
+
[
|
| 1092 |
+
node_subst_cost(G1.nodes[u], G2.nodes[v])
|
| 1093 |
+
for u in pending_u
|
| 1094 |
+
for v in pending_v
|
| 1095 |
+
]
|
| 1096 |
+
).reshape(m, n)
|
| 1097 |
+
if roots:
|
| 1098 |
+
initial_cost = node_subst_cost(G1.nodes[root_u], G2.nodes[root_v])
|
| 1099 |
+
elif node_match:
|
| 1100 |
+
C[0:m, 0:n] = np.array(
|
| 1101 |
+
[
|
| 1102 |
+
1 - int(node_match(G1.nodes[u], G2.nodes[v]))
|
| 1103 |
+
for u in pending_u
|
| 1104 |
+
for v in pending_v
|
| 1105 |
+
]
|
| 1106 |
+
).reshape(m, n)
|
| 1107 |
+
if roots:
|
| 1108 |
+
initial_cost = 1 - node_match(G1.nodes[root_u], G2.nodes[root_v])
|
| 1109 |
+
else:
|
| 1110 |
+
# all zeroes
|
| 1111 |
+
pass
|
| 1112 |
+
# assert not min(m, n) or C[0:m, 0:n].min() >= 0
|
| 1113 |
+
if node_del_cost:
|
| 1114 |
+
del_costs = [node_del_cost(G1.nodes[u]) for u in pending_u]
|
| 1115 |
+
else:
|
| 1116 |
+
del_costs = [1] * len(pending_u)
|
| 1117 |
+
# assert not m or min(del_costs) >= 0
|
| 1118 |
+
if node_ins_cost:
|
| 1119 |
+
ins_costs = [node_ins_cost(G2.nodes[v]) for v in pending_v]
|
| 1120 |
+
else:
|
| 1121 |
+
ins_costs = [1] * len(pending_v)
|
| 1122 |
+
# assert not n or min(ins_costs) >= 0
|
| 1123 |
+
inf = C[0:m, 0:n].sum() + sum(del_costs) + sum(ins_costs) + 1
|
| 1124 |
+
C[0:m, n : n + m] = np.array(
|
| 1125 |
+
[del_costs[i] if i == j else inf for i in range(m) for j in range(m)]
|
| 1126 |
+
).reshape(m, m)
|
| 1127 |
+
C[m : m + n, 0:n] = np.array(
|
| 1128 |
+
[ins_costs[i] if i == j else inf for i in range(n) for j in range(n)]
|
| 1129 |
+
).reshape(n, n)
|
| 1130 |
+
Cv = make_CostMatrix(C, m, n)
|
| 1131 |
+
# debug_print(f"Cv: {m} x {n}")
|
| 1132 |
+
# debug_print(Cv.C)
|
| 1133 |
+
|
| 1134 |
+
pending_g = list(G1.edges)
|
| 1135 |
+
pending_h = list(G2.edges)
|
| 1136 |
+
|
| 1137 |
+
# cost matrix of edge mappings
|
| 1138 |
+
m = len(pending_g)
|
| 1139 |
+
n = len(pending_h)
|
| 1140 |
+
C = np.zeros((m + n, m + n))
|
| 1141 |
+
if edge_subst_cost:
|
| 1142 |
+
C[0:m, 0:n] = np.array(
|
| 1143 |
+
[
|
| 1144 |
+
edge_subst_cost(G1.edges[g], G2.edges[h])
|
| 1145 |
+
for g in pending_g
|
| 1146 |
+
for h in pending_h
|
| 1147 |
+
]
|
| 1148 |
+
).reshape(m, n)
|
| 1149 |
+
elif edge_match:
|
| 1150 |
+
C[0:m, 0:n] = np.array(
|
| 1151 |
+
[
|
| 1152 |
+
1 - int(edge_match(G1.edges[g], G2.edges[h]))
|
| 1153 |
+
for g in pending_g
|
| 1154 |
+
for h in pending_h
|
| 1155 |
+
]
|
| 1156 |
+
).reshape(m, n)
|
| 1157 |
+
else:
|
| 1158 |
+
# all zeroes
|
| 1159 |
+
pass
|
| 1160 |
+
# assert not min(m, n) or C[0:m, 0:n].min() >= 0
|
| 1161 |
+
if edge_del_cost:
|
| 1162 |
+
del_costs = [edge_del_cost(G1.edges[g]) for g in pending_g]
|
| 1163 |
+
else:
|
| 1164 |
+
del_costs = [1] * len(pending_g)
|
| 1165 |
+
# assert not m or min(del_costs) >= 0
|
| 1166 |
+
if edge_ins_cost:
|
| 1167 |
+
ins_costs = [edge_ins_cost(G2.edges[h]) for h in pending_h]
|
| 1168 |
+
else:
|
| 1169 |
+
ins_costs = [1] * len(pending_h)
|
| 1170 |
+
# assert not n or min(ins_costs) >= 0
|
| 1171 |
+
inf = C[0:m, 0:n].sum() + sum(del_costs) + sum(ins_costs) + 1
|
| 1172 |
+
C[0:m, n : n + m] = np.array(
|
| 1173 |
+
[del_costs[i] if i == j else inf for i in range(m) for j in range(m)]
|
| 1174 |
+
).reshape(m, m)
|
| 1175 |
+
C[m : m + n, 0:n] = np.array(
|
| 1176 |
+
[ins_costs[i] if i == j else inf for i in range(n) for j in range(n)]
|
| 1177 |
+
).reshape(n, n)
|
| 1178 |
+
Ce = make_CostMatrix(C, m, n)
|
| 1179 |
+
# debug_print(f'Ce: {m} x {n}')
|
| 1180 |
+
# debug_print(Ce.C)
|
| 1181 |
+
# debug_print()
|
| 1182 |
+
|
| 1183 |
+
maxcost_value = Cv.C.sum() + Ce.C.sum() + 1
|
| 1184 |
+
|
| 1185 |
+
if timeout is not None:
|
| 1186 |
+
if timeout <= 0:
|
| 1187 |
+
raise nx.NetworkXError("Timeout value must be greater than 0")
|
| 1188 |
+
start = time.perf_counter()
|
| 1189 |
+
|
| 1190 |
+
def prune(cost):
|
| 1191 |
+
if timeout is not None:
|
| 1192 |
+
if time.perf_counter() - start > timeout:
|
| 1193 |
+
return True
|
| 1194 |
+
if upper_bound is not None:
|
| 1195 |
+
if cost > upper_bound:
|
| 1196 |
+
return True
|
| 1197 |
+
if cost > maxcost_value:
|
| 1198 |
+
return True
|
| 1199 |
+
if strictly_decreasing and cost >= maxcost_value:
|
| 1200 |
+
return True
|
| 1201 |
+
return False
|
| 1202 |
+
|
| 1203 |
+
# Now go!
|
| 1204 |
+
|
| 1205 |
+
done_uv = [] if roots is None else [roots]
|
| 1206 |
+
|
| 1207 |
+
for vertex_path, edge_path, cost in get_edit_paths(
|
| 1208 |
+
done_uv, pending_u, pending_v, Cv, [], pending_g, pending_h, Ce, initial_cost
|
| 1209 |
+
):
|
| 1210 |
+
# assert sorted(G1.nodes) == sorted(u for u, v in vertex_path if u is not None)
|
| 1211 |
+
# assert sorted(G2.nodes) == sorted(v for u, v in vertex_path if v is not None)
|
| 1212 |
+
# assert sorted(G1.edges) == sorted(g for g, h in edge_path if g is not None)
|
| 1213 |
+
# assert sorted(G2.edges) == sorted(h for g, h in edge_path if h is not None)
|
| 1214 |
+
# print(vertex_path, edge_path, cost, file = sys.stderr)
|
| 1215 |
+
# assert cost == maxcost_value
|
| 1216 |
+
yield list(vertex_path), list(edge_path), float(cost)
|
| 1217 |
+
|
| 1218 |
+
|
| 1219 |
+
@nx._dispatchable
|
| 1220 |
+
def simrank_similarity(
|
| 1221 |
+
G,
|
| 1222 |
+
source=None,
|
| 1223 |
+
target=None,
|
| 1224 |
+
importance_factor=0.9,
|
| 1225 |
+
max_iterations=1000,
|
| 1226 |
+
tolerance=1e-4,
|
| 1227 |
+
):
|
| 1228 |
+
"""Returns the SimRank similarity of nodes in the graph ``G``.
|
| 1229 |
+
|
| 1230 |
+
SimRank is a similarity metric that says "two objects are considered
|
| 1231 |
+
to be similar if they are referenced by similar objects." [1]_.
|
| 1232 |
+
|
| 1233 |
+
The pseudo-code definition from the paper is::
|
| 1234 |
+
|
| 1235 |
+
def simrank(G, u, v):
|
| 1236 |
+
in_neighbors_u = G.predecessors(u)
|
| 1237 |
+
in_neighbors_v = G.predecessors(v)
|
| 1238 |
+
scale = C / (len(in_neighbors_u) * len(in_neighbors_v))
|
| 1239 |
+
return scale * sum(
|
| 1240 |
+
simrank(G, w, x) for w, x in product(in_neighbors_u, in_neighbors_v)
|
| 1241 |
+
)
|
| 1242 |
+
|
| 1243 |
+
where ``G`` is the graph, ``u`` is the source, ``v`` is the target,
|
| 1244 |
+
and ``C`` is a float decay or importance factor between 0 and 1.
|
| 1245 |
+
|
| 1246 |
+
The SimRank algorithm for determining node similarity is defined in
|
| 1247 |
+
[2]_.
|
| 1248 |
+
|
| 1249 |
+
Parameters
|
| 1250 |
+
----------
|
| 1251 |
+
G : NetworkX graph
|
| 1252 |
+
A NetworkX graph
|
| 1253 |
+
|
| 1254 |
+
source : node
|
| 1255 |
+
If this is specified, the returned dictionary maps each node
|
| 1256 |
+
``v`` in the graph to the similarity between ``source`` and
|
| 1257 |
+
``v``.
|
| 1258 |
+
|
| 1259 |
+
target : node
|
| 1260 |
+
If both ``source`` and ``target`` are specified, the similarity
|
| 1261 |
+
value between ``source`` and ``target`` is returned. If
|
| 1262 |
+
``target`` is specified but ``source`` is not, this argument is
|
| 1263 |
+
ignored.
|
| 1264 |
+
|
| 1265 |
+
importance_factor : float
|
| 1266 |
+
The relative importance of indirect neighbors with respect to
|
| 1267 |
+
direct neighbors.
|
| 1268 |
+
|
| 1269 |
+
max_iterations : integer
|
| 1270 |
+
Maximum number of iterations.
|
| 1271 |
+
|
| 1272 |
+
tolerance : float
|
| 1273 |
+
Error tolerance used to check convergence. When an iteration of
|
| 1274 |
+
the algorithm finds that no similarity value changes more than
|
| 1275 |
+
this amount, the algorithm halts.
|
| 1276 |
+
|
| 1277 |
+
Returns
|
| 1278 |
+
-------
|
| 1279 |
+
similarity : dictionary or float
|
| 1280 |
+
If ``source`` and ``target`` are both ``None``, this returns a
|
| 1281 |
+
dictionary of dictionaries, where keys are node pairs and value
|
| 1282 |
+
are similarity of the pair of nodes.
|
| 1283 |
+
|
| 1284 |
+
If ``source`` is not ``None`` but ``target`` is, this returns a
|
| 1285 |
+
dictionary mapping node to the similarity of ``source`` and that
|
| 1286 |
+
node.
|
| 1287 |
+
|
| 1288 |
+
If neither ``source`` nor ``target`` is ``None``, this returns
|
| 1289 |
+
the similarity value for the given pair of nodes.
|
| 1290 |
+
|
| 1291 |
+
Raises
|
| 1292 |
+
------
|
| 1293 |
+
ExceededMaxIterations
|
| 1294 |
+
If the algorithm does not converge within ``max_iterations``.
|
| 1295 |
+
|
| 1296 |
+
NodeNotFound
|
| 1297 |
+
If either ``source`` or ``target`` is not in `G`.
|
| 1298 |
+
|
| 1299 |
+
Examples
|
| 1300 |
+
--------
|
| 1301 |
+
>>> G = nx.cycle_graph(2)
|
| 1302 |
+
>>> nx.simrank_similarity(G)
|
| 1303 |
+
{0: {0: 1.0, 1: 0.0}, 1: {0: 0.0, 1: 1.0}}
|
| 1304 |
+
>>> nx.simrank_similarity(G, source=0)
|
| 1305 |
+
{0: 1.0, 1: 0.0}
|
| 1306 |
+
>>> nx.simrank_similarity(G, source=0, target=0)
|
| 1307 |
+
1.0
|
| 1308 |
+
|
| 1309 |
+
The result of this function can be converted to a numpy array
|
| 1310 |
+
representing the SimRank matrix by using the node order of the
|
| 1311 |
+
graph to determine which row and column represent each node.
|
| 1312 |
+
Other ordering of nodes is also possible.
|
| 1313 |
+
|
| 1314 |
+
>>> import numpy as np
|
| 1315 |
+
>>> sim = nx.simrank_similarity(G)
|
| 1316 |
+
>>> np.array([[sim[u][v] for v in G] for u in G])
|
| 1317 |
+
array([[1., 0.],
|
| 1318 |
+
[0., 1.]])
|
| 1319 |
+
>>> sim_1d = nx.simrank_similarity(G, source=0)
|
| 1320 |
+
>>> np.array([sim[0][v] for v in G])
|
| 1321 |
+
array([1., 0.])
|
| 1322 |
+
|
| 1323 |
+
References
|
| 1324 |
+
----------
|
| 1325 |
+
.. [1] https://en.wikipedia.org/wiki/SimRank
|
| 1326 |
+
.. [2] G. Jeh and J. Widom.
|
| 1327 |
+
"SimRank: a measure of structural-context similarity",
|
| 1328 |
+
In KDD'02: Proceedings of the Eighth ACM SIGKDD
|
| 1329 |
+
International Conference on Knowledge Discovery and Data Mining,
|
| 1330 |
+
pp. 538--543. ACM Press, 2002.
|
| 1331 |
+
"""
|
| 1332 |
+
import numpy as np
|
| 1333 |
+
|
| 1334 |
+
nodelist = list(G)
|
| 1335 |
+
if source is not None:
|
| 1336 |
+
if source not in nodelist:
|
| 1337 |
+
raise nx.NodeNotFound(f"Source node {source} not in G")
|
| 1338 |
+
else:
|
| 1339 |
+
s_indx = nodelist.index(source)
|
| 1340 |
+
else:
|
| 1341 |
+
s_indx = None
|
| 1342 |
+
|
| 1343 |
+
if target is not None:
|
| 1344 |
+
if target not in nodelist:
|
| 1345 |
+
raise nx.NodeNotFound(f"Target node {target} not in G")
|
| 1346 |
+
else:
|
| 1347 |
+
t_indx = nodelist.index(target)
|
| 1348 |
+
else:
|
| 1349 |
+
t_indx = None
|
| 1350 |
+
|
| 1351 |
+
x = _simrank_similarity_numpy(
|
| 1352 |
+
G, s_indx, t_indx, importance_factor, max_iterations, tolerance
|
| 1353 |
+
)
|
| 1354 |
+
|
| 1355 |
+
if isinstance(x, np.ndarray):
|
| 1356 |
+
if x.ndim == 1:
|
| 1357 |
+
return dict(zip(G, x.tolist()))
|
| 1358 |
+
# else x.ndim == 2
|
| 1359 |
+
return {u: dict(zip(G, row)) for u, row in zip(G, x.tolist())}
|
| 1360 |
+
return float(x)
|
| 1361 |
+
|
| 1362 |
+
|
| 1363 |
+
def _simrank_similarity_python(
|
| 1364 |
+
G,
|
| 1365 |
+
source=None,
|
| 1366 |
+
target=None,
|
| 1367 |
+
importance_factor=0.9,
|
| 1368 |
+
max_iterations=1000,
|
| 1369 |
+
tolerance=1e-4,
|
| 1370 |
+
):
|
| 1371 |
+
"""Returns the SimRank similarity of nodes in the graph ``G``.
|
| 1372 |
+
|
| 1373 |
+
This pure Python version is provided for pedagogical purposes.
|
| 1374 |
+
|
| 1375 |
+
Examples
|
| 1376 |
+
--------
|
| 1377 |
+
>>> G = nx.cycle_graph(2)
|
| 1378 |
+
>>> nx.similarity._simrank_similarity_python(G)
|
| 1379 |
+
{0: {0: 1, 1: 0.0}, 1: {0: 0.0, 1: 1}}
|
| 1380 |
+
>>> nx.similarity._simrank_similarity_python(G, source=0)
|
| 1381 |
+
{0: 1, 1: 0.0}
|
| 1382 |
+
>>> nx.similarity._simrank_similarity_python(G, source=0, target=0)
|
| 1383 |
+
1
|
| 1384 |
+
"""
|
| 1385 |
+
# build up our similarity adjacency dictionary output
|
| 1386 |
+
newsim = {u: {v: 1 if u == v else 0 for v in G} for u in G}
|
| 1387 |
+
|
| 1388 |
+
# These functions compute the update to the similarity value of the nodes
|
| 1389 |
+
# `u` and `v` with respect to the previous similarity values.
|
| 1390 |
+
def avg_sim(s):
|
| 1391 |
+
return sum(newsim[w][x] for (w, x) in s) / len(s) if s else 0.0
|
| 1392 |
+
|
| 1393 |
+
Gadj = G.pred if G.is_directed() else G.adj
|
| 1394 |
+
|
| 1395 |
+
def sim(u, v):
|
| 1396 |
+
return importance_factor * avg_sim(list(product(Gadj[u], Gadj[v])))
|
| 1397 |
+
|
| 1398 |
+
for its in range(max_iterations):
|
| 1399 |
+
oldsim = newsim
|
| 1400 |
+
newsim = {u: {v: sim(u, v) if u != v else 1 for v in G} for u in G}
|
| 1401 |
+
is_close = all(
|
| 1402 |
+
all(
|
| 1403 |
+
abs(newsim[u][v] - old) <= tolerance * (1 + abs(old))
|
| 1404 |
+
for v, old in nbrs.items()
|
| 1405 |
+
)
|
| 1406 |
+
for u, nbrs in oldsim.items()
|
| 1407 |
+
)
|
| 1408 |
+
if is_close:
|
| 1409 |
+
break
|
| 1410 |
+
|
| 1411 |
+
if its + 1 == max_iterations:
|
| 1412 |
+
raise nx.ExceededMaxIterations(
|
| 1413 |
+
f"simrank did not converge after {max_iterations} iterations."
|
| 1414 |
+
)
|
| 1415 |
+
|
| 1416 |
+
if source is not None and target is not None:
|
| 1417 |
+
return newsim[source][target]
|
| 1418 |
+
if source is not None:
|
| 1419 |
+
return newsim[source]
|
| 1420 |
+
return newsim
|
| 1421 |
+
|
| 1422 |
+
|
| 1423 |
+
def _simrank_similarity_numpy(
|
| 1424 |
+
G,
|
| 1425 |
+
source=None,
|
| 1426 |
+
target=None,
|
| 1427 |
+
importance_factor=0.9,
|
| 1428 |
+
max_iterations=1000,
|
| 1429 |
+
tolerance=1e-4,
|
| 1430 |
+
):
|
| 1431 |
+
"""Calculate SimRank of nodes in ``G`` using matrices with ``numpy``.
|
| 1432 |
+
|
| 1433 |
+
The SimRank algorithm for determining node similarity is defined in
|
| 1434 |
+
[1]_.
|
| 1435 |
+
|
| 1436 |
+
Parameters
|
| 1437 |
+
----------
|
| 1438 |
+
G : NetworkX graph
|
| 1439 |
+
A NetworkX graph
|
| 1440 |
+
|
| 1441 |
+
source : node
|
| 1442 |
+
If this is specified, the returned dictionary maps each node
|
| 1443 |
+
``v`` in the graph to the similarity between ``source`` and
|
| 1444 |
+
``v``.
|
| 1445 |
+
|
| 1446 |
+
target : node
|
| 1447 |
+
If both ``source`` and ``target`` are specified, the similarity
|
| 1448 |
+
value between ``source`` and ``target`` is returned. If
|
| 1449 |
+
``target`` is specified but ``source`` is not, this argument is
|
| 1450 |
+
ignored.
|
| 1451 |
+
|
| 1452 |
+
importance_factor : float
|
| 1453 |
+
The relative importance of indirect neighbors with respect to
|
| 1454 |
+
direct neighbors.
|
| 1455 |
+
|
| 1456 |
+
max_iterations : integer
|
| 1457 |
+
Maximum number of iterations.
|
| 1458 |
+
|
| 1459 |
+
tolerance : float
|
| 1460 |
+
Error tolerance used to check convergence. When an iteration of
|
| 1461 |
+
the algorithm finds that no similarity value changes more than
|
| 1462 |
+
this amount, the algorithm halts.
|
| 1463 |
+
|
| 1464 |
+
Returns
|
| 1465 |
+
-------
|
| 1466 |
+
similarity : numpy array or float
|
| 1467 |
+
If ``source`` and ``target`` are both ``None``, this returns a
|
| 1468 |
+
2D array containing SimRank scores of the nodes.
|
| 1469 |
+
|
| 1470 |
+
If ``source`` is not ``None`` but ``target`` is, this returns an
|
| 1471 |
+
1D array containing SimRank scores of ``source`` and that
|
| 1472 |
+
node.
|
| 1473 |
+
|
| 1474 |
+
If neither ``source`` nor ``target`` is ``None``, this returns
|
| 1475 |
+
the similarity value for the given pair of nodes.
|
| 1476 |
+
|
| 1477 |
+
Examples
|
| 1478 |
+
--------
|
| 1479 |
+
>>> G = nx.cycle_graph(2)
|
| 1480 |
+
>>> nx.similarity._simrank_similarity_numpy(G)
|
| 1481 |
+
array([[1., 0.],
|
| 1482 |
+
[0., 1.]])
|
| 1483 |
+
>>> nx.similarity._simrank_similarity_numpy(G, source=0)
|
| 1484 |
+
array([1., 0.])
|
| 1485 |
+
>>> nx.similarity._simrank_similarity_numpy(G, source=0, target=0)
|
| 1486 |
+
1.0
|
| 1487 |
+
|
| 1488 |
+
References
|
| 1489 |
+
----------
|
| 1490 |
+
.. [1] G. Jeh and J. Widom.
|
| 1491 |
+
"SimRank: a measure of structural-context similarity",
|
| 1492 |
+
In KDD'02: Proceedings of the Eighth ACM SIGKDD
|
| 1493 |
+
International Conference on Knowledge Discovery and Data Mining,
|
| 1494 |
+
pp. 538--543. ACM Press, 2002.
|
| 1495 |
+
"""
|
| 1496 |
+
# This algorithm follows roughly
|
| 1497 |
+
#
|
| 1498 |
+
# S = max{C * (A.T * S * A), I}
|
| 1499 |
+
#
|
| 1500 |
+
# where C is the importance factor, A is the column normalized
|
| 1501 |
+
# adjacency matrix, and I is the identity matrix.
|
| 1502 |
+
import numpy as np
|
| 1503 |
+
|
| 1504 |
+
adjacency_matrix = nx.to_numpy_array(G)
|
| 1505 |
+
|
| 1506 |
+
# column-normalize the ``adjacency_matrix``
|
| 1507 |
+
s = np.array(adjacency_matrix.sum(axis=0))
|
| 1508 |
+
s[s == 0] = 1
|
| 1509 |
+
adjacency_matrix /= s # adjacency_matrix.sum(axis=0)
|
| 1510 |
+
|
| 1511 |
+
newsim = np.eye(len(G), dtype=np.float64)
|
| 1512 |
+
for its in range(max_iterations):
|
| 1513 |
+
prevsim = newsim.copy()
|
| 1514 |
+
newsim = importance_factor * ((adjacency_matrix.T @ prevsim) @ adjacency_matrix)
|
| 1515 |
+
np.fill_diagonal(newsim, 1.0)
|
| 1516 |
+
|
| 1517 |
+
if np.allclose(prevsim, newsim, atol=tolerance):
|
| 1518 |
+
break
|
| 1519 |
+
|
| 1520 |
+
if its + 1 == max_iterations:
|
| 1521 |
+
raise nx.ExceededMaxIterations(
|
| 1522 |
+
f"simrank did not converge after {max_iterations} iterations."
|
| 1523 |
+
)
|
| 1524 |
+
|
| 1525 |
+
if source is not None and target is not None:
|
| 1526 |
+
return float(newsim[source, target])
|
| 1527 |
+
if source is not None:
|
| 1528 |
+
return newsim[source]
|
| 1529 |
+
return newsim
|
| 1530 |
+
|
| 1531 |
+
|
| 1532 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 1533 |
+
def panther_similarity(
|
| 1534 |
+
G, source, k=5, path_length=5, c=0.5, delta=0.1, eps=None, weight="weight"
|
| 1535 |
+
):
|
| 1536 |
+
r"""Returns the Panther similarity of nodes in the graph `G` to node ``v``.
|
| 1537 |
+
|
| 1538 |
+
Panther is a similarity metric that says "two objects are considered
|
| 1539 |
+
to be similar if they frequently appear on the same paths." [1]_.
|
| 1540 |
+
|
| 1541 |
+
Parameters
|
| 1542 |
+
----------
|
| 1543 |
+
G : NetworkX graph
|
| 1544 |
+
A NetworkX graph
|
| 1545 |
+
source : node
|
| 1546 |
+
Source node for which to find the top `k` similar other nodes
|
| 1547 |
+
k : int (default = 5)
|
| 1548 |
+
The number of most similar nodes to return.
|
| 1549 |
+
path_length : int (default = 5)
|
| 1550 |
+
How long the randomly generated paths should be (``T`` in [1]_)
|
| 1551 |
+
c : float (default = 0.5)
|
| 1552 |
+
A universal positive constant used to scale the number
|
| 1553 |
+
of sample random paths to generate.
|
| 1554 |
+
delta : float (default = 0.1)
|
| 1555 |
+
The probability that the similarity $S$ is not an epsilon-approximation to (R, phi),
|
| 1556 |
+
where $R$ is the number of random paths and $\phi$ is the probability
|
| 1557 |
+
that an element sampled from a set $A \subseteq D$, where $D$ is the domain.
|
| 1558 |
+
eps : float or None (default = None)
|
| 1559 |
+
The error bound. Per [1]_, a good value is ``sqrt(1/|E|)``. Therefore,
|
| 1560 |
+
if no value is provided, the recommended computed value will be used.
|
| 1561 |
+
weight : string or None, optional (default="weight")
|
| 1562 |
+
The name of an edge attribute that holds the numerical value
|
| 1563 |
+
used as a weight. If None then each edge has weight 1.
|
| 1564 |
+
|
| 1565 |
+
Returns
|
| 1566 |
+
-------
|
| 1567 |
+
similarity : dictionary
|
| 1568 |
+
Dictionary of nodes to similarity scores (as floats). Note:
|
| 1569 |
+
the self-similarity (i.e., ``v``) will not be included in
|
| 1570 |
+
the returned dictionary. So, for ``k = 5``, a dictionary of
|
| 1571 |
+
top 4 nodes and their similarity scores will be returned.
|
| 1572 |
+
|
| 1573 |
+
Raises
|
| 1574 |
+
------
|
| 1575 |
+
NetworkXUnfeasible
|
| 1576 |
+
If `source` is an isolated node.
|
| 1577 |
+
|
| 1578 |
+
NodeNotFound
|
| 1579 |
+
If `source` is not in `G`.
|
| 1580 |
+
|
| 1581 |
+
Notes
|
| 1582 |
+
-----
|
| 1583 |
+
The isolated nodes in `G` are ignored.
|
| 1584 |
+
|
| 1585 |
+
Examples
|
| 1586 |
+
--------
|
| 1587 |
+
>>> G = nx.star_graph(10)
|
| 1588 |
+
>>> sim = nx.panther_similarity(G, 0)
|
| 1589 |
+
|
| 1590 |
+
References
|
| 1591 |
+
----------
|
| 1592 |
+
.. [1] Zhang, J., Tang, J., Ma, C., Tong, H., Jing, Y., & Li, J.
|
| 1593 |
+
Panther: Fast top-k similarity search on large networks.
|
| 1594 |
+
In Proceedings of the ACM SIGKDD International Conference
|
| 1595 |
+
on Knowledge Discovery and Data Mining (Vol. 2015-August, pp. 1445–1454).
|
| 1596 |
+
Association for Computing Machinery. https://doi.org/10.1145/2783258.2783267.
|
| 1597 |
+
"""
|
| 1598 |
+
import numpy as np
|
| 1599 |
+
|
| 1600 |
+
if source not in G:
|
| 1601 |
+
raise nx.NodeNotFound(f"Source node {source} not in G")
|
| 1602 |
+
|
| 1603 |
+
isolates = set(nx.isolates(G))
|
| 1604 |
+
|
| 1605 |
+
if source in isolates:
|
| 1606 |
+
raise nx.NetworkXUnfeasible(
|
| 1607 |
+
f"Panther similarity is not defined for the isolated source node {source}."
|
| 1608 |
+
)
|
| 1609 |
+
|
| 1610 |
+
G = G.subgraph([node for node in G.nodes if node not in isolates]).copy()
|
| 1611 |
+
|
| 1612 |
+
num_nodes = G.number_of_nodes()
|
| 1613 |
+
if num_nodes < k:
|
| 1614 |
+
warnings.warn(
|
| 1615 |
+
f"Number of nodes is {num_nodes}, but requested k is {k}. "
|
| 1616 |
+
"Setting k to number of nodes."
|
| 1617 |
+
)
|
| 1618 |
+
k = num_nodes
|
| 1619 |
+
# According to [1], they empirically determined
|
| 1620 |
+
# a good value for ``eps`` to be sqrt( 1 / |E| )
|
| 1621 |
+
if eps is None:
|
| 1622 |
+
eps = np.sqrt(1.0 / G.number_of_edges())
|
| 1623 |
+
|
| 1624 |
+
inv_node_map = {name: index for index, name in enumerate(G.nodes)}
|
| 1625 |
+
node_map = np.array(G)
|
| 1626 |
+
|
| 1627 |
+
# Calculate the sample size ``R`` for how many paths
|
| 1628 |
+
# to randomly generate
|
| 1629 |
+
t_choose_2 = math.comb(path_length, 2)
|
| 1630 |
+
sample_size = int((c / eps**2) * (np.log2(t_choose_2) + 1 + np.log(1 / delta)))
|
| 1631 |
+
index_map = {}
|
| 1632 |
+
_ = list(
|
| 1633 |
+
generate_random_paths(
|
| 1634 |
+
G, sample_size, path_length=path_length, index_map=index_map, weight=weight
|
| 1635 |
+
)
|
| 1636 |
+
)
|
| 1637 |
+
S = np.zeros(num_nodes)
|
| 1638 |
+
|
| 1639 |
+
inv_sample_size = 1 / sample_size
|
| 1640 |
+
|
| 1641 |
+
source_paths = set(index_map[source])
|
| 1642 |
+
|
| 1643 |
+
# Calculate the path similarities
|
| 1644 |
+
# between ``source`` (v) and ``node`` (v_j)
|
| 1645 |
+
# using our inverted index mapping of
|
| 1646 |
+
# vertices to paths
|
| 1647 |
+
for node, paths in index_map.items():
|
| 1648 |
+
# Only consider paths where both
|
| 1649 |
+
# ``node`` and ``source`` are present
|
| 1650 |
+
common_paths = source_paths.intersection(paths)
|
| 1651 |
+
S[inv_node_map[node]] = len(common_paths) * inv_sample_size
|
| 1652 |
+
|
| 1653 |
+
# Retrieve top ``k`` similar
|
| 1654 |
+
# Note: the below performed anywhere from 4-10x faster
|
| 1655 |
+
# (depending on input sizes) vs the equivalent ``np.argsort(S)[::-1]``
|
| 1656 |
+
top_k_unsorted = np.argpartition(S, -k)[-k:]
|
| 1657 |
+
top_k_sorted = top_k_unsorted[np.argsort(S[top_k_unsorted])][::-1]
|
| 1658 |
+
|
| 1659 |
+
# Add back the similarity scores
|
| 1660 |
+
top_k_with_val = dict(
|
| 1661 |
+
zip(node_map[top_k_sorted].tolist(), S[top_k_sorted].tolist())
|
| 1662 |
+
)
|
| 1663 |
+
|
| 1664 |
+
# Remove the self-similarity
|
| 1665 |
+
top_k_with_val.pop(source, None)
|
| 1666 |
+
return top_k_with_val
|
| 1667 |
+
|
| 1668 |
+
|
| 1669 |
+
@np_random_state(5)
|
| 1670 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 1671 |
+
def generate_random_paths(
|
| 1672 |
+
G, sample_size, path_length=5, index_map=None, weight="weight", seed=None
|
| 1673 |
+
):
|
| 1674 |
+
"""Randomly generate `sample_size` paths of length `path_length`.
|
| 1675 |
+
|
| 1676 |
+
Parameters
|
| 1677 |
+
----------
|
| 1678 |
+
G : NetworkX graph
|
| 1679 |
+
A NetworkX graph
|
| 1680 |
+
sample_size : integer
|
| 1681 |
+
The number of paths to generate. This is ``R`` in [1]_.
|
| 1682 |
+
path_length : integer (default = 5)
|
| 1683 |
+
The maximum size of the path to randomly generate.
|
| 1684 |
+
This is ``T`` in [1]_. According to the paper, ``T >= 5`` is
|
| 1685 |
+
recommended.
|
| 1686 |
+
index_map : dictionary, optional
|
| 1687 |
+
If provided, this will be populated with the inverted
|
| 1688 |
+
index of nodes mapped to the set of generated random path
|
| 1689 |
+
indices within ``paths``.
|
| 1690 |
+
weight : string or None, optional (default="weight")
|
| 1691 |
+
The name of an edge attribute that holds the numerical value
|
| 1692 |
+
used as a weight. If None then each edge has weight 1.
|
| 1693 |
+
seed : integer, random_state, or None (default)
|
| 1694 |
+
Indicator of random number generation state.
|
| 1695 |
+
See :ref:`Randomness<randomness>`.
|
| 1696 |
+
|
| 1697 |
+
Returns
|
| 1698 |
+
-------
|
| 1699 |
+
paths : generator of lists
|
| 1700 |
+
Generator of `sample_size` paths each with length `path_length`.
|
| 1701 |
+
|
| 1702 |
+
Examples
|
| 1703 |
+
--------
|
| 1704 |
+
Note that the return value is the list of paths:
|
| 1705 |
+
|
| 1706 |
+
>>> G = nx.star_graph(3)
|
| 1707 |
+
>>> random_path = nx.generate_random_paths(G, 2)
|
| 1708 |
+
|
| 1709 |
+
By passing a dictionary into `index_map`, it will build an
|
| 1710 |
+
inverted index mapping of nodes to the paths in which that node is present:
|
| 1711 |
+
|
| 1712 |
+
>>> G = nx.star_graph(3)
|
| 1713 |
+
>>> index_map = {}
|
| 1714 |
+
>>> random_path = nx.generate_random_paths(G, 3, index_map=index_map)
|
| 1715 |
+
>>> paths_containing_node_0 = [
|
| 1716 |
+
... random_path[path_idx] for path_idx in index_map.get(0, [])
|
| 1717 |
+
... ]
|
| 1718 |
+
|
| 1719 |
+
References
|
| 1720 |
+
----------
|
| 1721 |
+
.. [1] Zhang, J., Tang, J., Ma, C., Tong, H., Jing, Y., & Li, J.
|
| 1722 |
+
Panther: Fast top-k similarity search on large networks.
|
| 1723 |
+
In Proceedings of the ACM SIGKDD International Conference
|
| 1724 |
+
on Knowledge Discovery and Data Mining (Vol. 2015-August, pp. 1445–1454).
|
| 1725 |
+
Association for Computing Machinery. https://doi.org/10.1145/2783258.2783267.
|
| 1726 |
+
"""
|
| 1727 |
+
import numpy as np
|
| 1728 |
+
|
| 1729 |
+
randint_fn = (
|
| 1730 |
+
seed.integers if isinstance(seed, np.random.Generator) else seed.randint
|
| 1731 |
+
)
|
| 1732 |
+
|
| 1733 |
+
# Calculate transition probabilities between
|
| 1734 |
+
# every pair of vertices according to Eq. (3)
|
| 1735 |
+
adj_mat = nx.to_numpy_array(G, weight=weight)
|
| 1736 |
+
inv_row_sums = np.reciprocal(adj_mat.sum(axis=1)).reshape(-1, 1)
|
| 1737 |
+
transition_probabilities = adj_mat * inv_row_sums
|
| 1738 |
+
|
| 1739 |
+
node_map = list(G)
|
| 1740 |
+
num_nodes = G.number_of_nodes()
|
| 1741 |
+
|
| 1742 |
+
for path_index in range(sample_size):
|
| 1743 |
+
# Sample current vertex v = v_i uniformly at random
|
| 1744 |
+
node_index = randint_fn(num_nodes)
|
| 1745 |
+
node = node_map[node_index]
|
| 1746 |
+
|
| 1747 |
+
# Add v into p_r and add p_r into the path set
|
| 1748 |
+
# of v, i.e., P_v
|
| 1749 |
+
path = [node]
|
| 1750 |
+
|
| 1751 |
+
# Build the inverted index (P_v) of vertices to paths
|
| 1752 |
+
if index_map is not None:
|
| 1753 |
+
if node in index_map:
|
| 1754 |
+
index_map[node].add(path_index)
|
| 1755 |
+
else:
|
| 1756 |
+
index_map[node] = {path_index}
|
| 1757 |
+
|
| 1758 |
+
starting_index = node_index
|
| 1759 |
+
for _ in range(path_length):
|
| 1760 |
+
# Randomly sample a neighbor (v_j) according
|
| 1761 |
+
# to transition probabilities from ``node`` (v) to its neighbors
|
| 1762 |
+
nbr_index = seed.choice(
|
| 1763 |
+
num_nodes, p=transition_probabilities[starting_index]
|
| 1764 |
+
)
|
| 1765 |
+
|
| 1766 |
+
# Set current vertex (v = v_j)
|
| 1767 |
+
starting_index = nbr_index
|
| 1768 |
+
|
| 1769 |
+
# Add v into p_r
|
| 1770 |
+
nbr_node = node_map[nbr_index]
|
| 1771 |
+
path.append(nbr_node)
|
| 1772 |
+
|
| 1773 |
+
# Add p_r into P_v
|
| 1774 |
+
if index_map is not None:
|
| 1775 |
+
if nbr_node in index_map:
|
| 1776 |
+
index_map[nbr_node].add(path_index)
|
| 1777 |
+
else:
|
| 1778 |
+
index_map[nbr_node] = {path_index}
|
| 1779 |
+
|
| 1780 |
+
yield path
|
.venv/lib/python3.11/site-packages/networkx/algorithms/simple_paths.py
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from heapq import heappop, heappush
|
| 2 |
+
from itertools import count
|
| 3 |
+
|
| 4 |
+
import networkx as nx
|
| 5 |
+
from networkx.algorithms.shortest_paths.weighted import _weight_function
|
| 6 |
+
from networkx.utils import not_implemented_for, pairwise
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"all_simple_paths",
|
| 10 |
+
"is_simple_path",
|
| 11 |
+
"shortest_simple_paths",
|
| 12 |
+
"all_simple_edge_paths",
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@nx._dispatchable
|
| 17 |
+
def is_simple_path(G, nodes):
|
| 18 |
+
"""Returns True if and only if `nodes` form a simple path in `G`.
|
| 19 |
+
|
| 20 |
+
A *simple path* in a graph is a nonempty sequence of nodes in which
|
| 21 |
+
no node appears more than once in the sequence, and each adjacent
|
| 22 |
+
pair of nodes in the sequence is adjacent in the graph.
|
| 23 |
+
|
| 24 |
+
Parameters
|
| 25 |
+
----------
|
| 26 |
+
G : graph
|
| 27 |
+
A NetworkX graph.
|
| 28 |
+
nodes : list
|
| 29 |
+
A list of one or more nodes in the graph `G`.
|
| 30 |
+
|
| 31 |
+
Returns
|
| 32 |
+
-------
|
| 33 |
+
bool
|
| 34 |
+
Whether the given list of nodes represents a simple path in `G`.
|
| 35 |
+
|
| 36 |
+
Notes
|
| 37 |
+
-----
|
| 38 |
+
An empty list of nodes is not a path but a list of one node is a
|
| 39 |
+
path. Here's an explanation why.
|
| 40 |
+
|
| 41 |
+
This function operates on *node paths*. One could also consider
|
| 42 |
+
*edge paths*. There is a bijection between node paths and edge
|
| 43 |
+
paths.
|
| 44 |
+
|
| 45 |
+
The *length of a path* is the number of edges in the path, so a list
|
| 46 |
+
of nodes of length *n* corresponds to a path of length *n* - 1.
|
| 47 |
+
Thus the smallest edge path would be a list of zero edges, the empty
|
| 48 |
+
path. This corresponds to a list of one node.
|
| 49 |
+
|
| 50 |
+
To convert between a node path and an edge path, you can use code
|
| 51 |
+
like the following::
|
| 52 |
+
|
| 53 |
+
>>> from networkx.utils import pairwise
|
| 54 |
+
>>> nodes = [0, 1, 2, 3]
|
| 55 |
+
>>> edges = list(pairwise(nodes))
|
| 56 |
+
>>> edges
|
| 57 |
+
[(0, 1), (1, 2), (2, 3)]
|
| 58 |
+
>>> nodes = [edges[0][0]] + [v for u, v in edges]
|
| 59 |
+
>>> nodes
|
| 60 |
+
[0, 1, 2, 3]
|
| 61 |
+
|
| 62 |
+
Examples
|
| 63 |
+
--------
|
| 64 |
+
>>> G = nx.cycle_graph(4)
|
| 65 |
+
>>> nx.is_simple_path(G, [2, 3, 0])
|
| 66 |
+
True
|
| 67 |
+
>>> nx.is_simple_path(G, [0, 2])
|
| 68 |
+
False
|
| 69 |
+
|
| 70 |
+
"""
|
| 71 |
+
# The empty list is not a valid path. Could also return
|
| 72 |
+
# NetworkXPointlessConcept here.
|
| 73 |
+
if len(nodes) == 0:
|
| 74 |
+
return False
|
| 75 |
+
|
| 76 |
+
# If the list is a single node, just check that the node is actually
|
| 77 |
+
# in the graph.
|
| 78 |
+
if len(nodes) == 1:
|
| 79 |
+
return nodes[0] in G
|
| 80 |
+
|
| 81 |
+
# check that all nodes in the list are in the graph, if at least one
|
| 82 |
+
# is not in the graph, then this is not a simple path
|
| 83 |
+
if not all(n in G for n in nodes):
|
| 84 |
+
return False
|
| 85 |
+
|
| 86 |
+
# If the list contains repeated nodes, then it's not a simple path
|
| 87 |
+
if len(set(nodes)) != len(nodes):
|
| 88 |
+
return False
|
| 89 |
+
|
| 90 |
+
# Test that each adjacent pair of nodes is adjacent.
|
| 91 |
+
return all(v in G[u] for u, v in pairwise(nodes))
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@nx._dispatchable
|
| 95 |
+
def all_simple_paths(G, source, target, cutoff=None):
|
| 96 |
+
"""Generate all simple paths in the graph G from source to target.
|
| 97 |
+
|
| 98 |
+
A simple path is a path with no repeated nodes.
|
| 99 |
+
|
| 100 |
+
Parameters
|
| 101 |
+
----------
|
| 102 |
+
G : NetworkX graph
|
| 103 |
+
|
| 104 |
+
source : node
|
| 105 |
+
Starting node for path
|
| 106 |
+
|
| 107 |
+
target : nodes
|
| 108 |
+
Single node or iterable of nodes at which to end path
|
| 109 |
+
|
| 110 |
+
cutoff : integer, optional
|
| 111 |
+
Depth to stop the search. Only paths of length <= cutoff are returned.
|
| 112 |
+
|
| 113 |
+
Returns
|
| 114 |
+
-------
|
| 115 |
+
path_generator: generator
|
| 116 |
+
A generator that produces lists of simple paths. If there are no paths
|
| 117 |
+
between the source and target within the given cutoff the generator
|
| 118 |
+
produces no output. If it is possible to traverse the same sequence of
|
| 119 |
+
nodes in multiple ways, namely through parallel edges, then it will be
|
| 120 |
+
returned multiple times (once for each viable edge combination).
|
| 121 |
+
|
| 122 |
+
Examples
|
| 123 |
+
--------
|
| 124 |
+
This iterator generates lists of nodes::
|
| 125 |
+
|
| 126 |
+
>>> G = nx.complete_graph(4)
|
| 127 |
+
>>> for path in nx.all_simple_paths(G, source=0, target=3):
|
| 128 |
+
... print(path)
|
| 129 |
+
...
|
| 130 |
+
[0, 1, 2, 3]
|
| 131 |
+
[0, 1, 3]
|
| 132 |
+
[0, 2, 1, 3]
|
| 133 |
+
[0, 2, 3]
|
| 134 |
+
[0, 3]
|
| 135 |
+
|
| 136 |
+
You can generate only those paths that are shorter than a certain
|
| 137 |
+
length by using the `cutoff` keyword argument::
|
| 138 |
+
|
| 139 |
+
>>> paths = nx.all_simple_paths(G, source=0, target=3, cutoff=2)
|
| 140 |
+
>>> print(list(paths))
|
| 141 |
+
[[0, 1, 3], [0, 2, 3], [0, 3]]
|
| 142 |
+
|
| 143 |
+
To get each path as the corresponding list of edges, you can use the
|
| 144 |
+
:func:`networkx.utils.pairwise` helper function::
|
| 145 |
+
|
| 146 |
+
>>> paths = nx.all_simple_paths(G, source=0, target=3)
|
| 147 |
+
>>> for path in map(nx.utils.pairwise, paths):
|
| 148 |
+
... print(list(path))
|
| 149 |
+
[(0, 1), (1, 2), (2, 3)]
|
| 150 |
+
[(0, 1), (1, 3)]
|
| 151 |
+
[(0, 2), (2, 1), (1, 3)]
|
| 152 |
+
[(0, 2), (2, 3)]
|
| 153 |
+
[(0, 3)]
|
| 154 |
+
|
| 155 |
+
Pass an iterable of nodes as target to generate all paths ending in any of several nodes::
|
| 156 |
+
|
| 157 |
+
>>> G = nx.complete_graph(4)
|
| 158 |
+
>>> for path in nx.all_simple_paths(G, source=0, target=[3, 2]):
|
| 159 |
+
... print(path)
|
| 160 |
+
...
|
| 161 |
+
[0, 1, 2]
|
| 162 |
+
[0, 1, 2, 3]
|
| 163 |
+
[0, 1, 3]
|
| 164 |
+
[0, 1, 3, 2]
|
| 165 |
+
[0, 2]
|
| 166 |
+
[0, 2, 1, 3]
|
| 167 |
+
[0, 2, 3]
|
| 168 |
+
[0, 3]
|
| 169 |
+
[0, 3, 1, 2]
|
| 170 |
+
[0, 3, 2]
|
| 171 |
+
|
| 172 |
+
The singleton path from ``source`` to itself is considered a simple path and is
|
| 173 |
+
included in the results:
|
| 174 |
+
|
| 175 |
+
>>> G = nx.empty_graph(5)
|
| 176 |
+
>>> list(nx.all_simple_paths(G, source=0, target=0))
|
| 177 |
+
[[0]]
|
| 178 |
+
|
| 179 |
+
>>> G = nx.path_graph(3)
|
| 180 |
+
>>> list(nx.all_simple_paths(G, source=0, target={0, 1, 2}))
|
| 181 |
+
[[0], [0, 1], [0, 1, 2]]
|
| 182 |
+
|
| 183 |
+
Iterate over each path from the root nodes to the leaf nodes in a
|
| 184 |
+
directed acyclic graph using a functional programming approach::
|
| 185 |
+
|
| 186 |
+
>>> from itertools import chain
|
| 187 |
+
>>> from itertools import product
|
| 188 |
+
>>> from itertools import starmap
|
| 189 |
+
>>> from functools import partial
|
| 190 |
+
>>>
|
| 191 |
+
>>> chaini = chain.from_iterable
|
| 192 |
+
>>>
|
| 193 |
+
>>> G = nx.DiGraph([(0, 1), (1, 2), (0, 3), (3, 2)])
|
| 194 |
+
>>> roots = (v for v, d in G.in_degree() if d == 0)
|
| 195 |
+
>>> leaves = (v for v, d in G.out_degree() if d == 0)
|
| 196 |
+
>>> all_paths = partial(nx.all_simple_paths, G)
|
| 197 |
+
>>> list(chaini(starmap(all_paths, product(roots, leaves))))
|
| 198 |
+
[[0, 1, 2], [0, 3, 2]]
|
| 199 |
+
|
| 200 |
+
The same list computed using an iterative approach::
|
| 201 |
+
|
| 202 |
+
>>> G = nx.DiGraph([(0, 1), (1, 2), (0, 3), (3, 2)])
|
| 203 |
+
>>> roots = (v for v, d in G.in_degree() if d == 0)
|
| 204 |
+
>>> leaves = (v for v, d in G.out_degree() if d == 0)
|
| 205 |
+
>>> all_paths = []
|
| 206 |
+
>>> for root in roots:
|
| 207 |
+
... for leaf in leaves:
|
| 208 |
+
... paths = nx.all_simple_paths(G, root, leaf)
|
| 209 |
+
... all_paths.extend(paths)
|
| 210 |
+
>>> all_paths
|
| 211 |
+
[[0, 1, 2], [0, 3, 2]]
|
| 212 |
+
|
| 213 |
+
Iterate over each path from the root nodes to the leaf nodes in a
|
| 214 |
+
directed acyclic graph passing all leaves together to avoid unnecessary
|
| 215 |
+
compute::
|
| 216 |
+
|
| 217 |
+
>>> G = nx.DiGraph([(0, 1), (2, 1), (1, 3), (1, 4)])
|
| 218 |
+
>>> roots = (v for v, d in G.in_degree() if d == 0)
|
| 219 |
+
>>> leaves = [v for v, d in G.out_degree() if d == 0]
|
| 220 |
+
>>> all_paths = []
|
| 221 |
+
>>> for root in roots:
|
| 222 |
+
... paths = nx.all_simple_paths(G, root, leaves)
|
| 223 |
+
... all_paths.extend(paths)
|
| 224 |
+
>>> all_paths
|
| 225 |
+
[[0, 1, 3], [0, 1, 4], [2, 1, 3], [2, 1, 4]]
|
| 226 |
+
|
| 227 |
+
If parallel edges offer multiple ways to traverse a given sequence of
|
| 228 |
+
nodes, this sequence of nodes will be returned multiple times:
|
| 229 |
+
|
| 230 |
+
>>> G = nx.MultiDiGraph([(0, 1), (0, 1), (1, 2)])
|
| 231 |
+
>>> list(nx.all_simple_paths(G, 0, 2))
|
| 232 |
+
[[0, 1, 2], [0, 1, 2]]
|
| 233 |
+
|
| 234 |
+
Notes
|
| 235 |
+
-----
|
| 236 |
+
This algorithm uses a modified depth-first search to generate the
|
| 237 |
+
paths [1]_. A single path can be found in $O(V+E)$ time but the
|
| 238 |
+
number of simple paths in a graph can be very large, e.g. $O(n!)$ in
|
| 239 |
+
the complete graph of order $n$.
|
| 240 |
+
|
| 241 |
+
This function does not check that a path exists between `source` and
|
| 242 |
+
`target`. For large graphs, this may result in very long runtimes.
|
| 243 |
+
Consider using `has_path` to check that a path exists between `source` and
|
| 244 |
+
`target` before calling this function on large graphs.
|
| 245 |
+
|
| 246 |
+
References
|
| 247 |
+
----------
|
| 248 |
+
.. [1] R. Sedgewick, "Algorithms in C, Part 5: Graph Algorithms",
|
| 249 |
+
Addison Wesley Professional, 3rd ed., 2001.
|
| 250 |
+
|
| 251 |
+
See Also
|
| 252 |
+
--------
|
| 253 |
+
all_shortest_paths, shortest_path, has_path
|
| 254 |
+
|
| 255 |
+
"""
|
| 256 |
+
for edge_path in all_simple_edge_paths(G, source, target, cutoff):
|
| 257 |
+
yield [source] + [edge[1] for edge in edge_path]
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
@nx._dispatchable
|
| 261 |
+
def all_simple_edge_paths(G, source, target, cutoff=None):
|
| 262 |
+
"""Generate lists of edges for all simple paths in G from source to target.
|
| 263 |
+
|
| 264 |
+
A simple path is a path with no repeated nodes.
|
| 265 |
+
|
| 266 |
+
Parameters
|
| 267 |
+
----------
|
| 268 |
+
G : NetworkX graph
|
| 269 |
+
|
| 270 |
+
source : node
|
| 271 |
+
Starting node for path
|
| 272 |
+
|
| 273 |
+
target : nodes
|
| 274 |
+
Single node or iterable of nodes at which to end path
|
| 275 |
+
|
| 276 |
+
cutoff : integer, optional
|
| 277 |
+
Depth to stop the search. Only paths of length <= cutoff are returned.
|
| 278 |
+
|
| 279 |
+
Returns
|
| 280 |
+
-------
|
| 281 |
+
path_generator: generator
|
| 282 |
+
A generator that produces lists of simple paths. If there are no paths
|
| 283 |
+
between the source and target within the given cutoff the generator
|
| 284 |
+
produces no output.
|
| 285 |
+
For multigraphs, the list of edges have elements of the form `(u,v,k)`.
|
| 286 |
+
Where `k` corresponds to the edge key.
|
| 287 |
+
|
| 288 |
+
Examples
|
| 289 |
+
--------
|
| 290 |
+
|
| 291 |
+
Print the simple path edges of a Graph::
|
| 292 |
+
|
| 293 |
+
>>> g = nx.Graph([(1, 2), (2, 4), (1, 3), (3, 4)])
|
| 294 |
+
>>> for path in sorted(nx.all_simple_edge_paths(g, 1, 4)):
|
| 295 |
+
... print(path)
|
| 296 |
+
[(1, 2), (2, 4)]
|
| 297 |
+
[(1, 3), (3, 4)]
|
| 298 |
+
|
| 299 |
+
Print the simple path edges of a MultiGraph. Returned edges come with
|
| 300 |
+
their associated keys::
|
| 301 |
+
|
| 302 |
+
>>> mg = nx.MultiGraph()
|
| 303 |
+
>>> mg.add_edge(1, 2, key="k0")
|
| 304 |
+
'k0'
|
| 305 |
+
>>> mg.add_edge(1, 2, key="k1")
|
| 306 |
+
'k1'
|
| 307 |
+
>>> mg.add_edge(2, 3, key="k0")
|
| 308 |
+
'k0'
|
| 309 |
+
>>> for path in sorted(nx.all_simple_edge_paths(mg, 1, 3)):
|
| 310 |
+
... print(path)
|
| 311 |
+
[(1, 2, 'k0'), (2, 3, 'k0')]
|
| 312 |
+
[(1, 2, 'k1'), (2, 3, 'k0')]
|
| 313 |
+
|
| 314 |
+
When ``source`` is one of the targets, the empty path starting and ending at
|
| 315 |
+
``source`` without traversing any edge is considered a valid simple edge path
|
| 316 |
+
and is included in the results:
|
| 317 |
+
|
| 318 |
+
>>> G = nx.Graph()
|
| 319 |
+
>>> G.add_node(0)
|
| 320 |
+
>>> paths = list(nx.all_simple_edge_paths(G, 0, 0))
|
| 321 |
+
>>> for path in paths:
|
| 322 |
+
... print(path)
|
| 323 |
+
[]
|
| 324 |
+
>>> len(paths)
|
| 325 |
+
1
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
Notes
|
| 329 |
+
-----
|
| 330 |
+
This algorithm uses a modified depth-first search to generate the
|
| 331 |
+
paths [1]_. A single path can be found in $O(V+E)$ time but the
|
| 332 |
+
number of simple paths in a graph can be very large, e.g. $O(n!)$ in
|
| 333 |
+
the complete graph of order $n$.
|
| 334 |
+
|
| 335 |
+
References
|
| 336 |
+
----------
|
| 337 |
+
.. [1] R. Sedgewick, "Algorithms in C, Part 5: Graph Algorithms",
|
| 338 |
+
Addison Wesley Professional, 3rd ed., 2001.
|
| 339 |
+
|
| 340 |
+
See Also
|
| 341 |
+
--------
|
| 342 |
+
all_shortest_paths, shortest_path, all_simple_paths
|
| 343 |
+
|
| 344 |
+
"""
|
| 345 |
+
if source not in G:
|
| 346 |
+
raise nx.NodeNotFound(f"source node {source} not in graph")
|
| 347 |
+
|
| 348 |
+
if target in G:
|
| 349 |
+
targets = {target}
|
| 350 |
+
else:
|
| 351 |
+
try:
|
| 352 |
+
targets = set(target)
|
| 353 |
+
except TypeError as err:
|
| 354 |
+
raise nx.NodeNotFound(f"target node {target} not in graph") from err
|
| 355 |
+
|
| 356 |
+
cutoff = cutoff if cutoff is not None else len(G) - 1
|
| 357 |
+
|
| 358 |
+
if cutoff >= 0 and targets:
|
| 359 |
+
yield from _all_simple_edge_paths(G, source, targets, cutoff)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def _all_simple_edge_paths(G, source, targets, cutoff):
|
| 363 |
+
# We simulate recursion with a stack, keeping the current path being explored
|
| 364 |
+
# and the outgoing edge iterators at each point in the stack.
|
| 365 |
+
# To avoid unnecessary checks, the loop is structured in a way such that a path
|
| 366 |
+
# is considered for yielding only after a new node/edge is added.
|
| 367 |
+
# We bootstrap the search by adding a dummy iterator to the stack that only yields
|
| 368 |
+
# a dummy edge to source (so that the trivial path has a chance of being included).
|
| 369 |
+
|
| 370 |
+
get_edges = (
|
| 371 |
+
(lambda node: G.edges(node, keys=True))
|
| 372 |
+
if G.is_multigraph()
|
| 373 |
+
else (lambda node: G.edges(node))
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
# The current_path is a dictionary that maps nodes in the path to the edge that was
|
| 377 |
+
# used to enter that node (instead of a list of edges) because we want both a fast
|
| 378 |
+
# membership test for nodes in the path and the preservation of insertion order.
|
| 379 |
+
current_path = {None: None}
|
| 380 |
+
stack = [iter([(None, source)])]
|
| 381 |
+
|
| 382 |
+
while stack:
|
| 383 |
+
# 1. Try to extend the current path.
|
| 384 |
+
next_edge = next((e for e in stack[-1] if e[1] not in current_path), None)
|
| 385 |
+
if next_edge is None:
|
| 386 |
+
# All edges of the last node in the current path have been explored.
|
| 387 |
+
stack.pop()
|
| 388 |
+
current_path.popitem()
|
| 389 |
+
continue
|
| 390 |
+
previous_node, next_node, *_ = next_edge
|
| 391 |
+
|
| 392 |
+
# 2. Check if we've reached a target.
|
| 393 |
+
if next_node in targets:
|
| 394 |
+
yield (list(current_path.values()) + [next_edge])[2:] # remove dummy edge
|
| 395 |
+
|
| 396 |
+
# 3. Only expand the search through the next node if it makes sense.
|
| 397 |
+
if len(current_path) - 1 < cutoff and (
|
| 398 |
+
targets - current_path.keys() - {next_node}
|
| 399 |
+
):
|
| 400 |
+
current_path[next_node] = next_edge
|
| 401 |
+
stack.append(iter(get_edges(next_node)))
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
@not_implemented_for("multigraph")
|
| 405 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 406 |
+
def shortest_simple_paths(G, source, target, weight=None):
|
| 407 |
+
"""Generate all simple paths in the graph G from source to target,
|
| 408 |
+
starting from shortest ones.
|
| 409 |
+
|
| 410 |
+
A simple path is a path with no repeated nodes.
|
| 411 |
+
|
| 412 |
+
If a weighted shortest path search is to be used, no negative weights
|
| 413 |
+
are allowed.
|
| 414 |
+
|
| 415 |
+
Parameters
|
| 416 |
+
----------
|
| 417 |
+
G : NetworkX graph
|
| 418 |
+
|
| 419 |
+
source : node
|
| 420 |
+
Starting node for path
|
| 421 |
+
|
| 422 |
+
target : node
|
| 423 |
+
Ending node for path
|
| 424 |
+
|
| 425 |
+
weight : string or function
|
| 426 |
+
If it is a string, it is the name of the edge attribute to be
|
| 427 |
+
used as a weight.
|
| 428 |
+
|
| 429 |
+
If it is a function, the weight of an edge is the value returned
|
| 430 |
+
by the function. The function must accept exactly three positional
|
| 431 |
+
arguments: the two endpoints of an edge and the dictionary of edge
|
| 432 |
+
attributes for that edge. The function must return a number or None.
|
| 433 |
+
The weight function can be used to hide edges by returning None.
|
| 434 |
+
So ``weight = lambda u, v, d: 1 if d['color']=="red" else None``
|
| 435 |
+
will find the shortest red path.
|
| 436 |
+
|
| 437 |
+
If None all edges are considered to have unit weight. Default
|
| 438 |
+
value None.
|
| 439 |
+
|
| 440 |
+
Returns
|
| 441 |
+
-------
|
| 442 |
+
path_generator: generator
|
| 443 |
+
A generator that produces lists of simple paths, in order from
|
| 444 |
+
shortest to longest.
|
| 445 |
+
|
| 446 |
+
Raises
|
| 447 |
+
------
|
| 448 |
+
NetworkXNoPath
|
| 449 |
+
If no path exists between source and target.
|
| 450 |
+
|
| 451 |
+
NetworkXError
|
| 452 |
+
If source or target nodes are not in the input graph.
|
| 453 |
+
|
| 454 |
+
NetworkXNotImplemented
|
| 455 |
+
If the input graph is a Multi[Di]Graph.
|
| 456 |
+
|
| 457 |
+
Examples
|
| 458 |
+
--------
|
| 459 |
+
|
| 460 |
+
>>> G = nx.cycle_graph(7)
|
| 461 |
+
>>> paths = list(nx.shortest_simple_paths(G, 0, 3))
|
| 462 |
+
>>> print(paths)
|
| 463 |
+
[[0, 1, 2, 3], [0, 6, 5, 4, 3]]
|
| 464 |
+
|
| 465 |
+
You can use this function to efficiently compute the k shortest/best
|
| 466 |
+
paths between two nodes.
|
| 467 |
+
|
| 468 |
+
>>> from itertools import islice
|
| 469 |
+
>>> def k_shortest_paths(G, source, target, k, weight=None):
|
| 470 |
+
... return list(
|
| 471 |
+
... islice(nx.shortest_simple_paths(G, source, target, weight=weight), k)
|
| 472 |
+
... )
|
| 473 |
+
>>> for path in k_shortest_paths(G, 0, 3, 2):
|
| 474 |
+
... print(path)
|
| 475 |
+
[0, 1, 2, 3]
|
| 476 |
+
[0, 6, 5, 4, 3]
|
| 477 |
+
|
| 478 |
+
Notes
|
| 479 |
+
-----
|
| 480 |
+
This procedure is based on algorithm by Jin Y. Yen [1]_. Finding
|
| 481 |
+
the first $K$ paths requires $O(KN^3)$ operations.
|
| 482 |
+
|
| 483 |
+
See Also
|
| 484 |
+
--------
|
| 485 |
+
all_shortest_paths
|
| 486 |
+
shortest_path
|
| 487 |
+
all_simple_paths
|
| 488 |
+
|
| 489 |
+
References
|
| 490 |
+
----------
|
| 491 |
+
.. [1] Jin Y. Yen, "Finding the K Shortest Loopless Paths in a
|
| 492 |
+
Network", Management Science, Vol. 17, No. 11, Theory Series
|
| 493 |
+
(Jul., 1971), pp. 712-716.
|
| 494 |
+
|
| 495 |
+
"""
|
| 496 |
+
if source not in G:
|
| 497 |
+
raise nx.NodeNotFound(f"source node {source} not in graph")
|
| 498 |
+
|
| 499 |
+
if target not in G:
|
| 500 |
+
raise nx.NodeNotFound(f"target node {target} not in graph")
|
| 501 |
+
|
| 502 |
+
if weight is None:
|
| 503 |
+
length_func = len
|
| 504 |
+
shortest_path_func = _bidirectional_shortest_path
|
| 505 |
+
else:
|
| 506 |
+
wt = _weight_function(G, weight)
|
| 507 |
+
|
| 508 |
+
def length_func(path):
|
| 509 |
+
return sum(
|
| 510 |
+
wt(u, v, G.get_edge_data(u, v)) for (u, v) in zip(path, path[1:])
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
shortest_path_func = _bidirectional_dijkstra
|
| 514 |
+
|
| 515 |
+
listA = []
|
| 516 |
+
listB = PathBuffer()
|
| 517 |
+
prev_path = None
|
| 518 |
+
while True:
|
| 519 |
+
if not prev_path:
|
| 520 |
+
length, path = shortest_path_func(G, source, target, weight=weight)
|
| 521 |
+
listB.push(length, path)
|
| 522 |
+
else:
|
| 523 |
+
ignore_nodes = set()
|
| 524 |
+
ignore_edges = set()
|
| 525 |
+
for i in range(1, len(prev_path)):
|
| 526 |
+
root = prev_path[:i]
|
| 527 |
+
root_length = length_func(root)
|
| 528 |
+
for path in listA:
|
| 529 |
+
if path[:i] == root:
|
| 530 |
+
ignore_edges.add((path[i - 1], path[i]))
|
| 531 |
+
try:
|
| 532 |
+
length, spur = shortest_path_func(
|
| 533 |
+
G,
|
| 534 |
+
root[-1],
|
| 535 |
+
target,
|
| 536 |
+
ignore_nodes=ignore_nodes,
|
| 537 |
+
ignore_edges=ignore_edges,
|
| 538 |
+
weight=weight,
|
| 539 |
+
)
|
| 540 |
+
path = root[:-1] + spur
|
| 541 |
+
listB.push(root_length + length, path)
|
| 542 |
+
except nx.NetworkXNoPath:
|
| 543 |
+
pass
|
| 544 |
+
ignore_nodes.add(root[-1])
|
| 545 |
+
|
| 546 |
+
if listB:
|
| 547 |
+
path = listB.pop()
|
| 548 |
+
yield path
|
| 549 |
+
listA.append(path)
|
| 550 |
+
prev_path = path
|
| 551 |
+
else:
|
| 552 |
+
break
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
class PathBuffer:
|
| 556 |
+
def __init__(self):
|
| 557 |
+
self.paths = set()
|
| 558 |
+
self.sortedpaths = []
|
| 559 |
+
self.counter = count()
|
| 560 |
+
|
| 561 |
+
def __len__(self):
|
| 562 |
+
return len(self.sortedpaths)
|
| 563 |
+
|
| 564 |
+
def push(self, cost, path):
|
| 565 |
+
hashable_path = tuple(path)
|
| 566 |
+
if hashable_path not in self.paths:
|
| 567 |
+
heappush(self.sortedpaths, (cost, next(self.counter), path))
|
| 568 |
+
self.paths.add(hashable_path)
|
| 569 |
+
|
| 570 |
+
def pop(self):
|
| 571 |
+
(cost, num, path) = heappop(self.sortedpaths)
|
| 572 |
+
hashable_path = tuple(path)
|
| 573 |
+
self.paths.remove(hashable_path)
|
| 574 |
+
return path
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
def _bidirectional_shortest_path(
|
| 578 |
+
G, source, target, ignore_nodes=None, ignore_edges=None, weight=None
|
| 579 |
+
):
|
| 580 |
+
"""Returns the shortest path between source and target ignoring
|
| 581 |
+
nodes and edges in the containers ignore_nodes and ignore_edges.
|
| 582 |
+
|
| 583 |
+
This is a custom modification of the standard bidirectional shortest
|
| 584 |
+
path implementation at networkx.algorithms.unweighted
|
| 585 |
+
|
| 586 |
+
Parameters
|
| 587 |
+
----------
|
| 588 |
+
G : NetworkX graph
|
| 589 |
+
|
| 590 |
+
source : node
|
| 591 |
+
starting node for path
|
| 592 |
+
|
| 593 |
+
target : node
|
| 594 |
+
ending node for path
|
| 595 |
+
|
| 596 |
+
ignore_nodes : container of nodes
|
| 597 |
+
nodes to ignore, optional
|
| 598 |
+
|
| 599 |
+
ignore_edges : container of edges
|
| 600 |
+
edges to ignore, optional
|
| 601 |
+
|
| 602 |
+
weight : None
|
| 603 |
+
This function accepts a weight argument for convenience of
|
| 604 |
+
shortest_simple_paths function. It will be ignored.
|
| 605 |
+
|
| 606 |
+
Returns
|
| 607 |
+
-------
|
| 608 |
+
path: list
|
| 609 |
+
List of nodes in a path from source to target.
|
| 610 |
+
|
| 611 |
+
Raises
|
| 612 |
+
------
|
| 613 |
+
NetworkXNoPath
|
| 614 |
+
If no path exists between source and target.
|
| 615 |
+
|
| 616 |
+
See Also
|
| 617 |
+
--------
|
| 618 |
+
shortest_path
|
| 619 |
+
|
| 620 |
+
"""
|
| 621 |
+
# call helper to do the real work
|
| 622 |
+
results = _bidirectional_pred_succ(G, source, target, ignore_nodes, ignore_edges)
|
| 623 |
+
pred, succ, w = results
|
| 624 |
+
|
| 625 |
+
# build path from pred+w+succ
|
| 626 |
+
path = []
|
| 627 |
+
# from w to target
|
| 628 |
+
while w is not None:
|
| 629 |
+
path.append(w)
|
| 630 |
+
w = succ[w]
|
| 631 |
+
# from source to w
|
| 632 |
+
w = pred[path[0]]
|
| 633 |
+
while w is not None:
|
| 634 |
+
path.insert(0, w)
|
| 635 |
+
w = pred[w]
|
| 636 |
+
|
| 637 |
+
return len(path), path
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
def _bidirectional_pred_succ(G, source, target, ignore_nodes=None, ignore_edges=None):
|
| 641 |
+
"""Bidirectional shortest path helper.
|
| 642 |
+
Returns (pred,succ,w) where
|
| 643 |
+
pred is a dictionary of predecessors from w to the source, and
|
| 644 |
+
succ is a dictionary of successors from w to the target.
|
| 645 |
+
"""
|
| 646 |
+
# does BFS from both source and target and meets in the middle
|
| 647 |
+
if ignore_nodes and (source in ignore_nodes or target in ignore_nodes):
|
| 648 |
+
raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
|
| 649 |
+
if target == source:
|
| 650 |
+
return ({target: None}, {source: None}, source)
|
| 651 |
+
|
| 652 |
+
# handle either directed or undirected
|
| 653 |
+
if G.is_directed():
|
| 654 |
+
Gpred = G.predecessors
|
| 655 |
+
Gsucc = G.successors
|
| 656 |
+
else:
|
| 657 |
+
Gpred = G.neighbors
|
| 658 |
+
Gsucc = G.neighbors
|
| 659 |
+
|
| 660 |
+
# support optional nodes filter
|
| 661 |
+
if ignore_nodes:
|
| 662 |
+
|
| 663 |
+
def filter_iter(nodes):
|
| 664 |
+
def iterate(v):
|
| 665 |
+
for w in nodes(v):
|
| 666 |
+
if w not in ignore_nodes:
|
| 667 |
+
yield w
|
| 668 |
+
|
| 669 |
+
return iterate
|
| 670 |
+
|
| 671 |
+
Gpred = filter_iter(Gpred)
|
| 672 |
+
Gsucc = filter_iter(Gsucc)
|
| 673 |
+
|
| 674 |
+
# support optional edges filter
|
| 675 |
+
if ignore_edges:
|
| 676 |
+
if G.is_directed():
|
| 677 |
+
|
| 678 |
+
def filter_pred_iter(pred_iter):
|
| 679 |
+
def iterate(v):
|
| 680 |
+
for w in pred_iter(v):
|
| 681 |
+
if (w, v) not in ignore_edges:
|
| 682 |
+
yield w
|
| 683 |
+
|
| 684 |
+
return iterate
|
| 685 |
+
|
| 686 |
+
def filter_succ_iter(succ_iter):
|
| 687 |
+
def iterate(v):
|
| 688 |
+
for w in succ_iter(v):
|
| 689 |
+
if (v, w) not in ignore_edges:
|
| 690 |
+
yield w
|
| 691 |
+
|
| 692 |
+
return iterate
|
| 693 |
+
|
| 694 |
+
Gpred = filter_pred_iter(Gpred)
|
| 695 |
+
Gsucc = filter_succ_iter(Gsucc)
|
| 696 |
+
|
| 697 |
+
else:
|
| 698 |
+
|
| 699 |
+
def filter_iter(nodes):
|
| 700 |
+
def iterate(v):
|
| 701 |
+
for w in nodes(v):
|
| 702 |
+
if (v, w) not in ignore_edges and (w, v) not in ignore_edges:
|
| 703 |
+
yield w
|
| 704 |
+
|
| 705 |
+
return iterate
|
| 706 |
+
|
| 707 |
+
Gpred = filter_iter(Gpred)
|
| 708 |
+
Gsucc = filter_iter(Gsucc)
|
| 709 |
+
|
| 710 |
+
# predecessor and successors in search
|
| 711 |
+
pred = {source: None}
|
| 712 |
+
succ = {target: None}
|
| 713 |
+
|
| 714 |
+
# initialize fringes, start with forward
|
| 715 |
+
forward_fringe = [source]
|
| 716 |
+
reverse_fringe = [target]
|
| 717 |
+
|
| 718 |
+
while forward_fringe and reverse_fringe:
|
| 719 |
+
if len(forward_fringe) <= len(reverse_fringe):
|
| 720 |
+
this_level = forward_fringe
|
| 721 |
+
forward_fringe = []
|
| 722 |
+
for v in this_level:
|
| 723 |
+
for w in Gsucc(v):
|
| 724 |
+
if w not in pred:
|
| 725 |
+
forward_fringe.append(w)
|
| 726 |
+
pred[w] = v
|
| 727 |
+
if w in succ:
|
| 728 |
+
# found path
|
| 729 |
+
return pred, succ, w
|
| 730 |
+
else:
|
| 731 |
+
this_level = reverse_fringe
|
| 732 |
+
reverse_fringe = []
|
| 733 |
+
for v in this_level:
|
| 734 |
+
for w in Gpred(v):
|
| 735 |
+
if w not in succ:
|
| 736 |
+
succ[w] = v
|
| 737 |
+
reverse_fringe.append(w)
|
| 738 |
+
if w in pred:
|
| 739 |
+
# found path
|
| 740 |
+
return pred, succ, w
|
| 741 |
+
|
| 742 |
+
raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
def _bidirectional_dijkstra(
|
| 746 |
+
G, source, target, weight="weight", ignore_nodes=None, ignore_edges=None
|
| 747 |
+
):
|
| 748 |
+
"""Dijkstra's algorithm for shortest paths using bidirectional search.
|
| 749 |
+
|
| 750 |
+
This function returns the shortest path between source and target
|
| 751 |
+
ignoring nodes and edges in the containers ignore_nodes and
|
| 752 |
+
ignore_edges.
|
| 753 |
+
|
| 754 |
+
This is a custom modification of the standard Dijkstra bidirectional
|
| 755 |
+
shortest path implementation at networkx.algorithms.weighted
|
| 756 |
+
|
| 757 |
+
Parameters
|
| 758 |
+
----------
|
| 759 |
+
G : NetworkX graph
|
| 760 |
+
|
| 761 |
+
source : node
|
| 762 |
+
Starting node.
|
| 763 |
+
|
| 764 |
+
target : node
|
| 765 |
+
Ending node.
|
| 766 |
+
|
| 767 |
+
weight: string, function, optional (default='weight')
|
| 768 |
+
Edge data key or weight function corresponding to the edge weight
|
| 769 |
+
If this is a function, the weight of an edge is the value
|
| 770 |
+
returned by the function. The function must accept exactly three
|
| 771 |
+
positional arguments: the two endpoints of an edge and the
|
| 772 |
+
dictionary of edge attributes for that edge. The function must
|
| 773 |
+
return a number or None to indicate a hidden edge.
|
| 774 |
+
|
| 775 |
+
ignore_nodes : container of nodes
|
| 776 |
+
nodes to ignore, optional
|
| 777 |
+
|
| 778 |
+
ignore_edges : container of edges
|
| 779 |
+
edges to ignore, optional
|
| 780 |
+
|
| 781 |
+
Returns
|
| 782 |
+
-------
|
| 783 |
+
length : number
|
| 784 |
+
Shortest path length.
|
| 785 |
+
|
| 786 |
+
Returns a tuple of two dictionaries keyed by node.
|
| 787 |
+
The first dictionary stores distance from the source.
|
| 788 |
+
The second stores the path from the source to that node.
|
| 789 |
+
|
| 790 |
+
Raises
|
| 791 |
+
------
|
| 792 |
+
NetworkXNoPath
|
| 793 |
+
If no path exists between source and target.
|
| 794 |
+
|
| 795 |
+
Notes
|
| 796 |
+
-----
|
| 797 |
+
Edge weight attributes must be numerical.
|
| 798 |
+
Distances are calculated as sums of weighted edges traversed.
|
| 799 |
+
|
| 800 |
+
The weight function can be used to hide edges by returning None.
|
| 801 |
+
So ``weight = lambda u, v, d: 1 if d['color']=="red" else None``
|
| 802 |
+
will find the shortest red path.
|
| 803 |
+
|
| 804 |
+
In practice bidirectional Dijkstra is much more than twice as fast as
|
| 805 |
+
ordinary Dijkstra.
|
| 806 |
+
|
| 807 |
+
Ordinary Dijkstra expands nodes in a sphere-like manner from the
|
| 808 |
+
source. The radius of this sphere will eventually be the length
|
| 809 |
+
of the shortest path. Bidirectional Dijkstra will expand nodes
|
| 810 |
+
from both the source and the target, making two spheres of half
|
| 811 |
+
this radius. Volume of the first sphere is pi*r*r while the
|
| 812 |
+
others are 2*pi*r/2*r/2, making up half the volume.
|
| 813 |
+
|
| 814 |
+
This algorithm is not guaranteed to work if edge weights
|
| 815 |
+
are negative or are floating point numbers
|
| 816 |
+
(overflows and roundoff errors can cause problems).
|
| 817 |
+
|
| 818 |
+
See Also
|
| 819 |
+
--------
|
| 820 |
+
shortest_path
|
| 821 |
+
shortest_path_length
|
| 822 |
+
"""
|
| 823 |
+
if ignore_nodes and (source in ignore_nodes or target in ignore_nodes):
|
| 824 |
+
raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
|
| 825 |
+
if source == target:
|
| 826 |
+
if source not in G:
|
| 827 |
+
raise nx.NodeNotFound(f"Node {source} not in graph")
|
| 828 |
+
return (0, [source])
|
| 829 |
+
|
| 830 |
+
# handle either directed or undirected
|
| 831 |
+
if G.is_directed():
|
| 832 |
+
Gpred = G.predecessors
|
| 833 |
+
Gsucc = G.successors
|
| 834 |
+
else:
|
| 835 |
+
Gpred = G.neighbors
|
| 836 |
+
Gsucc = G.neighbors
|
| 837 |
+
|
| 838 |
+
# support optional nodes filter
|
| 839 |
+
if ignore_nodes:
|
| 840 |
+
|
| 841 |
+
def filter_iter(nodes):
|
| 842 |
+
def iterate(v):
|
| 843 |
+
for w in nodes(v):
|
| 844 |
+
if w not in ignore_nodes:
|
| 845 |
+
yield w
|
| 846 |
+
|
| 847 |
+
return iterate
|
| 848 |
+
|
| 849 |
+
Gpred = filter_iter(Gpred)
|
| 850 |
+
Gsucc = filter_iter(Gsucc)
|
| 851 |
+
|
| 852 |
+
# support optional edges filter
|
| 853 |
+
if ignore_edges:
|
| 854 |
+
if G.is_directed():
|
| 855 |
+
|
| 856 |
+
def filter_pred_iter(pred_iter):
|
| 857 |
+
def iterate(v):
|
| 858 |
+
for w in pred_iter(v):
|
| 859 |
+
if (w, v) not in ignore_edges:
|
| 860 |
+
yield w
|
| 861 |
+
|
| 862 |
+
return iterate
|
| 863 |
+
|
| 864 |
+
def filter_succ_iter(succ_iter):
|
| 865 |
+
def iterate(v):
|
| 866 |
+
for w in succ_iter(v):
|
| 867 |
+
if (v, w) not in ignore_edges:
|
| 868 |
+
yield w
|
| 869 |
+
|
| 870 |
+
return iterate
|
| 871 |
+
|
| 872 |
+
Gpred = filter_pred_iter(Gpred)
|
| 873 |
+
Gsucc = filter_succ_iter(Gsucc)
|
| 874 |
+
|
| 875 |
+
else:
|
| 876 |
+
|
| 877 |
+
def filter_iter(nodes):
|
| 878 |
+
def iterate(v):
|
| 879 |
+
for w in nodes(v):
|
| 880 |
+
if (v, w) not in ignore_edges and (w, v) not in ignore_edges:
|
| 881 |
+
yield w
|
| 882 |
+
|
| 883 |
+
return iterate
|
| 884 |
+
|
| 885 |
+
Gpred = filter_iter(Gpred)
|
| 886 |
+
Gsucc = filter_iter(Gsucc)
|
| 887 |
+
|
| 888 |
+
wt = _weight_function(G, weight)
|
| 889 |
+
push = heappush
|
| 890 |
+
pop = heappop
|
| 891 |
+
# Init: Forward Backward
|
| 892 |
+
dists = [{}, {}] # dictionary of final distances
|
| 893 |
+
paths = [{source: [source]}, {target: [target]}] # dictionary of paths
|
| 894 |
+
fringe = [[], []] # heap of (distance, node) tuples for
|
| 895 |
+
# extracting next node to expand
|
| 896 |
+
seen = [{source: 0}, {target: 0}] # dictionary of distances to
|
| 897 |
+
# nodes seen
|
| 898 |
+
c = count()
|
| 899 |
+
# initialize fringe heap
|
| 900 |
+
push(fringe[0], (0, next(c), source))
|
| 901 |
+
push(fringe[1], (0, next(c), target))
|
| 902 |
+
# neighs for extracting correct neighbor information
|
| 903 |
+
neighs = [Gsucc, Gpred]
|
| 904 |
+
# variables to hold shortest discovered path
|
| 905 |
+
# finaldist = 1e30000
|
| 906 |
+
finalpath = []
|
| 907 |
+
dir = 1
|
| 908 |
+
while fringe[0] and fringe[1]:
|
| 909 |
+
# choose direction
|
| 910 |
+
# dir == 0 is forward direction and dir == 1 is back
|
| 911 |
+
dir = 1 - dir
|
| 912 |
+
# extract closest to expand
|
| 913 |
+
(dist, _, v) = pop(fringe[dir])
|
| 914 |
+
if v in dists[dir]:
|
| 915 |
+
# Shortest path to v has already been found
|
| 916 |
+
continue
|
| 917 |
+
# update distance
|
| 918 |
+
dists[dir][v] = dist # equal to seen[dir][v]
|
| 919 |
+
if v in dists[1 - dir]:
|
| 920 |
+
# if we have scanned v in both directions we are done
|
| 921 |
+
# we have now discovered the shortest path
|
| 922 |
+
return (finaldist, finalpath)
|
| 923 |
+
|
| 924 |
+
for w in neighs[dir](v):
|
| 925 |
+
if dir == 0: # forward
|
| 926 |
+
minweight = wt(v, w, G.get_edge_data(v, w))
|
| 927 |
+
else: # back, must remember to change v,w->w,v
|
| 928 |
+
minweight = wt(w, v, G.get_edge_data(w, v))
|
| 929 |
+
if minweight is None:
|
| 930 |
+
continue
|
| 931 |
+
vwLength = dists[dir][v] + minweight
|
| 932 |
+
|
| 933 |
+
if w in dists[dir]:
|
| 934 |
+
if vwLength < dists[dir][w]:
|
| 935 |
+
raise ValueError("Contradictory paths found: negative weights?")
|
| 936 |
+
elif w not in seen[dir] or vwLength < seen[dir][w]:
|
| 937 |
+
# relaxing
|
| 938 |
+
seen[dir][w] = vwLength
|
| 939 |
+
push(fringe[dir], (vwLength, next(c), w))
|
| 940 |
+
paths[dir][w] = paths[dir][v] + [w]
|
| 941 |
+
if w in seen[0] and w in seen[1]:
|
| 942 |
+
# see if this path is better than the already
|
| 943 |
+
# discovered shortest path
|
| 944 |
+
totaldist = seen[0][w] + seen[1][w]
|
| 945 |
+
if finalpath == [] or finaldist > totaldist:
|
| 946 |
+
finaldist = totaldist
|
| 947 |
+
revpath = paths[1][w][:]
|
| 948 |
+
revpath.reverse()
|
| 949 |
+
finalpath = paths[0][w] + revpath[1:]
|
| 950 |
+
raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
|
.venv/lib/python3.11/site-packages/networkx/algorithms/smallworld.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions for estimating the small-world-ness of graphs.
|
| 2 |
+
|
| 3 |
+
A small world network is characterized by a small average shortest path length,
|
| 4 |
+
and a large clustering coefficient.
|
| 5 |
+
|
| 6 |
+
Small-worldness is commonly measured with the coefficient sigma or omega.
|
| 7 |
+
|
| 8 |
+
Both coefficients compare the average clustering coefficient and shortest path
|
| 9 |
+
length of a given graph against the same quantities for an equivalent random
|
| 10 |
+
or lattice graph.
|
| 11 |
+
|
| 12 |
+
For more information, see the Wikipedia article on small-world network [1]_.
|
| 13 |
+
|
| 14 |
+
.. [1] Small-world network:: https://en.wikipedia.org/wiki/Small-world_network
|
| 15 |
+
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import networkx as nx
|
| 19 |
+
from networkx.utils import not_implemented_for, py_random_state
|
| 20 |
+
|
| 21 |
+
__all__ = ["random_reference", "lattice_reference", "sigma", "omega"]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@not_implemented_for("directed")
|
| 25 |
+
@not_implemented_for("multigraph")
|
| 26 |
+
@py_random_state(3)
|
| 27 |
+
@nx._dispatchable(returns_graph=True)
|
| 28 |
+
def random_reference(G, niter=1, connectivity=True, seed=None):
|
| 29 |
+
"""Compute a random graph by swapping edges of a given graph.
|
| 30 |
+
|
| 31 |
+
Parameters
|
| 32 |
+
----------
|
| 33 |
+
G : graph
|
| 34 |
+
An undirected graph with 4 or more nodes.
|
| 35 |
+
|
| 36 |
+
niter : integer (optional, default=1)
|
| 37 |
+
An edge is rewired approximately `niter` times.
|
| 38 |
+
|
| 39 |
+
connectivity : boolean (optional, default=True)
|
| 40 |
+
When True, ensure connectivity for the randomized graph.
|
| 41 |
+
|
| 42 |
+
seed : integer, random_state, or None (default)
|
| 43 |
+
Indicator of random number generation state.
|
| 44 |
+
See :ref:`Randomness<randomness>`.
|
| 45 |
+
|
| 46 |
+
Returns
|
| 47 |
+
-------
|
| 48 |
+
G : graph
|
| 49 |
+
The randomized graph.
|
| 50 |
+
|
| 51 |
+
Raises
|
| 52 |
+
------
|
| 53 |
+
NetworkXError
|
| 54 |
+
If there are fewer than 4 nodes or 2 edges in `G`
|
| 55 |
+
|
| 56 |
+
Notes
|
| 57 |
+
-----
|
| 58 |
+
The implementation is adapted from the algorithm by Maslov and Sneppen
|
| 59 |
+
(2002) [1]_.
|
| 60 |
+
|
| 61 |
+
References
|
| 62 |
+
----------
|
| 63 |
+
.. [1] Maslov, Sergei, and Kim Sneppen.
|
| 64 |
+
"Specificity and stability in topology of protein networks."
|
| 65 |
+
Science 296.5569 (2002): 910-913.
|
| 66 |
+
"""
|
| 67 |
+
if len(G) < 4:
|
| 68 |
+
raise nx.NetworkXError("Graph has fewer than four nodes.")
|
| 69 |
+
if len(G.edges) < 2:
|
| 70 |
+
raise nx.NetworkXError("Graph has fewer that 2 edges")
|
| 71 |
+
|
| 72 |
+
from networkx.utils import cumulative_distribution, discrete_sequence
|
| 73 |
+
|
| 74 |
+
local_conn = nx.connectivity.local_edge_connectivity
|
| 75 |
+
|
| 76 |
+
G = G.copy()
|
| 77 |
+
keys, degrees = zip(*G.degree()) # keys, degree
|
| 78 |
+
cdf = cumulative_distribution(degrees) # cdf of degree
|
| 79 |
+
nnodes = len(G)
|
| 80 |
+
nedges = nx.number_of_edges(G)
|
| 81 |
+
niter = niter * nedges
|
| 82 |
+
ntries = int(nnodes * nedges / (nnodes * (nnodes - 1) / 2))
|
| 83 |
+
swapcount = 0
|
| 84 |
+
|
| 85 |
+
for i in range(niter):
|
| 86 |
+
n = 0
|
| 87 |
+
while n < ntries:
|
| 88 |
+
# pick two random edges without creating edge list
|
| 89 |
+
# choose source node indices from discrete distribution
|
| 90 |
+
(ai, ci) = discrete_sequence(2, cdistribution=cdf, seed=seed)
|
| 91 |
+
if ai == ci:
|
| 92 |
+
continue # same source, skip
|
| 93 |
+
a = keys[ai] # convert index to label
|
| 94 |
+
c = keys[ci]
|
| 95 |
+
# choose target uniformly from neighbors
|
| 96 |
+
b = seed.choice(list(G.neighbors(a)))
|
| 97 |
+
d = seed.choice(list(G.neighbors(c)))
|
| 98 |
+
if b in [a, c, d] or d in [a, b, c]:
|
| 99 |
+
continue # all vertices should be different
|
| 100 |
+
|
| 101 |
+
# don't create parallel edges
|
| 102 |
+
if (d not in G[a]) and (b not in G[c]):
|
| 103 |
+
G.add_edge(a, d)
|
| 104 |
+
G.add_edge(c, b)
|
| 105 |
+
G.remove_edge(a, b)
|
| 106 |
+
G.remove_edge(c, d)
|
| 107 |
+
|
| 108 |
+
# Check if the graph is still connected
|
| 109 |
+
if connectivity and local_conn(G, a, b) == 0:
|
| 110 |
+
# Not connected, revert the swap
|
| 111 |
+
G.remove_edge(a, d)
|
| 112 |
+
G.remove_edge(c, b)
|
| 113 |
+
G.add_edge(a, b)
|
| 114 |
+
G.add_edge(c, d)
|
| 115 |
+
else:
|
| 116 |
+
swapcount += 1
|
| 117 |
+
break
|
| 118 |
+
n += 1
|
| 119 |
+
return G
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
@not_implemented_for("directed")
|
| 123 |
+
@not_implemented_for("multigraph")
|
| 124 |
+
@py_random_state(4)
|
| 125 |
+
@nx._dispatchable(returns_graph=True)
|
| 126 |
+
def lattice_reference(G, niter=5, D=None, connectivity=True, seed=None):
|
| 127 |
+
"""Latticize the given graph by swapping edges.
|
| 128 |
+
|
| 129 |
+
Parameters
|
| 130 |
+
----------
|
| 131 |
+
G : graph
|
| 132 |
+
An undirected graph.
|
| 133 |
+
|
| 134 |
+
niter : integer (optional, default=1)
|
| 135 |
+
An edge is rewired approximately niter times.
|
| 136 |
+
|
| 137 |
+
D : numpy.array (optional, default=None)
|
| 138 |
+
Distance to the diagonal matrix.
|
| 139 |
+
|
| 140 |
+
connectivity : boolean (optional, default=True)
|
| 141 |
+
Ensure connectivity for the latticized graph when set to True.
|
| 142 |
+
|
| 143 |
+
seed : integer, random_state, or None (default)
|
| 144 |
+
Indicator of random number generation state.
|
| 145 |
+
See :ref:`Randomness<randomness>`.
|
| 146 |
+
|
| 147 |
+
Returns
|
| 148 |
+
-------
|
| 149 |
+
G : graph
|
| 150 |
+
The latticized graph.
|
| 151 |
+
|
| 152 |
+
Raises
|
| 153 |
+
------
|
| 154 |
+
NetworkXError
|
| 155 |
+
If there are fewer than 4 nodes or 2 edges in `G`
|
| 156 |
+
|
| 157 |
+
Notes
|
| 158 |
+
-----
|
| 159 |
+
The implementation is adapted from the algorithm by Sporns et al. [1]_.
|
| 160 |
+
which is inspired from the original work by Maslov and Sneppen(2002) [2]_.
|
| 161 |
+
|
| 162 |
+
References
|
| 163 |
+
----------
|
| 164 |
+
.. [1] Sporns, Olaf, and Jonathan D. Zwi.
|
| 165 |
+
"The small world of the cerebral cortex."
|
| 166 |
+
Neuroinformatics 2.2 (2004): 145-162.
|
| 167 |
+
.. [2] Maslov, Sergei, and Kim Sneppen.
|
| 168 |
+
"Specificity and stability in topology of protein networks."
|
| 169 |
+
Science 296.5569 (2002): 910-913.
|
| 170 |
+
"""
|
| 171 |
+
import numpy as np
|
| 172 |
+
|
| 173 |
+
from networkx.utils import cumulative_distribution, discrete_sequence
|
| 174 |
+
|
| 175 |
+
local_conn = nx.connectivity.local_edge_connectivity
|
| 176 |
+
|
| 177 |
+
if len(G) < 4:
|
| 178 |
+
raise nx.NetworkXError("Graph has fewer than four nodes.")
|
| 179 |
+
if len(G.edges) < 2:
|
| 180 |
+
raise nx.NetworkXError("Graph has fewer that 2 edges")
|
| 181 |
+
# Instead of choosing uniformly at random from a generated edge list,
|
| 182 |
+
# this algorithm chooses nonuniformly from the set of nodes with
|
| 183 |
+
# probability weighted by degree.
|
| 184 |
+
G = G.copy()
|
| 185 |
+
keys, degrees = zip(*G.degree()) # keys, degree
|
| 186 |
+
cdf = cumulative_distribution(degrees) # cdf of degree
|
| 187 |
+
|
| 188 |
+
nnodes = len(G)
|
| 189 |
+
nedges = nx.number_of_edges(G)
|
| 190 |
+
if D is None:
|
| 191 |
+
D = np.zeros((nnodes, nnodes))
|
| 192 |
+
un = np.arange(1, nnodes)
|
| 193 |
+
um = np.arange(nnodes - 1, 0, -1)
|
| 194 |
+
u = np.append((0,), np.where(un < um, un, um))
|
| 195 |
+
|
| 196 |
+
for v in range(int(np.ceil(nnodes / 2))):
|
| 197 |
+
D[nnodes - v - 1, :] = np.append(u[v + 1 :], u[: v + 1])
|
| 198 |
+
D[v, :] = D[nnodes - v - 1, :][::-1]
|
| 199 |
+
|
| 200 |
+
niter = niter * nedges
|
| 201 |
+
# maximal number of rewiring attempts per 'niter'
|
| 202 |
+
max_attempts = int(nnodes * nedges / (nnodes * (nnodes - 1) / 2))
|
| 203 |
+
|
| 204 |
+
for _ in range(niter):
|
| 205 |
+
n = 0
|
| 206 |
+
while n < max_attempts:
|
| 207 |
+
# pick two random edges without creating edge list
|
| 208 |
+
# choose source node indices from discrete distribution
|
| 209 |
+
(ai, ci) = discrete_sequence(2, cdistribution=cdf, seed=seed)
|
| 210 |
+
if ai == ci:
|
| 211 |
+
continue # same source, skip
|
| 212 |
+
a = keys[ai] # convert index to label
|
| 213 |
+
c = keys[ci]
|
| 214 |
+
# choose target uniformly from neighbors
|
| 215 |
+
b = seed.choice(list(G.neighbors(a)))
|
| 216 |
+
d = seed.choice(list(G.neighbors(c)))
|
| 217 |
+
bi = keys.index(b)
|
| 218 |
+
di = keys.index(d)
|
| 219 |
+
|
| 220 |
+
if b in [a, c, d] or d in [a, b, c]:
|
| 221 |
+
continue # all vertices should be different
|
| 222 |
+
|
| 223 |
+
# don't create parallel edges
|
| 224 |
+
if (d not in G[a]) and (b not in G[c]):
|
| 225 |
+
if D[ai, bi] + D[ci, di] >= D[ai, ci] + D[bi, di]:
|
| 226 |
+
# only swap if we get closer to the diagonal
|
| 227 |
+
G.add_edge(a, d)
|
| 228 |
+
G.add_edge(c, b)
|
| 229 |
+
G.remove_edge(a, b)
|
| 230 |
+
G.remove_edge(c, d)
|
| 231 |
+
|
| 232 |
+
# Check if the graph is still connected
|
| 233 |
+
if connectivity and local_conn(G, a, b) == 0:
|
| 234 |
+
# Not connected, revert the swap
|
| 235 |
+
G.remove_edge(a, d)
|
| 236 |
+
G.remove_edge(c, b)
|
| 237 |
+
G.add_edge(a, b)
|
| 238 |
+
G.add_edge(c, d)
|
| 239 |
+
else:
|
| 240 |
+
break
|
| 241 |
+
n += 1
|
| 242 |
+
|
| 243 |
+
return G
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
@not_implemented_for("directed")
|
| 247 |
+
@not_implemented_for("multigraph")
|
| 248 |
+
@py_random_state(3)
|
| 249 |
+
@nx._dispatchable
|
| 250 |
+
def sigma(G, niter=100, nrand=10, seed=None):
|
| 251 |
+
"""Returns the small-world coefficient (sigma) of the given graph.
|
| 252 |
+
|
| 253 |
+
The small-world coefficient is defined as:
|
| 254 |
+
sigma = C/Cr / L/Lr
|
| 255 |
+
where C and L are respectively the average clustering coefficient and
|
| 256 |
+
average shortest path length of G. Cr and Lr are respectively the average
|
| 257 |
+
clustering coefficient and average shortest path length of an equivalent
|
| 258 |
+
random graph.
|
| 259 |
+
|
| 260 |
+
A graph is commonly classified as small-world if sigma>1.
|
| 261 |
+
|
| 262 |
+
Parameters
|
| 263 |
+
----------
|
| 264 |
+
G : NetworkX graph
|
| 265 |
+
An undirected graph.
|
| 266 |
+
niter : integer (optional, default=100)
|
| 267 |
+
Approximate number of rewiring per edge to compute the equivalent
|
| 268 |
+
random graph.
|
| 269 |
+
nrand : integer (optional, default=10)
|
| 270 |
+
Number of random graphs generated to compute the average clustering
|
| 271 |
+
coefficient (Cr) and average shortest path length (Lr).
|
| 272 |
+
seed : integer, random_state, or None (default)
|
| 273 |
+
Indicator of random number generation state.
|
| 274 |
+
See :ref:`Randomness<randomness>`.
|
| 275 |
+
|
| 276 |
+
Returns
|
| 277 |
+
-------
|
| 278 |
+
sigma : float
|
| 279 |
+
The small-world coefficient of G.
|
| 280 |
+
|
| 281 |
+
Notes
|
| 282 |
+
-----
|
| 283 |
+
The implementation is adapted from Humphries et al. [1]_ [2]_.
|
| 284 |
+
|
| 285 |
+
References
|
| 286 |
+
----------
|
| 287 |
+
.. [1] The brainstem reticular formation is a small-world, not scale-free,
|
| 288 |
+
network M. D. Humphries, K. Gurney and T. J. Prescott,
|
| 289 |
+
Proc. Roy. Soc. B 2006 273, 503-511, doi:10.1098/rspb.2005.3354.
|
| 290 |
+
.. [2] Humphries and Gurney (2008).
|
| 291 |
+
"Network 'Small-World-Ness': A Quantitative Method for Determining
|
| 292 |
+
Canonical Network Equivalence".
|
| 293 |
+
PLoS One. 3 (4). PMID 18446219. doi:10.1371/journal.pone.0002051.
|
| 294 |
+
"""
|
| 295 |
+
import numpy as np
|
| 296 |
+
|
| 297 |
+
# Compute the mean clustering coefficient and average shortest path length
|
| 298 |
+
# for an equivalent random graph
|
| 299 |
+
randMetrics = {"C": [], "L": []}
|
| 300 |
+
for i in range(nrand):
|
| 301 |
+
Gr = random_reference(G, niter=niter, seed=seed)
|
| 302 |
+
randMetrics["C"].append(nx.transitivity(Gr))
|
| 303 |
+
randMetrics["L"].append(nx.average_shortest_path_length(Gr))
|
| 304 |
+
|
| 305 |
+
C = nx.transitivity(G)
|
| 306 |
+
L = nx.average_shortest_path_length(G)
|
| 307 |
+
Cr = np.mean(randMetrics["C"])
|
| 308 |
+
Lr = np.mean(randMetrics["L"])
|
| 309 |
+
|
| 310 |
+
sigma = (C / Cr) / (L / Lr)
|
| 311 |
+
|
| 312 |
+
return float(sigma)
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
@not_implemented_for("directed")
|
| 316 |
+
@not_implemented_for("multigraph")
|
| 317 |
+
@py_random_state(3)
|
| 318 |
+
@nx._dispatchable
|
| 319 |
+
def omega(G, niter=5, nrand=10, seed=None):
|
| 320 |
+
"""Returns the small-world coefficient (omega) of a graph
|
| 321 |
+
|
| 322 |
+
The small-world coefficient of a graph G is:
|
| 323 |
+
|
| 324 |
+
omega = Lr/L - C/Cl
|
| 325 |
+
|
| 326 |
+
where C and L are respectively the average clustering coefficient and
|
| 327 |
+
average shortest path length of G. Lr is the average shortest path length
|
| 328 |
+
of an equivalent random graph and Cl is the average clustering coefficient
|
| 329 |
+
of an equivalent lattice graph.
|
| 330 |
+
|
| 331 |
+
The small-world coefficient (omega) measures how much G is like a lattice
|
| 332 |
+
or a random graph. Negative values mean G is similar to a lattice whereas
|
| 333 |
+
positive values mean G is a random graph.
|
| 334 |
+
Values close to 0 mean that G has small-world characteristics.
|
| 335 |
+
|
| 336 |
+
Parameters
|
| 337 |
+
----------
|
| 338 |
+
G : NetworkX graph
|
| 339 |
+
An undirected graph.
|
| 340 |
+
|
| 341 |
+
niter: integer (optional, default=5)
|
| 342 |
+
Approximate number of rewiring per edge to compute the equivalent
|
| 343 |
+
random graph.
|
| 344 |
+
|
| 345 |
+
nrand: integer (optional, default=10)
|
| 346 |
+
Number of random graphs generated to compute the maximal clustering
|
| 347 |
+
coefficient (Cr) and average shortest path length (Lr).
|
| 348 |
+
|
| 349 |
+
seed : integer, random_state, or None (default)
|
| 350 |
+
Indicator of random number generation state.
|
| 351 |
+
See :ref:`Randomness<randomness>`.
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
Returns
|
| 355 |
+
-------
|
| 356 |
+
omega : float
|
| 357 |
+
The small-world coefficient (omega)
|
| 358 |
+
|
| 359 |
+
Notes
|
| 360 |
+
-----
|
| 361 |
+
The implementation is adapted from the algorithm by Telesford et al. [1]_.
|
| 362 |
+
|
| 363 |
+
References
|
| 364 |
+
----------
|
| 365 |
+
.. [1] Telesford, Joyce, Hayasaka, Burdette, and Laurienti (2011).
|
| 366 |
+
"The Ubiquity of Small-World Networks".
|
| 367 |
+
Brain Connectivity. 1 (0038): 367-75. PMC 3604768. PMID 22432451.
|
| 368 |
+
doi:10.1089/brain.2011.0038.
|
| 369 |
+
"""
|
| 370 |
+
import numpy as np
|
| 371 |
+
|
| 372 |
+
# Compute the mean clustering coefficient and average shortest path length
|
| 373 |
+
# for an equivalent random graph
|
| 374 |
+
randMetrics = {"C": [], "L": []}
|
| 375 |
+
|
| 376 |
+
# Calculate initial average clustering coefficient which potentially will
|
| 377 |
+
# get replaced by higher clustering coefficients from generated lattice
|
| 378 |
+
# reference graphs
|
| 379 |
+
Cl = nx.average_clustering(G)
|
| 380 |
+
|
| 381 |
+
niter_lattice_reference = niter
|
| 382 |
+
niter_random_reference = niter * 2
|
| 383 |
+
|
| 384 |
+
for _ in range(nrand):
|
| 385 |
+
# Generate random graph
|
| 386 |
+
Gr = random_reference(G, niter=niter_random_reference, seed=seed)
|
| 387 |
+
randMetrics["L"].append(nx.average_shortest_path_length(Gr))
|
| 388 |
+
|
| 389 |
+
# Generate lattice graph
|
| 390 |
+
Gl = lattice_reference(G, niter=niter_lattice_reference, seed=seed)
|
| 391 |
+
|
| 392 |
+
# Replace old clustering coefficient, if clustering is higher in
|
| 393 |
+
# generated lattice reference
|
| 394 |
+
Cl_temp = nx.average_clustering(Gl)
|
| 395 |
+
if Cl_temp > Cl:
|
| 396 |
+
Cl = Cl_temp
|
| 397 |
+
|
| 398 |
+
C = nx.average_clustering(G)
|
| 399 |
+
L = nx.average_shortest_path_length(G)
|
| 400 |
+
Lr = np.mean(randMetrics["L"])
|
| 401 |
+
|
| 402 |
+
omega = (Lr / L) - (C / Cl)
|
| 403 |
+
|
| 404 |
+
return float(omega)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/smetric.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import networkx as nx
|
| 2 |
+
|
| 3 |
+
__all__ = ["s_metric"]
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@nx._dispatchable
|
| 7 |
+
def s_metric(G):
|
| 8 |
+
"""Returns the s-metric [1]_ of graph.
|
| 9 |
+
|
| 10 |
+
The s-metric is defined as the sum of the products ``deg(u) * deg(v)``
|
| 11 |
+
for every edge ``(u, v)`` in `G`.
|
| 12 |
+
|
| 13 |
+
Parameters
|
| 14 |
+
----------
|
| 15 |
+
G : graph
|
| 16 |
+
The graph used to compute the s-metric.
|
| 17 |
+
|
| 18 |
+
Returns
|
| 19 |
+
-------
|
| 20 |
+
s : float
|
| 21 |
+
The s-metric of the graph.
|
| 22 |
+
|
| 23 |
+
References
|
| 24 |
+
----------
|
| 25 |
+
.. [1] Lun Li, David Alderson, John C. Doyle, and Walter Willinger,
|
| 26 |
+
Towards a Theory of Scale-Free Graphs:
|
| 27 |
+
Definition, Properties, and Implications (Extended Version), 2005.
|
| 28 |
+
https://arxiv.org/abs/cond-mat/0501169
|
| 29 |
+
"""
|
| 30 |
+
return float(sum(G.degree(u) * G.degree(v) for (u, v) in G.edges()))
|
.venv/lib/python3.11/site-packages/networkx/algorithms/sparsifiers.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions for computing sparsifiers of graphs."""
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.utils import not_implemented_for, py_random_state
|
| 7 |
+
|
| 8 |
+
__all__ = ["spanner"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@not_implemented_for("directed")
|
| 12 |
+
@not_implemented_for("multigraph")
|
| 13 |
+
@py_random_state(3)
|
| 14 |
+
@nx._dispatchable(edge_attrs="weight", returns_graph=True)
|
| 15 |
+
def spanner(G, stretch, weight=None, seed=None):
|
| 16 |
+
"""Returns a spanner of the given graph with the given stretch.
|
| 17 |
+
|
| 18 |
+
A spanner of a graph G = (V, E) with stretch t is a subgraph
|
| 19 |
+
H = (V, E_S) such that E_S is a subset of E and the distance between
|
| 20 |
+
any pair of nodes in H is at most t times the distance between the
|
| 21 |
+
nodes in G.
|
| 22 |
+
|
| 23 |
+
Parameters
|
| 24 |
+
----------
|
| 25 |
+
G : NetworkX graph
|
| 26 |
+
An undirected simple graph.
|
| 27 |
+
|
| 28 |
+
stretch : float
|
| 29 |
+
The stretch of the spanner.
|
| 30 |
+
|
| 31 |
+
weight : object
|
| 32 |
+
The edge attribute to use as distance.
|
| 33 |
+
|
| 34 |
+
seed : integer, random_state, or None (default)
|
| 35 |
+
Indicator of random number generation state.
|
| 36 |
+
See :ref:`Randomness<randomness>`.
|
| 37 |
+
|
| 38 |
+
Returns
|
| 39 |
+
-------
|
| 40 |
+
NetworkX graph
|
| 41 |
+
A spanner of the given graph with the given stretch.
|
| 42 |
+
|
| 43 |
+
Raises
|
| 44 |
+
------
|
| 45 |
+
ValueError
|
| 46 |
+
If a stretch less than 1 is given.
|
| 47 |
+
|
| 48 |
+
Notes
|
| 49 |
+
-----
|
| 50 |
+
This function implements the spanner algorithm by Baswana and Sen,
|
| 51 |
+
see [1].
|
| 52 |
+
|
| 53 |
+
This algorithm is a randomized las vegas algorithm: The expected
|
| 54 |
+
running time is O(km) where k = (stretch + 1) // 2 and m is the
|
| 55 |
+
number of edges in G. The returned graph is always a spanner of the
|
| 56 |
+
given graph with the specified stretch. For weighted graphs the
|
| 57 |
+
number of edges in the spanner is O(k * n^(1 + 1 / k)) where k is
|
| 58 |
+
defined as above and n is the number of nodes in G. For unweighted
|
| 59 |
+
graphs the number of edges is O(n^(1 + 1 / k) + kn).
|
| 60 |
+
|
| 61 |
+
References
|
| 62 |
+
----------
|
| 63 |
+
[1] S. Baswana, S. Sen. A Simple and Linear Time Randomized
|
| 64 |
+
Algorithm for Computing Sparse Spanners in Weighted Graphs.
|
| 65 |
+
Random Struct. Algorithms 30(4): 532-563 (2007).
|
| 66 |
+
"""
|
| 67 |
+
if stretch < 1:
|
| 68 |
+
raise ValueError("stretch must be at least 1")
|
| 69 |
+
|
| 70 |
+
k = (stretch + 1) // 2
|
| 71 |
+
|
| 72 |
+
# initialize spanner H with empty edge set
|
| 73 |
+
H = nx.empty_graph()
|
| 74 |
+
H.add_nodes_from(G.nodes)
|
| 75 |
+
|
| 76 |
+
# phase 1: forming the clusters
|
| 77 |
+
# the residual graph has V' from the paper as its node set
|
| 78 |
+
# and E' from the paper as its edge set
|
| 79 |
+
residual_graph = _setup_residual_graph(G, weight)
|
| 80 |
+
# clustering is a dictionary that maps nodes in a cluster to the
|
| 81 |
+
# cluster center
|
| 82 |
+
clustering = {v: v for v in G.nodes}
|
| 83 |
+
sample_prob = math.pow(G.number_of_nodes(), -1 / k)
|
| 84 |
+
size_limit = 2 * math.pow(G.number_of_nodes(), 1 + 1 / k)
|
| 85 |
+
|
| 86 |
+
i = 0
|
| 87 |
+
while i < k - 1:
|
| 88 |
+
# step 1: sample centers
|
| 89 |
+
sampled_centers = set()
|
| 90 |
+
for center in set(clustering.values()):
|
| 91 |
+
if seed.random() < sample_prob:
|
| 92 |
+
sampled_centers.add(center)
|
| 93 |
+
|
| 94 |
+
# combined loop for steps 2 and 3
|
| 95 |
+
edges_to_add = set()
|
| 96 |
+
edges_to_remove = set()
|
| 97 |
+
new_clustering = {}
|
| 98 |
+
for v in residual_graph.nodes:
|
| 99 |
+
if clustering[v] in sampled_centers:
|
| 100 |
+
continue
|
| 101 |
+
|
| 102 |
+
# step 2: find neighboring (sampled) clusters and
|
| 103 |
+
# lightest edges to them
|
| 104 |
+
lightest_edge_neighbor, lightest_edge_weight = _lightest_edge_dicts(
|
| 105 |
+
residual_graph, clustering, v
|
| 106 |
+
)
|
| 107 |
+
neighboring_sampled_centers = (
|
| 108 |
+
set(lightest_edge_weight.keys()) & sampled_centers
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# step 3: add edges to spanner
|
| 112 |
+
if not neighboring_sampled_centers:
|
| 113 |
+
# connect to each neighboring center via lightest edge
|
| 114 |
+
for neighbor in lightest_edge_neighbor.values():
|
| 115 |
+
edges_to_add.add((v, neighbor))
|
| 116 |
+
# remove all incident edges
|
| 117 |
+
for neighbor in residual_graph.adj[v]:
|
| 118 |
+
edges_to_remove.add((v, neighbor))
|
| 119 |
+
|
| 120 |
+
else: # there is a neighboring sampled center
|
| 121 |
+
closest_center = min(
|
| 122 |
+
neighboring_sampled_centers, key=lightest_edge_weight.get
|
| 123 |
+
)
|
| 124 |
+
closest_center_weight = lightest_edge_weight[closest_center]
|
| 125 |
+
closest_center_neighbor = lightest_edge_neighbor[closest_center]
|
| 126 |
+
|
| 127 |
+
edges_to_add.add((v, closest_center_neighbor))
|
| 128 |
+
new_clustering[v] = closest_center
|
| 129 |
+
|
| 130 |
+
# connect to centers with edge weight less than
|
| 131 |
+
# closest_center_weight
|
| 132 |
+
for center, edge_weight in lightest_edge_weight.items():
|
| 133 |
+
if edge_weight < closest_center_weight:
|
| 134 |
+
neighbor = lightest_edge_neighbor[center]
|
| 135 |
+
edges_to_add.add((v, neighbor))
|
| 136 |
+
|
| 137 |
+
# remove edges to centers with edge weight less than
|
| 138 |
+
# closest_center_weight
|
| 139 |
+
for neighbor in residual_graph.adj[v]:
|
| 140 |
+
nbr_cluster = clustering[neighbor]
|
| 141 |
+
nbr_weight = lightest_edge_weight[nbr_cluster]
|
| 142 |
+
if (
|
| 143 |
+
nbr_cluster == closest_center
|
| 144 |
+
or nbr_weight < closest_center_weight
|
| 145 |
+
):
|
| 146 |
+
edges_to_remove.add((v, neighbor))
|
| 147 |
+
|
| 148 |
+
# check whether iteration added too many edges to spanner,
|
| 149 |
+
# if so repeat
|
| 150 |
+
if len(edges_to_add) > size_limit:
|
| 151 |
+
# an iteration is repeated O(1) times on expectation
|
| 152 |
+
continue
|
| 153 |
+
|
| 154 |
+
# iteration succeeded
|
| 155 |
+
i = i + 1
|
| 156 |
+
|
| 157 |
+
# actually add edges to spanner
|
| 158 |
+
for u, v in edges_to_add:
|
| 159 |
+
_add_edge_to_spanner(H, residual_graph, u, v, weight)
|
| 160 |
+
|
| 161 |
+
# actually delete edges from residual graph
|
| 162 |
+
residual_graph.remove_edges_from(edges_to_remove)
|
| 163 |
+
|
| 164 |
+
# copy old clustering data to new_clustering
|
| 165 |
+
for node, center in clustering.items():
|
| 166 |
+
if center in sampled_centers:
|
| 167 |
+
new_clustering[node] = center
|
| 168 |
+
clustering = new_clustering
|
| 169 |
+
|
| 170 |
+
# step 4: remove intra-cluster edges
|
| 171 |
+
for u in residual_graph.nodes:
|
| 172 |
+
for v in list(residual_graph.adj[u]):
|
| 173 |
+
if clustering[u] == clustering[v]:
|
| 174 |
+
residual_graph.remove_edge(u, v)
|
| 175 |
+
|
| 176 |
+
# update residual graph node set
|
| 177 |
+
for v in list(residual_graph.nodes):
|
| 178 |
+
if v not in clustering:
|
| 179 |
+
residual_graph.remove_node(v)
|
| 180 |
+
|
| 181 |
+
# phase 2: vertex-cluster joining
|
| 182 |
+
for v in residual_graph.nodes:
|
| 183 |
+
lightest_edge_neighbor, _ = _lightest_edge_dicts(residual_graph, clustering, v)
|
| 184 |
+
for neighbor in lightest_edge_neighbor.values():
|
| 185 |
+
_add_edge_to_spanner(H, residual_graph, v, neighbor, weight)
|
| 186 |
+
|
| 187 |
+
return H
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _setup_residual_graph(G, weight):
|
| 191 |
+
"""Setup residual graph as a copy of G with unique edges weights.
|
| 192 |
+
|
| 193 |
+
The node set of the residual graph corresponds to the set V' from
|
| 194 |
+
the Baswana-Sen paper and the edge set corresponds to the set E'
|
| 195 |
+
from the paper.
|
| 196 |
+
|
| 197 |
+
This function associates distinct weights to the edges of the
|
| 198 |
+
residual graph (even for unweighted input graphs), as required by
|
| 199 |
+
the algorithm.
|
| 200 |
+
|
| 201 |
+
Parameters
|
| 202 |
+
----------
|
| 203 |
+
G : NetworkX graph
|
| 204 |
+
An undirected simple graph.
|
| 205 |
+
|
| 206 |
+
weight : object
|
| 207 |
+
The edge attribute to use as distance.
|
| 208 |
+
|
| 209 |
+
Returns
|
| 210 |
+
-------
|
| 211 |
+
NetworkX graph
|
| 212 |
+
The residual graph used for the Baswana-Sen algorithm.
|
| 213 |
+
"""
|
| 214 |
+
residual_graph = G.copy()
|
| 215 |
+
|
| 216 |
+
# establish unique edge weights, even for unweighted graphs
|
| 217 |
+
for u, v in G.edges():
|
| 218 |
+
if not weight:
|
| 219 |
+
residual_graph[u][v]["weight"] = (id(u), id(v))
|
| 220 |
+
else:
|
| 221 |
+
residual_graph[u][v]["weight"] = (G[u][v][weight], id(u), id(v))
|
| 222 |
+
|
| 223 |
+
return residual_graph
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _lightest_edge_dicts(residual_graph, clustering, node):
|
| 227 |
+
"""Find the lightest edge to each cluster.
|
| 228 |
+
|
| 229 |
+
Searches for the minimum-weight edge to each cluster adjacent to
|
| 230 |
+
the given node.
|
| 231 |
+
|
| 232 |
+
Parameters
|
| 233 |
+
----------
|
| 234 |
+
residual_graph : NetworkX graph
|
| 235 |
+
The residual graph used by the Baswana-Sen algorithm.
|
| 236 |
+
|
| 237 |
+
clustering : dictionary
|
| 238 |
+
The current clustering of the nodes.
|
| 239 |
+
|
| 240 |
+
node : node
|
| 241 |
+
The node from which the search originates.
|
| 242 |
+
|
| 243 |
+
Returns
|
| 244 |
+
-------
|
| 245 |
+
lightest_edge_neighbor, lightest_edge_weight : dictionary, dictionary
|
| 246 |
+
lightest_edge_neighbor is a dictionary that maps a center C to
|
| 247 |
+
a node v in the corresponding cluster such that the edge from
|
| 248 |
+
the given node to v is the lightest edge from the given node to
|
| 249 |
+
any node in cluster. lightest_edge_weight maps a center C to the
|
| 250 |
+
weight of the aforementioned edge.
|
| 251 |
+
|
| 252 |
+
Notes
|
| 253 |
+
-----
|
| 254 |
+
If a cluster has no node that is adjacent to the given node in the
|
| 255 |
+
residual graph then the center of the cluster is not a key in the
|
| 256 |
+
returned dictionaries.
|
| 257 |
+
"""
|
| 258 |
+
lightest_edge_neighbor = {}
|
| 259 |
+
lightest_edge_weight = {}
|
| 260 |
+
for neighbor in residual_graph.adj[node]:
|
| 261 |
+
nbr_center = clustering[neighbor]
|
| 262 |
+
weight = residual_graph[node][neighbor]["weight"]
|
| 263 |
+
if (
|
| 264 |
+
nbr_center not in lightest_edge_weight
|
| 265 |
+
or weight < lightest_edge_weight[nbr_center]
|
| 266 |
+
):
|
| 267 |
+
lightest_edge_neighbor[nbr_center] = neighbor
|
| 268 |
+
lightest_edge_weight[nbr_center] = weight
|
| 269 |
+
return lightest_edge_neighbor, lightest_edge_weight
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def _add_edge_to_spanner(H, residual_graph, u, v, weight):
|
| 273 |
+
"""Add the edge {u, v} to the spanner H and take weight from
|
| 274 |
+
the residual graph.
|
| 275 |
+
|
| 276 |
+
Parameters
|
| 277 |
+
----------
|
| 278 |
+
H : NetworkX graph
|
| 279 |
+
The spanner under construction.
|
| 280 |
+
|
| 281 |
+
residual_graph : NetworkX graph
|
| 282 |
+
The residual graph used by the Baswana-Sen algorithm. The weight
|
| 283 |
+
for the edge is taken from this graph.
|
| 284 |
+
|
| 285 |
+
u : node
|
| 286 |
+
One endpoint of the edge.
|
| 287 |
+
|
| 288 |
+
v : node
|
| 289 |
+
The other endpoint of the edge.
|
| 290 |
+
|
| 291 |
+
weight : object
|
| 292 |
+
The edge attribute to use as distance.
|
| 293 |
+
"""
|
| 294 |
+
H.add_edge(u, v)
|
| 295 |
+
if weight:
|
| 296 |
+
H[u][v][weight] = residual_graph[u][v]["weight"][0]
|