Add files using upload-large-folder tool
Browse files- pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/__pycache__/connectivity.cpython-310.pyc +0 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/__pycache__/correlation.cpython-310.pyc +0 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/__pycache__/mixing.cpython-310.pyc +0 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/__pycache__/neighbor_degree.cpython-310.pyc +0 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/tests/__pycache__/__init__.cpython-310.pyc +0 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/tests/__pycache__/test_neighbor_degree.cpython-310.pyc +0 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/tests/__pycache__/test_pairs.cpython-310.pyc +0 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/efficiency_measures.py +168 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/euler.py +469 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/graph_hashing.py +322 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/graphical.py +483 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/hierarchy.py +48 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/hybrid.py +195 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/isolate.py +107 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/link_prediction.py +688 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/lowest_common_ancestors.py +268 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/matching.py +1151 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/mis.py +77 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/moral.py +59 -0
- pythonProject/.venv/Lib/site-packages/networkx/algorithms/node_classification.py +218 -0
pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/__pycache__/connectivity.cpython-310.pyc
ADDED
|
Binary file (4.02 kB). View file
|
|
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/__pycache__/correlation.cpython-310.pyc
ADDED
|
Binary file (9.27 kB). View file
|
|
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/__pycache__/mixing.cpython-310.pyc
ADDED
|
Binary file (7.6 kB). View file
|
|
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/__pycache__/neighbor_degree.cpython-310.pyc
ADDED
|
Binary file (4.7 kB). View file
|
|
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/tests/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (196 Bytes). View file
|
|
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/tests/__pycache__/test_neighbor_degree.cpython-310.pyc
ADDED
|
Binary file (3.49 kB). View file
|
|
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/assortativity/tests/__pycache__/test_pairs.cpython-310.pyc
ADDED
|
Binary file (3.68 kB). View file
|
|
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/efficiency_measures.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# TODO This summation can be trivially parallelized.
|
| 167 |
+
efficiency_list = (global_efficiency(G.subgraph(G[v])) for v in G)
|
| 168 |
+
return sum(efficiency_list) / len(G)
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/euler.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Eulerian circuits and graphs.
|
| 3 |
+
"""
|
| 4 |
+
from itertools import combinations
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
|
| 8 |
+
from ..utils import arbitrary_element, not_implemented_for
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
"is_eulerian",
|
| 12 |
+
"eulerian_circuit",
|
| 13 |
+
"eulerize",
|
| 14 |
+
"is_semieulerian",
|
| 15 |
+
"has_eulerian_path",
|
| 16 |
+
"eulerian_path",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@nx._dispatchable
|
| 21 |
+
def is_eulerian(G):
|
| 22 |
+
"""Returns True if and only if `G` is Eulerian.
|
| 23 |
+
|
| 24 |
+
A graph is *Eulerian* if it has an Eulerian circuit. An *Eulerian
|
| 25 |
+
circuit* is a closed walk that includes each edge of a graph exactly
|
| 26 |
+
once.
|
| 27 |
+
|
| 28 |
+
Graphs with isolated vertices (i.e. vertices with zero degree) are not
|
| 29 |
+
considered to have Eulerian circuits. Therefore, if the graph is not
|
| 30 |
+
connected (or not strongly connected, for directed graphs), this function
|
| 31 |
+
returns False.
|
| 32 |
+
|
| 33 |
+
Parameters
|
| 34 |
+
----------
|
| 35 |
+
G : NetworkX graph
|
| 36 |
+
A graph, either directed or undirected.
|
| 37 |
+
|
| 38 |
+
Examples
|
| 39 |
+
--------
|
| 40 |
+
>>> nx.is_eulerian(nx.DiGraph({0: [3], 1: [2], 2: [3], 3: [0, 1]}))
|
| 41 |
+
True
|
| 42 |
+
>>> nx.is_eulerian(nx.complete_graph(5))
|
| 43 |
+
True
|
| 44 |
+
>>> nx.is_eulerian(nx.petersen_graph())
|
| 45 |
+
False
|
| 46 |
+
|
| 47 |
+
If you prefer to allow graphs with isolated vertices to have Eulerian circuits,
|
| 48 |
+
you can first remove such vertices and then call `is_eulerian` as below example shows.
|
| 49 |
+
|
| 50 |
+
>>> G = nx.Graph([(0, 1), (1, 2), (0, 2)])
|
| 51 |
+
>>> G.add_node(3)
|
| 52 |
+
>>> nx.is_eulerian(G)
|
| 53 |
+
False
|
| 54 |
+
|
| 55 |
+
>>> G.remove_nodes_from(list(nx.isolates(G)))
|
| 56 |
+
>>> nx.is_eulerian(G)
|
| 57 |
+
True
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
"""
|
| 61 |
+
if G.is_directed():
|
| 62 |
+
# Every node must have equal in degree and out degree and the
|
| 63 |
+
# graph must be strongly connected
|
| 64 |
+
return all(
|
| 65 |
+
G.in_degree(n) == G.out_degree(n) for n in G
|
| 66 |
+
) and nx.is_strongly_connected(G)
|
| 67 |
+
# An undirected Eulerian graph has no vertices of odd degree and
|
| 68 |
+
# must be connected.
|
| 69 |
+
return all(d % 2 == 0 for v, d in G.degree()) and nx.is_connected(G)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@nx._dispatchable
|
| 73 |
+
def is_semieulerian(G):
|
| 74 |
+
"""Return True iff `G` is semi-Eulerian.
|
| 75 |
+
|
| 76 |
+
G is semi-Eulerian if it has an Eulerian path but no Eulerian circuit.
|
| 77 |
+
|
| 78 |
+
See Also
|
| 79 |
+
--------
|
| 80 |
+
has_eulerian_path
|
| 81 |
+
is_eulerian
|
| 82 |
+
"""
|
| 83 |
+
return has_eulerian_path(G) and not is_eulerian(G)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _find_path_start(G):
|
| 87 |
+
"""Return a suitable starting vertex for an Eulerian path.
|
| 88 |
+
|
| 89 |
+
If no path exists, return None.
|
| 90 |
+
"""
|
| 91 |
+
if not has_eulerian_path(G):
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
if is_eulerian(G):
|
| 95 |
+
return arbitrary_element(G)
|
| 96 |
+
|
| 97 |
+
if G.is_directed():
|
| 98 |
+
v1, v2 = (v for v in G if G.in_degree(v) != G.out_degree(v))
|
| 99 |
+
# Determines which is the 'start' node (as opposed to the 'end')
|
| 100 |
+
if G.out_degree(v1) > G.in_degree(v1):
|
| 101 |
+
return v1
|
| 102 |
+
else:
|
| 103 |
+
return v2
|
| 104 |
+
|
| 105 |
+
else:
|
| 106 |
+
# In an undirected graph randomly choose one of the possibilities
|
| 107 |
+
start = [v for v in G if G.degree(v) % 2 != 0][0]
|
| 108 |
+
return start
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _simplegraph_eulerian_circuit(G, source):
|
| 112 |
+
if G.is_directed():
|
| 113 |
+
degree = G.out_degree
|
| 114 |
+
edges = G.out_edges
|
| 115 |
+
else:
|
| 116 |
+
degree = G.degree
|
| 117 |
+
edges = G.edges
|
| 118 |
+
vertex_stack = [source]
|
| 119 |
+
last_vertex = None
|
| 120 |
+
while vertex_stack:
|
| 121 |
+
current_vertex = vertex_stack[-1]
|
| 122 |
+
if degree(current_vertex) == 0:
|
| 123 |
+
if last_vertex is not None:
|
| 124 |
+
yield (last_vertex, current_vertex)
|
| 125 |
+
last_vertex = current_vertex
|
| 126 |
+
vertex_stack.pop()
|
| 127 |
+
else:
|
| 128 |
+
_, next_vertex = arbitrary_element(edges(current_vertex))
|
| 129 |
+
vertex_stack.append(next_vertex)
|
| 130 |
+
G.remove_edge(current_vertex, next_vertex)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _multigraph_eulerian_circuit(G, source):
|
| 134 |
+
if G.is_directed():
|
| 135 |
+
degree = G.out_degree
|
| 136 |
+
edges = G.out_edges
|
| 137 |
+
else:
|
| 138 |
+
degree = G.degree
|
| 139 |
+
edges = G.edges
|
| 140 |
+
vertex_stack = [(source, None)]
|
| 141 |
+
last_vertex = None
|
| 142 |
+
last_key = None
|
| 143 |
+
while vertex_stack:
|
| 144 |
+
current_vertex, current_key = vertex_stack[-1]
|
| 145 |
+
if degree(current_vertex) == 0:
|
| 146 |
+
if last_vertex is not None:
|
| 147 |
+
yield (last_vertex, current_vertex, last_key)
|
| 148 |
+
last_vertex, last_key = current_vertex, current_key
|
| 149 |
+
vertex_stack.pop()
|
| 150 |
+
else:
|
| 151 |
+
triple = arbitrary_element(edges(current_vertex, keys=True))
|
| 152 |
+
_, next_vertex, next_key = triple
|
| 153 |
+
vertex_stack.append((next_vertex, next_key))
|
| 154 |
+
G.remove_edge(current_vertex, next_vertex, next_key)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@nx._dispatchable
|
| 158 |
+
def eulerian_circuit(G, source=None, keys=False):
|
| 159 |
+
"""Returns an iterator over the edges of an Eulerian circuit in `G`.
|
| 160 |
+
|
| 161 |
+
An *Eulerian circuit* is a closed walk that includes each edge of a
|
| 162 |
+
graph exactly once.
|
| 163 |
+
|
| 164 |
+
Parameters
|
| 165 |
+
----------
|
| 166 |
+
G : NetworkX graph
|
| 167 |
+
A graph, either directed or undirected.
|
| 168 |
+
|
| 169 |
+
source : node, optional
|
| 170 |
+
Starting node for circuit.
|
| 171 |
+
|
| 172 |
+
keys : bool
|
| 173 |
+
If False, edges generated by this function will be of the form
|
| 174 |
+
``(u, v)``. Otherwise, edges will be of the form ``(u, v, k)``.
|
| 175 |
+
This option is ignored unless `G` is a multigraph.
|
| 176 |
+
|
| 177 |
+
Returns
|
| 178 |
+
-------
|
| 179 |
+
edges : iterator
|
| 180 |
+
An iterator over edges in the Eulerian circuit.
|
| 181 |
+
|
| 182 |
+
Raises
|
| 183 |
+
------
|
| 184 |
+
NetworkXError
|
| 185 |
+
If the graph is not Eulerian.
|
| 186 |
+
|
| 187 |
+
See Also
|
| 188 |
+
--------
|
| 189 |
+
is_eulerian
|
| 190 |
+
|
| 191 |
+
Notes
|
| 192 |
+
-----
|
| 193 |
+
This is a linear time implementation of an algorithm adapted from [1]_.
|
| 194 |
+
|
| 195 |
+
For general information about Euler tours, see [2]_.
|
| 196 |
+
|
| 197 |
+
References
|
| 198 |
+
----------
|
| 199 |
+
.. [1] J. Edmonds, E. L. Johnson.
|
| 200 |
+
Matching, Euler tours and the Chinese postman.
|
| 201 |
+
Mathematical programming, Volume 5, Issue 1 (1973), 111-114.
|
| 202 |
+
.. [2] https://en.wikipedia.org/wiki/Eulerian_path
|
| 203 |
+
|
| 204 |
+
Examples
|
| 205 |
+
--------
|
| 206 |
+
To get an Eulerian circuit in an undirected graph::
|
| 207 |
+
|
| 208 |
+
>>> G = nx.complete_graph(3)
|
| 209 |
+
>>> list(nx.eulerian_circuit(G))
|
| 210 |
+
[(0, 2), (2, 1), (1, 0)]
|
| 211 |
+
>>> list(nx.eulerian_circuit(G, source=1))
|
| 212 |
+
[(1, 2), (2, 0), (0, 1)]
|
| 213 |
+
|
| 214 |
+
To get the sequence of vertices in an Eulerian circuit::
|
| 215 |
+
|
| 216 |
+
>>> [u for u, v in nx.eulerian_circuit(G)]
|
| 217 |
+
[0, 2, 1]
|
| 218 |
+
|
| 219 |
+
"""
|
| 220 |
+
if not is_eulerian(G):
|
| 221 |
+
raise nx.NetworkXError("G is not Eulerian.")
|
| 222 |
+
if G.is_directed():
|
| 223 |
+
G = G.reverse()
|
| 224 |
+
else:
|
| 225 |
+
G = G.copy()
|
| 226 |
+
if source is None:
|
| 227 |
+
source = arbitrary_element(G)
|
| 228 |
+
if G.is_multigraph():
|
| 229 |
+
for u, v, k in _multigraph_eulerian_circuit(G, source):
|
| 230 |
+
if keys:
|
| 231 |
+
yield u, v, k
|
| 232 |
+
else:
|
| 233 |
+
yield u, v
|
| 234 |
+
else:
|
| 235 |
+
yield from _simplegraph_eulerian_circuit(G, source)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
@nx._dispatchable
|
| 239 |
+
def has_eulerian_path(G, source=None):
|
| 240 |
+
"""Return True iff `G` has an Eulerian path.
|
| 241 |
+
|
| 242 |
+
An Eulerian path is a path in a graph which uses each edge of a graph
|
| 243 |
+
exactly once. If `source` is specified, then this function checks
|
| 244 |
+
whether an Eulerian path that starts at node `source` exists.
|
| 245 |
+
|
| 246 |
+
A directed graph has an Eulerian path iff:
|
| 247 |
+
- at most one vertex has out_degree - in_degree = 1,
|
| 248 |
+
- at most one vertex has in_degree - out_degree = 1,
|
| 249 |
+
- every other vertex has equal in_degree and out_degree,
|
| 250 |
+
- and all of its vertices belong to a single connected
|
| 251 |
+
component of the underlying undirected graph.
|
| 252 |
+
|
| 253 |
+
If `source` is not None, an Eulerian path starting at `source` exists if no
|
| 254 |
+
other node has out_degree - in_degree = 1. This is equivalent to either
|
| 255 |
+
there exists an Eulerian circuit or `source` has out_degree - in_degree = 1
|
| 256 |
+
and the conditions above hold.
|
| 257 |
+
|
| 258 |
+
An undirected graph has an Eulerian path iff:
|
| 259 |
+
- exactly zero or two vertices have odd degree,
|
| 260 |
+
- and all of its vertices belong to a single connected component.
|
| 261 |
+
|
| 262 |
+
If `source` is not None, an Eulerian path starting at `source` exists if
|
| 263 |
+
either there exists an Eulerian circuit or `source` has an odd degree and the
|
| 264 |
+
conditions above hold.
|
| 265 |
+
|
| 266 |
+
Graphs with isolated vertices (i.e. vertices with zero degree) are not considered
|
| 267 |
+
to have an Eulerian path. Therefore, if the graph is not connected (or not strongly
|
| 268 |
+
connected, for directed graphs), this function returns False.
|
| 269 |
+
|
| 270 |
+
Parameters
|
| 271 |
+
----------
|
| 272 |
+
G : NetworkX Graph
|
| 273 |
+
The graph to find an euler path in.
|
| 274 |
+
|
| 275 |
+
source : node, optional
|
| 276 |
+
Starting node for path.
|
| 277 |
+
|
| 278 |
+
Returns
|
| 279 |
+
-------
|
| 280 |
+
Bool : True if G has an Eulerian path.
|
| 281 |
+
|
| 282 |
+
Examples
|
| 283 |
+
--------
|
| 284 |
+
If you prefer to allow graphs with isolated vertices to have Eulerian path,
|
| 285 |
+
you can first remove such vertices and then call `has_eulerian_path` as below example shows.
|
| 286 |
+
|
| 287 |
+
>>> G = nx.Graph([(0, 1), (1, 2), (0, 2)])
|
| 288 |
+
>>> G.add_node(3)
|
| 289 |
+
>>> nx.has_eulerian_path(G)
|
| 290 |
+
False
|
| 291 |
+
|
| 292 |
+
>>> G.remove_nodes_from(list(nx.isolates(G)))
|
| 293 |
+
>>> nx.has_eulerian_path(G)
|
| 294 |
+
True
|
| 295 |
+
|
| 296 |
+
See Also
|
| 297 |
+
--------
|
| 298 |
+
is_eulerian
|
| 299 |
+
eulerian_path
|
| 300 |
+
"""
|
| 301 |
+
if nx.is_eulerian(G):
|
| 302 |
+
return True
|
| 303 |
+
|
| 304 |
+
if G.is_directed():
|
| 305 |
+
ins = G.in_degree
|
| 306 |
+
outs = G.out_degree
|
| 307 |
+
# Since we know it is not eulerian, outs - ins must be 1 for source
|
| 308 |
+
if source is not None and outs[source] - ins[source] != 1:
|
| 309 |
+
return False
|
| 310 |
+
|
| 311 |
+
unbalanced_ins = 0
|
| 312 |
+
unbalanced_outs = 0
|
| 313 |
+
for v in G:
|
| 314 |
+
if ins[v] - outs[v] == 1:
|
| 315 |
+
unbalanced_ins += 1
|
| 316 |
+
elif outs[v] - ins[v] == 1:
|
| 317 |
+
unbalanced_outs += 1
|
| 318 |
+
elif ins[v] != outs[v]:
|
| 319 |
+
return False
|
| 320 |
+
|
| 321 |
+
return (
|
| 322 |
+
unbalanced_ins <= 1 and unbalanced_outs <= 1 and nx.is_weakly_connected(G)
|
| 323 |
+
)
|
| 324 |
+
else:
|
| 325 |
+
# We know it is not eulerian, so degree of source must be odd.
|
| 326 |
+
if source is not None and G.degree[source] % 2 != 1:
|
| 327 |
+
return False
|
| 328 |
+
|
| 329 |
+
# Sum is 2 since we know it is not eulerian (which implies sum is 0)
|
| 330 |
+
return sum(d % 2 == 1 for v, d in G.degree()) == 2 and nx.is_connected(G)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
@nx._dispatchable
|
| 334 |
+
def eulerian_path(G, source=None, keys=False):
|
| 335 |
+
"""Return an iterator over the edges of an Eulerian path in `G`.
|
| 336 |
+
|
| 337 |
+
Parameters
|
| 338 |
+
----------
|
| 339 |
+
G : NetworkX Graph
|
| 340 |
+
The graph in which to look for an eulerian path.
|
| 341 |
+
source : node or None (default: None)
|
| 342 |
+
The node at which to start the search. None means search over all
|
| 343 |
+
starting nodes.
|
| 344 |
+
keys : Bool (default: False)
|
| 345 |
+
Indicates whether to yield edge 3-tuples (u, v, edge_key).
|
| 346 |
+
The default yields edge 2-tuples
|
| 347 |
+
|
| 348 |
+
Yields
|
| 349 |
+
------
|
| 350 |
+
Edge tuples along the eulerian path.
|
| 351 |
+
|
| 352 |
+
Warning: If `source` provided is not the start node of an Euler path
|
| 353 |
+
will raise error even if an Euler Path exists.
|
| 354 |
+
"""
|
| 355 |
+
if not has_eulerian_path(G, source):
|
| 356 |
+
raise nx.NetworkXError("Graph has no Eulerian paths.")
|
| 357 |
+
if G.is_directed():
|
| 358 |
+
G = G.reverse()
|
| 359 |
+
if source is None or nx.is_eulerian(G) is False:
|
| 360 |
+
source = _find_path_start(G)
|
| 361 |
+
if G.is_multigraph():
|
| 362 |
+
for u, v, k in _multigraph_eulerian_circuit(G, source):
|
| 363 |
+
if keys:
|
| 364 |
+
yield u, v, k
|
| 365 |
+
else:
|
| 366 |
+
yield u, v
|
| 367 |
+
else:
|
| 368 |
+
yield from _simplegraph_eulerian_circuit(G, source)
|
| 369 |
+
else:
|
| 370 |
+
G = G.copy()
|
| 371 |
+
if source is None:
|
| 372 |
+
source = _find_path_start(G)
|
| 373 |
+
if G.is_multigraph():
|
| 374 |
+
if keys:
|
| 375 |
+
yield from reversed(
|
| 376 |
+
[(v, u, k) for u, v, k in _multigraph_eulerian_circuit(G, source)]
|
| 377 |
+
)
|
| 378 |
+
else:
|
| 379 |
+
yield from reversed(
|
| 380 |
+
[(v, u) for u, v, k in _multigraph_eulerian_circuit(G, source)]
|
| 381 |
+
)
|
| 382 |
+
else:
|
| 383 |
+
yield from reversed(
|
| 384 |
+
[(v, u) for u, v in _simplegraph_eulerian_circuit(G, source)]
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
@not_implemented_for("directed")
|
| 389 |
+
@nx._dispatchable(returns_graph=True)
|
| 390 |
+
def eulerize(G):
|
| 391 |
+
"""Transforms a graph into an Eulerian graph.
|
| 392 |
+
|
| 393 |
+
If `G` is Eulerian the result is `G` as a MultiGraph, otherwise the result is a smallest
|
| 394 |
+
(in terms of the number of edges) multigraph whose underlying simple graph is `G`.
|
| 395 |
+
|
| 396 |
+
Parameters
|
| 397 |
+
----------
|
| 398 |
+
G : NetworkX graph
|
| 399 |
+
An undirected graph
|
| 400 |
+
|
| 401 |
+
Returns
|
| 402 |
+
-------
|
| 403 |
+
G : NetworkX multigraph
|
| 404 |
+
|
| 405 |
+
Raises
|
| 406 |
+
------
|
| 407 |
+
NetworkXError
|
| 408 |
+
If the graph is not connected.
|
| 409 |
+
|
| 410 |
+
See Also
|
| 411 |
+
--------
|
| 412 |
+
is_eulerian
|
| 413 |
+
eulerian_circuit
|
| 414 |
+
|
| 415 |
+
References
|
| 416 |
+
----------
|
| 417 |
+
.. [1] J. Edmonds, E. L. Johnson.
|
| 418 |
+
Matching, Euler tours and the Chinese postman.
|
| 419 |
+
Mathematical programming, Volume 5, Issue 1 (1973), 111-114.
|
| 420 |
+
.. [2] https://en.wikipedia.org/wiki/Eulerian_path
|
| 421 |
+
.. [3] http://web.math.princeton.edu/math_alive/5/Notes1.pdf
|
| 422 |
+
|
| 423 |
+
Examples
|
| 424 |
+
--------
|
| 425 |
+
>>> G = nx.complete_graph(10)
|
| 426 |
+
>>> H = nx.eulerize(G)
|
| 427 |
+
>>> nx.is_eulerian(H)
|
| 428 |
+
True
|
| 429 |
+
|
| 430 |
+
"""
|
| 431 |
+
if G.order() == 0:
|
| 432 |
+
raise nx.NetworkXPointlessConcept("Cannot Eulerize null graph")
|
| 433 |
+
if not nx.is_connected(G):
|
| 434 |
+
raise nx.NetworkXError("G is not connected")
|
| 435 |
+
odd_degree_nodes = [n for n, d in G.degree() if d % 2 == 1]
|
| 436 |
+
G = nx.MultiGraph(G)
|
| 437 |
+
if len(odd_degree_nodes) == 0:
|
| 438 |
+
return G
|
| 439 |
+
|
| 440 |
+
# get all shortest paths between vertices of odd degree
|
| 441 |
+
odd_deg_pairs_paths = [
|
| 442 |
+
(m, {n: nx.shortest_path(G, source=m, target=n)})
|
| 443 |
+
for m, n in combinations(odd_degree_nodes, 2)
|
| 444 |
+
]
|
| 445 |
+
|
| 446 |
+
# use the number of vertices in a graph + 1 as an upper bound on
|
| 447 |
+
# the maximum length of a path in G
|
| 448 |
+
upper_bound_on_max_path_length = len(G) + 1
|
| 449 |
+
|
| 450 |
+
# use "len(G) + 1 - len(P)",
|
| 451 |
+
# where P is a shortest path between vertices n and m,
|
| 452 |
+
# as edge-weights in a new graph
|
| 453 |
+
# store the paths in the graph for easy indexing later
|
| 454 |
+
Gp = nx.Graph()
|
| 455 |
+
for n, Ps in odd_deg_pairs_paths:
|
| 456 |
+
for m, P in Ps.items():
|
| 457 |
+
if n != m:
|
| 458 |
+
Gp.add_edge(
|
| 459 |
+
m, n, weight=upper_bound_on_max_path_length - len(P), path=P
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
# find the minimum weight matching of edges in the weighted graph
|
| 463 |
+
best_matching = nx.Graph(list(nx.max_weight_matching(Gp)))
|
| 464 |
+
|
| 465 |
+
# duplicate each edge along each path in the set of paths in Gp
|
| 466 |
+
for m, n in best_matching.edges():
|
| 467 |
+
path = Gp[m][n]["path"]
|
| 468 |
+
G.add_edges_from(nx.utils.pairwise(path))
|
| 469 |
+
return G
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/graph_hashing.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr")
|
| 41 |
+
def weisfeiler_lehman_graph_hash(
|
| 42 |
+
G, edge_attr=None, node_attr=None, iterations=3, digest_size=16
|
| 43 |
+
):
|
| 44 |
+
"""Return Weisfeiler Lehman (WL) graph hash.
|
| 45 |
+
|
| 46 |
+
The function iteratively aggregates and hashes neighborhoods of each node.
|
| 47 |
+
After each node's neighbors are hashed to obtain updated node labels,
|
| 48 |
+
a hashed histogram of resulting labels is returned as the final hash.
|
| 49 |
+
|
| 50 |
+
Hashes are identical for isomorphic graphs and strong guarantees that
|
| 51 |
+
non-isomorphic graphs will get different hashes. See [1]_ for details.
|
| 52 |
+
|
| 53 |
+
If no node or edge attributes are provided, the degree of each node
|
| 54 |
+
is used as its initial label.
|
| 55 |
+
Otherwise, node and/or edge labels are used to compute the hash.
|
| 56 |
+
|
| 57 |
+
Parameters
|
| 58 |
+
----------
|
| 59 |
+
G : graph
|
| 60 |
+
The graph to be hashed.
|
| 61 |
+
Can have node and/or edge attributes. Can also have no attributes.
|
| 62 |
+
edge_attr : string, optional (default=None)
|
| 63 |
+
The key in edge attribute dictionary to be used for hashing.
|
| 64 |
+
If None, edge labels are ignored.
|
| 65 |
+
node_attr: string, optional (default=None)
|
| 66 |
+
The key in node attribute dictionary to be used for hashing.
|
| 67 |
+
If None, and no edge_attr given, use the degrees of the nodes as labels.
|
| 68 |
+
iterations: int, optional (default=3)
|
| 69 |
+
Number of neighbor aggregations to perform.
|
| 70 |
+
Should be larger for larger graphs.
|
| 71 |
+
digest_size: int, optional (default=16)
|
| 72 |
+
Size (in bits) of blake2b hash digest to use for hashing node labels.
|
| 73 |
+
|
| 74 |
+
Returns
|
| 75 |
+
-------
|
| 76 |
+
h : string
|
| 77 |
+
Hexadecimal string corresponding to hash of the input graph.
|
| 78 |
+
|
| 79 |
+
Examples
|
| 80 |
+
--------
|
| 81 |
+
Two graphs with edge attributes that are isomorphic, except for
|
| 82 |
+
differences in the edge labels.
|
| 83 |
+
|
| 84 |
+
>>> G1 = nx.Graph()
|
| 85 |
+
>>> G1.add_edges_from(
|
| 86 |
+
... [
|
| 87 |
+
... (1, 2, {"label": "A"}),
|
| 88 |
+
... (2, 3, {"label": "A"}),
|
| 89 |
+
... (3, 1, {"label": "A"}),
|
| 90 |
+
... (1, 4, {"label": "B"}),
|
| 91 |
+
... ]
|
| 92 |
+
... )
|
| 93 |
+
>>> G2 = nx.Graph()
|
| 94 |
+
>>> G2.add_edges_from(
|
| 95 |
+
... [
|
| 96 |
+
... (5, 6, {"label": "B"}),
|
| 97 |
+
... (6, 7, {"label": "A"}),
|
| 98 |
+
... (7, 5, {"label": "A"}),
|
| 99 |
+
... (7, 8, {"label": "A"}),
|
| 100 |
+
... ]
|
| 101 |
+
... )
|
| 102 |
+
|
| 103 |
+
Omitting the `edge_attr` option, results in identical hashes.
|
| 104 |
+
|
| 105 |
+
>>> nx.weisfeiler_lehman_graph_hash(G1)
|
| 106 |
+
'7bc4dde9a09d0b94c5097b219891d81a'
|
| 107 |
+
>>> nx.weisfeiler_lehman_graph_hash(G2)
|
| 108 |
+
'7bc4dde9a09d0b94c5097b219891d81a'
|
| 109 |
+
|
| 110 |
+
With edge labels, the graphs are no longer assigned
|
| 111 |
+
the same hash digest.
|
| 112 |
+
|
| 113 |
+
>>> nx.weisfeiler_lehman_graph_hash(G1, edge_attr="label")
|
| 114 |
+
'c653d85538bcf041d88c011f4f905f10'
|
| 115 |
+
>>> nx.weisfeiler_lehman_graph_hash(G2, edge_attr="label")
|
| 116 |
+
'3dcd84af1ca855d0eff3c978d88e7ec7'
|
| 117 |
+
|
| 118 |
+
Notes
|
| 119 |
+
-----
|
| 120 |
+
To return the WL hashes of each subgraph of a graph, use
|
| 121 |
+
`weisfeiler_lehman_subgraph_hashes`
|
| 122 |
+
|
| 123 |
+
Similarity between hashes does not imply similarity between graphs.
|
| 124 |
+
|
| 125 |
+
References
|
| 126 |
+
----------
|
| 127 |
+
.. [1] Shervashidze, Nino, Pascal Schweitzer, Erik Jan Van Leeuwen,
|
| 128 |
+
Kurt Mehlhorn, and Karsten M. Borgwardt. Weisfeiler Lehman
|
| 129 |
+
Graph Kernels. Journal of Machine Learning Research. 2011.
|
| 130 |
+
http://www.jmlr.org/papers/volume12/shervashidze11a/shervashidze11a.pdf
|
| 131 |
+
|
| 132 |
+
See also
|
| 133 |
+
--------
|
| 134 |
+
weisfeiler_lehman_subgraph_hashes
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
def weisfeiler_lehman_step(G, labels, edge_attr=None):
|
| 138 |
+
"""
|
| 139 |
+
Apply neighborhood aggregation to each node
|
| 140 |
+
in the graph.
|
| 141 |
+
Computes a dictionary with labels for each node.
|
| 142 |
+
"""
|
| 143 |
+
new_labels = {}
|
| 144 |
+
for node in G.nodes():
|
| 145 |
+
label = _neighborhood_aggregate(G, node, labels, edge_attr=edge_attr)
|
| 146 |
+
new_labels[node] = _hash_label(label, digest_size)
|
| 147 |
+
return new_labels
|
| 148 |
+
|
| 149 |
+
# set initial node labels
|
| 150 |
+
node_labels = _init_node_labels(G, edge_attr, node_attr)
|
| 151 |
+
|
| 152 |
+
subgraph_hash_counts = []
|
| 153 |
+
for _ in range(iterations):
|
| 154 |
+
node_labels = weisfeiler_lehman_step(G, node_labels, edge_attr=edge_attr)
|
| 155 |
+
counter = Counter(node_labels.values())
|
| 156 |
+
# sort the counter, extend total counts
|
| 157 |
+
subgraph_hash_counts.extend(sorted(counter.items(), key=lambda x: x[0]))
|
| 158 |
+
|
| 159 |
+
# hash the final counter
|
| 160 |
+
return _hash_label(str(tuple(subgraph_hash_counts)), digest_size)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr")
|
| 164 |
+
def weisfeiler_lehman_subgraph_hashes(
|
| 165 |
+
G,
|
| 166 |
+
edge_attr=None,
|
| 167 |
+
node_attr=None,
|
| 168 |
+
iterations=3,
|
| 169 |
+
digest_size=16,
|
| 170 |
+
include_initial_labels=False,
|
| 171 |
+
):
|
| 172 |
+
"""
|
| 173 |
+
Return a dictionary of subgraph hashes by node.
|
| 174 |
+
|
| 175 |
+
Dictionary keys are nodes in `G`, and values are a list of hashes.
|
| 176 |
+
Each hash corresponds to a subgraph rooted at a given node u in `G`.
|
| 177 |
+
Lists of subgraph hashes are sorted in increasing order of depth from
|
| 178 |
+
their root node, with the hash at index i corresponding to a subgraph
|
| 179 |
+
of nodes at most i edges distance from u. Thus, each list will contain
|
| 180 |
+
`iterations` elements - a hash for a subgraph at each depth. If
|
| 181 |
+
`include_initial_labels` is set to `True`, each list will additionally
|
| 182 |
+
have contain a hash of the initial node label (or equivalently a
|
| 183 |
+
subgraph of depth 0) prepended, totalling ``iterations + 1`` elements.
|
| 184 |
+
|
| 185 |
+
The function iteratively aggregates and hashes neighborhoods of each node.
|
| 186 |
+
This is achieved for each step by replacing for each node its label from
|
| 187 |
+
the previous iteration with its hashed 1-hop neighborhood aggregate.
|
| 188 |
+
The new node label is then appended to a list of node labels for each
|
| 189 |
+
node.
|
| 190 |
+
|
| 191 |
+
To aggregate neighborhoods for a node $u$ at each step, all labels of
|
| 192 |
+
nodes adjacent to $u$ are concatenated. If the `edge_attr` parameter is set,
|
| 193 |
+
labels for each neighboring node are prefixed with the value of this attribute
|
| 194 |
+
along the connecting edge from this neighbor to node $u$. The resulting string
|
| 195 |
+
is then hashed to compress this information into a fixed digest size.
|
| 196 |
+
|
| 197 |
+
Thus, at the $i$-th iteration, nodes within $i$ hops influence any given
|
| 198 |
+
hashed node label. We can therefore say that at depth $i$ for node $u$
|
| 199 |
+
we have a hash for a subgraph induced by the $i$-hop neighborhood of $u$.
|
| 200 |
+
|
| 201 |
+
The output can be used to to create general Weisfeiler-Lehman graph kernels,
|
| 202 |
+
or generate features for graphs or nodes - for example to generate 'words' in
|
| 203 |
+
a graph as seen in the 'graph2vec' algorithm.
|
| 204 |
+
See [1]_ & [2]_ respectively for details.
|
| 205 |
+
|
| 206 |
+
Hashes are identical for isomorphic subgraphs and there exist strong
|
| 207 |
+
guarantees that non-isomorphic graphs will get different hashes.
|
| 208 |
+
See [1]_ for details.
|
| 209 |
+
|
| 210 |
+
If no node or edge attributes are provided, the degree of each node
|
| 211 |
+
is used as its initial label.
|
| 212 |
+
Otherwise, node and/or edge labels are used to compute the hash.
|
| 213 |
+
|
| 214 |
+
Parameters
|
| 215 |
+
----------
|
| 216 |
+
G : graph
|
| 217 |
+
The graph to be hashed.
|
| 218 |
+
Can have node and/or edge attributes. Can also have no attributes.
|
| 219 |
+
edge_attr : string, optional (default=None)
|
| 220 |
+
The key in edge attribute dictionary to be used for hashing.
|
| 221 |
+
If None, edge labels are ignored.
|
| 222 |
+
node_attr : string, optional (default=None)
|
| 223 |
+
The key in node attribute dictionary to be used for hashing.
|
| 224 |
+
If None, and no edge_attr given, use the degrees of the nodes as labels.
|
| 225 |
+
If None, and edge_attr is given, each node starts with an identical label.
|
| 226 |
+
iterations : int, optional (default=3)
|
| 227 |
+
Number of neighbor aggregations to perform.
|
| 228 |
+
Should be larger for larger graphs.
|
| 229 |
+
digest_size : int, optional (default=16)
|
| 230 |
+
Size (in bits) of blake2b hash digest to use for hashing node labels.
|
| 231 |
+
The default size is 16 bits.
|
| 232 |
+
include_initial_labels : bool, optional (default=False)
|
| 233 |
+
If True, include the hashed initial node label as the first subgraph
|
| 234 |
+
hash for each node.
|
| 235 |
+
|
| 236 |
+
Returns
|
| 237 |
+
-------
|
| 238 |
+
node_subgraph_hashes : dict
|
| 239 |
+
A dictionary with each key given by a node in G, and each value given
|
| 240 |
+
by the subgraph hashes in order of depth from the key node.
|
| 241 |
+
|
| 242 |
+
Examples
|
| 243 |
+
--------
|
| 244 |
+
Finding similar nodes in different graphs:
|
| 245 |
+
|
| 246 |
+
>>> G1 = nx.Graph()
|
| 247 |
+
>>> G1.add_edges_from([(1, 2), (2, 3), (2, 4), (3, 5), (4, 6), (5, 7), (6, 7)])
|
| 248 |
+
>>> G2 = nx.Graph()
|
| 249 |
+
>>> G2.add_edges_from([(1, 3), (2, 3), (1, 6), (1, 5), (4, 6)])
|
| 250 |
+
>>> g1_hashes = nx.weisfeiler_lehman_subgraph_hashes(G1, iterations=3, digest_size=8)
|
| 251 |
+
>>> g2_hashes = nx.weisfeiler_lehman_subgraph_hashes(G2, iterations=3, digest_size=8)
|
| 252 |
+
|
| 253 |
+
Even though G1 and G2 are not isomorphic (they have different numbers of edges),
|
| 254 |
+
the hash sequence of depth 3 for node 1 in G1 and node 5 in G2 are similar:
|
| 255 |
+
|
| 256 |
+
>>> g1_hashes[1]
|
| 257 |
+
['a93b64973cfc8897', 'db1b43ae35a1878f', '57872a7d2059c1c0']
|
| 258 |
+
>>> g2_hashes[5]
|
| 259 |
+
['a93b64973cfc8897', 'db1b43ae35a1878f', '1716d2a4012fa4bc']
|
| 260 |
+
|
| 261 |
+
The first 2 WL subgraph hashes match. From this we can conclude that it's very
|
| 262 |
+
likely the neighborhood of 2 hops around these nodes are isomorphic.
|
| 263 |
+
|
| 264 |
+
However the 3-hop neighborhoods of ``G1`` and ``G2`` are not isomorphic since the
|
| 265 |
+
3rd hashes in the lists above are not equal.
|
| 266 |
+
|
| 267 |
+
These nodes may be candidates to be classified together since their local topology
|
| 268 |
+
is similar.
|
| 269 |
+
|
| 270 |
+
Notes
|
| 271 |
+
-----
|
| 272 |
+
To hash the full graph when subgraph hashes are not needed, use
|
| 273 |
+
`weisfeiler_lehman_graph_hash` for efficiency.
|
| 274 |
+
|
| 275 |
+
Similarity between hashes does not imply similarity between graphs.
|
| 276 |
+
|
| 277 |
+
References
|
| 278 |
+
----------
|
| 279 |
+
.. [1] Shervashidze, Nino, Pascal Schweitzer, Erik Jan Van Leeuwen,
|
| 280 |
+
Kurt Mehlhorn, and Karsten M. Borgwardt. Weisfeiler Lehman
|
| 281 |
+
Graph Kernels. Journal of Machine Learning Research. 2011.
|
| 282 |
+
http://www.jmlr.org/papers/volume12/shervashidze11a/shervashidze11a.pdf
|
| 283 |
+
.. [2] Annamalai Narayanan, Mahinthan Chandramohan, Rajasekar Venkatesan,
|
| 284 |
+
Lihui Chen, Yang Liu and Shantanu Jaiswa. graph2vec: Learning
|
| 285 |
+
Distributed Representations of Graphs. arXiv. 2017
|
| 286 |
+
https://arxiv.org/pdf/1707.05005.pdf
|
| 287 |
+
|
| 288 |
+
See also
|
| 289 |
+
--------
|
| 290 |
+
weisfeiler_lehman_graph_hash
|
| 291 |
+
"""
|
| 292 |
+
|
| 293 |
+
def weisfeiler_lehman_step(G, labels, node_subgraph_hashes, edge_attr=None):
|
| 294 |
+
"""
|
| 295 |
+
Apply neighborhood aggregation to each node
|
| 296 |
+
in the graph.
|
| 297 |
+
Computes a dictionary with labels for each node.
|
| 298 |
+
Appends the new hashed label to the dictionary of subgraph hashes
|
| 299 |
+
originating from and indexed by each node in G
|
| 300 |
+
"""
|
| 301 |
+
new_labels = {}
|
| 302 |
+
for node in G.nodes():
|
| 303 |
+
label = _neighborhood_aggregate(G, node, labels, edge_attr=edge_attr)
|
| 304 |
+
hashed_label = _hash_label(label, digest_size)
|
| 305 |
+
new_labels[node] = hashed_label
|
| 306 |
+
node_subgraph_hashes[node].append(hashed_label)
|
| 307 |
+
return new_labels
|
| 308 |
+
|
| 309 |
+
node_labels = _init_node_labels(G, edge_attr, node_attr)
|
| 310 |
+
if include_initial_labels:
|
| 311 |
+
node_subgraph_hashes = {
|
| 312 |
+
k: [_hash_label(v, digest_size)] for k, v in node_labels.items()
|
| 313 |
+
}
|
| 314 |
+
else:
|
| 315 |
+
node_subgraph_hashes = defaultdict(list)
|
| 316 |
+
|
| 317 |
+
for _ in range(iterations):
|
| 318 |
+
node_labels = weisfeiler_lehman_step(
|
| 319 |
+
G, node_labels, node_subgraph_hashes, edge_attr
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
return dict(node_subgraph_hashes)
|
pythonProject/.venv/Lib/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
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/hierarchy.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Flow Hierarchy.
|
| 3 |
+
"""
|
| 4 |
+
import networkx as nx
|
| 5 |
+
|
| 6 |
+
__all__ = ["flow_hierarchy"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 10 |
+
def flow_hierarchy(G, weight=None):
|
| 11 |
+
"""Returns the flow hierarchy of a directed network.
|
| 12 |
+
|
| 13 |
+
Flow hierarchy is defined as the fraction of edges not participating
|
| 14 |
+
in cycles in a directed graph [1]_.
|
| 15 |
+
|
| 16 |
+
Parameters
|
| 17 |
+
----------
|
| 18 |
+
G : DiGraph or MultiDiGraph
|
| 19 |
+
A directed graph
|
| 20 |
+
|
| 21 |
+
weight : string, optional (default=None)
|
| 22 |
+
Attribute to use for edge weights. If None the weight defaults to 1.
|
| 23 |
+
|
| 24 |
+
Returns
|
| 25 |
+
-------
|
| 26 |
+
h : float
|
| 27 |
+
Flow hierarchy value
|
| 28 |
+
|
| 29 |
+
Notes
|
| 30 |
+
-----
|
| 31 |
+
The algorithm described in [1]_ computes the flow hierarchy through
|
| 32 |
+
exponentiation of the adjacency matrix. This function implements an
|
| 33 |
+
alternative approach that finds strongly connected components.
|
| 34 |
+
An edge is in a cycle if and only if it is in a strongly connected
|
| 35 |
+
component, which can be found in $O(m)$ time using Tarjan's algorithm.
|
| 36 |
+
|
| 37 |
+
References
|
| 38 |
+
----------
|
| 39 |
+
.. [1] Luo, J.; Magee, C.L. (2011),
|
| 40 |
+
Detecting evolving patterns of self-organizing networks by flow
|
| 41 |
+
hierarchy measurement, Complexity, Volume 16 Issue 6 53-61.
|
| 42 |
+
DOI: 10.1002/cplx.20368
|
| 43 |
+
http://web.mit.edu/~cmagee/www/documents/28-DetectingEvolvingPatterns_FlowHierarchy.pdf
|
| 44 |
+
"""
|
| 45 |
+
if not G.is_directed():
|
| 46 |
+
raise nx.NetworkXError("G must be a digraph in flow_hierarchy")
|
| 47 |
+
scc = nx.strongly_connected_components(G)
|
| 48 |
+
return 1 - sum(G.subgraph(c).size(weight) for c in scc) / G.size(weight)
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/hybrid.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Provides functions for finding and testing for locally `(k, l)`-connected
|
| 3 |
+
graphs.
|
| 4 |
+
|
| 5 |
+
"""
|
| 6 |
+
import copy
|
| 7 |
+
|
| 8 |
+
import networkx as nx
|
| 9 |
+
|
| 10 |
+
__all__ = ["kl_connected_subgraph", "is_kl_connected"]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@nx._dispatchable(returns_graph=True)
|
| 14 |
+
def kl_connected_subgraph(G, k, l, low_memory=False, same_as_graph=False):
|
| 15 |
+
"""Returns the maximum locally `(k, l)`-connected subgraph of `G`.
|
| 16 |
+
|
| 17 |
+
A graph is locally `(k, l)`-connected if for each edge `(u, v)` in the
|
| 18 |
+
graph there are at least `l` edge-disjoint paths of length at most `k`
|
| 19 |
+
joining `u` to `v`.
|
| 20 |
+
|
| 21 |
+
Parameters
|
| 22 |
+
----------
|
| 23 |
+
G : NetworkX graph
|
| 24 |
+
The graph in which to find a maximum locally `(k, l)`-connected
|
| 25 |
+
subgraph.
|
| 26 |
+
|
| 27 |
+
k : integer
|
| 28 |
+
The maximum length of paths to consider. A higher number means a looser
|
| 29 |
+
connectivity requirement.
|
| 30 |
+
|
| 31 |
+
l : integer
|
| 32 |
+
The number of edge-disjoint paths. A higher number means a stricter
|
| 33 |
+
connectivity requirement.
|
| 34 |
+
|
| 35 |
+
low_memory : bool
|
| 36 |
+
If this is True, this function uses an algorithm that uses slightly
|
| 37 |
+
more time but less memory.
|
| 38 |
+
|
| 39 |
+
same_as_graph : bool
|
| 40 |
+
If True then return a tuple of the form `(H, is_same)`,
|
| 41 |
+
where `H` is the maximum locally `(k, l)`-connected subgraph and
|
| 42 |
+
`is_same` is a Boolean representing whether `G` is locally `(k,
|
| 43 |
+
l)`-connected (and hence, whether `H` is simply a copy of the input
|
| 44 |
+
graph `G`).
|
| 45 |
+
|
| 46 |
+
Returns
|
| 47 |
+
-------
|
| 48 |
+
NetworkX graph or two-tuple
|
| 49 |
+
If `same_as_graph` is True, then this function returns a
|
| 50 |
+
two-tuple as described above. Otherwise, it returns only the maximum
|
| 51 |
+
locally `(k, l)`-connected subgraph.
|
| 52 |
+
|
| 53 |
+
See also
|
| 54 |
+
--------
|
| 55 |
+
is_kl_connected
|
| 56 |
+
|
| 57 |
+
References
|
| 58 |
+
----------
|
| 59 |
+
.. [1] Chung, Fan and Linyuan Lu. "The Small World Phenomenon in Hybrid
|
| 60 |
+
Power Law Graphs." *Complex Networks*. Springer Berlin Heidelberg,
|
| 61 |
+
2004. 89--104.
|
| 62 |
+
|
| 63 |
+
"""
|
| 64 |
+
H = copy.deepcopy(G) # subgraph we construct by removing from G
|
| 65 |
+
|
| 66 |
+
graphOK = True
|
| 67 |
+
deleted_some = True # hack to start off the while loop
|
| 68 |
+
while deleted_some:
|
| 69 |
+
deleted_some = False
|
| 70 |
+
# We use `for edge in list(H.edges()):` instead of
|
| 71 |
+
# `for edge in H.edges():` because we edit the graph `H` in
|
| 72 |
+
# the loop. Hence using an iterator will result in
|
| 73 |
+
# `RuntimeError: dictionary changed size during iteration`
|
| 74 |
+
for edge in list(H.edges()):
|
| 75 |
+
(u, v) = edge
|
| 76 |
+
# Get copy of graph needed for this search
|
| 77 |
+
if low_memory:
|
| 78 |
+
verts = {u, v}
|
| 79 |
+
for i in range(k):
|
| 80 |
+
for w in verts.copy():
|
| 81 |
+
verts.update(G[w])
|
| 82 |
+
G2 = G.subgraph(verts).copy()
|
| 83 |
+
else:
|
| 84 |
+
G2 = copy.deepcopy(G)
|
| 85 |
+
###
|
| 86 |
+
path = [u, v]
|
| 87 |
+
cnt = 0
|
| 88 |
+
accept = 0
|
| 89 |
+
while path:
|
| 90 |
+
cnt += 1 # Found a path
|
| 91 |
+
if cnt >= l:
|
| 92 |
+
accept = 1
|
| 93 |
+
break
|
| 94 |
+
# record edges along this graph
|
| 95 |
+
prev = u
|
| 96 |
+
for w in path:
|
| 97 |
+
if prev != w:
|
| 98 |
+
G2.remove_edge(prev, w)
|
| 99 |
+
prev = w
|
| 100 |
+
# path = shortest_path(G2, u, v, k) # ??? should "Cutoff" be k+1?
|
| 101 |
+
try:
|
| 102 |
+
path = nx.shortest_path(G2, u, v) # ??? should "Cutoff" be k+1?
|
| 103 |
+
except nx.NetworkXNoPath:
|
| 104 |
+
path = False
|
| 105 |
+
# No Other Paths
|
| 106 |
+
if accept == 0:
|
| 107 |
+
H.remove_edge(u, v)
|
| 108 |
+
deleted_some = True
|
| 109 |
+
if graphOK:
|
| 110 |
+
graphOK = False
|
| 111 |
+
# We looked through all edges and removed none of them.
|
| 112 |
+
# So, H is the maximal (k,l)-connected subgraph of G
|
| 113 |
+
if same_as_graph:
|
| 114 |
+
return (H, graphOK)
|
| 115 |
+
return H
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@nx._dispatchable
|
| 119 |
+
def is_kl_connected(G, k, l, low_memory=False):
|
| 120 |
+
"""Returns True if and only if `G` is locally `(k, l)`-connected.
|
| 121 |
+
|
| 122 |
+
A graph is locally `(k, l)`-connected if for each edge `(u, v)` in the
|
| 123 |
+
graph there are at least `l` edge-disjoint paths of length at most `k`
|
| 124 |
+
joining `u` to `v`.
|
| 125 |
+
|
| 126 |
+
Parameters
|
| 127 |
+
----------
|
| 128 |
+
G : NetworkX graph
|
| 129 |
+
The graph to test for local `(k, l)`-connectedness.
|
| 130 |
+
|
| 131 |
+
k : integer
|
| 132 |
+
The maximum length of paths to consider. A higher number means a looser
|
| 133 |
+
connectivity requirement.
|
| 134 |
+
|
| 135 |
+
l : integer
|
| 136 |
+
The number of edge-disjoint paths. A higher number means a stricter
|
| 137 |
+
connectivity requirement.
|
| 138 |
+
|
| 139 |
+
low_memory : bool
|
| 140 |
+
If this is True, this function uses an algorithm that uses slightly
|
| 141 |
+
more time but less memory.
|
| 142 |
+
|
| 143 |
+
Returns
|
| 144 |
+
-------
|
| 145 |
+
bool
|
| 146 |
+
Whether the graph is locally `(k, l)`-connected subgraph.
|
| 147 |
+
|
| 148 |
+
See also
|
| 149 |
+
--------
|
| 150 |
+
kl_connected_subgraph
|
| 151 |
+
|
| 152 |
+
References
|
| 153 |
+
----------
|
| 154 |
+
.. [1] Chung, Fan and Linyuan Lu. "The Small World Phenomenon in Hybrid
|
| 155 |
+
Power Law Graphs." *Complex Networks*. Springer Berlin Heidelberg,
|
| 156 |
+
2004. 89--104.
|
| 157 |
+
|
| 158 |
+
"""
|
| 159 |
+
graphOK = True
|
| 160 |
+
for edge in G.edges():
|
| 161 |
+
(u, v) = edge
|
| 162 |
+
# Get copy of graph needed for this search
|
| 163 |
+
if low_memory:
|
| 164 |
+
verts = {u, v}
|
| 165 |
+
for i in range(k):
|
| 166 |
+
[verts.update(G.neighbors(w)) for w in verts.copy()]
|
| 167 |
+
G2 = G.subgraph(verts)
|
| 168 |
+
else:
|
| 169 |
+
G2 = copy.deepcopy(G)
|
| 170 |
+
###
|
| 171 |
+
path = [u, v]
|
| 172 |
+
cnt = 0
|
| 173 |
+
accept = 0
|
| 174 |
+
while path:
|
| 175 |
+
cnt += 1 # Found a path
|
| 176 |
+
if cnt >= l:
|
| 177 |
+
accept = 1
|
| 178 |
+
break
|
| 179 |
+
# record edges along this graph
|
| 180 |
+
prev = u
|
| 181 |
+
for w in path:
|
| 182 |
+
if w != prev:
|
| 183 |
+
G2.remove_edge(prev, w)
|
| 184 |
+
prev = w
|
| 185 |
+
# path = shortest_path(G2, u, v, k) # ??? should "Cutoff" be k+1?
|
| 186 |
+
try:
|
| 187 |
+
path = nx.shortest_path(G2, u, v) # ??? should "Cutoff" be k+1?
|
| 188 |
+
except nx.NetworkXNoPath:
|
| 189 |
+
path = False
|
| 190 |
+
# No Other Paths
|
| 191 |
+
if accept == 0:
|
| 192 |
+
graphOK = False
|
| 193 |
+
break
|
| 194 |
+
# return status
|
| 195 |
+
return graphOK
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/isolate.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Functions for identifying isolate (degree zero) nodes.
|
| 3 |
+
"""
|
| 4 |
+
import networkx as nx
|
| 5 |
+
|
| 6 |
+
__all__ = ["is_isolate", "isolates", "number_of_isolates"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@nx._dispatchable
|
| 10 |
+
def is_isolate(G, n):
|
| 11 |
+
"""Determines whether a node is an isolate.
|
| 12 |
+
|
| 13 |
+
An *isolate* is a node with no neighbors (that is, with degree
|
| 14 |
+
zero). For directed graphs, this means no in-neighbors and no
|
| 15 |
+
out-neighbors.
|
| 16 |
+
|
| 17 |
+
Parameters
|
| 18 |
+
----------
|
| 19 |
+
G : NetworkX graph
|
| 20 |
+
|
| 21 |
+
n : node
|
| 22 |
+
A node in `G`.
|
| 23 |
+
|
| 24 |
+
Returns
|
| 25 |
+
-------
|
| 26 |
+
is_isolate : bool
|
| 27 |
+
True if and only if `n` has no neighbors.
|
| 28 |
+
|
| 29 |
+
Examples
|
| 30 |
+
--------
|
| 31 |
+
>>> G = nx.Graph()
|
| 32 |
+
>>> G.add_edge(1, 2)
|
| 33 |
+
>>> G.add_node(3)
|
| 34 |
+
>>> nx.is_isolate(G, 2)
|
| 35 |
+
False
|
| 36 |
+
>>> nx.is_isolate(G, 3)
|
| 37 |
+
True
|
| 38 |
+
"""
|
| 39 |
+
return G.degree(n) == 0
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@nx._dispatchable
|
| 43 |
+
def isolates(G):
|
| 44 |
+
"""Iterator over isolates in the graph.
|
| 45 |
+
|
| 46 |
+
An *isolate* is a node with no neighbors (that is, with degree
|
| 47 |
+
zero). For directed graphs, this means no in-neighbors and no
|
| 48 |
+
out-neighbors.
|
| 49 |
+
|
| 50 |
+
Parameters
|
| 51 |
+
----------
|
| 52 |
+
G : NetworkX graph
|
| 53 |
+
|
| 54 |
+
Returns
|
| 55 |
+
-------
|
| 56 |
+
iterator
|
| 57 |
+
An iterator over the isolates of `G`.
|
| 58 |
+
|
| 59 |
+
Examples
|
| 60 |
+
--------
|
| 61 |
+
To get a list of all isolates of a graph, use the :class:`list`
|
| 62 |
+
constructor::
|
| 63 |
+
|
| 64 |
+
>>> G = nx.Graph()
|
| 65 |
+
>>> G.add_edge(1, 2)
|
| 66 |
+
>>> G.add_node(3)
|
| 67 |
+
>>> list(nx.isolates(G))
|
| 68 |
+
[3]
|
| 69 |
+
|
| 70 |
+
To remove all isolates in the graph, first create a list of the
|
| 71 |
+
isolates, then use :meth:`Graph.remove_nodes_from`::
|
| 72 |
+
|
| 73 |
+
>>> G.remove_nodes_from(list(nx.isolates(G)))
|
| 74 |
+
>>> list(G)
|
| 75 |
+
[1, 2]
|
| 76 |
+
|
| 77 |
+
For digraphs, isolates have zero in-degree and zero out_degre::
|
| 78 |
+
|
| 79 |
+
>>> G = nx.DiGraph([(0, 1), (1, 2)])
|
| 80 |
+
>>> G.add_node(3)
|
| 81 |
+
>>> list(nx.isolates(G))
|
| 82 |
+
[3]
|
| 83 |
+
|
| 84 |
+
"""
|
| 85 |
+
return (n for n, d in G.degree() if d == 0)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@nx._dispatchable
|
| 89 |
+
def number_of_isolates(G):
|
| 90 |
+
"""Returns the number of isolates in the graph.
|
| 91 |
+
|
| 92 |
+
An *isolate* is a node with no neighbors (that is, with degree
|
| 93 |
+
zero). For directed graphs, this means no in-neighbors and no
|
| 94 |
+
out-neighbors.
|
| 95 |
+
|
| 96 |
+
Parameters
|
| 97 |
+
----------
|
| 98 |
+
G : NetworkX graph
|
| 99 |
+
|
| 100 |
+
Returns
|
| 101 |
+
-------
|
| 102 |
+
int
|
| 103 |
+
The number of degree zero nodes in the graph `G`.
|
| 104 |
+
|
| 105 |
+
"""
|
| 106 |
+
# TODO This can be parallelized.
|
| 107 |
+
return sum(1 for v in isolates(G))
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/link_prediction.py
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Link prediction algorithms.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
from math import log
|
| 7 |
+
|
| 8 |
+
import networkx as nx
|
| 9 |
+
from networkx.utils import not_implemented_for
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"resource_allocation_index",
|
| 13 |
+
"jaccard_coefficient",
|
| 14 |
+
"adamic_adar_index",
|
| 15 |
+
"preferential_attachment",
|
| 16 |
+
"cn_soundarajan_hopcroft",
|
| 17 |
+
"ra_index_soundarajan_hopcroft",
|
| 18 |
+
"within_inter_cluster",
|
| 19 |
+
"common_neighbor_centrality",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _apply_prediction(G, func, ebunch=None):
|
| 24 |
+
"""Applies the given function to each edge in the specified iterable
|
| 25 |
+
of edges.
|
| 26 |
+
|
| 27 |
+
`G` is an instance of :class:`networkx.Graph`.
|
| 28 |
+
|
| 29 |
+
`func` is a function on two inputs, each of which is a node in the
|
| 30 |
+
graph. The function can return anything, but it should return a
|
| 31 |
+
value representing a prediction of the likelihood of a "link"
|
| 32 |
+
joining the two nodes.
|
| 33 |
+
|
| 34 |
+
`ebunch` is an iterable of pairs of nodes. If not specified, all
|
| 35 |
+
non-edges in the graph `G` will be used.
|
| 36 |
+
|
| 37 |
+
"""
|
| 38 |
+
if ebunch is None:
|
| 39 |
+
ebunch = nx.non_edges(G)
|
| 40 |
+
else:
|
| 41 |
+
for u, v in ebunch:
|
| 42 |
+
if u not in G:
|
| 43 |
+
raise nx.NodeNotFound(f"Node {u} not in G.")
|
| 44 |
+
if v not in G:
|
| 45 |
+
raise nx.NodeNotFound(f"Node {v} not in G.")
|
| 46 |
+
return ((u, v, func(u, v)) for u, v in ebunch)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@not_implemented_for("directed")
|
| 50 |
+
@not_implemented_for("multigraph")
|
| 51 |
+
@nx._dispatchable
|
| 52 |
+
def resource_allocation_index(G, ebunch=None):
|
| 53 |
+
r"""Compute the resource allocation index of all node pairs in ebunch.
|
| 54 |
+
|
| 55 |
+
Resource allocation index of `u` and `v` is defined as
|
| 56 |
+
|
| 57 |
+
.. math::
|
| 58 |
+
|
| 59 |
+
\sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{1}{|\Gamma(w)|}
|
| 60 |
+
|
| 61 |
+
where $\Gamma(u)$ denotes the set of neighbors of $u$.
|
| 62 |
+
|
| 63 |
+
Parameters
|
| 64 |
+
----------
|
| 65 |
+
G : graph
|
| 66 |
+
A NetworkX undirected graph.
|
| 67 |
+
|
| 68 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 69 |
+
Resource allocation index will be computed for each pair of
|
| 70 |
+
nodes given in the iterable. The pairs must be given as
|
| 71 |
+
2-tuples (u, v) where u and v are nodes in the graph. If ebunch
|
| 72 |
+
is None then all nonexistent edges in the graph will be used.
|
| 73 |
+
Default value: None.
|
| 74 |
+
|
| 75 |
+
Returns
|
| 76 |
+
-------
|
| 77 |
+
piter : iterator
|
| 78 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 79 |
+
pair of nodes and p is their resource allocation index.
|
| 80 |
+
|
| 81 |
+
Raises
|
| 82 |
+
------
|
| 83 |
+
NetworkXNotImplemented
|
| 84 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 85 |
+
|
| 86 |
+
NodeNotFound
|
| 87 |
+
If `ebunch` has a node that is not in `G`.
|
| 88 |
+
|
| 89 |
+
Examples
|
| 90 |
+
--------
|
| 91 |
+
>>> G = nx.complete_graph(5)
|
| 92 |
+
>>> preds = nx.resource_allocation_index(G, [(0, 1), (2, 3)])
|
| 93 |
+
>>> for u, v, p in preds:
|
| 94 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 95 |
+
(0, 1) -> 0.75000000
|
| 96 |
+
(2, 3) -> 0.75000000
|
| 97 |
+
|
| 98 |
+
References
|
| 99 |
+
----------
|
| 100 |
+
.. [1] T. Zhou, L. Lu, Y.-C. Zhang.
|
| 101 |
+
Predicting missing links via local information.
|
| 102 |
+
Eur. Phys. J. B 71 (2009) 623.
|
| 103 |
+
https://arxiv.org/pdf/0901.0553.pdf
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
def predict(u, v):
|
| 107 |
+
return sum(1 / G.degree(w) for w in nx.common_neighbors(G, u, v))
|
| 108 |
+
|
| 109 |
+
return _apply_prediction(G, predict, ebunch)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@not_implemented_for("directed")
|
| 113 |
+
@not_implemented_for("multigraph")
|
| 114 |
+
@nx._dispatchable
|
| 115 |
+
def jaccard_coefficient(G, ebunch=None):
|
| 116 |
+
r"""Compute the Jaccard coefficient of all node pairs in ebunch.
|
| 117 |
+
|
| 118 |
+
Jaccard coefficient of nodes `u` and `v` is defined as
|
| 119 |
+
|
| 120 |
+
.. math::
|
| 121 |
+
|
| 122 |
+
\frac{|\Gamma(u) \cap \Gamma(v)|}{|\Gamma(u) \cup \Gamma(v)|}
|
| 123 |
+
|
| 124 |
+
where $\Gamma(u)$ denotes the set of neighbors of $u$.
|
| 125 |
+
|
| 126 |
+
Parameters
|
| 127 |
+
----------
|
| 128 |
+
G : graph
|
| 129 |
+
A NetworkX undirected graph.
|
| 130 |
+
|
| 131 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 132 |
+
Jaccard coefficient will be computed for each pair of nodes
|
| 133 |
+
given in the iterable. The pairs must be given as 2-tuples
|
| 134 |
+
(u, v) where u and v are nodes in the graph. If ebunch is None
|
| 135 |
+
then all nonexistent edges in the graph will be used.
|
| 136 |
+
Default value: None.
|
| 137 |
+
|
| 138 |
+
Returns
|
| 139 |
+
-------
|
| 140 |
+
piter : iterator
|
| 141 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 142 |
+
pair of nodes and p is their Jaccard coefficient.
|
| 143 |
+
|
| 144 |
+
Raises
|
| 145 |
+
------
|
| 146 |
+
NetworkXNotImplemented
|
| 147 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 148 |
+
|
| 149 |
+
NodeNotFound
|
| 150 |
+
If `ebunch` has a node that is not in `G`.
|
| 151 |
+
|
| 152 |
+
Examples
|
| 153 |
+
--------
|
| 154 |
+
>>> G = nx.complete_graph(5)
|
| 155 |
+
>>> preds = nx.jaccard_coefficient(G, [(0, 1), (2, 3)])
|
| 156 |
+
>>> for u, v, p in preds:
|
| 157 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 158 |
+
(0, 1) -> 0.60000000
|
| 159 |
+
(2, 3) -> 0.60000000
|
| 160 |
+
|
| 161 |
+
References
|
| 162 |
+
----------
|
| 163 |
+
.. [1] D. Liben-Nowell, J. Kleinberg.
|
| 164 |
+
The Link Prediction Problem for Social Networks (2004).
|
| 165 |
+
http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
def predict(u, v):
|
| 169 |
+
union_size = len(set(G[u]) | set(G[v]))
|
| 170 |
+
if union_size == 0:
|
| 171 |
+
return 0
|
| 172 |
+
return len(nx.common_neighbors(G, u, v)) / union_size
|
| 173 |
+
|
| 174 |
+
return _apply_prediction(G, predict, ebunch)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@not_implemented_for("directed")
|
| 178 |
+
@not_implemented_for("multigraph")
|
| 179 |
+
@nx._dispatchable
|
| 180 |
+
def adamic_adar_index(G, ebunch=None):
|
| 181 |
+
r"""Compute the Adamic-Adar index of all node pairs in ebunch.
|
| 182 |
+
|
| 183 |
+
Adamic-Adar index of `u` and `v` is defined as
|
| 184 |
+
|
| 185 |
+
.. math::
|
| 186 |
+
|
| 187 |
+
\sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{1}{\log |\Gamma(w)|}
|
| 188 |
+
|
| 189 |
+
where $\Gamma(u)$ denotes the set of neighbors of $u$.
|
| 190 |
+
This index leads to zero-division for nodes only connected via self-loops.
|
| 191 |
+
It is intended to be used when no self-loops are present.
|
| 192 |
+
|
| 193 |
+
Parameters
|
| 194 |
+
----------
|
| 195 |
+
G : graph
|
| 196 |
+
NetworkX undirected graph.
|
| 197 |
+
|
| 198 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 199 |
+
Adamic-Adar index will be computed for each pair of nodes given
|
| 200 |
+
in the iterable. The pairs must be given as 2-tuples (u, v)
|
| 201 |
+
where u and v are nodes in the graph. If ebunch is None then all
|
| 202 |
+
nonexistent edges in the graph will be used.
|
| 203 |
+
Default value: None.
|
| 204 |
+
|
| 205 |
+
Returns
|
| 206 |
+
-------
|
| 207 |
+
piter : iterator
|
| 208 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 209 |
+
pair of nodes and p is their Adamic-Adar index.
|
| 210 |
+
|
| 211 |
+
Raises
|
| 212 |
+
------
|
| 213 |
+
NetworkXNotImplemented
|
| 214 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 215 |
+
|
| 216 |
+
NodeNotFound
|
| 217 |
+
If `ebunch` has a node that is not in `G`.
|
| 218 |
+
|
| 219 |
+
Examples
|
| 220 |
+
--------
|
| 221 |
+
>>> G = nx.complete_graph(5)
|
| 222 |
+
>>> preds = nx.adamic_adar_index(G, [(0, 1), (2, 3)])
|
| 223 |
+
>>> for u, v, p in preds:
|
| 224 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 225 |
+
(0, 1) -> 2.16404256
|
| 226 |
+
(2, 3) -> 2.16404256
|
| 227 |
+
|
| 228 |
+
References
|
| 229 |
+
----------
|
| 230 |
+
.. [1] D. Liben-Nowell, J. Kleinberg.
|
| 231 |
+
The Link Prediction Problem for Social Networks (2004).
|
| 232 |
+
http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
|
| 233 |
+
"""
|
| 234 |
+
|
| 235 |
+
def predict(u, v):
|
| 236 |
+
return sum(1 / log(G.degree(w)) for w in nx.common_neighbors(G, u, v))
|
| 237 |
+
|
| 238 |
+
return _apply_prediction(G, predict, ebunch)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
@not_implemented_for("directed")
|
| 242 |
+
@not_implemented_for("multigraph")
|
| 243 |
+
@nx._dispatchable
|
| 244 |
+
def common_neighbor_centrality(G, ebunch=None, alpha=0.8):
|
| 245 |
+
r"""Return the CCPA score for each pair of nodes.
|
| 246 |
+
|
| 247 |
+
Compute the Common Neighbor and Centrality based Parameterized Algorithm(CCPA)
|
| 248 |
+
score of all node pairs in ebunch.
|
| 249 |
+
|
| 250 |
+
CCPA score of `u` and `v` is defined as
|
| 251 |
+
|
| 252 |
+
.. math::
|
| 253 |
+
|
| 254 |
+
\alpha \cdot (|\Gamma (u){\cap }^{}\Gamma (v)|)+(1-\alpha )\cdot \frac{N}{{d}_{uv}}
|
| 255 |
+
|
| 256 |
+
where $\Gamma(u)$ denotes the set of neighbors of $u$, $\Gamma(v)$ denotes the
|
| 257 |
+
set of neighbors of $v$, $\alpha$ is parameter varies between [0,1], $N$ denotes
|
| 258 |
+
total number of nodes in the Graph and ${d}_{uv}$ denotes shortest distance
|
| 259 |
+
between $u$ and $v$.
|
| 260 |
+
|
| 261 |
+
This algorithm is based on two vital properties of nodes, namely the number
|
| 262 |
+
of common neighbors and their centrality. Common neighbor refers to the common
|
| 263 |
+
nodes between two nodes. Centrality refers to the prestige that a node enjoys
|
| 264 |
+
in a network.
|
| 265 |
+
|
| 266 |
+
.. seealso::
|
| 267 |
+
|
| 268 |
+
:func:`common_neighbors`
|
| 269 |
+
|
| 270 |
+
Parameters
|
| 271 |
+
----------
|
| 272 |
+
G : graph
|
| 273 |
+
NetworkX undirected graph.
|
| 274 |
+
|
| 275 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 276 |
+
Preferential attachment score will be computed for each pair of
|
| 277 |
+
nodes given in the iterable. The pairs must be given as
|
| 278 |
+
2-tuples (u, v) where u and v are nodes in the graph. If ebunch
|
| 279 |
+
is None then all nonexistent edges in the graph will be used.
|
| 280 |
+
Default value: None.
|
| 281 |
+
|
| 282 |
+
alpha : Parameter defined for participation of Common Neighbor
|
| 283 |
+
and Centrality Algorithm share. Values for alpha should
|
| 284 |
+
normally be between 0 and 1. Default value set to 0.8
|
| 285 |
+
because author found better performance at 0.8 for all the
|
| 286 |
+
dataset.
|
| 287 |
+
Default value: 0.8
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
Returns
|
| 291 |
+
-------
|
| 292 |
+
piter : iterator
|
| 293 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 294 |
+
pair of nodes and p is their Common Neighbor and Centrality based
|
| 295 |
+
Parameterized Algorithm(CCPA) score.
|
| 296 |
+
|
| 297 |
+
Raises
|
| 298 |
+
------
|
| 299 |
+
NetworkXNotImplemented
|
| 300 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 301 |
+
|
| 302 |
+
NetworkXAlgorithmError
|
| 303 |
+
If self loops exsists in `ebunch` or in `G` (if `ebunch` is `None`).
|
| 304 |
+
|
| 305 |
+
NodeNotFound
|
| 306 |
+
If `ebunch` has a node that is not in `G`.
|
| 307 |
+
|
| 308 |
+
Examples
|
| 309 |
+
--------
|
| 310 |
+
>>> G = nx.complete_graph(5)
|
| 311 |
+
>>> preds = nx.common_neighbor_centrality(G, [(0, 1), (2, 3)])
|
| 312 |
+
>>> for u, v, p in preds:
|
| 313 |
+
... print(f"({u}, {v}) -> {p}")
|
| 314 |
+
(0, 1) -> 3.4000000000000004
|
| 315 |
+
(2, 3) -> 3.4000000000000004
|
| 316 |
+
|
| 317 |
+
References
|
| 318 |
+
----------
|
| 319 |
+
.. [1] Ahmad, I., Akhtar, M.U., Noor, S. et al.
|
| 320 |
+
Missing Link Prediction using Common Neighbor and Centrality based Parameterized Algorithm.
|
| 321 |
+
Sci Rep 10, 364 (2020).
|
| 322 |
+
https://doi.org/10.1038/s41598-019-57304-y
|
| 323 |
+
"""
|
| 324 |
+
|
| 325 |
+
# When alpha == 1, the CCPA score simplifies to the number of common neighbors.
|
| 326 |
+
if alpha == 1:
|
| 327 |
+
|
| 328 |
+
def predict(u, v):
|
| 329 |
+
if u == v:
|
| 330 |
+
raise nx.NetworkXAlgorithmError("Self loops are not supported")
|
| 331 |
+
|
| 332 |
+
return len(nx.common_neighbors(G, u, v))
|
| 333 |
+
|
| 334 |
+
else:
|
| 335 |
+
spl = dict(nx.shortest_path_length(G))
|
| 336 |
+
inf = float("inf")
|
| 337 |
+
|
| 338 |
+
def predict(u, v):
|
| 339 |
+
if u == v:
|
| 340 |
+
raise nx.NetworkXAlgorithmError("Self loops are not supported")
|
| 341 |
+
path_len = spl[u].get(v, inf)
|
| 342 |
+
|
| 343 |
+
n_nbrs = len(nx.common_neighbors(G, u, v))
|
| 344 |
+
return alpha * n_nbrs + (1 - alpha) * len(G) / path_len
|
| 345 |
+
|
| 346 |
+
return _apply_prediction(G, predict, ebunch)
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
@not_implemented_for("directed")
|
| 350 |
+
@not_implemented_for("multigraph")
|
| 351 |
+
@nx._dispatchable
|
| 352 |
+
def preferential_attachment(G, ebunch=None):
|
| 353 |
+
r"""Compute the preferential attachment score of all node pairs in ebunch.
|
| 354 |
+
|
| 355 |
+
Preferential attachment score of `u` and `v` is defined as
|
| 356 |
+
|
| 357 |
+
.. math::
|
| 358 |
+
|
| 359 |
+
|\Gamma(u)| |\Gamma(v)|
|
| 360 |
+
|
| 361 |
+
where $\Gamma(u)$ denotes the set of neighbors of $u$.
|
| 362 |
+
|
| 363 |
+
Parameters
|
| 364 |
+
----------
|
| 365 |
+
G : graph
|
| 366 |
+
NetworkX undirected graph.
|
| 367 |
+
|
| 368 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 369 |
+
Preferential attachment score will be computed for each pair of
|
| 370 |
+
nodes given in the iterable. The pairs must be given as
|
| 371 |
+
2-tuples (u, v) where u and v are nodes in the graph. If ebunch
|
| 372 |
+
is None then all nonexistent edges in the graph will be used.
|
| 373 |
+
Default value: None.
|
| 374 |
+
|
| 375 |
+
Returns
|
| 376 |
+
-------
|
| 377 |
+
piter : iterator
|
| 378 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 379 |
+
pair of nodes and p is their preferential attachment score.
|
| 380 |
+
|
| 381 |
+
Raises
|
| 382 |
+
------
|
| 383 |
+
NetworkXNotImplemented
|
| 384 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 385 |
+
|
| 386 |
+
NodeNotFound
|
| 387 |
+
If `ebunch` has a node that is not in `G`.
|
| 388 |
+
|
| 389 |
+
Examples
|
| 390 |
+
--------
|
| 391 |
+
>>> G = nx.complete_graph(5)
|
| 392 |
+
>>> preds = nx.preferential_attachment(G, [(0, 1), (2, 3)])
|
| 393 |
+
>>> for u, v, p in preds:
|
| 394 |
+
... print(f"({u}, {v}) -> {p}")
|
| 395 |
+
(0, 1) -> 16
|
| 396 |
+
(2, 3) -> 16
|
| 397 |
+
|
| 398 |
+
References
|
| 399 |
+
----------
|
| 400 |
+
.. [1] D. Liben-Nowell, J. Kleinberg.
|
| 401 |
+
The Link Prediction Problem for Social Networks (2004).
|
| 402 |
+
http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
|
| 403 |
+
"""
|
| 404 |
+
|
| 405 |
+
def predict(u, v):
|
| 406 |
+
return G.degree(u) * G.degree(v)
|
| 407 |
+
|
| 408 |
+
return _apply_prediction(G, predict, ebunch)
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
@not_implemented_for("directed")
|
| 412 |
+
@not_implemented_for("multigraph")
|
| 413 |
+
@nx._dispatchable(node_attrs="community")
|
| 414 |
+
def cn_soundarajan_hopcroft(G, ebunch=None, community="community"):
|
| 415 |
+
r"""Count the number of common neighbors of all node pairs in ebunch
|
| 416 |
+
using community information.
|
| 417 |
+
|
| 418 |
+
For two nodes $u$ and $v$, this function computes the number of
|
| 419 |
+
common neighbors and bonus one for each common neighbor belonging to
|
| 420 |
+
the same community as $u$ and $v$. Mathematically,
|
| 421 |
+
|
| 422 |
+
.. math::
|
| 423 |
+
|
| 424 |
+
|\Gamma(u) \cap \Gamma(v)| + \sum_{w \in \Gamma(u) \cap \Gamma(v)} f(w)
|
| 425 |
+
|
| 426 |
+
where $f(w)$ equals 1 if $w$ belongs to the same community as $u$
|
| 427 |
+
and $v$ or 0 otherwise and $\Gamma(u)$ denotes the set of
|
| 428 |
+
neighbors of $u$.
|
| 429 |
+
|
| 430 |
+
Parameters
|
| 431 |
+
----------
|
| 432 |
+
G : graph
|
| 433 |
+
A NetworkX undirected graph.
|
| 434 |
+
|
| 435 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 436 |
+
The score will be computed for each pair of nodes given in the
|
| 437 |
+
iterable. The pairs must be given as 2-tuples (u, v) where u
|
| 438 |
+
and v are nodes in the graph. If ebunch is None then all
|
| 439 |
+
nonexistent edges in the graph will be used.
|
| 440 |
+
Default value: None.
|
| 441 |
+
|
| 442 |
+
community : string, optional (default = 'community')
|
| 443 |
+
Nodes attribute name containing the community information.
|
| 444 |
+
G[u][community] identifies which community u belongs to. Each
|
| 445 |
+
node belongs to at most one community. Default value: 'community'.
|
| 446 |
+
|
| 447 |
+
Returns
|
| 448 |
+
-------
|
| 449 |
+
piter : iterator
|
| 450 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 451 |
+
pair of nodes and p is their score.
|
| 452 |
+
|
| 453 |
+
Raises
|
| 454 |
+
------
|
| 455 |
+
NetworkXNotImplemented
|
| 456 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 457 |
+
|
| 458 |
+
NetworkXAlgorithmError
|
| 459 |
+
If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
|
| 460 |
+
|
| 461 |
+
NodeNotFound
|
| 462 |
+
If `ebunch` has a node that is not in `G`.
|
| 463 |
+
|
| 464 |
+
Examples
|
| 465 |
+
--------
|
| 466 |
+
>>> G = nx.path_graph(3)
|
| 467 |
+
>>> G.nodes[0]["community"] = 0
|
| 468 |
+
>>> G.nodes[1]["community"] = 0
|
| 469 |
+
>>> G.nodes[2]["community"] = 0
|
| 470 |
+
>>> preds = nx.cn_soundarajan_hopcroft(G, [(0, 2)])
|
| 471 |
+
>>> for u, v, p in preds:
|
| 472 |
+
... print(f"({u}, {v}) -> {p}")
|
| 473 |
+
(0, 2) -> 2
|
| 474 |
+
|
| 475 |
+
References
|
| 476 |
+
----------
|
| 477 |
+
.. [1] Sucheta Soundarajan and John Hopcroft.
|
| 478 |
+
Using community information to improve the precision of link
|
| 479 |
+
prediction methods.
|
| 480 |
+
In Proceedings of the 21st international conference companion on
|
| 481 |
+
World Wide Web (WWW '12 Companion). ACM, New York, NY, USA, 607-608.
|
| 482 |
+
http://doi.acm.org/10.1145/2187980.2188150
|
| 483 |
+
"""
|
| 484 |
+
|
| 485 |
+
def predict(u, v):
|
| 486 |
+
Cu = _community(G, u, community)
|
| 487 |
+
Cv = _community(G, v, community)
|
| 488 |
+
cnbors = nx.common_neighbors(G, u, v)
|
| 489 |
+
neighbors = (
|
| 490 |
+
sum(_community(G, w, community) == Cu for w in cnbors) if Cu == Cv else 0
|
| 491 |
+
)
|
| 492 |
+
return len(cnbors) + neighbors
|
| 493 |
+
|
| 494 |
+
return _apply_prediction(G, predict, ebunch)
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
@not_implemented_for("directed")
|
| 498 |
+
@not_implemented_for("multigraph")
|
| 499 |
+
@nx._dispatchable(node_attrs="community")
|
| 500 |
+
def ra_index_soundarajan_hopcroft(G, ebunch=None, community="community"):
|
| 501 |
+
r"""Compute the resource allocation index of all node pairs in
|
| 502 |
+
ebunch using community information.
|
| 503 |
+
|
| 504 |
+
For two nodes $u$ and $v$, this function computes the resource
|
| 505 |
+
allocation index considering only common neighbors belonging to the
|
| 506 |
+
same community as $u$ and $v$. Mathematically,
|
| 507 |
+
|
| 508 |
+
.. math::
|
| 509 |
+
|
| 510 |
+
\sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{f(w)}{|\Gamma(w)|}
|
| 511 |
+
|
| 512 |
+
where $f(w)$ equals 1 if $w$ belongs to the same community as $u$
|
| 513 |
+
and $v$ or 0 otherwise and $\Gamma(u)$ denotes the set of
|
| 514 |
+
neighbors of $u$.
|
| 515 |
+
|
| 516 |
+
Parameters
|
| 517 |
+
----------
|
| 518 |
+
G : graph
|
| 519 |
+
A NetworkX undirected graph.
|
| 520 |
+
|
| 521 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 522 |
+
The score will be computed for each pair of nodes given in the
|
| 523 |
+
iterable. The pairs must be given as 2-tuples (u, v) where u
|
| 524 |
+
and v are nodes in the graph. If ebunch is None then all
|
| 525 |
+
nonexistent edges in the graph will be used.
|
| 526 |
+
Default value: None.
|
| 527 |
+
|
| 528 |
+
community : string, optional (default = 'community')
|
| 529 |
+
Nodes attribute name containing the community information.
|
| 530 |
+
G[u][community] identifies which community u belongs to. Each
|
| 531 |
+
node belongs to at most one community. Default value: 'community'.
|
| 532 |
+
|
| 533 |
+
Returns
|
| 534 |
+
-------
|
| 535 |
+
piter : iterator
|
| 536 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 537 |
+
pair of nodes and p is their score.
|
| 538 |
+
|
| 539 |
+
Raises
|
| 540 |
+
------
|
| 541 |
+
NetworkXNotImplemented
|
| 542 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 543 |
+
|
| 544 |
+
NetworkXAlgorithmError
|
| 545 |
+
If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
|
| 546 |
+
|
| 547 |
+
NodeNotFound
|
| 548 |
+
If `ebunch` has a node that is not in `G`.
|
| 549 |
+
|
| 550 |
+
Examples
|
| 551 |
+
--------
|
| 552 |
+
>>> G = nx.Graph()
|
| 553 |
+
>>> G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 554 |
+
>>> G.nodes[0]["community"] = 0
|
| 555 |
+
>>> G.nodes[1]["community"] = 0
|
| 556 |
+
>>> G.nodes[2]["community"] = 1
|
| 557 |
+
>>> G.nodes[3]["community"] = 0
|
| 558 |
+
>>> preds = nx.ra_index_soundarajan_hopcroft(G, [(0, 3)])
|
| 559 |
+
>>> for u, v, p in preds:
|
| 560 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 561 |
+
(0, 3) -> 0.50000000
|
| 562 |
+
|
| 563 |
+
References
|
| 564 |
+
----------
|
| 565 |
+
.. [1] Sucheta Soundarajan and John Hopcroft.
|
| 566 |
+
Using community information to improve the precision of link
|
| 567 |
+
prediction methods.
|
| 568 |
+
In Proceedings of the 21st international conference companion on
|
| 569 |
+
World Wide Web (WWW '12 Companion). ACM, New York, NY, USA, 607-608.
|
| 570 |
+
http://doi.acm.org/10.1145/2187980.2188150
|
| 571 |
+
"""
|
| 572 |
+
|
| 573 |
+
def predict(u, v):
|
| 574 |
+
Cu = _community(G, u, community)
|
| 575 |
+
Cv = _community(G, v, community)
|
| 576 |
+
if Cu != Cv:
|
| 577 |
+
return 0
|
| 578 |
+
cnbors = nx.common_neighbors(G, u, v)
|
| 579 |
+
return sum(1 / G.degree(w) for w in cnbors if _community(G, w, community) == Cu)
|
| 580 |
+
|
| 581 |
+
return _apply_prediction(G, predict, ebunch)
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
@not_implemented_for("directed")
|
| 585 |
+
@not_implemented_for("multigraph")
|
| 586 |
+
@nx._dispatchable(node_attrs="community")
|
| 587 |
+
def within_inter_cluster(G, ebunch=None, delta=0.001, community="community"):
|
| 588 |
+
"""Compute the ratio of within- and inter-cluster common neighbors
|
| 589 |
+
of all node pairs in ebunch.
|
| 590 |
+
|
| 591 |
+
For two nodes `u` and `v`, if a common neighbor `w` belongs to the
|
| 592 |
+
same community as them, `w` is considered as within-cluster common
|
| 593 |
+
neighbor of `u` and `v`. Otherwise, it is considered as
|
| 594 |
+
inter-cluster common neighbor of `u` and `v`. The ratio between the
|
| 595 |
+
size of the set of within- and inter-cluster common neighbors is
|
| 596 |
+
defined as the WIC measure. [1]_
|
| 597 |
+
|
| 598 |
+
Parameters
|
| 599 |
+
----------
|
| 600 |
+
G : graph
|
| 601 |
+
A NetworkX undirected graph.
|
| 602 |
+
|
| 603 |
+
ebunch : iterable of node pairs, optional (default = None)
|
| 604 |
+
The WIC measure will be computed for each pair of nodes given in
|
| 605 |
+
the iterable. The pairs must be given as 2-tuples (u, v) where
|
| 606 |
+
u and v are nodes in the graph. If ebunch is None then all
|
| 607 |
+
nonexistent edges in the graph will be used.
|
| 608 |
+
Default value: None.
|
| 609 |
+
|
| 610 |
+
delta : float, optional (default = 0.001)
|
| 611 |
+
Value to prevent division by zero in case there is no
|
| 612 |
+
inter-cluster common neighbor between two nodes. See [1]_ for
|
| 613 |
+
details. Default value: 0.001.
|
| 614 |
+
|
| 615 |
+
community : string, optional (default = 'community')
|
| 616 |
+
Nodes attribute name containing the community information.
|
| 617 |
+
G[u][community] identifies which community u belongs to. Each
|
| 618 |
+
node belongs to at most one community. Default value: 'community'.
|
| 619 |
+
|
| 620 |
+
Returns
|
| 621 |
+
-------
|
| 622 |
+
piter : iterator
|
| 623 |
+
An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
|
| 624 |
+
pair of nodes and p is their WIC measure.
|
| 625 |
+
|
| 626 |
+
Raises
|
| 627 |
+
------
|
| 628 |
+
NetworkXNotImplemented
|
| 629 |
+
If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
|
| 630 |
+
|
| 631 |
+
NetworkXAlgorithmError
|
| 632 |
+
- If `delta` is less than or equal to zero.
|
| 633 |
+
- If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
|
| 634 |
+
|
| 635 |
+
NodeNotFound
|
| 636 |
+
If `ebunch` has a node that is not in `G`.
|
| 637 |
+
|
| 638 |
+
Examples
|
| 639 |
+
--------
|
| 640 |
+
>>> G = nx.Graph()
|
| 641 |
+
>>> G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)])
|
| 642 |
+
>>> G.nodes[0]["community"] = 0
|
| 643 |
+
>>> G.nodes[1]["community"] = 1
|
| 644 |
+
>>> G.nodes[2]["community"] = 0
|
| 645 |
+
>>> G.nodes[3]["community"] = 0
|
| 646 |
+
>>> G.nodes[4]["community"] = 0
|
| 647 |
+
>>> preds = nx.within_inter_cluster(G, [(0, 4)])
|
| 648 |
+
>>> for u, v, p in preds:
|
| 649 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 650 |
+
(0, 4) -> 1.99800200
|
| 651 |
+
>>> preds = nx.within_inter_cluster(G, [(0, 4)], delta=0.5)
|
| 652 |
+
>>> for u, v, p in preds:
|
| 653 |
+
... print(f"({u}, {v}) -> {p:.8f}")
|
| 654 |
+
(0, 4) -> 1.33333333
|
| 655 |
+
|
| 656 |
+
References
|
| 657 |
+
----------
|
| 658 |
+
.. [1] Jorge Carlos Valverde-Rebaza and Alneu de Andrade Lopes.
|
| 659 |
+
Link prediction in complex networks based on cluster information.
|
| 660 |
+
In Proceedings of the 21st Brazilian conference on Advances in
|
| 661 |
+
Artificial Intelligence (SBIA'12)
|
| 662 |
+
https://doi.org/10.1007/978-3-642-34459-6_10
|
| 663 |
+
"""
|
| 664 |
+
if delta <= 0:
|
| 665 |
+
raise nx.NetworkXAlgorithmError("Delta must be greater than zero")
|
| 666 |
+
|
| 667 |
+
def predict(u, v):
|
| 668 |
+
Cu = _community(G, u, community)
|
| 669 |
+
Cv = _community(G, v, community)
|
| 670 |
+
if Cu != Cv:
|
| 671 |
+
return 0
|
| 672 |
+
cnbors = nx.common_neighbors(G, u, v)
|
| 673 |
+
within = {w for w in cnbors if _community(G, w, community) == Cu}
|
| 674 |
+
inter = cnbors - within
|
| 675 |
+
return len(within) / (len(inter) + delta)
|
| 676 |
+
|
| 677 |
+
return _apply_prediction(G, predict, ebunch)
|
| 678 |
+
|
| 679 |
+
|
| 680 |
+
def _community(G, u, community):
|
| 681 |
+
"""Get the community of the given node."""
|
| 682 |
+
node_u = G.nodes[u]
|
| 683 |
+
try:
|
| 684 |
+
return node_u[community]
|
| 685 |
+
except KeyError as err:
|
| 686 |
+
raise nx.NetworkXAlgorithmError(
|
| 687 |
+
f"No community information available for Node {u}"
|
| 688 |
+
) from err
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/lowest_common_ancestors.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Algorithms for finding the lowest common ancestor of trees and DAGs."""
|
| 2 |
+
from collections import defaultdict
|
| 3 |
+
from collections.abc import Mapping, Set
|
| 4 |
+
from itertools import combinations_with_replacement
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from networkx.utils import UnionFind, arbitrary_element, not_implemented_for
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"all_pairs_lowest_common_ancestor",
|
| 11 |
+
"tree_all_pairs_lowest_common_ancestor",
|
| 12 |
+
"lowest_common_ancestor",
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@not_implemented_for("undirected")
|
| 17 |
+
@nx._dispatchable
|
| 18 |
+
def all_pairs_lowest_common_ancestor(G, pairs=None):
|
| 19 |
+
"""Return the lowest common ancestor of all pairs or the provided pairs
|
| 20 |
+
|
| 21 |
+
Parameters
|
| 22 |
+
----------
|
| 23 |
+
G : NetworkX directed graph
|
| 24 |
+
|
| 25 |
+
pairs : iterable of pairs of nodes, optional (default: all pairs)
|
| 26 |
+
The pairs of nodes of interest.
|
| 27 |
+
If None, will find the LCA of all pairs of nodes.
|
| 28 |
+
|
| 29 |
+
Yields
|
| 30 |
+
------
|
| 31 |
+
((node1, node2), lca) : 2-tuple
|
| 32 |
+
Where lca is least common ancestor of node1 and node2.
|
| 33 |
+
Note that for the default case, the order of the node pair is not considered,
|
| 34 |
+
e.g. you will not get both ``(a, b)`` and ``(b, a)``
|
| 35 |
+
|
| 36 |
+
Raises
|
| 37 |
+
------
|
| 38 |
+
NetworkXPointlessConcept
|
| 39 |
+
If `G` is null.
|
| 40 |
+
NetworkXError
|
| 41 |
+
If `G` is not a DAG.
|
| 42 |
+
|
| 43 |
+
Examples
|
| 44 |
+
--------
|
| 45 |
+
The default behavior is to yield the lowest common ancestor for all
|
| 46 |
+
possible combinations of nodes in `G`, including self-pairings:
|
| 47 |
+
|
| 48 |
+
>>> G = nx.DiGraph([(0, 1), (0, 3), (1, 2)])
|
| 49 |
+
>>> dict(nx.all_pairs_lowest_common_ancestor(G))
|
| 50 |
+
{(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}
|
| 51 |
+
|
| 52 |
+
The pairs argument can be used to limit the output to only the
|
| 53 |
+
specified node pairings:
|
| 54 |
+
|
| 55 |
+
>>> dict(nx.all_pairs_lowest_common_ancestor(G, pairs=[(1, 2), (2, 3)]))
|
| 56 |
+
{(1, 2): 1, (2, 3): 0}
|
| 57 |
+
|
| 58 |
+
Notes
|
| 59 |
+
-----
|
| 60 |
+
Only defined on non-null directed acyclic graphs.
|
| 61 |
+
|
| 62 |
+
See Also
|
| 63 |
+
--------
|
| 64 |
+
lowest_common_ancestor
|
| 65 |
+
"""
|
| 66 |
+
if not nx.is_directed_acyclic_graph(G):
|
| 67 |
+
raise nx.NetworkXError("LCA only defined on directed acyclic graphs.")
|
| 68 |
+
if len(G) == 0:
|
| 69 |
+
raise nx.NetworkXPointlessConcept("LCA meaningless on null graphs.")
|
| 70 |
+
|
| 71 |
+
if pairs is None:
|
| 72 |
+
pairs = combinations_with_replacement(G, 2)
|
| 73 |
+
else:
|
| 74 |
+
# Convert iterator to iterable, if necessary. Trim duplicates.
|
| 75 |
+
pairs = dict.fromkeys(pairs)
|
| 76 |
+
# Verify that each of the nodes in the provided pairs is in G
|
| 77 |
+
nodeset = set(G)
|
| 78 |
+
for pair in pairs:
|
| 79 |
+
if set(pair) - nodeset:
|
| 80 |
+
raise nx.NodeNotFound(
|
| 81 |
+
f"Node(s) {set(pair) - nodeset} from pair {pair} not in G."
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# Once input validation is done, construct the generator
|
| 85 |
+
def generate_lca_from_pairs(G, pairs):
|
| 86 |
+
ancestor_cache = {}
|
| 87 |
+
|
| 88 |
+
for v, w in pairs:
|
| 89 |
+
if v not in ancestor_cache:
|
| 90 |
+
ancestor_cache[v] = nx.ancestors(G, v)
|
| 91 |
+
ancestor_cache[v].add(v)
|
| 92 |
+
if w not in ancestor_cache:
|
| 93 |
+
ancestor_cache[w] = nx.ancestors(G, w)
|
| 94 |
+
ancestor_cache[w].add(w)
|
| 95 |
+
|
| 96 |
+
common_ancestors = ancestor_cache[v] & ancestor_cache[w]
|
| 97 |
+
|
| 98 |
+
if common_ancestors:
|
| 99 |
+
common_ancestor = next(iter(common_ancestors))
|
| 100 |
+
while True:
|
| 101 |
+
successor = None
|
| 102 |
+
for lower_ancestor in G.successors(common_ancestor):
|
| 103 |
+
if lower_ancestor in common_ancestors:
|
| 104 |
+
successor = lower_ancestor
|
| 105 |
+
break
|
| 106 |
+
if successor is None:
|
| 107 |
+
break
|
| 108 |
+
common_ancestor = successor
|
| 109 |
+
yield ((v, w), common_ancestor)
|
| 110 |
+
|
| 111 |
+
return generate_lca_from_pairs(G, pairs)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
@not_implemented_for("undirected")
|
| 115 |
+
@nx._dispatchable
|
| 116 |
+
def lowest_common_ancestor(G, node1, node2, default=None):
|
| 117 |
+
"""Compute the lowest common ancestor of the given pair of nodes.
|
| 118 |
+
|
| 119 |
+
Parameters
|
| 120 |
+
----------
|
| 121 |
+
G : NetworkX directed graph
|
| 122 |
+
|
| 123 |
+
node1, node2 : nodes in the graph.
|
| 124 |
+
|
| 125 |
+
default : object
|
| 126 |
+
Returned if no common ancestor between `node1` and `node2`
|
| 127 |
+
|
| 128 |
+
Returns
|
| 129 |
+
-------
|
| 130 |
+
The lowest common ancestor of node1 and node2,
|
| 131 |
+
or default if they have no common ancestors.
|
| 132 |
+
|
| 133 |
+
Examples
|
| 134 |
+
--------
|
| 135 |
+
>>> G = nx.DiGraph()
|
| 136 |
+
>>> nx.add_path(G, (0, 1, 2, 3))
|
| 137 |
+
>>> nx.add_path(G, (0, 4, 3))
|
| 138 |
+
>>> nx.lowest_common_ancestor(G, 2, 4)
|
| 139 |
+
0
|
| 140 |
+
|
| 141 |
+
See Also
|
| 142 |
+
--------
|
| 143 |
+
all_pairs_lowest_common_ancestor"""
|
| 144 |
+
|
| 145 |
+
ans = list(all_pairs_lowest_common_ancestor(G, pairs=[(node1, node2)]))
|
| 146 |
+
if ans:
|
| 147 |
+
assert len(ans) == 1
|
| 148 |
+
return ans[0][1]
|
| 149 |
+
return default
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
@not_implemented_for("undirected")
|
| 153 |
+
@nx._dispatchable
|
| 154 |
+
def tree_all_pairs_lowest_common_ancestor(G, root=None, pairs=None):
|
| 155 |
+
r"""Yield the lowest common ancestor for sets of pairs in a tree.
|
| 156 |
+
|
| 157 |
+
Parameters
|
| 158 |
+
----------
|
| 159 |
+
G : NetworkX directed graph (must be a tree)
|
| 160 |
+
|
| 161 |
+
root : node, optional (default: None)
|
| 162 |
+
The root of the subtree to operate on.
|
| 163 |
+
If None, assume the entire graph has exactly one source and use that.
|
| 164 |
+
|
| 165 |
+
pairs : iterable or iterator of pairs of nodes, optional (default: None)
|
| 166 |
+
The pairs of interest. If None, Defaults to all pairs of nodes
|
| 167 |
+
under `root` that have a lowest common ancestor.
|
| 168 |
+
|
| 169 |
+
Returns
|
| 170 |
+
-------
|
| 171 |
+
lcas : generator of tuples `((u, v), lca)` where `u` and `v` are nodes
|
| 172 |
+
in `pairs` and `lca` is their lowest common ancestor.
|
| 173 |
+
|
| 174 |
+
Examples
|
| 175 |
+
--------
|
| 176 |
+
>>> import pprint
|
| 177 |
+
>>> G = nx.DiGraph([(1, 3), (2, 4), (1, 2)])
|
| 178 |
+
>>> pprint.pprint(dict(nx.tree_all_pairs_lowest_common_ancestor(G)))
|
| 179 |
+
{(1, 1): 1,
|
| 180 |
+
(2, 1): 1,
|
| 181 |
+
(2, 2): 2,
|
| 182 |
+
(3, 1): 1,
|
| 183 |
+
(3, 2): 1,
|
| 184 |
+
(3, 3): 3,
|
| 185 |
+
(3, 4): 1,
|
| 186 |
+
(4, 1): 1,
|
| 187 |
+
(4, 2): 2,
|
| 188 |
+
(4, 4): 4}
|
| 189 |
+
|
| 190 |
+
We can also use `pairs` argument to specify the pairs of nodes for which we
|
| 191 |
+
want to compute lowest common ancestors. Here is an example:
|
| 192 |
+
|
| 193 |
+
>>> dict(nx.tree_all_pairs_lowest_common_ancestor(G, pairs=[(1, 4), (2, 3)]))
|
| 194 |
+
{(2, 3): 1, (1, 4): 1}
|
| 195 |
+
|
| 196 |
+
Notes
|
| 197 |
+
-----
|
| 198 |
+
Only defined on non-null trees represented with directed edges from
|
| 199 |
+
parents to children. Uses Tarjan's off-line lowest-common-ancestors
|
| 200 |
+
algorithm. Runs in time $O(4 \times (V + E + P))$ time, where 4 is the largest
|
| 201 |
+
value of the inverse Ackermann function likely to ever come up in actual
|
| 202 |
+
use, and $P$ is the number of pairs requested (or $V^2$ if all are needed).
|
| 203 |
+
|
| 204 |
+
Tarjan, R. E. (1979), "Applications of path compression on balanced trees",
|
| 205 |
+
Journal of the ACM 26 (4): 690-715, doi:10.1145/322154.322161.
|
| 206 |
+
|
| 207 |
+
See Also
|
| 208 |
+
--------
|
| 209 |
+
all_pairs_lowest_common_ancestor: similar routine for general DAGs
|
| 210 |
+
lowest_common_ancestor: just a single pair for general DAGs
|
| 211 |
+
"""
|
| 212 |
+
if len(G) == 0:
|
| 213 |
+
raise nx.NetworkXPointlessConcept("LCA meaningless on null graphs.")
|
| 214 |
+
|
| 215 |
+
# Index pairs of interest for efficient lookup from either side.
|
| 216 |
+
if pairs is not None:
|
| 217 |
+
pair_dict = defaultdict(set)
|
| 218 |
+
# See note on all_pairs_lowest_common_ancestor.
|
| 219 |
+
if not isinstance(pairs, Mapping | Set):
|
| 220 |
+
pairs = set(pairs)
|
| 221 |
+
for u, v in pairs:
|
| 222 |
+
for n in (u, v):
|
| 223 |
+
if n not in G:
|
| 224 |
+
msg = f"The node {str(n)} is not in the digraph."
|
| 225 |
+
raise nx.NodeNotFound(msg)
|
| 226 |
+
pair_dict[u].add(v)
|
| 227 |
+
pair_dict[v].add(u)
|
| 228 |
+
|
| 229 |
+
# If root is not specified, find the exactly one node with in degree 0 and
|
| 230 |
+
# use it. Raise an error if none are found, or more than one is. Also check
|
| 231 |
+
# for any nodes with in degree larger than 1, which would imply G is not a
|
| 232 |
+
# tree.
|
| 233 |
+
if root is None:
|
| 234 |
+
for n, deg in G.in_degree:
|
| 235 |
+
if deg == 0:
|
| 236 |
+
if root is not None:
|
| 237 |
+
msg = "No root specified and tree has multiple sources."
|
| 238 |
+
raise nx.NetworkXError(msg)
|
| 239 |
+
root = n
|
| 240 |
+
# checking deg>1 is not sufficient for MultiDiGraphs
|
| 241 |
+
elif deg > 1 and len(G.pred[n]) > 1:
|
| 242 |
+
msg = "Tree LCA only defined on trees; use DAG routine."
|
| 243 |
+
raise nx.NetworkXError(msg)
|
| 244 |
+
if root is None:
|
| 245 |
+
raise nx.NetworkXError("Graph contains a cycle.")
|
| 246 |
+
|
| 247 |
+
# Iterative implementation of Tarjan's offline lca algorithm
|
| 248 |
+
# as described in CLRS on page 521 (2nd edition)/page 584 (3rd edition)
|
| 249 |
+
uf = UnionFind()
|
| 250 |
+
ancestors = {}
|
| 251 |
+
for node in G:
|
| 252 |
+
ancestors[node] = uf[node]
|
| 253 |
+
|
| 254 |
+
colors = defaultdict(bool)
|
| 255 |
+
for node in nx.dfs_postorder_nodes(G, root):
|
| 256 |
+
colors[node] = True
|
| 257 |
+
for v in pair_dict[node] if pairs is not None else G:
|
| 258 |
+
if colors[v]:
|
| 259 |
+
# If the user requested both directions of a pair, give it.
|
| 260 |
+
# Otherwise, just give one.
|
| 261 |
+
if pairs is not None and (node, v) in pairs:
|
| 262 |
+
yield (node, v), ancestors[uf[v]]
|
| 263 |
+
if pairs is None or (v, node) in pairs:
|
| 264 |
+
yield (v, node), ancestors[uf[v]]
|
| 265 |
+
if node != root:
|
| 266 |
+
parent = arbitrary_element(G.pred[node])
|
| 267 |
+
uf.union(parent, node)
|
| 268 |
+
ancestors[uf[parent]] = parent
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/matching.py
ADDED
|
@@ -0,0 +1,1151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Functions for computing and verifying matchings in a graph."""
|
| 2 |
+
from collections import Counter
|
| 3 |
+
from itertools import combinations, repeat
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.utils import not_implemented_for
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"is_matching",
|
| 10 |
+
"is_maximal_matching",
|
| 11 |
+
"is_perfect_matching",
|
| 12 |
+
"max_weight_matching",
|
| 13 |
+
"min_weight_matching",
|
| 14 |
+
"maximal_matching",
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@not_implemented_for("multigraph")
|
| 19 |
+
@not_implemented_for("directed")
|
| 20 |
+
@nx._dispatchable
|
| 21 |
+
def maximal_matching(G):
|
| 22 |
+
r"""Find a maximal matching in the graph.
|
| 23 |
+
|
| 24 |
+
A matching is a subset of edges in which no node occurs more than once.
|
| 25 |
+
A maximal matching cannot add more edges and still be a matching.
|
| 26 |
+
|
| 27 |
+
Parameters
|
| 28 |
+
----------
|
| 29 |
+
G : NetworkX graph
|
| 30 |
+
Undirected graph
|
| 31 |
+
|
| 32 |
+
Returns
|
| 33 |
+
-------
|
| 34 |
+
matching : set
|
| 35 |
+
A maximal matching of the graph.
|
| 36 |
+
|
| 37 |
+
Examples
|
| 38 |
+
--------
|
| 39 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)])
|
| 40 |
+
>>> sorted(nx.maximal_matching(G))
|
| 41 |
+
[(1, 2), (3, 5)]
|
| 42 |
+
|
| 43 |
+
Notes
|
| 44 |
+
-----
|
| 45 |
+
The algorithm greedily selects a maximal matching M of the graph G
|
| 46 |
+
(i.e. no superset of M exists). It runs in $O(|E|)$ time.
|
| 47 |
+
"""
|
| 48 |
+
matching = set()
|
| 49 |
+
nodes = set()
|
| 50 |
+
for edge in G.edges():
|
| 51 |
+
# If the edge isn't covered, add it to the matching
|
| 52 |
+
# then remove neighborhood of u and v from consideration.
|
| 53 |
+
u, v = edge
|
| 54 |
+
if u not in nodes and v not in nodes and u != v:
|
| 55 |
+
matching.add(edge)
|
| 56 |
+
nodes.update(edge)
|
| 57 |
+
return matching
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def matching_dict_to_set(matching):
|
| 61 |
+
"""Converts matching dict format to matching set format
|
| 62 |
+
|
| 63 |
+
Converts a dictionary representing a matching (as returned by
|
| 64 |
+
:func:`max_weight_matching`) to a set representing a matching (as
|
| 65 |
+
returned by :func:`maximal_matching`).
|
| 66 |
+
|
| 67 |
+
In the definition of maximal matching adopted by NetworkX,
|
| 68 |
+
self-loops are not allowed, so the provided dictionary is expected
|
| 69 |
+
to never have any mapping from a key to itself. However, the
|
| 70 |
+
dictionary is expected to have mirrored key/value pairs, for
|
| 71 |
+
example, key ``u`` with value ``v`` and key ``v`` with value ``u``.
|
| 72 |
+
|
| 73 |
+
"""
|
| 74 |
+
edges = set()
|
| 75 |
+
for edge in matching.items():
|
| 76 |
+
u, v = edge
|
| 77 |
+
if (v, u) in edges or edge in edges:
|
| 78 |
+
continue
|
| 79 |
+
if u == v:
|
| 80 |
+
raise nx.NetworkXError(f"Selfloops cannot appear in matchings {edge}")
|
| 81 |
+
edges.add(edge)
|
| 82 |
+
return edges
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@nx._dispatchable
|
| 86 |
+
def is_matching(G, matching):
|
| 87 |
+
"""Return True if ``matching`` is a valid matching of ``G``
|
| 88 |
+
|
| 89 |
+
A *matching* in a graph is a set of edges in which no two distinct
|
| 90 |
+
edges share a common endpoint. Each node is incident to at most one
|
| 91 |
+
edge in the matching. The edges are said to be independent.
|
| 92 |
+
|
| 93 |
+
Parameters
|
| 94 |
+
----------
|
| 95 |
+
G : NetworkX graph
|
| 96 |
+
|
| 97 |
+
matching : dict or set
|
| 98 |
+
A dictionary or set representing a matching. If a dictionary, it
|
| 99 |
+
must have ``matching[u] == v`` and ``matching[v] == u`` for each
|
| 100 |
+
edge ``(u, v)`` in the matching. If a set, it must have elements
|
| 101 |
+
of the form ``(u, v)``, where ``(u, v)`` is an edge in the
|
| 102 |
+
matching.
|
| 103 |
+
|
| 104 |
+
Returns
|
| 105 |
+
-------
|
| 106 |
+
bool
|
| 107 |
+
Whether the given set or dictionary represents a valid matching
|
| 108 |
+
in the graph.
|
| 109 |
+
|
| 110 |
+
Raises
|
| 111 |
+
------
|
| 112 |
+
NetworkXError
|
| 113 |
+
If the proposed matching has an edge to a node not in G.
|
| 114 |
+
Or if the matching is not a collection of 2-tuple edges.
|
| 115 |
+
|
| 116 |
+
Examples
|
| 117 |
+
--------
|
| 118 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)])
|
| 119 |
+
>>> nx.is_maximal_matching(G, {1: 3, 2: 4}) # using dict to represent matching
|
| 120 |
+
True
|
| 121 |
+
|
| 122 |
+
>>> nx.is_matching(G, {(1, 3), (2, 4)}) # using set to represent matching
|
| 123 |
+
True
|
| 124 |
+
|
| 125 |
+
"""
|
| 126 |
+
if isinstance(matching, dict):
|
| 127 |
+
matching = matching_dict_to_set(matching)
|
| 128 |
+
|
| 129 |
+
nodes = set()
|
| 130 |
+
for edge in matching:
|
| 131 |
+
if len(edge) != 2:
|
| 132 |
+
raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
|
| 133 |
+
u, v = edge
|
| 134 |
+
if u not in G or v not in G:
|
| 135 |
+
raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
|
| 136 |
+
if u == v:
|
| 137 |
+
return False
|
| 138 |
+
if not G.has_edge(u, v):
|
| 139 |
+
return False
|
| 140 |
+
if u in nodes or v in nodes:
|
| 141 |
+
return False
|
| 142 |
+
nodes.update(edge)
|
| 143 |
+
return True
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
@nx._dispatchable
|
| 147 |
+
def is_maximal_matching(G, matching):
|
| 148 |
+
"""Return True if ``matching`` is a maximal matching of ``G``
|
| 149 |
+
|
| 150 |
+
A *maximal matching* in a graph is a matching in which adding any
|
| 151 |
+
edge would cause the set to no longer be a valid matching.
|
| 152 |
+
|
| 153 |
+
Parameters
|
| 154 |
+
----------
|
| 155 |
+
G : NetworkX graph
|
| 156 |
+
|
| 157 |
+
matching : dict or set
|
| 158 |
+
A dictionary or set representing a matching. If a dictionary, it
|
| 159 |
+
must have ``matching[u] == v`` and ``matching[v] == u`` for each
|
| 160 |
+
edge ``(u, v)`` in the matching. If a set, it must have elements
|
| 161 |
+
of the form ``(u, v)``, where ``(u, v)`` is an edge in the
|
| 162 |
+
matching.
|
| 163 |
+
|
| 164 |
+
Returns
|
| 165 |
+
-------
|
| 166 |
+
bool
|
| 167 |
+
Whether the given set or dictionary represents a valid maximal
|
| 168 |
+
matching in the graph.
|
| 169 |
+
|
| 170 |
+
Examples
|
| 171 |
+
--------
|
| 172 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (3, 5)])
|
| 173 |
+
>>> nx.is_maximal_matching(G, {(1, 2), (3, 4)})
|
| 174 |
+
True
|
| 175 |
+
|
| 176 |
+
"""
|
| 177 |
+
if isinstance(matching, dict):
|
| 178 |
+
matching = matching_dict_to_set(matching)
|
| 179 |
+
# If the given set is not a matching, then it is not a maximal matching.
|
| 180 |
+
edges = set()
|
| 181 |
+
nodes = set()
|
| 182 |
+
for edge in matching:
|
| 183 |
+
if len(edge) != 2:
|
| 184 |
+
raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
|
| 185 |
+
u, v = edge
|
| 186 |
+
if u not in G or v not in G:
|
| 187 |
+
raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
|
| 188 |
+
if u == v:
|
| 189 |
+
return False
|
| 190 |
+
if not G.has_edge(u, v):
|
| 191 |
+
return False
|
| 192 |
+
if u in nodes or v in nodes:
|
| 193 |
+
return False
|
| 194 |
+
nodes.update(edge)
|
| 195 |
+
edges.add(edge)
|
| 196 |
+
edges.add((v, u))
|
| 197 |
+
# A matching is maximal if adding any new edge from G to it
|
| 198 |
+
# causes the resulting set to match some node twice.
|
| 199 |
+
# Be careful to check for adding selfloops
|
| 200 |
+
for u, v in G.edges:
|
| 201 |
+
if (u, v) not in edges:
|
| 202 |
+
# could add edge (u, v) to edges and have a bigger matching
|
| 203 |
+
if u not in nodes and v not in nodes and u != v:
|
| 204 |
+
return False
|
| 205 |
+
return True
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
@nx._dispatchable
|
| 209 |
+
def is_perfect_matching(G, matching):
|
| 210 |
+
"""Return True if ``matching`` is a perfect matching for ``G``
|
| 211 |
+
|
| 212 |
+
A *perfect matching* in a graph is a matching in which exactly one edge
|
| 213 |
+
is incident upon each vertex.
|
| 214 |
+
|
| 215 |
+
Parameters
|
| 216 |
+
----------
|
| 217 |
+
G : NetworkX graph
|
| 218 |
+
|
| 219 |
+
matching : dict or set
|
| 220 |
+
A dictionary or set representing a matching. If a dictionary, it
|
| 221 |
+
must have ``matching[u] == v`` and ``matching[v] == u`` for each
|
| 222 |
+
edge ``(u, v)`` in the matching. If a set, it must have elements
|
| 223 |
+
of the form ``(u, v)``, where ``(u, v)`` is an edge in the
|
| 224 |
+
matching.
|
| 225 |
+
|
| 226 |
+
Returns
|
| 227 |
+
-------
|
| 228 |
+
bool
|
| 229 |
+
Whether the given set or dictionary represents a valid perfect
|
| 230 |
+
matching in the graph.
|
| 231 |
+
|
| 232 |
+
Examples
|
| 233 |
+
--------
|
| 234 |
+
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5), (4, 6)])
|
| 235 |
+
>>> my_match = {1: 2, 3: 5, 4: 6}
|
| 236 |
+
>>> nx.is_perfect_matching(G, my_match)
|
| 237 |
+
True
|
| 238 |
+
|
| 239 |
+
"""
|
| 240 |
+
if isinstance(matching, dict):
|
| 241 |
+
matching = matching_dict_to_set(matching)
|
| 242 |
+
|
| 243 |
+
nodes = set()
|
| 244 |
+
for edge in matching:
|
| 245 |
+
if len(edge) != 2:
|
| 246 |
+
raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
|
| 247 |
+
u, v = edge
|
| 248 |
+
if u not in G or v not in G:
|
| 249 |
+
raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
|
| 250 |
+
if u == v:
|
| 251 |
+
return False
|
| 252 |
+
if not G.has_edge(u, v):
|
| 253 |
+
return False
|
| 254 |
+
if u in nodes or v in nodes:
|
| 255 |
+
return False
|
| 256 |
+
nodes.update(edge)
|
| 257 |
+
return len(nodes) == len(G)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
@not_implemented_for("multigraph")
|
| 261 |
+
@not_implemented_for("directed")
|
| 262 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 263 |
+
def min_weight_matching(G, weight="weight"):
|
| 264 |
+
"""Computing a minimum-weight maximal matching of G.
|
| 265 |
+
|
| 266 |
+
Use the maximum-weight algorithm with edge weights subtracted
|
| 267 |
+
from the maximum weight of all edges.
|
| 268 |
+
|
| 269 |
+
A matching is a subset of edges in which no node occurs more than once.
|
| 270 |
+
The weight of a matching is the sum of the weights of its edges.
|
| 271 |
+
A maximal matching cannot add more edges and still be a matching.
|
| 272 |
+
The cardinality of a matching is the number of matched edges.
|
| 273 |
+
|
| 274 |
+
This method replaces the edge weights with 1 plus the maximum edge weight
|
| 275 |
+
minus the original edge weight.
|
| 276 |
+
|
| 277 |
+
new_weight = (max_weight + 1) - edge_weight
|
| 278 |
+
|
| 279 |
+
then runs :func:`max_weight_matching` with the new weights.
|
| 280 |
+
The max weight matching with these new weights corresponds
|
| 281 |
+
to the min weight matching using the original weights.
|
| 282 |
+
Adding 1 to the max edge weight keeps all edge weights positive
|
| 283 |
+
and as integers if they started as integers.
|
| 284 |
+
|
| 285 |
+
You might worry that adding 1 to each weight would make the algorithm
|
| 286 |
+
favor matchings with more edges. But we use the parameter
|
| 287 |
+
`maxcardinality=True` in `max_weight_matching` to ensure that the
|
| 288 |
+
number of edges in the competing matchings are the same and thus
|
| 289 |
+
the optimum does not change due to changes in the number of edges.
|
| 290 |
+
|
| 291 |
+
Read the documentation of `max_weight_matching` for more information.
|
| 292 |
+
|
| 293 |
+
Parameters
|
| 294 |
+
----------
|
| 295 |
+
G : NetworkX graph
|
| 296 |
+
Undirected graph
|
| 297 |
+
|
| 298 |
+
weight: string, optional (default='weight')
|
| 299 |
+
Edge data key corresponding to the edge weight.
|
| 300 |
+
If key not found, uses 1 as weight.
|
| 301 |
+
|
| 302 |
+
Returns
|
| 303 |
+
-------
|
| 304 |
+
matching : set
|
| 305 |
+
A minimal weight matching of the graph.
|
| 306 |
+
|
| 307 |
+
See Also
|
| 308 |
+
--------
|
| 309 |
+
max_weight_matching
|
| 310 |
+
"""
|
| 311 |
+
if len(G.edges) == 0:
|
| 312 |
+
return max_weight_matching(G, maxcardinality=True, weight=weight)
|
| 313 |
+
G_edges = G.edges(data=weight, default=1)
|
| 314 |
+
max_weight = 1 + max(w for _, _, w in G_edges)
|
| 315 |
+
InvG = nx.Graph()
|
| 316 |
+
edges = ((u, v, max_weight - w) for u, v, w in G_edges)
|
| 317 |
+
InvG.add_weighted_edges_from(edges, weight=weight)
|
| 318 |
+
return max_weight_matching(InvG, maxcardinality=True, weight=weight)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
@not_implemented_for("multigraph")
|
| 322 |
+
@not_implemented_for("directed")
|
| 323 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 324 |
+
def max_weight_matching(G, maxcardinality=False, weight="weight"):
|
| 325 |
+
"""Compute a maximum-weighted matching of G.
|
| 326 |
+
|
| 327 |
+
A matching is a subset of edges in which no node occurs more than once.
|
| 328 |
+
The weight of a matching is the sum of the weights of its edges.
|
| 329 |
+
A maximal matching cannot add more edges and still be a matching.
|
| 330 |
+
The cardinality of a matching is the number of matched edges.
|
| 331 |
+
|
| 332 |
+
Parameters
|
| 333 |
+
----------
|
| 334 |
+
G : NetworkX graph
|
| 335 |
+
Undirected graph
|
| 336 |
+
|
| 337 |
+
maxcardinality: bool, optional (default=False)
|
| 338 |
+
If maxcardinality is True, compute the maximum-cardinality matching
|
| 339 |
+
with maximum weight among all maximum-cardinality matchings.
|
| 340 |
+
|
| 341 |
+
weight: string, optional (default='weight')
|
| 342 |
+
Edge data key corresponding to the edge weight.
|
| 343 |
+
If key not found, uses 1 as weight.
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
Returns
|
| 347 |
+
-------
|
| 348 |
+
matching : set
|
| 349 |
+
A maximal matching of the graph.
|
| 350 |
+
|
| 351 |
+
Examples
|
| 352 |
+
--------
|
| 353 |
+
>>> G = nx.Graph()
|
| 354 |
+
>>> edges = [(1, 2, 6), (1, 3, 2), (2, 3, 1), (2, 4, 7), (3, 5, 9), (4, 5, 3)]
|
| 355 |
+
>>> G.add_weighted_edges_from(edges)
|
| 356 |
+
>>> sorted(nx.max_weight_matching(G))
|
| 357 |
+
[(2, 4), (5, 3)]
|
| 358 |
+
|
| 359 |
+
Notes
|
| 360 |
+
-----
|
| 361 |
+
If G has edges with weight attributes the edge data are used as
|
| 362 |
+
weight values else the weights are assumed to be 1.
|
| 363 |
+
|
| 364 |
+
This function takes time O(number_of_nodes ** 3).
|
| 365 |
+
|
| 366 |
+
If all edge weights are integers, the algorithm uses only integer
|
| 367 |
+
computations. If floating point weights are used, the algorithm
|
| 368 |
+
could return a slightly suboptimal matching due to numeric
|
| 369 |
+
precision errors.
|
| 370 |
+
|
| 371 |
+
This method is based on the "blossom" method for finding augmenting
|
| 372 |
+
paths and the "primal-dual" method for finding a matching of maximum
|
| 373 |
+
weight, both methods invented by Jack Edmonds [1]_.
|
| 374 |
+
|
| 375 |
+
Bipartite graphs can also be matched using the functions present in
|
| 376 |
+
:mod:`networkx.algorithms.bipartite.matching`.
|
| 377 |
+
|
| 378 |
+
References
|
| 379 |
+
----------
|
| 380 |
+
.. [1] "Efficient Algorithms for Finding Maximum Matching in Graphs",
|
| 381 |
+
Zvi Galil, ACM Computing Surveys, 1986.
|
| 382 |
+
"""
|
| 383 |
+
#
|
| 384 |
+
# The algorithm is taken from "Efficient Algorithms for Finding Maximum
|
| 385 |
+
# Matching in Graphs" by Zvi Galil, ACM Computing Surveys, 1986.
|
| 386 |
+
# It is based on the "blossom" method for finding augmenting paths and
|
| 387 |
+
# the "primal-dual" method for finding a matching of maximum weight, both
|
| 388 |
+
# methods invented by Jack Edmonds.
|
| 389 |
+
#
|
| 390 |
+
# A C program for maximum weight matching by Ed Rothberg was used
|
| 391 |
+
# extensively to validate this new code.
|
| 392 |
+
#
|
| 393 |
+
# Many terms used in the code comments are explained in the paper
|
| 394 |
+
# by Galil. You will probably need the paper to make sense of this code.
|
| 395 |
+
#
|
| 396 |
+
|
| 397 |
+
class NoNode:
|
| 398 |
+
"""Dummy value which is different from any node."""
|
| 399 |
+
|
| 400 |
+
class Blossom:
|
| 401 |
+
"""Representation of a non-trivial blossom or sub-blossom."""
|
| 402 |
+
|
| 403 |
+
__slots__ = ["childs", "edges", "mybestedges"]
|
| 404 |
+
|
| 405 |
+
# b.childs is an ordered list of b's sub-blossoms, starting with
|
| 406 |
+
# the base and going round the blossom.
|
| 407 |
+
|
| 408 |
+
# b.edges is the list of b's connecting edges, such that
|
| 409 |
+
# b.edges[i] = (v, w) where v is a vertex in b.childs[i]
|
| 410 |
+
# and w is a vertex in b.childs[wrap(i+1)].
|
| 411 |
+
|
| 412 |
+
# If b is a top-level S-blossom,
|
| 413 |
+
# b.mybestedges is a list of least-slack edges to neighboring
|
| 414 |
+
# S-blossoms, or None if no such list has been computed yet.
|
| 415 |
+
# This is used for efficient computation of delta3.
|
| 416 |
+
|
| 417 |
+
# Generate the blossom's leaf vertices.
|
| 418 |
+
def leaves(self):
|
| 419 |
+
stack = [*self.childs]
|
| 420 |
+
while stack:
|
| 421 |
+
t = stack.pop()
|
| 422 |
+
if isinstance(t, Blossom):
|
| 423 |
+
stack.extend(t.childs)
|
| 424 |
+
else:
|
| 425 |
+
yield t
|
| 426 |
+
|
| 427 |
+
# Get a list of vertices.
|
| 428 |
+
gnodes = list(G)
|
| 429 |
+
if not gnodes:
|
| 430 |
+
return set() # don't bother with empty graphs
|
| 431 |
+
|
| 432 |
+
# Find the maximum edge weight.
|
| 433 |
+
maxweight = 0
|
| 434 |
+
allinteger = True
|
| 435 |
+
for i, j, d in G.edges(data=True):
|
| 436 |
+
wt = d.get(weight, 1)
|
| 437 |
+
if i != j and wt > maxweight:
|
| 438 |
+
maxweight = wt
|
| 439 |
+
allinteger = allinteger and (str(type(wt)).split("'")[1] in ("int", "long"))
|
| 440 |
+
|
| 441 |
+
# If v is a matched vertex, mate[v] is its partner vertex.
|
| 442 |
+
# If v is a single vertex, v does not occur as a key in mate.
|
| 443 |
+
# Initially all vertices are single; updated during augmentation.
|
| 444 |
+
mate = {}
|
| 445 |
+
|
| 446 |
+
# If b is a top-level blossom,
|
| 447 |
+
# label.get(b) is None if b is unlabeled (free),
|
| 448 |
+
# 1 if b is an S-blossom,
|
| 449 |
+
# 2 if b is a T-blossom.
|
| 450 |
+
# The label of a vertex is found by looking at the label of its top-level
|
| 451 |
+
# containing blossom.
|
| 452 |
+
# If v is a vertex inside a T-blossom, label[v] is 2 iff v is reachable
|
| 453 |
+
# from an S-vertex outside the blossom.
|
| 454 |
+
# Labels are assigned during a stage and reset after each augmentation.
|
| 455 |
+
label = {}
|
| 456 |
+
|
| 457 |
+
# If b is a labeled top-level blossom,
|
| 458 |
+
# labeledge[b] = (v, w) is the edge through which b obtained its label
|
| 459 |
+
# such that w is a vertex in b, or None if b's base vertex is single.
|
| 460 |
+
# If w is a vertex inside a T-blossom and label[w] == 2,
|
| 461 |
+
# labeledge[w] = (v, w) is an edge through which w is reachable from
|
| 462 |
+
# outside the blossom.
|
| 463 |
+
labeledge = {}
|
| 464 |
+
|
| 465 |
+
# If v is a vertex, inblossom[v] is the top-level blossom to which v
|
| 466 |
+
# belongs.
|
| 467 |
+
# If v is a top-level vertex, inblossom[v] == v since v is itself
|
| 468 |
+
# a (trivial) top-level blossom.
|
| 469 |
+
# Initially all vertices are top-level trivial blossoms.
|
| 470 |
+
inblossom = dict(zip(gnodes, gnodes))
|
| 471 |
+
|
| 472 |
+
# If b is a sub-blossom,
|
| 473 |
+
# blossomparent[b] is its immediate parent (sub-)blossom.
|
| 474 |
+
# If b is a top-level blossom, blossomparent[b] is None.
|
| 475 |
+
blossomparent = dict(zip(gnodes, repeat(None)))
|
| 476 |
+
|
| 477 |
+
# If b is a (sub-)blossom,
|
| 478 |
+
# blossombase[b] is its base VERTEX (i.e. recursive sub-blossom).
|
| 479 |
+
blossombase = dict(zip(gnodes, gnodes))
|
| 480 |
+
|
| 481 |
+
# If w is a free vertex (or an unreached vertex inside a T-blossom),
|
| 482 |
+
# bestedge[w] = (v, w) is the least-slack edge from an S-vertex,
|
| 483 |
+
# or None if there is no such edge.
|
| 484 |
+
# If b is a (possibly trivial) top-level S-blossom,
|
| 485 |
+
# bestedge[b] = (v, w) is the least-slack edge to a different S-blossom
|
| 486 |
+
# (v inside b), or None if there is no such edge.
|
| 487 |
+
# This is used for efficient computation of delta2 and delta3.
|
| 488 |
+
bestedge = {}
|
| 489 |
+
|
| 490 |
+
# If v is a vertex,
|
| 491 |
+
# dualvar[v] = 2 * u(v) where u(v) is the v's variable in the dual
|
| 492 |
+
# optimization problem (if all edge weights are integers, multiplication
|
| 493 |
+
# by two ensures that all values remain integers throughout the algorithm).
|
| 494 |
+
# Initially, u(v) = maxweight / 2.
|
| 495 |
+
dualvar = dict(zip(gnodes, repeat(maxweight)))
|
| 496 |
+
|
| 497 |
+
# If b is a non-trivial blossom,
|
| 498 |
+
# blossomdual[b] = z(b) where z(b) is b's variable in the dual
|
| 499 |
+
# optimization problem.
|
| 500 |
+
blossomdual = {}
|
| 501 |
+
|
| 502 |
+
# If (v, w) in allowedge or (w, v) in allowedg, then the edge
|
| 503 |
+
# (v, w) is known to have zero slack in the optimization problem;
|
| 504 |
+
# otherwise the edge may or may not have zero slack.
|
| 505 |
+
allowedge = {}
|
| 506 |
+
|
| 507 |
+
# Queue of newly discovered S-vertices.
|
| 508 |
+
queue = []
|
| 509 |
+
|
| 510 |
+
# Return 2 * slack of edge (v, w) (does not work inside blossoms).
|
| 511 |
+
def slack(v, w):
|
| 512 |
+
return dualvar[v] + dualvar[w] - 2 * G[v][w].get(weight, 1)
|
| 513 |
+
|
| 514 |
+
# Assign label t to the top-level blossom containing vertex w,
|
| 515 |
+
# coming through an edge from vertex v.
|
| 516 |
+
def assignLabel(w, t, v):
|
| 517 |
+
b = inblossom[w]
|
| 518 |
+
assert label.get(w) is None and label.get(b) is None
|
| 519 |
+
label[w] = label[b] = t
|
| 520 |
+
if v is not None:
|
| 521 |
+
labeledge[w] = labeledge[b] = (v, w)
|
| 522 |
+
else:
|
| 523 |
+
labeledge[w] = labeledge[b] = None
|
| 524 |
+
bestedge[w] = bestedge[b] = None
|
| 525 |
+
if t == 1:
|
| 526 |
+
# b became an S-vertex/blossom; add it(s vertices) to the queue.
|
| 527 |
+
if isinstance(b, Blossom):
|
| 528 |
+
queue.extend(b.leaves())
|
| 529 |
+
else:
|
| 530 |
+
queue.append(b)
|
| 531 |
+
elif t == 2:
|
| 532 |
+
# b became a T-vertex/blossom; assign label S to its mate.
|
| 533 |
+
# (If b is a non-trivial blossom, its base is the only vertex
|
| 534 |
+
# with an external mate.)
|
| 535 |
+
base = blossombase[b]
|
| 536 |
+
assignLabel(mate[base], 1, base)
|
| 537 |
+
|
| 538 |
+
# Trace back from vertices v and w to discover either a new blossom
|
| 539 |
+
# or an augmenting path. Return the base vertex of the new blossom,
|
| 540 |
+
# or NoNode if an augmenting path was found.
|
| 541 |
+
def scanBlossom(v, w):
|
| 542 |
+
# Trace back from v and w, placing breadcrumbs as we go.
|
| 543 |
+
path = []
|
| 544 |
+
base = NoNode
|
| 545 |
+
while v is not NoNode:
|
| 546 |
+
# Look for a breadcrumb in v's blossom or put a new breadcrumb.
|
| 547 |
+
b = inblossom[v]
|
| 548 |
+
if label[b] & 4:
|
| 549 |
+
base = blossombase[b]
|
| 550 |
+
break
|
| 551 |
+
assert label[b] == 1
|
| 552 |
+
path.append(b)
|
| 553 |
+
label[b] = 5
|
| 554 |
+
# Trace one step back.
|
| 555 |
+
if labeledge[b] is None:
|
| 556 |
+
# The base of blossom b is single; stop tracing this path.
|
| 557 |
+
assert blossombase[b] not in mate
|
| 558 |
+
v = NoNode
|
| 559 |
+
else:
|
| 560 |
+
assert labeledge[b][0] == mate[blossombase[b]]
|
| 561 |
+
v = labeledge[b][0]
|
| 562 |
+
b = inblossom[v]
|
| 563 |
+
assert label[b] == 2
|
| 564 |
+
# b is a T-blossom; trace one more step back.
|
| 565 |
+
v = labeledge[b][0]
|
| 566 |
+
# Swap v and w so that we alternate between both paths.
|
| 567 |
+
if w is not NoNode:
|
| 568 |
+
v, w = w, v
|
| 569 |
+
# Remove breadcrumbs.
|
| 570 |
+
for b in path:
|
| 571 |
+
label[b] = 1
|
| 572 |
+
# Return base vertex, if we found one.
|
| 573 |
+
return base
|
| 574 |
+
|
| 575 |
+
# Construct a new blossom with given base, through S-vertices v and w.
|
| 576 |
+
# Label the new blossom as S; set its dual variable to zero;
|
| 577 |
+
# relabel its T-vertices to S and add them to the queue.
|
| 578 |
+
def addBlossom(base, v, w):
|
| 579 |
+
bb = inblossom[base]
|
| 580 |
+
bv = inblossom[v]
|
| 581 |
+
bw = inblossom[w]
|
| 582 |
+
# Create blossom.
|
| 583 |
+
b = Blossom()
|
| 584 |
+
blossombase[b] = base
|
| 585 |
+
blossomparent[b] = None
|
| 586 |
+
blossomparent[bb] = b
|
| 587 |
+
# Make list of sub-blossoms and their interconnecting edge endpoints.
|
| 588 |
+
b.childs = path = []
|
| 589 |
+
b.edges = edgs = [(v, w)]
|
| 590 |
+
# Trace back from v to base.
|
| 591 |
+
while bv != bb:
|
| 592 |
+
# Add bv to the new blossom.
|
| 593 |
+
blossomparent[bv] = b
|
| 594 |
+
path.append(bv)
|
| 595 |
+
edgs.append(labeledge[bv])
|
| 596 |
+
assert label[bv] == 2 or (
|
| 597 |
+
label[bv] == 1 and labeledge[bv][0] == mate[blossombase[bv]]
|
| 598 |
+
)
|
| 599 |
+
# Trace one step back.
|
| 600 |
+
v = labeledge[bv][0]
|
| 601 |
+
bv = inblossom[v]
|
| 602 |
+
# Add base sub-blossom; reverse lists.
|
| 603 |
+
path.append(bb)
|
| 604 |
+
path.reverse()
|
| 605 |
+
edgs.reverse()
|
| 606 |
+
# Trace back from w to base.
|
| 607 |
+
while bw != bb:
|
| 608 |
+
# Add bw to the new blossom.
|
| 609 |
+
blossomparent[bw] = b
|
| 610 |
+
path.append(bw)
|
| 611 |
+
edgs.append((labeledge[bw][1], labeledge[bw][0]))
|
| 612 |
+
assert label[bw] == 2 or (
|
| 613 |
+
label[bw] == 1 and labeledge[bw][0] == mate[blossombase[bw]]
|
| 614 |
+
)
|
| 615 |
+
# Trace one step back.
|
| 616 |
+
w = labeledge[bw][0]
|
| 617 |
+
bw = inblossom[w]
|
| 618 |
+
# Set label to S.
|
| 619 |
+
assert label[bb] == 1
|
| 620 |
+
label[b] = 1
|
| 621 |
+
labeledge[b] = labeledge[bb]
|
| 622 |
+
# Set dual variable to zero.
|
| 623 |
+
blossomdual[b] = 0
|
| 624 |
+
# Relabel vertices.
|
| 625 |
+
for v in b.leaves():
|
| 626 |
+
if label[inblossom[v]] == 2:
|
| 627 |
+
# This T-vertex now turns into an S-vertex because it becomes
|
| 628 |
+
# part of an S-blossom; add it to the queue.
|
| 629 |
+
queue.append(v)
|
| 630 |
+
inblossom[v] = b
|
| 631 |
+
# Compute b.mybestedges.
|
| 632 |
+
bestedgeto = {}
|
| 633 |
+
for bv in path:
|
| 634 |
+
if isinstance(bv, Blossom):
|
| 635 |
+
if bv.mybestedges is not None:
|
| 636 |
+
# Walk this subblossom's least-slack edges.
|
| 637 |
+
nblist = bv.mybestedges
|
| 638 |
+
# The sub-blossom won't need this data again.
|
| 639 |
+
bv.mybestedges = None
|
| 640 |
+
else:
|
| 641 |
+
# This subblossom does not have a list of least-slack
|
| 642 |
+
# edges; get the information from the vertices.
|
| 643 |
+
nblist = [
|
| 644 |
+
(v, w) for v in bv.leaves() for w in G.neighbors(v) if v != w
|
| 645 |
+
]
|
| 646 |
+
else:
|
| 647 |
+
nblist = [(bv, w) for w in G.neighbors(bv) if bv != w]
|
| 648 |
+
for k in nblist:
|
| 649 |
+
(i, j) = k
|
| 650 |
+
if inblossom[j] == b:
|
| 651 |
+
i, j = j, i
|
| 652 |
+
bj = inblossom[j]
|
| 653 |
+
if (
|
| 654 |
+
bj != b
|
| 655 |
+
and label.get(bj) == 1
|
| 656 |
+
and ((bj not in bestedgeto) or slack(i, j) < slack(*bestedgeto[bj]))
|
| 657 |
+
):
|
| 658 |
+
bestedgeto[bj] = k
|
| 659 |
+
# Forget about least-slack edge of the subblossom.
|
| 660 |
+
bestedge[bv] = None
|
| 661 |
+
b.mybestedges = list(bestedgeto.values())
|
| 662 |
+
# Select bestedge[b].
|
| 663 |
+
mybestedge = None
|
| 664 |
+
bestedge[b] = None
|
| 665 |
+
for k in b.mybestedges:
|
| 666 |
+
kslack = slack(*k)
|
| 667 |
+
if mybestedge is None or kslack < mybestslack:
|
| 668 |
+
mybestedge = k
|
| 669 |
+
mybestslack = kslack
|
| 670 |
+
bestedge[b] = mybestedge
|
| 671 |
+
|
| 672 |
+
# Expand the given top-level blossom.
|
| 673 |
+
def expandBlossom(b, endstage):
|
| 674 |
+
# This is an obnoxiously complicated recursive function for the sake of
|
| 675 |
+
# a stack-transformation. So, we hack around the complexity by using
|
| 676 |
+
# a trampoline pattern. By yielding the arguments to each recursive
|
| 677 |
+
# call, we keep the actual callstack flat.
|
| 678 |
+
|
| 679 |
+
def _recurse(b, endstage):
|
| 680 |
+
# Convert sub-blossoms into top-level blossoms.
|
| 681 |
+
for s in b.childs:
|
| 682 |
+
blossomparent[s] = None
|
| 683 |
+
if isinstance(s, Blossom):
|
| 684 |
+
if endstage and blossomdual[s] == 0:
|
| 685 |
+
# Recursively expand this sub-blossom.
|
| 686 |
+
yield s
|
| 687 |
+
else:
|
| 688 |
+
for v in s.leaves():
|
| 689 |
+
inblossom[v] = s
|
| 690 |
+
else:
|
| 691 |
+
inblossom[s] = s
|
| 692 |
+
# If we expand a T-blossom during a stage, its sub-blossoms must be
|
| 693 |
+
# relabeled.
|
| 694 |
+
if (not endstage) and label.get(b) == 2:
|
| 695 |
+
# Start at the sub-blossom through which the expanding
|
| 696 |
+
# blossom obtained its label, and relabel sub-blossoms untili
|
| 697 |
+
# we reach the base.
|
| 698 |
+
# Figure out through which sub-blossom the expanding blossom
|
| 699 |
+
# obtained its label initially.
|
| 700 |
+
entrychild = inblossom[labeledge[b][1]]
|
| 701 |
+
# Decide in which direction we will go round the blossom.
|
| 702 |
+
j = b.childs.index(entrychild)
|
| 703 |
+
if j & 1:
|
| 704 |
+
# Start index is odd; go forward and wrap.
|
| 705 |
+
j -= len(b.childs)
|
| 706 |
+
jstep = 1
|
| 707 |
+
else:
|
| 708 |
+
# Start index is even; go backward.
|
| 709 |
+
jstep = -1
|
| 710 |
+
# Move along the blossom until we get to the base.
|
| 711 |
+
v, w = labeledge[b]
|
| 712 |
+
while j != 0:
|
| 713 |
+
# Relabel the T-sub-blossom.
|
| 714 |
+
if jstep == 1:
|
| 715 |
+
p, q = b.edges[j]
|
| 716 |
+
else:
|
| 717 |
+
q, p = b.edges[j - 1]
|
| 718 |
+
label[w] = None
|
| 719 |
+
label[q] = None
|
| 720 |
+
assignLabel(w, 2, v)
|
| 721 |
+
# Step to the next S-sub-blossom and note its forward edge.
|
| 722 |
+
allowedge[(p, q)] = allowedge[(q, p)] = True
|
| 723 |
+
j += jstep
|
| 724 |
+
if jstep == 1:
|
| 725 |
+
v, w = b.edges[j]
|
| 726 |
+
else:
|
| 727 |
+
w, v = b.edges[j - 1]
|
| 728 |
+
# Step to the next T-sub-blossom.
|
| 729 |
+
allowedge[(v, w)] = allowedge[(w, v)] = True
|
| 730 |
+
j += jstep
|
| 731 |
+
# Relabel the base T-sub-blossom WITHOUT stepping through to
|
| 732 |
+
# its mate (so don't call assignLabel).
|
| 733 |
+
bw = b.childs[j]
|
| 734 |
+
label[w] = label[bw] = 2
|
| 735 |
+
labeledge[w] = labeledge[bw] = (v, w)
|
| 736 |
+
bestedge[bw] = None
|
| 737 |
+
# Continue along the blossom until we get back to entrychild.
|
| 738 |
+
j += jstep
|
| 739 |
+
while b.childs[j] != entrychild:
|
| 740 |
+
# Examine the vertices of the sub-blossom to see whether
|
| 741 |
+
# it is reachable from a neighboring S-vertex outside the
|
| 742 |
+
# expanding blossom.
|
| 743 |
+
bv = b.childs[j]
|
| 744 |
+
if label.get(bv) == 1:
|
| 745 |
+
# This sub-blossom just got label S through one of its
|
| 746 |
+
# neighbors; leave it be.
|
| 747 |
+
j += jstep
|
| 748 |
+
continue
|
| 749 |
+
if isinstance(bv, Blossom):
|
| 750 |
+
for v in bv.leaves():
|
| 751 |
+
if label.get(v):
|
| 752 |
+
break
|
| 753 |
+
else:
|
| 754 |
+
v = bv
|
| 755 |
+
# If the sub-blossom contains a reachable vertex, assign
|
| 756 |
+
# label T to the sub-blossom.
|
| 757 |
+
if label.get(v):
|
| 758 |
+
assert label[v] == 2
|
| 759 |
+
assert inblossom[v] == bv
|
| 760 |
+
label[v] = None
|
| 761 |
+
label[mate[blossombase[bv]]] = None
|
| 762 |
+
assignLabel(v, 2, labeledge[v][0])
|
| 763 |
+
j += jstep
|
| 764 |
+
# Remove the expanded blossom entirely.
|
| 765 |
+
label.pop(b, None)
|
| 766 |
+
labeledge.pop(b, None)
|
| 767 |
+
bestedge.pop(b, None)
|
| 768 |
+
del blossomparent[b]
|
| 769 |
+
del blossombase[b]
|
| 770 |
+
del blossomdual[b]
|
| 771 |
+
|
| 772 |
+
# Now, we apply the trampoline pattern. We simulate a recursive
|
| 773 |
+
# callstack by maintaining a stack of generators, each yielding a
|
| 774 |
+
# sequence of function arguments. We grow the stack by appending a call
|
| 775 |
+
# to _recurse on each argument tuple, and shrink the stack whenever a
|
| 776 |
+
# generator is exhausted.
|
| 777 |
+
stack = [_recurse(b, endstage)]
|
| 778 |
+
while stack:
|
| 779 |
+
top = stack[-1]
|
| 780 |
+
for s in top:
|
| 781 |
+
stack.append(_recurse(s, endstage))
|
| 782 |
+
break
|
| 783 |
+
else:
|
| 784 |
+
stack.pop()
|
| 785 |
+
|
| 786 |
+
# Swap matched/unmatched edges over an alternating path through blossom b
|
| 787 |
+
# between vertex v and the base vertex. Keep blossom bookkeeping
|
| 788 |
+
# consistent.
|
| 789 |
+
def augmentBlossom(b, v):
|
| 790 |
+
# This is an obnoxiously complicated recursive function for the sake of
|
| 791 |
+
# a stack-transformation. So, we hack around the complexity by using
|
| 792 |
+
# a trampoline pattern. By yielding the arguments to each recursive
|
| 793 |
+
# call, we keep the actual callstack flat.
|
| 794 |
+
|
| 795 |
+
def _recurse(b, v):
|
| 796 |
+
# Bubble up through the blossom tree from vertex v to an immediate
|
| 797 |
+
# sub-blossom of b.
|
| 798 |
+
t = v
|
| 799 |
+
while blossomparent[t] != b:
|
| 800 |
+
t = blossomparent[t]
|
| 801 |
+
# Recursively deal with the first sub-blossom.
|
| 802 |
+
if isinstance(t, Blossom):
|
| 803 |
+
yield (t, v)
|
| 804 |
+
# Decide in which direction we will go round the blossom.
|
| 805 |
+
i = j = b.childs.index(t)
|
| 806 |
+
if i & 1:
|
| 807 |
+
# Start index is odd; go forward and wrap.
|
| 808 |
+
j -= len(b.childs)
|
| 809 |
+
jstep = 1
|
| 810 |
+
else:
|
| 811 |
+
# Start index is even; go backward.
|
| 812 |
+
jstep = -1
|
| 813 |
+
# Move along the blossom until we get to the base.
|
| 814 |
+
while j != 0:
|
| 815 |
+
# Step to the next sub-blossom and augment it recursively.
|
| 816 |
+
j += jstep
|
| 817 |
+
t = b.childs[j]
|
| 818 |
+
if jstep == 1:
|
| 819 |
+
w, x = b.edges[j]
|
| 820 |
+
else:
|
| 821 |
+
x, w = b.edges[j - 1]
|
| 822 |
+
if isinstance(t, Blossom):
|
| 823 |
+
yield (t, w)
|
| 824 |
+
# Step to the next sub-blossom and augment it recursively.
|
| 825 |
+
j += jstep
|
| 826 |
+
t = b.childs[j]
|
| 827 |
+
if isinstance(t, Blossom):
|
| 828 |
+
yield (t, x)
|
| 829 |
+
# Match the edge connecting those sub-blossoms.
|
| 830 |
+
mate[w] = x
|
| 831 |
+
mate[x] = w
|
| 832 |
+
# Rotate the list of sub-blossoms to put the new base at the front.
|
| 833 |
+
b.childs = b.childs[i:] + b.childs[:i]
|
| 834 |
+
b.edges = b.edges[i:] + b.edges[:i]
|
| 835 |
+
blossombase[b] = blossombase[b.childs[0]]
|
| 836 |
+
assert blossombase[b] == v
|
| 837 |
+
|
| 838 |
+
# Now, we apply the trampoline pattern. We simulate a recursive
|
| 839 |
+
# callstack by maintaining a stack of generators, each yielding a
|
| 840 |
+
# sequence of function arguments. We grow the stack by appending a call
|
| 841 |
+
# to _recurse on each argument tuple, and shrink the stack whenever a
|
| 842 |
+
# generator is exhausted.
|
| 843 |
+
stack = [_recurse(b, v)]
|
| 844 |
+
while stack:
|
| 845 |
+
top = stack[-1]
|
| 846 |
+
for args in top:
|
| 847 |
+
stack.append(_recurse(*args))
|
| 848 |
+
break
|
| 849 |
+
else:
|
| 850 |
+
stack.pop()
|
| 851 |
+
|
| 852 |
+
# Swap matched/unmatched edges over an alternating path between two
|
| 853 |
+
# single vertices. The augmenting path runs through S-vertices v and w.
|
| 854 |
+
def augmentMatching(v, w):
|
| 855 |
+
for s, j in ((v, w), (w, v)):
|
| 856 |
+
# Match vertex s to vertex j. Then trace back from s
|
| 857 |
+
# until we find a single vertex, swapping matched and unmatched
|
| 858 |
+
# edges as we go.
|
| 859 |
+
while 1:
|
| 860 |
+
bs = inblossom[s]
|
| 861 |
+
assert label[bs] == 1
|
| 862 |
+
assert (labeledge[bs] is None and blossombase[bs] not in mate) or (
|
| 863 |
+
labeledge[bs][0] == mate[blossombase[bs]]
|
| 864 |
+
)
|
| 865 |
+
# Augment through the S-blossom from s to base.
|
| 866 |
+
if isinstance(bs, Blossom):
|
| 867 |
+
augmentBlossom(bs, s)
|
| 868 |
+
# Update mate[s]
|
| 869 |
+
mate[s] = j
|
| 870 |
+
# Trace one step back.
|
| 871 |
+
if labeledge[bs] is None:
|
| 872 |
+
# Reached single vertex; stop.
|
| 873 |
+
break
|
| 874 |
+
t = labeledge[bs][0]
|
| 875 |
+
bt = inblossom[t]
|
| 876 |
+
assert label[bt] == 2
|
| 877 |
+
# Trace one more step back.
|
| 878 |
+
s, j = labeledge[bt]
|
| 879 |
+
# Augment through the T-blossom from j to base.
|
| 880 |
+
assert blossombase[bt] == t
|
| 881 |
+
if isinstance(bt, Blossom):
|
| 882 |
+
augmentBlossom(bt, j)
|
| 883 |
+
# Update mate[j]
|
| 884 |
+
mate[j] = s
|
| 885 |
+
|
| 886 |
+
# Verify that the optimum solution has been reached.
|
| 887 |
+
def verifyOptimum():
|
| 888 |
+
if maxcardinality:
|
| 889 |
+
# Vertices may have negative dual;
|
| 890 |
+
# find a constant non-negative number to add to all vertex duals.
|
| 891 |
+
vdualoffset = max(0, -min(dualvar.values()))
|
| 892 |
+
else:
|
| 893 |
+
vdualoffset = 0
|
| 894 |
+
# 0. all dual variables are non-negative
|
| 895 |
+
assert min(dualvar.values()) + vdualoffset >= 0
|
| 896 |
+
assert len(blossomdual) == 0 or min(blossomdual.values()) >= 0
|
| 897 |
+
# 0. all edges have non-negative slack and
|
| 898 |
+
# 1. all matched edges have zero slack;
|
| 899 |
+
for i, j, d in G.edges(data=True):
|
| 900 |
+
wt = d.get(weight, 1)
|
| 901 |
+
if i == j:
|
| 902 |
+
continue # ignore self-loops
|
| 903 |
+
s = dualvar[i] + dualvar[j] - 2 * wt
|
| 904 |
+
iblossoms = [i]
|
| 905 |
+
jblossoms = [j]
|
| 906 |
+
while blossomparent[iblossoms[-1]] is not None:
|
| 907 |
+
iblossoms.append(blossomparent[iblossoms[-1]])
|
| 908 |
+
while blossomparent[jblossoms[-1]] is not None:
|
| 909 |
+
jblossoms.append(blossomparent[jblossoms[-1]])
|
| 910 |
+
iblossoms.reverse()
|
| 911 |
+
jblossoms.reverse()
|
| 912 |
+
for bi, bj in zip(iblossoms, jblossoms):
|
| 913 |
+
if bi != bj:
|
| 914 |
+
break
|
| 915 |
+
s += 2 * blossomdual[bi]
|
| 916 |
+
assert s >= 0
|
| 917 |
+
if mate.get(i) == j or mate.get(j) == i:
|
| 918 |
+
assert mate[i] == j and mate[j] == i
|
| 919 |
+
assert s == 0
|
| 920 |
+
# 2. all single vertices have zero dual value;
|
| 921 |
+
for v in gnodes:
|
| 922 |
+
assert (v in mate) or dualvar[v] + vdualoffset == 0
|
| 923 |
+
# 3. all blossoms with positive dual value are full.
|
| 924 |
+
for b in blossomdual:
|
| 925 |
+
if blossomdual[b] > 0:
|
| 926 |
+
assert len(b.edges) % 2 == 1
|
| 927 |
+
for i, j in b.edges[1::2]:
|
| 928 |
+
assert mate[i] == j and mate[j] == i
|
| 929 |
+
# Ok.
|
| 930 |
+
|
| 931 |
+
# Main loop: continue until no further improvement is possible.
|
| 932 |
+
while 1:
|
| 933 |
+
# Each iteration of this loop is a "stage".
|
| 934 |
+
# A stage finds an augmenting path and uses that to improve
|
| 935 |
+
# the matching.
|
| 936 |
+
|
| 937 |
+
# Remove labels from top-level blossoms/vertices.
|
| 938 |
+
label.clear()
|
| 939 |
+
labeledge.clear()
|
| 940 |
+
|
| 941 |
+
# Forget all about least-slack edges.
|
| 942 |
+
bestedge.clear()
|
| 943 |
+
for b in blossomdual:
|
| 944 |
+
b.mybestedges = None
|
| 945 |
+
|
| 946 |
+
# Loss of labeling means that we can not be sure that currently
|
| 947 |
+
# allowable edges remain allowable throughout this stage.
|
| 948 |
+
allowedge.clear()
|
| 949 |
+
|
| 950 |
+
# Make queue empty.
|
| 951 |
+
queue[:] = []
|
| 952 |
+
|
| 953 |
+
# Label single blossoms/vertices with S and put them in the queue.
|
| 954 |
+
for v in gnodes:
|
| 955 |
+
if (v not in mate) and label.get(inblossom[v]) is None:
|
| 956 |
+
assignLabel(v, 1, None)
|
| 957 |
+
|
| 958 |
+
# Loop until we succeed in augmenting the matching.
|
| 959 |
+
augmented = 0
|
| 960 |
+
while 1:
|
| 961 |
+
# Each iteration of this loop is a "substage".
|
| 962 |
+
# A substage tries to find an augmenting path;
|
| 963 |
+
# if found, the path is used to improve the matching and
|
| 964 |
+
# the stage ends. If there is no augmenting path, the
|
| 965 |
+
# primal-dual method is used to pump some slack out of
|
| 966 |
+
# the dual variables.
|
| 967 |
+
|
| 968 |
+
# Continue labeling until all vertices which are reachable
|
| 969 |
+
# through an alternating path have got a label.
|
| 970 |
+
while queue and not augmented:
|
| 971 |
+
# Take an S vertex from the queue.
|
| 972 |
+
v = queue.pop()
|
| 973 |
+
assert label[inblossom[v]] == 1
|
| 974 |
+
|
| 975 |
+
# Scan its neighbors:
|
| 976 |
+
for w in G.neighbors(v):
|
| 977 |
+
if w == v:
|
| 978 |
+
continue # ignore self-loops
|
| 979 |
+
# w is a neighbor to v
|
| 980 |
+
bv = inblossom[v]
|
| 981 |
+
bw = inblossom[w]
|
| 982 |
+
if bv == bw:
|
| 983 |
+
# this edge is internal to a blossom; ignore it
|
| 984 |
+
continue
|
| 985 |
+
if (v, w) not in allowedge:
|
| 986 |
+
kslack = slack(v, w)
|
| 987 |
+
if kslack <= 0:
|
| 988 |
+
# edge k has zero slack => it is allowable
|
| 989 |
+
allowedge[(v, w)] = allowedge[(w, v)] = True
|
| 990 |
+
if (v, w) in allowedge:
|
| 991 |
+
if label.get(bw) is None:
|
| 992 |
+
# (C1) w is a free vertex;
|
| 993 |
+
# label w with T and label its mate with S (R12).
|
| 994 |
+
assignLabel(w, 2, v)
|
| 995 |
+
elif label.get(bw) == 1:
|
| 996 |
+
# (C2) w is an S-vertex (not in the same blossom);
|
| 997 |
+
# follow back-links to discover either an
|
| 998 |
+
# augmenting path or a new blossom.
|
| 999 |
+
base = scanBlossom(v, w)
|
| 1000 |
+
if base is not NoNode:
|
| 1001 |
+
# Found a new blossom; add it to the blossom
|
| 1002 |
+
# bookkeeping and turn it into an S-blossom.
|
| 1003 |
+
addBlossom(base, v, w)
|
| 1004 |
+
else:
|
| 1005 |
+
# Found an augmenting path; augment the
|
| 1006 |
+
# matching and end this stage.
|
| 1007 |
+
augmentMatching(v, w)
|
| 1008 |
+
augmented = 1
|
| 1009 |
+
break
|
| 1010 |
+
elif label.get(w) is None:
|
| 1011 |
+
# w is inside a T-blossom, but w itself has not
|
| 1012 |
+
# yet been reached from outside the blossom;
|
| 1013 |
+
# mark it as reached (we need this to relabel
|
| 1014 |
+
# during T-blossom expansion).
|
| 1015 |
+
assert label[bw] == 2
|
| 1016 |
+
label[w] = 2
|
| 1017 |
+
labeledge[w] = (v, w)
|
| 1018 |
+
elif label.get(bw) == 1:
|
| 1019 |
+
# keep track of the least-slack non-allowable edge to
|
| 1020 |
+
# a different S-blossom.
|
| 1021 |
+
if bestedge.get(bv) is None or kslack < slack(*bestedge[bv]):
|
| 1022 |
+
bestedge[bv] = (v, w)
|
| 1023 |
+
elif label.get(w) is None:
|
| 1024 |
+
# w is a free vertex (or an unreached vertex inside
|
| 1025 |
+
# a T-blossom) but we can not reach it yet;
|
| 1026 |
+
# keep track of the least-slack edge that reaches w.
|
| 1027 |
+
if bestedge.get(w) is None or kslack < slack(*bestedge[w]):
|
| 1028 |
+
bestedge[w] = (v, w)
|
| 1029 |
+
|
| 1030 |
+
if augmented:
|
| 1031 |
+
break
|
| 1032 |
+
|
| 1033 |
+
# There is no augmenting path under these constraints;
|
| 1034 |
+
# compute delta and reduce slack in the optimization problem.
|
| 1035 |
+
# (Note that our vertex dual variables, edge slacks and delta's
|
| 1036 |
+
# are pre-multiplied by two.)
|
| 1037 |
+
deltatype = -1
|
| 1038 |
+
delta = deltaedge = deltablossom = None
|
| 1039 |
+
|
| 1040 |
+
# Compute delta1: the minimum value of any vertex dual.
|
| 1041 |
+
if not maxcardinality:
|
| 1042 |
+
deltatype = 1
|
| 1043 |
+
delta = min(dualvar.values())
|
| 1044 |
+
|
| 1045 |
+
# Compute delta2: the minimum slack on any edge between
|
| 1046 |
+
# an S-vertex and a free vertex.
|
| 1047 |
+
for v in G.nodes():
|
| 1048 |
+
if label.get(inblossom[v]) is None and bestedge.get(v) is not None:
|
| 1049 |
+
d = slack(*bestedge[v])
|
| 1050 |
+
if deltatype == -1 or d < delta:
|
| 1051 |
+
delta = d
|
| 1052 |
+
deltatype = 2
|
| 1053 |
+
deltaedge = bestedge[v]
|
| 1054 |
+
|
| 1055 |
+
# Compute delta3: half the minimum slack on any edge between
|
| 1056 |
+
# a pair of S-blossoms.
|
| 1057 |
+
for b in blossomparent:
|
| 1058 |
+
if (
|
| 1059 |
+
blossomparent[b] is None
|
| 1060 |
+
and label.get(b) == 1
|
| 1061 |
+
and bestedge.get(b) is not None
|
| 1062 |
+
):
|
| 1063 |
+
kslack = slack(*bestedge[b])
|
| 1064 |
+
if allinteger:
|
| 1065 |
+
assert (kslack % 2) == 0
|
| 1066 |
+
d = kslack // 2
|
| 1067 |
+
else:
|
| 1068 |
+
d = kslack / 2.0
|
| 1069 |
+
if deltatype == -1 or d < delta:
|
| 1070 |
+
delta = d
|
| 1071 |
+
deltatype = 3
|
| 1072 |
+
deltaedge = bestedge[b]
|
| 1073 |
+
|
| 1074 |
+
# Compute delta4: minimum z variable of any T-blossom.
|
| 1075 |
+
for b in blossomdual:
|
| 1076 |
+
if (
|
| 1077 |
+
blossomparent[b] is None
|
| 1078 |
+
and label.get(b) == 2
|
| 1079 |
+
and (deltatype == -1 or blossomdual[b] < delta)
|
| 1080 |
+
):
|
| 1081 |
+
delta = blossomdual[b]
|
| 1082 |
+
deltatype = 4
|
| 1083 |
+
deltablossom = b
|
| 1084 |
+
|
| 1085 |
+
if deltatype == -1:
|
| 1086 |
+
# No further improvement possible; max-cardinality optimum
|
| 1087 |
+
# reached. Do a final delta update to make the optimum
|
| 1088 |
+
# verifiable.
|
| 1089 |
+
assert maxcardinality
|
| 1090 |
+
deltatype = 1
|
| 1091 |
+
delta = max(0, min(dualvar.values()))
|
| 1092 |
+
|
| 1093 |
+
# Update dual variables according to delta.
|
| 1094 |
+
for v in gnodes:
|
| 1095 |
+
if label.get(inblossom[v]) == 1:
|
| 1096 |
+
# S-vertex: 2*u = 2*u - 2*delta
|
| 1097 |
+
dualvar[v] -= delta
|
| 1098 |
+
elif label.get(inblossom[v]) == 2:
|
| 1099 |
+
# T-vertex: 2*u = 2*u + 2*delta
|
| 1100 |
+
dualvar[v] += delta
|
| 1101 |
+
for b in blossomdual:
|
| 1102 |
+
if blossomparent[b] is None:
|
| 1103 |
+
if label.get(b) == 1:
|
| 1104 |
+
# top-level S-blossom: z = z + 2*delta
|
| 1105 |
+
blossomdual[b] += delta
|
| 1106 |
+
elif label.get(b) == 2:
|
| 1107 |
+
# top-level T-blossom: z = z - 2*delta
|
| 1108 |
+
blossomdual[b] -= delta
|
| 1109 |
+
|
| 1110 |
+
# Take action at the point where minimum delta occurred.
|
| 1111 |
+
if deltatype == 1:
|
| 1112 |
+
# No further improvement possible; optimum reached.
|
| 1113 |
+
break
|
| 1114 |
+
elif deltatype == 2:
|
| 1115 |
+
# Use the least-slack edge to continue the search.
|
| 1116 |
+
(v, w) = deltaedge
|
| 1117 |
+
assert label[inblossom[v]] == 1
|
| 1118 |
+
allowedge[(v, w)] = allowedge[(w, v)] = True
|
| 1119 |
+
queue.append(v)
|
| 1120 |
+
elif deltatype == 3:
|
| 1121 |
+
# Use the least-slack edge to continue the search.
|
| 1122 |
+
(v, w) = deltaedge
|
| 1123 |
+
allowedge[(v, w)] = allowedge[(w, v)] = True
|
| 1124 |
+
assert label[inblossom[v]] == 1
|
| 1125 |
+
queue.append(v)
|
| 1126 |
+
elif deltatype == 4:
|
| 1127 |
+
# Expand the least-z blossom.
|
| 1128 |
+
expandBlossom(deltablossom, False)
|
| 1129 |
+
|
| 1130 |
+
# End of a this substage.
|
| 1131 |
+
|
| 1132 |
+
# Paranoia check that the matching is symmetric.
|
| 1133 |
+
for v in mate:
|
| 1134 |
+
assert mate[mate[v]] == v
|
| 1135 |
+
|
| 1136 |
+
# Stop when no more augmenting path can be found.
|
| 1137 |
+
if not augmented:
|
| 1138 |
+
break
|
| 1139 |
+
|
| 1140 |
+
# End of a stage; expand all S-blossoms which have zero dual.
|
| 1141 |
+
for b in list(blossomdual.keys()):
|
| 1142 |
+
if b not in blossomdual:
|
| 1143 |
+
continue # already expanded
|
| 1144 |
+
if blossomparent[b] is None and label.get(b) == 1 and blossomdual[b] == 0:
|
| 1145 |
+
expandBlossom(b, True)
|
| 1146 |
+
|
| 1147 |
+
# Verify that we reached the optimum solution (only for integer weights).
|
| 1148 |
+
if allinteger:
|
| 1149 |
+
verifyOptimum()
|
| 1150 |
+
|
| 1151 |
+
return matching_dict_to_set(mate)
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/mis.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Algorithm to find a maximal (not maximum) independent set.
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.utils import not_implemented_for, py_random_state
|
| 7 |
+
|
| 8 |
+
__all__ = ["maximal_independent_set"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@not_implemented_for("directed")
|
| 12 |
+
@py_random_state(2)
|
| 13 |
+
@nx._dispatchable
|
| 14 |
+
def maximal_independent_set(G, nodes=None, seed=None):
|
| 15 |
+
"""Returns a random maximal independent set guaranteed to contain
|
| 16 |
+
a given set of nodes.
|
| 17 |
+
|
| 18 |
+
An independent set is a set of nodes such that the subgraph
|
| 19 |
+
of G induced by these nodes contains no edges. A maximal
|
| 20 |
+
independent set is an independent set such that it is not possible
|
| 21 |
+
to add a new node and still get an independent set.
|
| 22 |
+
|
| 23 |
+
Parameters
|
| 24 |
+
----------
|
| 25 |
+
G : NetworkX graph
|
| 26 |
+
|
| 27 |
+
nodes : list or iterable
|
| 28 |
+
Nodes that must be part of the independent set. This set of nodes
|
| 29 |
+
must be independent.
|
| 30 |
+
|
| 31 |
+
seed : integer, random_state, or None (default)
|
| 32 |
+
Indicator of random number generation state.
|
| 33 |
+
See :ref:`Randomness<randomness>`.
|
| 34 |
+
|
| 35 |
+
Returns
|
| 36 |
+
-------
|
| 37 |
+
indep_nodes : list
|
| 38 |
+
List of nodes that are part of a maximal independent set.
|
| 39 |
+
|
| 40 |
+
Raises
|
| 41 |
+
------
|
| 42 |
+
NetworkXUnfeasible
|
| 43 |
+
If the nodes in the provided list are not part of the graph or
|
| 44 |
+
do not form an independent set, an exception is raised.
|
| 45 |
+
|
| 46 |
+
NetworkXNotImplemented
|
| 47 |
+
If `G` is directed.
|
| 48 |
+
|
| 49 |
+
Examples
|
| 50 |
+
--------
|
| 51 |
+
>>> G = nx.path_graph(5)
|
| 52 |
+
>>> nx.maximal_independent_set(G) # doctest: +SKIP
|
| 53 |
+
[4, 0, 2]
|
| 54 |
+
>>> nx.maximal_independent_set(G, [1]) # doctest: +SKIP
|
| 55 |
+
[1, 3]
|
| 56 |
+
|
| 57 |
+
Notes
|
| 58 |
+
-----
|
| 59 |
+
This algorithm does not solve the maximum independent set problem.
|
| 60 |
+
|
| 61 |
+
"""
|
| 62 |
+
if not nodes:
|
| 63 |
+
nodes = {seed.choice(list(G))}
|
| 64 |
+
else:
|
| 65 |
+
nodes = set(nodes)
|
| 66 |
+
if not nodes.issubset(G):
|
| 67 |
+
raise nx.NetworkXUnfeasible(f"{nodes} is not a subset of the nodes of G")
|
| 68 |
+
neighbors = set.union(*[set(G.adj[v]) for v in nodes])
|
| 69 |
+
if set.intersection(neighbors, nodes):
|
| 70 |
+
raise nx.NetworkXUnfeasible(f"{nodes} is not an independent set of G")
|
| 71 |
+
indep_nodes = list(nodes)
|
| 72 |
+
available_nodes = set(G.nodes()).difference(neighbors.union(nodes))
|
| 73 |
+
while available_nodes:
|
| 74 |
+
node = seed.choice(list(available_nodes))
|
| 75 |
+
indep_nodes.append(node)
|
| 76 |
+
available_nodes.difference_update(list(G.adj[node]) + [node])
|
| 77 |
+
return indep_nodes
|
pythonProject/.venv/Lib/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
|
pythonProject/.venv/Lib/site-packages/networkx/algorithms/node_classification.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
""" This module provides the functions for node classification problem.
|
| 2 |
+
|
| 3 |
+
The functions in this module are not imported
|
| 4 |
+
into the top level `networkx` namespace.
|
| 5 |
+
You can access these functions by importing
|
| 6 |
+
the `networkx.algorithms.node_classification` modules,
|
| 7 |
+
then accessing the functions as attributes of `node_classification`.
|
| 8 |
+
For example:
|
| 9 |
+
|
| 10 |
+
>>> from networkx.algorithms import node_classification
|
| 11 |
+
>>> G = nx.path_graph(4)
|
| 12 |
+
>>> G.edges()
|
| 13 |
+
EdgeView([(0, 1), (1, 2), (2, 3)])
|
| 14 |
+
>>> G.nodes[0]["label"] = "A"
|
| 15 |
+
>>> G.nodes[3]["label"] = "B"
|
| 16 |
+
>>> node_classification.harmonic_function(G)
|
| 17 |
+
['A', 'A', 'B', 'B']
|
| 18 |
+
|
| 19 |
+
References
|
| 20 |
+
----------
|
| 21 |
+
Zhu, X., Ghahramani, Z., & Lafferty, J. (2003, August).
|
| 22 |
+
Semi-supervised learning using gaussian fields and harmonic functions.
|
| 23 |
+
In ICML (Vol. 3, pp. 912-919).
|
| 24 |
+
"""
|
| 25 |
+
import networkx as nx
|
| 26 |
+
|
| 27 |
+
__all__ = ["harmonic_function", "local_and_global_consistency"]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@nx.utils.not_implemented_for("directed")
|
| 31 |
+
@nx._dispatchable(node_attrs="label_name")
|
| 32 |
+
def harmonic_function(G, max_iter=30, label_name="label"):
|
| 33 |
+
"""Node classification by Harmonic function
|
| 34 |
+
|
| 35 |
+
Function for computing Harmonic function algorithm by Zhu et al.
|
| 36 |
+
|
| 37 |
+
Parameters
|
| 38 |
+
----------
|
| 39 |
+
G : NetworkX Graph
|
| 40 |
+
max_iter : int
|
| 41 |
+
maximum number of iterations allowed
|
| 42 |
+
label_name : string
|
| 43 |
+
name of target labels to predict
|
| 44 |
+
|
| 45 |
+
Returns
|
| 46 |
+
-------
|
| 47 |
+
predicted : list
|
| 48 |
+
List of length ``len(G)`` with the predicted labels for each node.
|
| 49 |
+
|
| 50 |
+
Raises
|
| 51 |
+
------
|
| 52 |
+
NetworkXError
|
| 53 |
+
If no nodes in `G` have attribute `label_name`.
|
| 54 |
+
|
| 55 |
+
Examples
|
| 56 |
+
--------
|
| 57 |
+
>>> from networkx.algorithms import node_classification
|
| 58 |
+
>>> G = nx.path_graph(4)
|
| 59 |
+
>>> G.nodes[0]["label"] = "A"
|
| 60 |
+
>>> G.nodes[3]["label"] = "B"
|
| 61 |
+
>>> G.nodes(data=True)
|
| 62 |
+
NodeDataView({0: {'label': 'A'}, 1: {}, 2: {}, 3: {'label': 'B'}})
|
| 63 |
+
>>> G.edges()
|
| 64 |
+
EdgeView([(0, 1), (1, 2), (2, 3)])
|
| 65 |
+
>>> predicted = node_classification.harmonic_function(G)
|
| 66 |
+
>>> predicted
|
| 67 |
+
['A', 'A', 'B', 'B']
|
| 68 |
+
|
| 69 |
+
References
|
| 70 |
+
----------
|
| 71 |
+
Zhu, X., Ghahramani, Z., & Lafferty, J. (2003, August).
|
| 72 |
+
Semi-supervised learning using gaussian fields and harmonic functions.
|
| 73 |
+
In ICML (Vol. 3, pp. 912-919).
|
| 74 |
+
"""
|
| 75 |
+
import numpy as np
|
| 76 |
+
import scipy as sp
|
| 77 |
+
|
| 78 |
+
X = nx.to_scipy_sparse_array(G) # adjacency matrix
|
| 79 |
+
labels, label_dict = _get_label_info(G, label_name)
|
| 80 |
+
|
| 81 |
+
if labels.shape[0] == 0:
|
| 82 |
+
raise nx.NetworkXError(
|
| 83 |
+
f"No node on the input graph is labeled by '{label_name}'."
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
n_samples = X.shape[0]
|
| 87 |
+
n_classes = label_dict.shape[0]
|
| 88 |
+
F = np.zeros((n_samples, n_classes))
|
| 89 |
+
|
| 90 |
+
# Build propagation matrix
|
| 91 |
+
degrees = X.sum(axis=0)
|
| 92 |
+
degrees[degrees == 0] = 1 # Avoid division by 0
|
| 93 |
+
# TODO: csr_array
|
| 94 |
+
D = sp.sparse.csr_array(sp.sparse.diags((1.0 / degrees), offsets=0))
|
| 95 |
+
P = (D @ X).tolil()
|
| 96 |
+
P[labels[:, 0]] = 0 # labels[:, 0] indicates IDs of labeled nodes
|
| 97 |
+
# Build base matrix
|
| 98 |
+
B = np.zeros((n_samples, n_classes))
|
| 99 |
+
B[labels[:, 0], labels[:, 1]] = 1
|
| 100 |
+
|
| 101 |
+
for _ in range(max_iter):
|
| 102 |
+
F = (P @ F) + B
|
| 103 |
+
|
| 104 |
+
return label_dict[np.argmax(F, axis=1)].tolist()
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
@nx.utils.not_implemented_for("directed")
|
| 108 |
+
@nx._dispatchable(node_attrs="label_name")
|
| 109 |
+
def local_and_global_consistency(G, alpha=0.99, max_iter=30, label_name="label"):
|
| 110 |
+
"""Node classification by Local and Global Consistency
|
| 111 |
+
|
| 112 |
+
Function for computing Local and global consistency algorithm by Zhou et al.
|
| 113 |
+
|
| 114 |
+
Parameters
|
| 115 |
+
----------
|
| 116 |
+
G : NetworkX Graph
|
| 117 |
+
alpha : float
|
| 118 |
+
Clamping factor
|
| 119 |
+
max_iter : int
|
| 120 |
+
Maximum number of iterations allowed
|
| 121 |
+
label_name : string
|
| 122 |
+
Name of target labels to predict
|
| 123 |
+
|
| 124 |
+
Returns
|
| 125 |
+
-------
|
| 126 |
+
predicted : list
|
| 127 |
+
List of length ``len(G)`` with the predicted labels for each node.
|
| 128 |
+
|
| 129 |
+
Raises
|
| 130 |
+
------
|
| 131 |
+
NetworkXError
|
| 132 |
+
If no nodes in `G` have attribute `label_name`.
|
| 133 |
+
|
| 134 |
+
Examples
|
| 135 |
+
--------
|
| 136 |
+
>>> from networkx.algorithms import node_classification
|
| 137 |
+
>>> G = nx.path_graph(4)
|
| 138 |
+
>>> G.nodes[0]["label"] = "A"
|
| 139 |
+
>>> G.nodes[3]["label"] = "B"
|
| 140 |
+
>>> G.nodes(data=True)
|
| 141 |
+
NodeDataView({0: {'label': 'A'}, 1: {}, 2: {}, 3: {'label': 'B'}})
|
| 142 |
+
>>> G.edges()
|
| 143 |
+
EdgeView([(0, 1), (1, 2), (2, 3)])
|
| 144 |
+
>>> predicted = node_classification.local_and_global_consistency(G)
|
| 145 |
+
>>> predicted
|
| 146 |
+
['A', 'A', 'B', 'B']
|
| 147 |
+
|
| 148 |
+
References
|
| 149 |
+
----------
|
| 150 |
+
Zhou, D., Bousquet, O., Lal, T. N., Weston, J., & Schölkopf, B. (2004).
|
| 151 |
+
Learning with local and global consistency.
|
| 152 |
+
Advances in neural information processing systems, 16(16), 321-328.
|
| 153 |
+
"""
|
| 154 |
+
import numpy as np
|
| 155 |
+
import scipy as sp
|
| 156 |
+
|
| 157 |
+
X = nx.to_scipy_sparse_array(G) # adjacency matrix
|
| 158 |
+
labels, label_dict = _get_label_info(G, label_name)
|
| 159 |
+
|
| 160 |
+
if labels.shape[0] == 0:
|
| 161 |
+
raise nx.NetworkXError(
|
| 162 |
+
f"No node on the input graph is labeled by '{label_name}'."
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
n_samples = X.shape[0]
|
| 166 |
+
n_classes = label_dict.shape[0]
|
| 167 |
+
F = np.zeros((n_samples, n_classes))
|
| 168 |
+
|
| 169 |
+
# Build propagation matrix
|
| 170 |
+
degrees = X.sum(axis=0)
|
| 171 |
+
degrees[degrees == 0] = 1 # Avoid division by 0
|
| 172 |
+
# TODO: csr_array
|
| 173 |
+
D2 = np.sqrt(sp.sparse.csr_array(sp.sparse.diags((1.0 / degrees), offsets=0)))
|
| 174 |
+
P = alpha * ((D2 @ X) @ D2)
|
| 175 |
+
# Build base matrix
|
| 176 |
+
B = np.zeros((n_samples, n_classes))
|
| 177 |
+
B[labels[:, 0], labels[:, 1]] = 1 - alpha
|
| 178 |
+
|
| 179 |
+
for _ in range(max_iter):
|
| 180 |
+
F = (P @ F) + B
|
| 181 |
+
|
| 182 |
+
return label_dict[np.argmax(F, axis=1)].tolist()
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _get_label_info(G, label_name):
|
| 186 |
+
"""Get and return information of labels from the input graph
|
| 187 |
+
|
| 188 |
+
Parameters
|
| 189 |
+
----------
|
| 190 |
+
G : Network X graph
|
| 191 |
+
label_name : string
|
| 192 |
+
Name of the target label
|
| 193 |
+
|
| 194 |
+
Returns
|
| 195 |
+
-------
|
| 196 |
+
labels : numpy array, shape = [n_labeled_samples, 2]
|
| 197 |
+
Array of pairs of labeled node ID and label ID
|
| 198 |
+
label_dict : numpy array, shape = [n_classes]
|
| 199 |
+
Array of labels
|
| 200 |
+
i-th element contains the label corresponding label ID `i`
|
| 201 |
+
"""
|
| 202 |
+
import numpy as np
|
| 203 |
+
|
| 204 |
+
labels = []
|
| 205 |
+
label_to_id = {}
|
| 206 |
+
lid = 0
|
| 207 |
+
for i, n in enumerate(G.nodes(data=True)):
|
| 208 |
+
if label_name in n[1]:
|
| 209 |
+
label = n[1][label_name]
|
| 210 |
+
if label not in label_to_id:
|
| 211 |
+
label_to_id[label] = lid
|
| 212 |
+
lid += 1
|
| 213 |
+
labels.append([i, label_to_id[label]])
|
| 214 |
+
labels = np.array(labels)
|
| 215 |
+
label_dict = np.array(
|
| 216 |
+
[label for label, _ in sorted(label_to_id.items(), key=lambda x: x[1])]
|
| 217 |
+
)
|
| 218 |
+
return (labels, label_dict)
|