koichi12 commited on
Commit
53f3a8e
·
verified ·
1 Parent(s): 9354bbb

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +4 -0
  2. .venv/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so +3 -0
  3. .venv/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so +3 -0
  4. .venv/lib/python3.11/site-packages/networkx/algorithms/asteroidal.py +171 -0
  5. .venv/lib/python3.11/site-packages/networkx/algorithms/boundary.py +168 -0
  6. .venv/lib/python3.11/site-packages/networkx/algorithms/bridges.py +205 -0
  7. .venv/lib/python3.11/site-packages/networkx/algorithms/chains.py +172 -0
  8. .venv/lib/python3.11/site-packages/networkx/algorithms/chordal.py +443 -0
  9. .venv/lib/python3.11/site-packages/networkx/algorithms/clique.py +755 -0
  10. .venv/lib/python3.11/site-packages/networkx/algorithms/cluster.py +609 -0
  11. .venv/lib/python3.11/site-packages/networkx/algorithms/communicability_alg.py +163 -0
  12. .venv/lib/python3.11/site-packages/networkx/algorithms/core.py +649 -0
  13. .venv/lib/python3.11/site-packages/networkx/algorithms/covering.py +142 -0
  14. .venv/lib/python3.11/site-packages/networkx/algorithms/cuts.py +398 -0
  15. .venv/lib/python3.11/site-packages/networkx/algorithms/cycles.py +1230 -0
  16. .venv/lib/python3.11/site-packages/networkx/algorithms/d_separation.py +722 -0
  17. .venv/lib/python3.11/site-packages/networkx/algorithms/dag.py +1418 -0
  18. .venv/lib/python3.11/site-packages/networkx/algorithms/distance_measures.py +1022 -0
  19. .venv/lib/python3.11/site-packages/networkx/algorithms/distance_regular.py +238 -0
  20. .venv/lib/python3.11/site-packages/networkx/algorithms/dominance.py +135 -0
  21. .venv/lib/python3.11/site-packages/networkx/algorithms/efficiency_measures.py +167 -0
  22. .venv/lib/python3.11/site-packages/networkx/algorithms/euler.py +470 -0
  23. .venv/lib/python3.11/site-packages/networkx/algorithms/flow/__init__.py +11 -0
  24. .venv/lib/python3.11/site-packages/networkx/algorithms/flow/boykovkolmogorov.py +370 -0
  25. .venv/lib/python3.11/site-packages/networkx/algorithms/flow/capacityscaling.py +407 -0
  26. .venv/lib/python3.11/site-packages/networkx/algorithms/flow/dinitz_alg.py +238 -0
  27. .venv/lib/python3.11/site-packages/networkx/algorithms/flow/gomory_hu.py +178 -0
  28. .venv/lib/python3.11/site-packages/networkx/algorithms/flow/maxflow.py +607 -0
  29. .venv/lib/python3.11/site-packages/networkx/algorithms/flow/networksimplex.py +666 -0
  30. .venv/lib/python3.11/site-packages/networkx/algorithms/flow/preflowpush.py +425 -0
  31. .venv/lib/python3.11/site-packages/networkx/algorithms/graph_hashing.py +328 -0
  32. .venv/lib/python3.11/site-packages/networkx/algorithms/graphical.py +483 -0
  33. .venv/lib/python3.11/site-packages/networkx/algorithms/hierarchy.py +57 -0
  34. .venv/lib/python3.11/site-packages/networkx/algorithms/hybrid.py +196 -0
  35. .venv/lib/python3.11/site-packages/networkx/algorithms/isolate.py +107 -0
  36. .venv/lib/python3.11/site-packages/networkx/algorithms/link_prediction.py +687 -0
  37. .venv/lib/python3.11/site-packages/networkx/algorithms/lowest_common_ancestors.py +269 -0
  38. .venv/lib/python3.11/site-packages/networkx/algorithms/matching.py +1152 -0
  39. .venv/lib/python3.11/site-packages/networkx/algorithms/moral.py +59 -0
  40. .venv/lib/python3.11/site-packages/networkx/algorithms/non_randomness.py +98 -0
  41. .venv/lib/python3.11/site-packages/networkx/algorithms/planar_drawing.py +464 -0
  42. .venv/lib/python3.11/site-packages/networkx/algorithms/polynomials.py +306 -0
  43. .venv/lib/python3.11/site-packages/networkx/algorithms/reciprocity.py +98 -0
  44. .venv/lib/python3.11/site-packages/networkx/algorithms/regular.py +215 -0
  45. .venv/lib/python3.11/site-packages/networkx/algorithms/richclub.py +138 -0
  46. .venv/lib/python3.11/site-packages/networkx/algorithms/similarity.py +1780 -0
  47. .venv/lib/python3.11/site-packages/networkx/algorithms/simple_paths.py +950 -0
  48. .venv/lib/python3.11/site-packages/networkx/algorithms/smallworld.py +404 -0
  49. .venv/lib/python3.11/site-packages/networkx/algorithms/smetric.py +30 -0
  50. .venv/lib/python3.11/site-packages/networkx/algorithms/sparsifiers.py +296 -0
.gitattributes CHANGED
@@ -301,3 +301,7 @@ tuning-competition-baseline/.venv/lib/python3.11/site-packages/nvidia/cudnn/lib/
301
  .venv/lib/python3.11/site-packages/torchaudio/lib/pybind11_prefixctc.so filter=lfs diff=lfs merge=lfs -text
302
  .venv/lib/python3.11/site-packages/torchaudio/lib/libctc_prefix_decoder.so filter=lfs diff=lfs merge=lfs -text
303
  .venv/lib/python3.11/site-packages/torchaudio/lib/libtorchaudio_sox.so filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
301
  .venv/lib/python3.11/site-packages/torchaudio/lib/pybind11_prefixctc.so filter=lfs diff=lfs merge=lfs -text
302
  .venv/lib/python3.11/site-packages/torchaudio/lib/libctc_prefix_decoder.so filter=lfs diff=lfs merge=lfs -text
303
  .venv/lib/python3.11/site-packages/torchaudio/lib/libtorchaudio_sox.so filter=lfs diff=lfs merge=lfs -text
304
+ .venv/lib/python3.11/site-packages/torchaudio/lib/_torchaudio_sox.so filter=lfs diff=lfs merge=lfs -text
305
+ .venv/lib/python3.11/site-packages/torchaudio/transforms/__pycache__/_transforms.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
306
+ .venv/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
307
+ .venv/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
.venv/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e423efe7a4ff703ed5e70eff519697c0cf641e674f29d2446c4af895f0a4f1be
3
+ size 145401
.venv/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1cc372f73a5562f6a0a364bfd1c42234e42e67403f814a83aab901136cce3a29
3
+ size 149024
.venv/lib/python3.11/site-packages/networkx/algorithms/asteroidal.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Algorithms for asteroidal triples and asteroidal numbers in graphs.
3
+
4
+ An asteroidal triple in a graph G is a set of three non-adjacent vertices
5
+ u, v and w such that there exist a path between any two of them that avoids
6
+ closed neighborhood of the third. More formally, v_j, v_k belongs to the same
7
+ connected component of G - N[v_i], where N[v_i] denotes the closed neighborhood
8
+ of v_i. A graph which does not contain any asteroidal triples is called
9
+ an AT-free graph. The class of AT-free graphs is a graph class for which
10
+ many NP-complete problems are solvable in polynomial time. Amongst them,
11
+ independent set and coloring.
12
+ """
13
+
14
+ import networkx as nx
15
+ from networkx.utils import not_implemented_for
16
+
17
+ __all__ = ["is_at_free", "find_asteroidal_triple"]
18
+
19
+
20
+ @not_implemented_for("directed")
21
+ @not_implemented_for("multigraph")
22
+ @nx._dispatchable
23
+ def find_asteroidal_triple(G):
24
+ r"""Find an asteroidal triple in the given graph.
25
+
26
+ An asteroidal triple is a triple of non-adjacent vertices such that
27
+ there exists a path between any two of them which avoids the closed
28
+ neighborhood of the third. It checks all independent triples of vertices
29
+ and whether they are an asteroidal triple or not. This is done with the
30
+ help of a data structure called a component structure.
31
+ A component structure encodes information about which vertices belongs to
32
+ the same connected component when the closed neighborhood of a given vertex
33
+ is removed from the graph. The algorithm used to check is the trivial
34
+ one, outlined in [1]_, which has a runtime of
35
+ :math:`O(|V||\overline{E} + |V||E|)`, where the second term is the
36
+ creation of the component structure.
37
+
38
+ Parameters
39
+ ----------
40
+ G : NetworkX Graph
41
+ The graph to check whether is AT-free or not
42
+
43
+ Returns
44
+ -------
45
+ list or None
46
+ An asteroidal triple is returned as a list of nodes. If no asteroidal
47
+ triple exists, i.e. the graph is AT-free, then None is returned.
48
+ The returned value depends on the certificate parameter. The default
49
+ option is a bool which is True if the graph is AT-free, i.e. the
50
+ given graph contains no asteroidal triples, and False otherwise, i.e.
51
+ if the graph contains at least one asteroidal triple.
52
+
53
+ Notes
54
+ -----
55
+ The component structure and the algorithm is described in [1]_. The current
56
+ implementation implements the trivial algorithm for simple graphs.
57
+
58
+ References
59
+ ----------
60
+ .. [1] Ekkehard Köhler,
61
+ "Recognizing Graphs without asteroidal triples",
62
+ Journal of Discrete Algorithms 2, pages 439-452, 2004.
63
+ https://www.sciencedirect.com/science/article/pii/S157086670400019X
64
+ """
65
+ V = set(G.nodes)
66
+
67
+ if len(V) < 6:
68
+ # An asteroidal triple cannot exist in a graph with 5 or less vertices.
69
+ return None
70
+
71
+ component_structure = create_component_structure(G)
72
+ E_complement = set(nx.complement(G).edges)
73
+
74
+ for e in E_complement:
75
+ u = e[0]
76
+ v = e[1]
77
+ u_neighborhood = set(G[u]).union([u])
78
+ v_neighborhood = set(G[v]).union([v])
79
+ union_of_neighborhoods = u_neighborhood.union(v_neighborhood)
80
+ for w in V - union_of_neighborhoods:
81
+ # Check for each pair of vertices whether they belong to the
82
+ # same connected component when the closed neighborhood of the
83
+ # third is removed.
84
+ if (
85
+ component_structure[u][v] == component_structure[u][w]
86
+ and component_structure[v][u] == component_structure[v][w]
87
+ and component_structure[w][u] == component_structure[w][v]
88
+ ):
89
+ return [u, v, w]
90
+ return None
91
+
92
+
93
+ @not_implemented_for("directed")
94
+ @not_implemented_for("multigraph")
95
+ @nx._dispatchable
96
+ def is_at_free(G):
97
+ """Check if a graph is AT-free.
98
+
99
+ The method uses the `find_asteroidal_triple` method to recognize
100
+ an AT-free graph. If no asteroidal triple is found the graph is
101
+ AT-free and True is returned. If at least one asteroidal triple is
102
+ found the graph is not AT-free and False is returned.
103
+
104
+ Parameters
105
+ ----------
106
+ G : NetworkX Graph
107
+ The graph to check whether is AT-free or not.
108
+
109
+ Returns
110
+ -------
111
+ bool
112
+ True if G is AT-free and False otherwise.
113
+
114
+ Examples
115
+ --------
116
+ >>> G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)])
117
+ >>> nx.is_at_free(G)
118
+ True
119
+
120
+ >>> G = nx.cycle_graph(6)
121
+ >>> nx.is_at_free(G)
122
+ False
123
+ """
124
+ return find_asteroidal_triple(G) is None
125
+
126
+
127
+ @not_implemented_for("directed")
128
+ @not_implemented_for("multigraph")
129
+ @nx._dispatchable
130
+ def create_component_structure(G):
131
+ r"""Create component structure for G.
132
+
133
+ A *component structure* is an `nxn` array, denoted `c`, where `n` is
134
+ the number of vertices, where each row and column corresponds to a vertex.
135
+
136
+ .. math::
137
+ c_{uv} = \begin{cases} 0, if v \in N[u] \\
138
+ k, if v \in component k of G \setminus N[u] \end{cases}
139
+
140
+ Where `k` is an arbitrary label for each component. The structure is used
141
+ to simplify the detection of asteroidal triples.
142
+
143
+ Parameters
144
+ ----------
145
+ G : NetworkX Graph
146
+ Undirected, simple graph.
147
+
148
+ Returns
149
+ -------
150
+ component_structure : dictionary
151
+ A dictionary of dictionaries, keyed by pairs of vertices.
152
+
153
+ """
154
+ V = set(G.nodes)
155
+ component_structure = {}
156
+ for v in V:
157
+ label = 0
158
+ closed_neighborhood = set(G[v]).union({v})
159
+ row_dict = {}
160
+ for u in closed_neighborhood:
161
+ row_dict[u] = 0
162
+
163
+ G_reduced = G.subgraph(set(G.nodes) - closed_neighborhood)
164
+ for cc in nx.connected_components(G_reduced):
165
+ label += 1
166
+ for u in cc:
167
+ row_dict[u] = label
168
+
169
+ component_structure[v] = row_dict
170
+
171
+ return component_structure
.venv/lib/python3.11/site-packages/networkx/algorithms/boundary.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Routines to find the boundary of a set of nodes.
2
+
3
+ An edge boundary is a set of edges, each of which has exactly one
4
+ endpoint in a given set of nodes (or, in the case of directed graphs,
5
+ the set of edges whose source node is in the set).
6
+
7
+ A node boundary of a set *S* of nodes is the set of (out-)neighbors of
8
+ nodes in *S* that are outside *S*.
9
+
10
+ """
11
+
12
+ from itertools import chain
13
+
14
+ import networkx as nx
15
+
16
+ __all__ = ["edge_boundary", "node_boundary"]
17
+
18
+
19
+ @nx._dispatchable(edge_attrs={"data": "default"}, preserve_edge_attrs="data")
20
+ def edge_boundary(G, nbunch1, nbunch2=None, data=False, keys=False, default=None):
21
+ """Returns the edge boundary of `nbunch1`.
22
+
23
+ The *edge boundary* of a set *S* with respect to a set *T* is the
24
+ set of edges (*u*, *v*) such that *u* is in *S* and *v* is in *T*.
25
+ If *T* is not specified, it is assumed to be the set of all nodes
26
+ not in *S*.
27
+
28
+ Parameters
29
+ ----------
30
+ G : NetworkX graph
31
+
32
+ nbunch1 : iterable
33
+ Iterable of nodes in the graph representing the set of nodes
34
+ whose edge boundary will be returned. (This is the set *S* from
35
+ the definition above.)
36
+
37
+ nbunch2 : iterable
38
+ Iterable of nodes representing the target (or "exterior") set of
39
+ nodes. (This is the set *T* from the definition above.) If not
40
+ specified, this is assumed to be the set of all nodes in `G`
41
+ not in `nbunch1`.
42
+
43
+ keys : bool
44
+ This parameter has the same meaning as in
45
+ :meth:`MultiGraph.edges`.
46
+
47
+ data : bool or object
48
+ This parameter has the same meaning as in
49
+ :meth:`MultiGraph.edges`.
50
+
51
+ default : object
52
+ This parameter has the same meaning as in
53
+ :meth:`MultiGraph.edges`.
54
+
55
+ Returns
56
+ -------
57
+ iterator
58
+ An iterator over the edges in the boundary of `nbunch1` with
59
+ respect to `nbunch2`. If `keys`, `data`, or `default`
60
+ are specified and `G` is a multigraph, then edges are returned
61
+ with keys and/or data, as in :meth:`MultiGraph.edges`.
62
+
63
+ Examples
64
+ --------
65
+ >>> G = nx.wheel_graph(6)
66
+
67
+ When nbunch2=None:
68
+
69
+ >>> list(nx.edge_boundary(G, (1, 3)))
70
+ [(1, 0), (1, 2), (1, 5), (3, 0), (3, 2), (3, 4)]
71
+
72
+ When nbunch2 is given:
73
+
74
+ >>> list(nx.edge_boundary(G, (1, 3), (2, 0)))
75
+ [(1, 0), (1, 2), (3, 0), (3, 2)]
76
+
77
+ Notes
78
+ -----
79
+ Any element of `nbunch` that is not in the graph `G` will be
80
+ ignored.
81
+
82
+ `nbunch1` and `nbunch2` are usually meant to be disjoint, but in
83
+ the interest of speed and generality, that is not required here.
84
+
85
+ """
86
+ nset1 = {n for n in nbunch1 if n in G}
87
+ # Here we create an iterator over edges incident to nodes in the set
88
+ # `nset1`. The `Graph.edges()` method does not provide a guarantee
89
+ # on the orientation of the edges, so our algorithm below must
90
+ # handle the case in which exactly one orientation, either (u, v) or
91
+ # (v, u), appears in this iterable.
92
+ if G.is_multigraph():
93
+ edges = G.edges(nset1, data=data, keys=keys, default=default)
94
+ else:
95
+ edges = G.edges(nset1, data=data, default=default)
96
+ # If `nbunch2` is not provided, then it is assumed to be the set
97
+ # complement of `nbunch1`. For the sake of efficiency, this is
98
+ # implemented by using the `not in` operator, instead of by creating
99
+ # an additional set and using the `in` operator.
100
+ if nbunch2 is None:
101
+ return (e for e in edges if (e[0] in nset1) ^ (e[1] in nset1))
102
+ nset2 = set(nbunch2)
103
+ return (
104
+ e
105
+ for e in edges
106
+ if (e[0] in nset1 and e[1] in nset2) or (e[1] in nset1 and e[0] in nset2)
107
+ )
108
+
109
+
110
+ @nx._dispatchable
111
+ def node_boundary(G, nbunch1, nbunch2=None):
112
+ """Returns the node boundary of `nbunch1`.
113
+
114
+ The *node boundary* of a set *S* with respect to a set *T* is the
115
+ set of nodes *v* in *T* such that for some *u* in *S*, there is an
116
+ edge joining *u* to *v*. If *T* is not specified, it is assumed to
117
+ be the set of all nodes not in *S*.
118
+
119
+ Parameters
120
+ ----------
121
+ G : NetworkX graph
122
+
123
+ nbunch1 : iterable
124
+ Iterable of nodes in the graph representing the set of nodes
125
+ whose node boundary will be returned. (This is the set *S* from
126
+ the definition above.)
127
+
128
+ nbunch2 : iterable
129
+ Iterable of nodes representing the target (or "exterior") set of
130
+ nodes. (This is the set *T* from the definition above.) If not
131
+ specified, this is assumed to be the set of all nodes in `G`
132
+ not in `nbunch1`.
133
+
134
+ Returns
135
+ -------
136
+ set
137
+ The node boundary of `nbunch1` with respect to `nbunch2`.
138
+
139
+ Examples
140
+ --------
141
+ >>> G = nx.wheel_graph(6)
142
+
143
+ When nbunch2=None:
144
+
145
+ >>> list(nx.node_boundary(G, (3, 4)))
146
+ [0, 2, 5]
147
+
148
+ When nbunch2 is given:
149
+
150
+ >>> list(nx.node_boundary(G, (3, 4), (0, 1, 5)))
151
+ [0, 5]
152
+
153
+ Notes
154
+ -----
155
+ Any element of `nbunch` that is not in the graph `G` will be
156
+ ignored.
157
+
158
+ `nbunch1` and `nbunch2` are usually meant to be disjoint, but in
159
+ the interest of speed and generality, that is not required here.
160
+
161
+ """
162
+ nset1 = {n for n in nbunch1 if n in G}
163
+ bdy = set(chain.from_iterable(G[v] for v in nset1)) - nset1
164
+ # If `nbunch2` is not specified, it is assumed to be the set
165
+ # complement of `nbunch1`.
166
+ if nbunch2 is not None:
167
+ bdy &= set(nbunch2)
168
+ return bdy
.venv/lib/python3.11/site-packages/networkx/algorithms/bridges.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Bridge-finding algorithms."""
2
+
3
+ from itertools import chain
4
+
5
+ import networkx as nx
6
+ from networkx.utils import not_implemented_for
7
+
8
+ __all__ = ["bridges", "has_bridges", "local_bridges"]
9
+
10
+
11
+ @not_implemented_for("directed")
12
+ @nx._dispatchable
13
+ def bridges(G, root=None):
14
+ """Generate all bridges in a graph.
15
+
16
+ A *bridge* in a graph is an edge whose removal causes the number of
17
+ connected components of the graph to increase. Equivalently, a bridge is an
18
+ edge that does not belong to any cycle. Bridges are also known as cut-edges,
19
+ isthmuses, or cut arcs.
20
+
21
+ Parameters
22
+ ----------
23
+ G : undirected graph
24
+
25
+ root : node (optional)
26
+ A node in the graph `G`. If specified, only the bridges in the
27
+ connected component containing this node will be returned.
28
+
29
+ Yields
30
+ ------
31
+ e : edge
32
+ An edge in the graph whose removal disconnects the graph (or
33
+ causes the number of connected components to increase).
34
+
35
+ Raises
36
+ ------
37
+ NodeNotFound
38
+ If `root` is not in the graph `G`.
39
+
40
+ NetworkXNotImplemented
41
+ If `G` is a directed graph.
42
+
43
+ Examples
44
+ --------
45
+ The barbell graph with parameter zero has a single bridge:
46
+
47
+ >>> G = nx.barbell_graph(10, 0)
48
+ >>> list(nx.bridges(G))
49
+ [(9, 10)]
50
+
51
+ Notes
52
+ -----
53
+ This is an implementation of the algorithm described in [1]_. An edge is a
54
+ bridge if and only if it is not contained in any chain. Chains are found
55
+ using the :func:`networkx.chain_decomposition` function.
56
+
57
+ The algorithm described in [1]_ requires a simple graph. If the provided
58
+ graph is a multigraph, we convert it to a simple graph and verify that any
59
+ bridges discovered by the chain decomposition algorithm are not multi-edges.
60
+
61
+ Ignoring polylogarithmic factors, the worst-case time complexity is the
62
+ same as the :func:`networkx.chain_decomposition` function,
63
+ $O(m + n)$, where $n$ is the number of nodes in the graph and $m$ is
64
+ the number of edges.
65
+
66
+ References
67
+ ----------
68
+ .. [1] https://en.wikipedia.org/wiki/Bridge_%28graph_theory%29#Bridge-Finding_with_Chain_Decompositions
69
+ """
70
+ multigraph = G.is_multigraph()
71
+ H = nx.Graph(G) if multigraph else G
72
+ chains = nx.chain_decomposition(H, root=root)
73
+ chain_edges = set(chain.from_iterable(chains))
74
+ if root is not None:
75
+ H = H.subgraph(nx.node_connected_component(H, root)).copy()
76
+ for u, v in H.edges():
77
+ if (u, v) not in chain_edges and (v, u) not in chain_edges:
78
+ if multigraph and len(G[u][v]) > 1:
79
+ continue
80
+ yield u, v
81
+
82
+
83
+ @not_implemented_for("directed")
84
+ @nx._dispatchable
85
+ def has_bridges(G, root=None):
86
+ """Decide whether a graph has any bridges.
87
+
88
+ A *bridge* in a graph is an edge whose removal causes the number of
89
+ connected components of the graph to increase.
90
+
91
+ Parameters
92
+ ----------
93
+ G : undirected graph
94
+
95
+ root : node (optional)
96
+ A node in the graph `G`. If specified, only the bridges in the
97
+ connected component containing this node will be considered.
98
+
99
+ Returns
100
+ -------
101
+ bool
102
+ Whether the graph (or the connected component containing `root`)
103
+ has any bridges.
104
+
105
+ Raises
106
+ ------
107
+ NodeNotFound
108
+ If `root` is not in the graph `G`.
109
+
110
+ NetworkXNotImplemented
111
+ If `G` is a directed graph.
112
+
113
+ Examples
114
+ --------
115
+ The barbell graph with parameter zero has a single bridge::
116
+
117
+ >>> G = nx.barbell_graph(10, 0)
118
+ >>> nx.has_bridges(G)
119
+ True
120
+
121
+ On the other hand, the cycle graph has no bridges::
122
+
123
+ >>> G = nx.cycle_graph(5)
124
+ >>> nx.has_bridges(G)
125
+ False
126
+
127
+ Notes
128
+ -----
129
+ This implementation uses the :func:`networkx.bridges` function, so
130
+ it shares its worst-case time complexity, $O(m + n)$, ignoring
131
+ polylogarithmic factors, where $n$ is the number of nodes in the
132
+ graph and $m$ is the number of edges.
133
+
134
+ """
135
+ try:
136
+ next(bridges(G, root=root))
137
+ except StopIteration:
138
+ return False
139
+ else:
140
+ return True
141
+
142
+
143
+ @not_implemented_for("multigraph")
144
+ @not_implemented_for("directed")
145
+ @nx._dispatchable(edge_attrs="weight")
146
+ def local_bridges(G, with_span=True, weight=None):
147
+ """Iterate over local bridges of `G` optionally computing the span
148
+
149
+ A *local bridge* is an edge whose endpoints have no common neighbors.
150
+ That is, the edge is not part of a triangle in the graph.
151
+
152
+ The *span* of a *local bridge* is the shortest path length between
153
+ the endpoints if the local bridge is removed.
154
+
155
+ Parameters
156
+ ----------
157
+ G : undirected graph
158
+
159
+ with_span : bool
160
+ If True, yield a 3-tuple `(u, v, span)`
161
+
162
+ weight : function, string or None (default: None)
163
+ If function, used to compute edge weights for the span.
164
+ If string, the edge data attribute used in calculating span.
165
+ If None, all edges have weight 1.
166
+
167
+ Yields
168
+ ------
169
+ e : edge
170
+ The local bridges as an edge 2-tuple of nodes `(u, v)` or
171
+ as a 3-tuple `(u, v, span)` when `with_span is True`.
172
+
173
+ Raises
174
+ ------
175
+ NetworkXNotImplemented
176
+ If `G` is a directed graph or multigraph.
177
+
178
+ Examples
179
+ --------
180
+ A cycle graph has every edge a local bridge with span N-1.
181
+
182
+ >>> G = nx.cycle_graph(9)
183
+ >>> (0, 8, 8) in set(nx.local_bridges(G))
184
+ True
185
+ """
186
+ if with_span is not True:
187
+ for u, v in G.edges:
188
+ if not (set(G[u]) & set(G[v])):
189
+ yield u, v
190
+ else:
191
+ wt = nx.weighted._weight_function(G, weight)
192
+ for u, v in G.edges:
193
+ if not (set(G[u]) & set(G[v])):
194
+ enodes = {u, v}
195
+
196
+ def hide_edge(n, nbr, d):
197
+ if n not in enodes or nbr not in enodes:
198
+ return wt(n, nbr, d)
199
+ return None
200
+
201
+ try:
202
+ span = nx.shortest_path_length(G, u, v, weight=hide_edge)
203
+ yield u, v, span
204
+ except nx.NetworkXNoPath:
205
+ yield u, v, float("inf")
.venv/lib/python3.11/site-packages/networkx/algorithms/chains.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions for finding chains in a graph."""
2
+
3
+ import networkx as nx
4
+ from networkx.utils import not_implemented_for
5
+
6
+ __all__ = ["chain_decomposition"]
7
+
8
+
9
+ @not_implemented_for("directed")
10
+ @not_implemented_for("multigraph")
11
+ @nx._dispatchable
12
+ def chain_decomposition(G, root=None):
13
+ """Returns the chain decomposition of a graph.
14
+
15
+ The *chain decomposition* of a graph with respect a depth-first
16
+ search tree is a set of cycles or paths derived from the set of
17
+ fundamental cycles of the tree in the following manner. Consider
18
+ each fundamental cycle with respect to the given tree, represented
19
+ as a list of edges beginning with the nontree edge oriented away
20
+ from the root of the tree. For each fundamental cycle, if it
21
+ overlaps with any previous fundamental cycle, just take the initial
22
+ non-overlapping segment, which is a path instead of a cycle. Each
23
+ cycle or path is called a *chain*. For more information, see [1]_.
24
+
25
+ Parameters
26
+ ----------
27
+ G : undirected graph
28
+
29
+ root : node (optional)
30
+ A node in the graph `G`. If specified, only the chain
31
+ decomposition for the connected component containing this node
32
+ will be returned. This node indicates the root of the depth-first
33
+ search tree.
34
+
35
+ Yields
36
+ ------
37
+ chain : list
38
+ A list of edges representing a chain. There is no guarantee on
39
+ the orientation of the edges in each chain (for example, if a
40
+ chain includes the edge joining nodes 1 and 2, the chain may
41
+ include either (1, 2) or (2, 1)).
42
+
43
+ Raises
44
+ ------
45
+ NodeNotFound
46
+ If `root` is not in the graph `G`.
47
+
48
+ Examples
49
+ --------
50
+ >>> G = nx.Graph([(0, 1), (1, 4), (3, 4), (3, 5), (4, 5)])
51
+ >>> list(nx.chain_decomposition(G))
52
+ [[(4, 5), (5, 3), (3, 4)]]
53
+
54
+ Notes
55
+ -----
56
+ The worst-case running time of this implementation is linear in the
57
+ number of nodes and number of edges [1]_.
58
+
59
+ References
60
+ ----------
61
+ .. [1] Jens M. Schmidt (2013). "A simple test on 2-vertex-
62
+ and 2-edge-connectivity." *Information Processing Letters*,
63
+ 113, 241–244. Elsevier. <https://doi.org/10.1016/j.ipl.2013.01.016>
64
+
65
+ """
66
+
67
+ def _dfs_cycle_forest(G, root=None):
68
+ """Builds a directed graph composed of cycles from the given graph.
69
+
70
+ `G` is an undirected simple graph. `root` is a node in the graph
71
+ from which the depth-first search is started.
72
+
73
+ This function returns both the depth-first search cycle graph
74
+ (as a :class:`~networkx.DiGraph`) and the list of nodes in
75
+ depth-first preorder. The depth-first search cycle graph is a
76
+ directed graph whose edges are the edges of `G` oriented toward
77
+ the root if the edge is a tree edge and away from the root if
78
+ the edge is a non-tree edge. If `root` is not specified, this
79
+ performs a depth-first search on each connected component of `G`
80
+ and returns a directed forest instead.
81
+
82
+ If `root` is not in the graph, this raises :exc:`KeyError`.
83
+
84
+ """
85
+ # Create a directed graph from the depth-first search tree with
86
+ # root node `root` in which tree edges are directed toward the
87
+ # root and nontree edges are directed away from the root. For
88
+ # each node with an incident nontree edge, this creates a
89
+ # directed cycle starting with the nontree edge and returning to
90
+ # that node.
91
+ #
92
+ # The `parent` node attribute stores the parent of each node in
93
+ # the DFS tree. The `nontree` edge attribute indicates whether
94
+ # the edge is a tree edge or a nontree edge.
95
+ #
96
+ # We also store the order of the nodes found in the depth-first
97
+ # search in the `nodes` list.
98
+ H = nx.DiGraph()
99
+ nodes = []
100
+ for u, v, d in nx.dfs_labeled_edges(G, source=root):
101
+ if d == "forward":
102
+ # `dfs_labeled_edges()` yields (root, root, 'forward')
103
+ # if it is beginning the search on a new connected
104
+ # component.
105
+ if u == v:
106
+ H.add_node(v, parent=None)
107
+ nodes.append(v)
108
+ else:
109
+ H.add_node(v, parent=u)
110
+ H.add_edge(v, u, nontree=False)
111
+ nodes.append(v)
112
+ # `dfs_labeled_edges` considers nontree edges in both
113
+ # orientations, so we need to not add the edge if it its
114
+ # other orientation has been added.
115
+ elif d == "nontree" and v not in H[u]:
116
+ H.add_edge(v, u, nontree=True)
117
+ else:
118
+ # Do nothing on 'reverse' edges; we only care about
119
+ # forward and nontree edges.
120
+ pass
121
+ return H, nodes
122
+
123
+ def _build_chain(G, u, v, visited):
124
+ """Generate the chain starting from the given nontree edge.
125
+
126
+ `G` is a DFS cycle graph as constructed by
127
+ :func:`_dfs_cycle_graph`. The edge (`u`, `v`) is a nontree edge
128
+ that begins a chain. `visited` is a set representing the nodes
129
+ in `G` that have already been visited.
130
+
131
+ This function yields the edges in an initial segment of the
132
+ fundamental cycle of `G` starting with the nontree edge (`u`,
133
+ `v`) that includes all the edges up until the first node that
134
+ appears in `visited`. The tree edges are given by the 'parent'
135
+ node attribute. The `visited` set is updated to add each node in
136
+ an edge yielded by this function.
137
+
138
+ """
139
+ while v not in visited:
140
+ yield u, v
141
+ visited.add(v)
142
+ u, v = v, G.nodes[v]["parent"]
143
+ yield u, v
144
+
145
+ # Check if the root is in the graph G. If not, raise NodeNotFound
146
+ if root is not None and root not in G:
147
+ raise nx.NodeNotFound(f"Root node {root} is not in graph")
148
+
149
+ # Create a directed version of H that has the DFS edges directed
150
+ # toward the root and the nontree edges directed away from the root
151
+ # (in each connected component).
152
+ H, nodes = _dfs_cycle_forest(G, root)
153
+
154
+ # Visit the nodes again in DFS order. For each node, and for each
155
+ # nontree edge leaving that node, compute the fundamental cycle for
156
+ # that nontree edge starting with that edge. If the fundamental
157
+ # cycle overlaps with any visited nodes, just take the prefix of the
158
+ # cycle up to the point of visited nodes.
159
+ #
160
+ # We repeat this process for each connected component (implicitly,
161
+ # since `nodes` already has a list of the nodes grouped by connected
162
+ # component).
163
+ visited = set()
164
+ for u in nodes:
165
+ visited.add(u)
166
+ # For each nontree edge going out of node u...
167
+ edges = ((u, v) for u, v, d in H.out_edges(u, data="nontree") if d)
168
+ for u, v in edges:
169
+ # Create the cycle or cycle prefix starting with the
170
+ # nontree edge.
171
+ chain = list(_build_chain(H, u, v, visited))
172
+ yield chain
.venv/lib/python3.11/site-packages/networkx/algorithms/chordal.py ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Algorithms for chordal graphs.
3
+
4
+ A graph is chordal if every cycle of length at least 4 has a chord
5
+ (an edge joining two nodes not adjacent in the cycle).
6
+ https://en.wikipedia.org/wiki/Chordal_graph
7
+ """
8
+
9
+ import sys
10
+
11
+ import networkx as nx
12
+ from networkx.algorithms.components import connected_components
13
+ from networkx.utils import arbitrary_element, not_implemented_for
14
+
15
+ __all__ = [
16
+ "is_chordal",
17
+ "find_induced_nodes",
18
+ "chordal_graph_cliques",
19
+ "chordal_graph_treewidth",
20
+ "NetworkXTreewidthBoundExceeded",
21
+ "complete_to_chordal_graph",
22
+ ]
23
+
24
+
25
+ class NetworkXTreewidthBoundExceeded(nx.NetworkXException):
26
+ """Exception raised when a treewidth bound has been provided and it has
27
+ been exceeded"""
28
+
29
+
30
+ @not_implemented_for("directed")
31
+ @not_implemented_for("multigraph")
32
+ @nx._dispatchable
33
+ def is_chordal(G):
34
+ """Checks whether G is a chordal graph.
35
+
36
+ A graph is chordal if every cycle of length at least 4 has a chord
37
+ (an edge joining two nodes not adjacent in the cycle).
38
+
39
+ Parameters
40
+ ----------
41
+ G : graph
42
+ A NetworkX graph.
43
+
44
+ Returns
45
+ -------
46
+ chordal : bool
47
+ True if G is a chordal graph and False otherwise.
48
+
49
+ Raises
50
+ ------
51
+ NetworkXNotImplemented
52
+ The algorithm does not support DiGraph, MultiGraph and MultiDiGraph.
53
+
54
+ Examples
55
+ --------
56
+ >>> e = [
57
+ ... (1, 2),
58
+ ... (1, 3),
59
+ ... (2, 3),
60
+ ... (2, 4),
61
+ ... (3, 4),
62
+ ... (3, 5),
63
+ ... (3, 6),
64
+ ... (4, 5),
65
+ ... (4, 6),
66
+ ... (5, 6),
67
+ ... ]
68
+ >>> G = nx.Graph(e)
69
+ >>> nx.is_chordal(G)
70
+ True
71
+
72
+ Notes
73
+ -----
74
+ The routine tries to go through every node following maximum cardinality
75
+ search. It returns False when it finds that the separator for any node
76
+ is not a clique. Based on the algorithms in [1]_.
77
+
78
+ Self loops are ignored.
79
+
80
+ References
81
+ ----------
82
+ .. [1] R. E. Tarjan and M. Yannakakis, Simple linear-time algorithms
83
+ to test chordality of graphs, test acyclicity of hypergraphs, and
84
+ selectively reduce acyclic hypergraphs, SIAM J. Comput., 13 (1984),
85
+ pp. 566–579.
86
+ """
87
+ if len(G.nodes) <= 3:
88
+ return True
89
+ return len(_find_chordality_breaker(G)) == 0
90
+
91
+
92
+ @nx._dispatchable
93
+ def find_induced_nodes(G, s, t, treewidth_bound=sys.maxsize):
94
+ """Returns the set of induced nodes in the path from s to t.
95
+
96
+ Parameters
97
+ ----------
98
+ G : graph
99
+ A chordal NetworkX graph
100
+ s : node
101
+ Source node to look for induced nodes
102
+ t : node
103
+ Destination node to look for induced nodes
104
+ treewidth_bound: float
105
+ Maximum treewidth acceptable for the graph H. The search
106
+ for induced nodes will end as soon as the treewidth_bound is exceeded.
107
+
108
+ Returns
109
+ -------
110
+ induced_nodes : Set of nodes
111
+ The set of induced nodes in the path from s to t in G
112
+
113
+ Raises
114
+ ------
115
+ NetworkXError
116
+ The algorithm does not support DiGraph, MultiGraph and MultiDiGraph.
117
+ If the input graph is an instance of one of these classes, a
118
+ :exc:`NetworkXError` is raised.
119
+ The algorithm can only be applied to chordal graphs. If the input
120
+ graph is found to be non-chordal, a :exc:`NetworkXError` is raised.
121
+
122
+ Examples
123
+ --------
124
+ >>> G = nx.Graph()
125
+ >>> G = nx.generators.classic.path_graph(10)
126
+ >>> induced_nodes = nx.find_induced_nodes(G, 1, 9, 2)
127
+ >>> sorted(induced_nodes)
128
+ [1, 2, 3, 4, 5, 6, 7, 8, 9]
129
+
130
+ Notes
131
+ -----
132
+ G must be a chordal graph and (s,t) an edge that is not in G.
133
+
134
+ If a treewidth_bound is provided, the search for induced nodes will end
135
+ as soon as the treewidth_bound is exceeded.
136
+
137
+ The algorithm is inspired by Algorithm 4 in [1]_.
138
+ A formal definition of induced node can also be found on that reference.
139
+
140
+ Self Loops are ignored
141
+
142
+ References
143
+ ----------
144
+ .. [1] Learning Bounded Treewidth Bayesian Networks.
145
+ Gal Elidan, Stephen Gould; JMLR, 9(Dec):2699--2731, 2008.
146
+ http://jmlr.csail.mit.edu/papers/volume9/elidan08a/elidan08a.pdf
147
+ """
148
+ if not is_chordal(G):
149
+ raise nx.NetworkXError("Input graph is not chordal.")
150
+
151
+ H = nx.Graph(G)
152
+ H.add_edge(s, t)
153
+ induced_nodes = set()
154
+ triplet = _find_chordality_breaker(H, s, treewidth_bound)
155
+ while triplet:
156
+ (u, v, w) = triplet
157
+ induced_nodes.update(triplet)
158
+ for n in triplet:
159
+ if n != s:
160
+ H.add_edge(s, n)
161
+ triplet = _find_chordality_breaker(H, s, treewidth_bound)
162
+ if induced_nodes:
163
+ # Add t and the second node in the induced path from s to t.
164
+ induced_nodes.add(t)
165
+ for u in G[s]:
166
+ if len(induced_nodes & set(G[u])) == 2:
167
+ induced_nodes.add(u)
168
+ break
169
+ return induced_nodes
170
+
171
+
172
+ @nx._dispatchable
173
+ def chordal_graph_cliques(G):
174
+ """Returns all maximal cliques of a chordal graph.
175
+
176
+ The algorithm breaks the graph in connected components and performs a
177
+ maximum cardinality search in each component to get the cliques.
178
+
179
+ Parameters
180
+ ----------
181
+ G : graph
182
+ A NetworkX graph
183
+
184
+ Yields
185
+ ------
186
+ frozenset of nodes
187
+ Maximal cliques, each of which is a frozenset of
188
+ nodes in `G`. The order of cliques is arbitrary.
189
+
190
+ Raises
191
+ ------
192
+ NetworkXError
193
+ The algorithm does not support DiGraph, MultiGraph and MultiDiGraph.
194
+ The algorithm can only be applied to chordal graphs. If the input
195
+ graph is found to be non-chordal, a :exc:`NetworkXError` is raised.
196
+
197
+ Examples
198
+ --------
199
+ >>> e = [
200
+ ... (1, 2),
201
+ ... (1, 3),
202
+ ... (2, 3),
203
+ ... (2, 4),
204
+ ... (3, 4),
205
+ ... (3, 5),
206
+ ... (3, 6),
207
+ ... (4, 5),
208
+ ... (4, 6),
209
+ ... (5, 6),
210
+ ... (7, 8),
211
+ ... ]
212
+ >>> G = nx.Graph(e)
213
+ >>> G.add_node(9)
214
+ >>> cliques = [c for c in chordal_graph_cliques(G)]
215
+ >>> cliques[0]
216
+ frozenset({1, 2, 3})
217
+ """
218
+ for C in (G.subgraph(c).copy() for c in connected_components(G)):
219
+ if C.number_of_nodes() == 1:
220
+ if nx.number_of_selfloops(C) > 0:
221
+ raise nx.NetworkXError("Input graph is not chordal.")
222
+ yield frozenset(C.nodes())
223
+ else:
224
+ unnumbered = set(C.nodes())
225
+ v = arbitrary_element(C)
226
+ unnumbered.remove(v)
227
+ numbered = {v}
228
+ clique_wanna_be = {v}
229
+ while unnumbered:
230
+ v = _max_cardinality_node(C, unnumbered, numbered)
231
+ unnumbered.remove(v)
232
+ numbered.add(v)
233
+ new_clique_wanna_be = set(C.neighbors(v)) & numbered
234
+ sg = C.subgraph(clique_wanna_be)
235
+ if _is_complete_graph(sg):
236
+ new_clique_wanna_be.add(v)
237
+ if not new_clique_wanna_be >= clique_wanna_be:
238
+ yield frozenset(clique_wanna_be)
239
+ clique_wanna_be = new_clique_wanna_be
240
+ else:
241
+ raise nx.NetworkXError("Input graph is not chordal.")
242
+ yield frozenset(clique_wanna_be)
243
+
244
+
245
+ @nx._dispatchable
246
+ def chordal_graph_treewidth(G):
247
+ """Returns the treewidth of the chordal graph G.
248
+
249
+ Parameters
250
+ ----------
251
+ G : graph
252
+ A NetworkX graph
253
+
254
+ Returns
255
+ -------
256
+ treewidth : int
257
+ The size of the largest clique in the graph minus one.
258
+
259
+ Raises
260
+ ------
261
+ NetworkXError
262
+ The algorithm does not support DiGraph, MultiGraph and MultiDiGraph.
263
+ The algorithm can only be applied to chordal graphs. If the input
264
+ graph is found to be non-chordal, a :exc:`NetworkXError` is raised.
265
+
266
+ Examples
267
+ --------
268
+ >>> e = [
269
+ ... (1, 2),
270
+ ... (1, 3),
271
+ ... (2, 3),
272
+ ... (2, 4),
273
+ ... (3, 4),
274
+ ... (3, 5),
275
+ ... (3, 6),
276
+ ... (4, 5),
277
+ ... (4, 6),
278
+ ... (5, 6),
279
+ ... (7, 8),
280
+ ... ]
281
+ >>> G = nx.Graph(e)
282
+ >>> G.add_node(9)
283
+ >>> nx.chordal_graph_treewidth(G)
284
+ 3
285
+
286
+ References
287
+ ----------
288
+ .. [1] https://en.wikipedia.org/wiki/Tree_decomposition#Treewidth
289
+ """
290
+ if not is_chordal(G):
291
+ raise nx.NetworkXError("Input graph is not chordal.")
292
+
293
+ max_clique = -1
294
+ for clique in nx.chordal_graph_cliques(G):
295
+ max_clique = max(max_clique, len(clique))
296
+ return max_clique - 1
297
+
298
+
299
+ def _is_complete_graph(G):
300
+ """Returns True if G is a complete graph."""
301
+ if nx.number_of_selfloops(G) > 0:
302
+ raise nx.NetworkXError("Self loop found in _is_complete_graph()")
303
+ n = G.number_of_nodes()
304
+ if n < 2:
305
+ return True
306
+ e = G.number_of_edges()
307
+ max_edges = (n * (n - 1)) / 2
308
+ return e == max_edges
309
+
310
+
311
+ def _find_missing_edge(G):
312
+ """Given a non-complete graph G, returns a missing edge."""
313
+ nodes = set(G)
314
+ for u in G:
315
+ missing = nodes - set(list(G[u].keys()) + [u])
316
+ if missing:
317
+ return (u, missing.pop())
318
+
319
+
320
+ def _max_cardinality_node(G, choices, wanna_connect):
321
+ """Returns a the node in choices that has more connections in G
322
+ to nodes in wanna_connect.
323
+ """
324
+ max_number = -1
325
+ for x in choices:
326
+ number = len([y for y in G[x] if y in wanna_connect])
327
+ if number > max_number:
328
+ max_number = number
329
+ max_cardinality_node = x
330
+ return max_cardinality_node
331
+
332
+
333
+ def _find_chordality_breaker(G, s=None, treewidth_bound=sys.maxsize):
334
+ """Given a graph G, starts a max cardinality search
335
+ (starting from s if s is given and from an arbitrary node otherwise)
336
+ trying to find a non-chordal cycle.
337
+
338
+ If it does find one, it returns (u,v,w) where u,v,w are the three
339
+ nodes that together with s are involved in the cycle.
340
+
341
+ It ignores any self loops.
342
+ """
343
+ if len(G) == 0:
344
+ raise nx.NetworkXPointlessConcept("Graph has no nodes.")
345
+ unnumbered = set(G)
346
+ if s is None:
347
+ s = arbitrary_element(G)
348
+ unnumbered.remove(s)
349
+ numbered = {s}
350
+ current_treewidth = -1
351
+ while unnumbered: # and current_treewidth <= treewidth_bound:
352
+ v = _max_cardinality_node(G, unnumbered, numbered)
353
+ unnumbered.remove(v)
354
+ numbered.add(v)
355
+ clique_wanna_be = set(G[v]) & numbered
356
+ sg = G.subgraph(clique_wanna_be)
357
+ if _is_complete_graph(sg):
358
+ # The graph seems to be chordal by now. We update the treewidth
359
+ current_treewidth = max(current_treewidth, len(clique_wanna_be))
360
+ if current_treewidth > treewidth_bound:
361
+ raise nx.NetworkXTreewidthBoundExceeded(
362
+ f"treewidth_bound exceeded: {current_treewidth}"
363
+ )
364
+ else:
365
+ # sg is not a clique,
366
+ # look for an edge that is not included in sg
367
+ (u, w) = _find_missing_edge(sg)
368
+ return (u, v, w)
369
+ return ()
370
+
371
+
372
+ @not_implemented_for("directed")
373
+ @nx._dispatchable(returns_graph=True)
374
+ def complete_to_chordal_graph(G):
375
+ """Return a copy of G completed to a chordal graph
376
+
377
+ Adds edges to a copy of G to create a chordal graph. A graph G=(V,E) is
378
+ called chordal if for each cycle with length bigger than 3, there exist
379
+ two non-adjacent nodes connected by an edge (called a chord).
380
+
381
+ Parameters
382
+ ----------
383
+ G : NetworkX graph
384
+ Undirected graph
385
+
386
+ Returns
387
+ -------
388
+ H : NetworkX graph
389
+ The chordal enhancement of G
390
+ alpha : Dictionary
391
+ The elimination ordering of nodes of G
392
+
393
+ Notes
394
+ -----
395
+ There are different approaches to calculate the chordal
396
+ enhancement of a graph. The algorithm used here is called
397
+ MCS-M and gives at least minimal (local) triangulation of graph. Note
398
+ that this triangulation is not necessarily a global minimum.
399
+
400
+ https://en.wikipedia.org/wiki/Chordal_graph
401
+
402
+ References
403
+ ----------
404
+ .. [1] Berry, Anne & Blair, Jean & Heggernes, Pinar & Peyton, Barry. (2004)
405
+ Maximum Cardinality Search for Computing Minimal Triangulations of
406
+ Graphs. Algorithmica. 39. 287-298. 10.1007/s00453-004-1084-3.
407
+
408
+ Examples
409
+ --------
410
+ >>> from networkx.algorithms.chordal import complete_to_chordal_graph
411
+ >>> G = nx.wheel_graph(10)
412
+ >>> H, alpha = complete_to_chordal_graph(G)
413
+ """
414
+ H = G.copy()
415
+ alpha = {node: 0 for node in H}
416
+ if nx.is_chordal(H):
417
+ return H, alpha
418
+ chords = set()
419
+ weight = {node: 0 for node in H.nodes()}
420
+ unnumbered_nodes = list(H.nodes())
421
+ for i in range(len(H.nodes()), 0, -1):
422
+ # get the node in unnumbered_nodes with the maximum weight
423
+ z = max(unnumbered_nodes, key=lambda node: weight[node])
424
+ unnumbered_nodes.remove(z)
425
+ alpha[z] = i
426
+ update_nodes = []
427
+ for y in unnumbered_nodes:
428
+ if G.has_edge(y, z):
429
+ update_nodes.append(y)
430
+ else:
431
+ # y_weight will be bigger than node weights between y and z
432
+ y_weight = weight[y]
433
+ lower_nodes = [
434
+ node for node in unnumbered_nodes if weight[node] < y_weight
435
+ ]
436
+ if nx.has_path(H.subgraph(lower_nodes + [z, y]), y, z):
437
+ update_nodes.append(y)
438
+ chords.add((z, y))
439
+ # during calculation of paths the weights should not be updated
440
+ for node in update_nodes:
441
+ weight[node] += 1
442
+ H.add_edges_from(chords)
443
+ return H, alpha
.venv/lib/python3.11/site-packages/networkx/algorithms/clique.py ADDED
@@ -0,0 +1,755 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions for finding and manipulating cliques.
2
+
3
+ Finding the largest clique in a graph is NP-complete problem, so most of
4
+ these algorithms have an exponential running time; for more information,
5
+ see the Wikipedia article on the clique problem [1]_.
6
+
7
+ .. [1] clique problem:: https://en.wikipedia.org/wiki/Clique_problem
8
+
9
+ """
10
+
11
+ from collections import defaultdict, deque
12
+ from itertools import chain, combinations, islice
13
+
14
+ import networkx as nx
15
+ from networkx.utils import not_implemented_for
16
+
17
+ __all__ = [
18
+ "find_cliques",
19
+ "find_cliques_recursive",
20
+ "make_max_clique_graph",
21
+ "make_clique_bipartite",
22
+ "node_clique_number",
23
+ "number_of_cliques",
24
+ "enumerate_all_cliques",
25
+ "max_weight_clique",
26
+ ]
27
+
28
+
29
+ @not_implemented_for("directed")
30
+ @nx._dispatchable
31
+ def enumerate_all_cliques(G):
32
+ """Returns all cliques in an undirected graph.
33
+
34
+ This function returns an iterator over cliques, each of which is a
35
+ list of nodes. The iteration is ordered by cardinality of the
36
+ cliques: first all cliques of size one, then all cliques of size
37
+ two, etc.
38
+
39
+ Parameters
40
+ ----------
41
+ G : NetworkX graph
42
+ An undirected graph.
43
+
44
+ Returns
45
+ -------
46
+ iterator
47
+ An iterator over cliques, each of which is a list of nodes in
48
+ `G`. The cliques are ordered according to size.
49
+
50
+ Notes
51
+ -----
52
+ To obtain a list of all cliques, use
53
+ `list(enumerate_all_cliques(G))`. However, be aware that in the
54
+ worst-case, the length of this list can be exponential in the number
55
+ of nodes in the graph (for example, when the graph is the complete
56
+ graph). This function avoids storing all cliques in memory by only
57
+ keeping current candidate node lists in memory during its search.
58
+
59
+ The implementation is adapted from the algorithm by Zhang, et
60
+ al. (2005) [1]_ to output all cliques discovered.
61
+
62
+ This algorithm ignores self-loops and parallel edges, since cliques
63
+ are not conventionally defined with such edges.
64
+
65
+ References
66
+ ----------
67
+ .. [1] Yun Zhang, Abu-Khzam, F.N., Baldwin, N.E., Chesler, E.J.,
68
+ Langston, M.A., Samatova, N.F.,
69
+ "Genome-Scale Computational Approaches to Memory-Intensive
70
+ Applications in Systems Biology".
71
+ *Supercomputing*, 2005. Proceedings of the ACM/IEEE SC 2005
72
+ Conference, pp. 12, 12--18 Nov. 2005.
73
+ <https://doi.org/10.1109/SC.2005.29>.
74
+
75
+ """
76
+ index = {}
77
+ nbrs = {}
78
+ for u in G:
79
+ index[u] = len(index)
80
+ # Neighbors of u that appear after u in the iteration order of G.
81
+ nbrs[u] = {v for v in G[u] if v not in index}
82
+
83
+ queue = deque(([u], sorted(nbrs[u], key=index.__getitem__)) for u in G)
84
+ # Loop invariants:
85
+ # 1. len(base) is nondecreasing.
86
+ # 2. (base + cnbrs) is sorted with respect to the iteration order of G.
87
+ # 3. cnbrs is a set of common neighbors of nodes in base.
88
+ while queue:
89
+ base, cnbrs = map(list, queue.popleft())
90
+ yield base
91
+ for i, u in enumerate(cnbrs):
92
+ # Use generators to reduce memory consumption.
93
+ queue.append(
94
+ (
95
+ chain(base, [u]),
96
+ filter(nbrs[u].__contains__, islice(cnbrs, i + 1, None)),
97
+ )
98
+ )
99
+
100
+
101
+ @not_implemented_for("directed")
102
+ @nx._dispatchable
103
+ def find_cliques(G, nodes=None):
104
+ """Returns all maximal cliques in an undirected graph.
105
+
106
+ For each node *n*, a *maximal clique for n* is a largest complete
107
+ subgraph containing *n*. The largest maximal clique is sometimes
108
+ called the *maximum clique*.
109
+
110
+ This function returns an iterator over cliques, each of which is a
111
+ list of nodes. It is an iterative implementation, so should not
112
+ suffer from recursion depth issues.
113
+
114
+ This function accepts a list of `nodes` and only the maximal cliques
115
+ containing all of these `nodes` are returned. It can considerably speed up
116
+ the running time if some specific cliques are desired.
117
+
118
+ Parameters
119
+ ----------
120
+ G : NetworkX graph
121
+ An undirected graph.
122
+
123
+ nodes : list, optional (default=None)
124
+ If provided, only yield *maximal cliques* containing all nodes in `nodes`.
125
+ If `nodes` isn't a clique itself, a ValueError is raised.
126
+
127
+ Returns
128
+ -------
129
+ iterator
130
+ An iterator over maximal cliques, each of which is a list of
131
+ nodes in `G`. If `nodes` is provided, only the maximal cliques
132
+ containing all the nodes in `nodes` are returned. The order of
133
+ cliques is arbitrary.
134
+
135
+ Raises
136
+ ------
137
+ ValueError
138
+ If `nodes` is not a clique.
139
+
140
+ Examples
141
+ --------
142
+ >>> from pprint import pprint # For nice dict formatting
143
+ >>> G = nx.karate_club_graph()
144
+ >>> sum(1 for c in nx.find_cliques(G)) # The number of maximal cliques in G
145
+ 36
146
+ >>> max(nx.find_cliques(G), key=len) # The largest maximal clique in G
147
+ [0, 1, 2, 3, 13]
148
+
149
+ The size of the largest maximal clique is known as the *clique number* of
150
+ the graph, which can be found directly with:
151
+
152
+ >>> max(len(c) for c in nx.find_cliques(G))
153
+ 5
154
+
155
+ One can also compute the number of maximal cliques in `G` that contain a given
156
+ node. The following produces a dictionary keyed by node whose
157
+ values are the number of maximal cliques in `G` that contain the node:
158
+
159
+ >>> pprint({n: sum(1 for c in nx.find_cliques(G) if n in c) for n in G})
160
+ {0: 13,
161
+ 1: 6,
162
+ 2: 7,
163
+ 3: 3,
164
+ 4: 2,
165
+ 5: 3,
166
+ 6: 3,
167
+ 7: 1,
168
+ 8: 3,
169
+ 9: 2,
170
+ 10: 2,
171
+ 11: 1,
172
+ 12: 1,
173
+ 13: 2,
174
+ 14: 1,
175
+ 15: 1,
176
+ 16: 1,
177
+ 17: 1,
178
+ 18: 1,
179
+ 19: 2,
180
+ 20: 1,
181
+ 21: 1,
182
+ 22: 1,
183
+ 23: 3,
184
+ 24: 2,
185
+ 25: 2,
186
+ 26: 1,
187
+ 27: 3,
188
+ 28: 2,
189
+ 29: 2,
190
+ 30: 2,
191
+ 31: 4,
192
+ 32: 9,
193
+ 33: 14}
194
+
195
+ Or, similarly, the maximal cliques in `G` that contain a given node.
196
+ For example, the 4 maximal cliques that contain node 31:
197
+
198
+ >>> [c for c in nx.find_cliques(G) if 31 in c]
199
+ [[0, 31], [33, 32, 31], [33, 28, 31], [24, 25, 31]]
200
+
201
+ See Also
202
+ --------
203
+ find_cliques_recursive
204
+ A recursive version of the same algorithm.
205
+
206
+ Notes
207
+ -----
208
+ To obtain a list of all maximal cliques, use
209
+ `list(find_cliques(G))`. However, be aware that in the worst-case,
210
+ the length of this list can be exponential in the number of nodes in
211
+ the graph. This function avoids storing all cliques in memory by
212
+ only keeping current candidate node lists in memory during its search.
213
+
214
+ This implementation is based on the algorithm published by Bron and
215
+ Kerbosch (1973) [1]_, as adapted by Tomita, Tanaka and Takahashi
216
+ (2006) [2]_ and discussed in Cazals and Karande (2008) [3]_. It
217
+ essentially unrolls the recursion used in the references to avoid
218
+ issues of recursion stack depth (for a recursive implementation, see
219
+ :func:`find_cliques_recursive`).
220
+
221
+ This algorithm ignores self-loops and parallel edges, since cliques
222
+ are not conventionally defined with such edges.
223
+
224
+ References
225
+ ----------
226
+ .. [1] Bron, C. and Kerbosch, J.
227
+ "Algorithm 457: finding all cliques of an undirected graph".
228
+ *Communications of the ACM* 16, 9 (Sep. 1973), 575--577.
229
+ <http://portal.acm.org/citation.cfm?doid=362342.362367>
230
+
231
+ .. [2] Etsuji Tomita, Akira Tanaka, Haruhisa Takahashi,
232
+ "The worst-case time complexity for generating all maximal
233
+ cliques and computational experiments",
234
+ *Theoretical Computer Science*, Volume 363, Issue 1,
235
+ Computing and Combinatorics,
236
+ 10th Annual International Conference on
237
+ Computing and Combinatorics (COCOON 2004), 25 October 2006, Pages 28--42
238
+ <https://doi.org/10.1016/j.tcs.2006.06.015>
239
+
240
+ .. [3] F. Cazals, C. Karande,
241
+ "A note on the problem of reporting maximal cliques",
242
+ *Theoretical Computer Science*,
243
+ Volume 407, Issues 1--3, 6 November 2008, Pages 564--568,
244
+ <https://doi.org/10.1016/j.tcs.2008.05.010>
245
+
246
+ """
247
+ if len(G) == 0:
248
+ return
249
+
250
+ adj = {u: {v for v in G[u] if v != u} for u in G}
251
+
252
+ # Initialize Q with the given nodes and subg, cand with their nbrs
253
+ Q = nodes[:] if nodes is not None else []
254
+ cand = set(G)
255
+ for node in Q:
256
+ if node not in cand:
257
+ raise ValueError(f"The given `nodes` {nodes} do not form a clique")
258
+ cand &= adj[node]
259
+
260
+ if not cand:
261
+ yield Q[:]
262
+ return
263
+
264
+ subg = cand.copy()
265
+ stack = []
266
+ Q.append(None)
267
+
268
+ u = max(subg, key=lambda u: len(cand & adj[u]))
269
+ ext_u = cand - adj[u]
270
+
271
+ try:
272
+ while True:
273
+ if ext_u:
274
+ q = ext_u.pop()
275
+ cand.remove(q)
276
+ Q[-1] = q
277
+ adj_q = adj[q]
278
+ subg_q = subg & adj_q
279
+ if not subg_q:
280
+ yield Q[:]
281
+ else:
282
+ cand_q = cand & adj_q
283
+ if cand_q:
284
+ stack.append((subg, cand, ext_u))
285
+ Q.append(None)
286
+ subg = subg_q
287
+ cand = cand_q
288
+ u = max(subg, key=lambda u: len(cand & adj[u]))
289
+ ext_u = cand - adj[u]
290
+ else:
291
+ Q.pop()
292
+ subg, cand, ext_u = stack.pop()
293
+ except IndexError:
294
+ pass
295
+
296
+
297
+ # TODO Should this also be not implemented for directed graphs?
298
+ @nx._dispatchable
299
+ def find_cliques_recursive(G, nodes=None):
300
+ """Returns all maximal cliques in a graph.
301
+
302
+ For each node *v*, a *maximal clique for v* is a largest complete
303
+ subgraph containing *v*. The largest maximal clique is sometimes
304
+ called the *maximum clique*.
305
+
306
+ This function returns an iterator over cliques, each of which is a
307
+ list of nodes. It is a recursive implementation, so may suffer from
308
+ recursion depth issues, but is included for pedagogical reasons.
309
+ For a non-recursive implementation, see :func:`find_cliques`.
310
+
311
+ This function accepts a list of `nodes` and only the maximal cliques
312
+ containing all of these `nodes` are returned. It can considerably speed up
313
+ the running time if some specific cliques are desired.
314
+
315
+ Parameters
316
+ ----------
317
+ G : NetworkX graph
318
+
319
+ nodes : list, optional (default=None)
320
+ If provided, only yield *maximal cliques* containing all nodes in `nodes`.
321
+ If `nodes` isn't a clique itself, a ValueError is raised.
322
+
323
+ Returns
324
+ -------
325
+ iterator
326
+ An iterator over maximal cliques, each of which is a list of
327
+ nodes in `G`. If `nodes` is provided, only the maximal cliques
328
+ containing all the nodes in `nodes` are yielded. The order of
329
+ cliques is arbitrary.
330
+
331
+ Raises
332
+ ------
333
+ ValueError
334
+ If `nodes` is not a clique.
335
+
336
+ See Also
337
+ --------
338
+ find_cliques
339
+ An iterative version of the same algorithm. See docstring for examples.
340
+
341
+ Notes
342
+ -----
343
+ To obtain a list of all maximal cliques, use
344
+ `list(find_cliques_recursive(G))`. However, be aware that in the
345
+ worst-case, the length of this list can be exponential in the number
346
+ of nodes in the graph. This function avoids storing all cliques in memory
347
+ by only keeping current candidate node lists in memory during its search.
348
+
349
+ This implementation is based on the algorithm published by Bron and
350
+ Kerbosch (1973) [1]_, as adapted by Tomita, Tanaka and Takahashi
351
+ (2006) [2]_ and discussed in Cazals and Karande (2008) [3]_. For a
352
+ non-recursive implementation, see :func:`find_cliques`.
353
+
354
+ This algorithm ignores self-loops and parallel edges, since cliques
355
+ are not conventionally defined with such edges.
356
+
357
+ References
358
+ ----------
359
+ .. [1] Bron, C. and Kerbosch, J.
360
+ "Algorithm 457: finding all cliques of an undirected graph".
361
+ *Communications of the ACM* 16, 9 (Sep. 1973), 575--577.
362
+ <http://portal.acm.org/citation.cfm?doid=362342.362367>
363
+
364
+ .. [2] Etsuji Tomita, Akira Tanaka, Haruhisa Takahashi,
365
+ "The worst-case time complexity for generating all maximal
366
+ cliques and computational experiments",
367
+ *Theoretical Computer Science*, Volume 363, Issue 1,
368
+ Computing and Combinatorics,
369
+ 10th Annual International Conference on
370
+ Computing and Combinatorics (COCOON 2004), 25 October 2006, Pages 28--42
371
+ <https://doi.org/10.1016/j.tcs.2006.06.015>
372
+
373
+ .. [3] F. Cazals, C. Karande,
374
+ "A note on the problem of reporting maximal cliques",
375
+ *Theoretical Computer Science*,
376
+ Volume 407, Issues 1--3, 6 November 2008, Pages 564--568,
377
+ <https://doi.org/10.1016/j.tcs.2008.05.010>
378
+
379
+ """
380
+ if len(G) == 0:
381
+ return iter([])
382
+
383
+ adj = {u: {v for v in G[u] if v != u} for u in G}
384
+
385
+ # Initialize Q with the given nodes and subg, cand with their nbrs
386
+ Q = nodes[:] if nodes is not None else []
387
+ cand_init = set(G)
388
+ for node in Q:
389
+ if node not in cand_init:
390
+ raise ValueError(f"The given `nodes` {nodes} do not form a clique")
391
+ cand_init &= adj[node]
392
+
393
+ if not cand_init:
394
+ return iter([Q])
395
+
396
+ subg_init = cand_init.copy()
397
+
398
+ def expand(subg, cand):
399
+ u = max(subg, key=lambda u: len(cand & adj[u]))
400
+ for q in cand - adj[u]:
401
+ cand.remove(q)
402
+ Q.append(q)
403
+ adj_q = adj[q]
404
+ subg_q = subg & adj_q
405
+ if not subg_q:
406
+ yield Q[:]
407
+ else:
408
+ cand_q = cand & adj_q
409
+ if cand_q:
410
+ yield from expand(subg_q, cand_q)
411
+ Q.pop()
412
+
413
+ return expand(subg_init, cand_init)
414
+
415
+
416
+ @nx._dispatchable(returns_graph=True)
417
+ def make_max_clique_graph(G, create_using=None):
418
+ """Returns the maximal clique graph of the given graph.
419
+
420
+ The nodes of the maximal clique graph of `G` are the cliques of
421
+ `G` and an edge joins two cliques if the cliques are not disjoint.
422
+
423
+ Parameters
424
+ ----------
425
+ G : NetworkX graph
426
+
427
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
428
+ Graph type to create. If graph instance, then cleared before populated.
429
+
430
+ Returns
431
+ -------
432
+ NetworkX graph
433
+ A graph whose nodes are the cliques of `G` and whose edges
434
+ join two cliques if they are not disjoint.
435
+
436
+ Notes
437
+ -----
438
+ This function behaves like the following code::
439
+
440
+ import networkx as nx
441
+
442
+ G = nx.make_clique_bipartite(G)
443
+ cliques = [v for v in G.nodes() if G.nodes[v]["bipartite"] == 0]
444
+ G = nx.bipartite.projected_graph(G, cliques)
445
+ G = nx.relabel_nodes(G, {-v: v - 1 for v in G})
446
+
447
+ It should be faster, though, since it skips all the intermediate
448
+ steps.
449
+
450
+ """
451
+ if create_using is None:
452
+ B = G.__class__()
453
+ else:
454
+ B = nx.empty_graph(0, create_using)
455
+ cliques = list(enumerate(set(c) for c in find_cliques(G)))
456
+ # Add a numbered node for each clique.
457
+ B.add_nodes_from(i for i, c in cliques)
458
+ # Join cliques by an edge if they share a node.
459
+ clique_pairs = combinations(cliques, 2)
460
+ B.add_edges_from((i, j) for (i, c1), (j, c2) in clique_pairs if c1 & c2)
461
+ return B
462
+
463
+
464
+ @nx._dispatchable(returns_graph=True)
465
+ def make_clique_bipartite(G, fpos=None, create_using=None, name=None):
466
+ """Returns the bipartite clique graph corresponding to `G`.
467
+
468
+ In the returned bipartite graph, the "bottom" nodes are the nodes of
469
+ `G` and the "top" nodes represent the maximal cliques of `G`.
470
+ There is an edge from node *v* to clique *C* in the returned graph
471
+ if and only if *v* is an element of *C*.
472
+
473
+ Parameters
474
+ ----------
475
+ G : NetworkX graph
476
+ An undirected graph.
477
+
478
+ fpos : bool
479
+ If True or not None, the returned graph will have an
480
+ additional attribute, `pos`, a dictionary mapping node to
481
+ position in the Euclidean plane.
482
+
483
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
484
+ Graph type to create. If graph instance, then cleared before populated.
485
+
486
+ Returns
487
+ -------
488
+ NetworkX graph
489
+ A bipartite graph whose "bottom" set is the nodes of the graph
490
+ `G`, whose "top" set is the cliques of `G`, and whose edges
491
+ join nodes of `G` to the cliques that contain them.
492
+
493
+ The nodes of the graph `G` have the node attribute
494
+ 'bipartite' set to 1 and the nodes representing cliques
495
+ have the node attribute 'bipartite' set to 0, as is the
496
+ convention for bipartite graphs in NetworkX.
497
+
498
+ """
499
+ B = nx.empty_graph(0, create_using)
500
+ B.clear()
501
+ # The "bottom" nodes in the bipartite graph are the nodes of the
502
+ # original graph, G.
503
+ B.add_nodes_from(G, bipartite=1)
504
+ for i, cl in enumerate(find_cliques(G)):
505
+ # The "top" nodes in the bipartite graph are the cliques. These
506
+ # nodes get negative numbers as labels.
507
+ name = -i - 1
508
+ B.add_node(name, bipartite=0)
509
+ B.add_edges_from((v, name) for v in cl)
510
+ return B
511
+
512
+
513
+ @nx._dispatchable
514
+ def node_clique_number(G, nodes=None, cliques=None, separate_nodes=False):
515
+ """Returns the size of the largest maximal clique containing each given node.
516
+
517
+ Returns a single or list depending on input nodes.
518
+ An optional list of cliques can be input if already computed.
519
+
520
+ Parameters
521
+ ----------
522
+ G : NetworkX graph
523
+ An undirected graph.
524
+
525
+ cliques : list, optional (default=None)
526
+ A list of cliques, each of which is itself a list of nodes.
527
+ If not specified, the list of all cliques will be computed
528
+ using :func:`find_cliques`.
529
+
530
+ Returns
531
+ -------
532
+ int or dict
533
+ If `nodes` is a single node, returns the size of the
534
+ largest maximal clique in `G` containing that node.
535
+ Otherwise return a dict keyed by node to the size
536
+ of the largest maximal clique containing that node.
537
+
538
+ See Also
539
+ --------
540
+ find_cliques
541
+ find_cliques yields the maximal cliques of G.
542
+ It accepts a `nodes` argument which restricts consideration to
543
+ maximal cliques containing all the given `nodes`.
544
+ The search for the cliques is optimized for `nodes`.
545
+ """
546
+ if cliques is None:
547
+ if nodes is not None:
548
+ # Use ego_graph to decrease size of graph
549
+ # check for single node
550
+ if nodes in G:
551
+ return max(len(c) for c in find_cliques(nx.ego_graph(G, nodes)))
552
+ # handle multiple nodes
553
+ return {
554
+ n: max(len(c) for c in find_cliques(nx.ego_graph(G, n))) for n in nodes
555
+ }
556
+
557
+ # nodes is None--find all cliques
558
+ cliques = list(find_cliques(G))
559
+
560
+ # single node requested
561
+ if nodes in G:
562
+ return max(len(c) for c in cliques if nodes in c)
563
+
564
+ # multiple nodes requested
565
+ # preprocess all nodes (faster than one at a time for even 2 nodes)
566
+ size_for_n = defaultdict(int)
567
+ for c in cliques:
568
+ size_of_c = len(c)
569
+ for n in c:
570
+ if size_for_n[n] < size_of_c:
571
+ size_for_n[n] = size_of_c
572
+ if nodes is None:
573
+ return size_for_n
574
+ return {n: size_for_n[n] for n in nodes}
575
+
576
+
577
+ def number_of_cliques(G, nodes=None, cliques=None):
578
+ """Returns the number of maximal cliques for each node.
579
+
580
+ Returns a single or list depending on input nodes.
581
+ Optional list of cliques can be input if already computed.
582
+ """
583
+ if cliques is None:
584
+ cliques = list(find_cliques(G))
585
+
586
+ if nodes is None:
587
+ nodes = list(G.nodes()) # none, get entire graph
588
+
589
+ if not isinstance(nodes, list): # check for a list
590
+ v = nodes
591
+ # assume it is a single value
592
+ numcliq = len([1 for c in cliques if v in c])
593
+ else:
594
+ numcliq = {}
595
+ for v in nodes:
596
+ numcliq[v] = len([1 for c in cliques if v in c])
597
+ return numcliq
598
+
599
+
600
+ class MaxWeightClique:
601
+ """A class for the maximum weight clique algorithm.
602
+
603
+ This class is a helper for the `max_weight_clique` function. The class
604
+ should not normally be used directly.
605
+
606
+ Parameters
607
+ ----------
608
+ G : NetworkX graph
609
+ The undirected graph for which a maximum weight clique is sought
610
+ weight : string or None, optional (default='weight')
611
+ The node attribute that holds the integer value used as a weight.
612
+ If None, then each node has weight 1.
613
+
614
+ Attributes
615
+ ----------
616
+ G : NetworkX graph
617
+ The undirected graph for which a maximum weight clique is sought
618
+ node_weights: dict
619
+ The weight of each node
620
+ incumbent_nodes : list
621
+ The nodes of the incumbent clique (the best clique found so far)
622
+ incumbent_weight: int
623
+ The weight of the incumbent clique
624
+ """
625
+
626
+ def __init__(self, G, weight):
627
+ self.G = G
628
+ self.incumbent_nodes = []
629
+ self.incumbent_weight = 0
630
+
631
+ if weight is None:
632
+ self.node_weights = {v: 1 for v in G.nodes()}
633
+ else:
634
+ for v in G.nodes():
635
+ if weight not in G.nodes[v]:
636
+ errmsg = f"Node {v!r} does not have the requested weight field."
637
+ raise KeyError(errmsg)
638
+ if not isinstance(G.nodes[v][weight], int):
639
+ errmsg = f"The {weight!r} field of node {v!r} is not an integer."
640
+ raise ValueError(errmsg)
641
+ self.node_weights = {v: G.nodes[v][weight] for v in G.nodes()}
642
+
643
+ def update_incumbent_if_improved(self, C, C_weight):
644
+ """Update the incumbent if the node set C has greater weight.
645
+
646
+ C is assumed to be a clique.
647
+ """
648
+ if C_weight > self.incumbent_weight:
649
+ self.incumbent_nodes = C[:]
650
+ self.incumbent_weight = C_weight
651
+
652
+ def greedily_find_independent_set(self, P):
653
+ """Greedily find an independent set of nodes from a set of
654
+ nodes P."""
655
+ independent_set = []
656
+ P = P[:]
657
+ while P:
658
+ v = P[0]
659
+ independent_set.append(v)
660
+ P = [w for w in P if v != w and not self.G.has_edge(v, w)]
661
+ return independent_set
662
+
663
+ def find_branching_nodes(self, P, target):
664
+ """Find a set of nodes to branch on."""
665
+ residual_wt = {v: self.node_weights[v] for v in P}
666
+ total_wt = 0
667
+ P = P[:]
668
+ while P:
669
+ independent_set = self.greedily_find_independent_set(P)
670
+ min_wt_in_class = min(residual_wt[v] for v in independent_set)
671
+ total_wt += min_wt_in_class
672
+ if total_wt > target:
673
+ break
674
+ for v in independent_set:
675
+ residual_wt[v] -= min_wt_in_class
676
+ P = [v for v in P if residual_wt[v] != 0]
677
+ return P
678
+
679
+ def expand(self, C, C_weight, P):
680
+ """Look for the best clique that contains all the nodes in C and zero or
681
+ more of the nodes in P, backtracking if it can be shown that no such
682
+ clique has greater weight than the incumbent.
683
+ """
684
+ self.update_incumbent_if_improved(C, C_weight)
685
+ branching_nodes = self.find_branching_nodes(P, self.incumbent_weight - C_weight)
686
+ while branching_nodes:
687
+ v = branching_nodes.pop()
688
+ P.remove(v)
689
+ new_C = C + [v]
690
+ new_C_weight = C_weight + self.node_weights[v]
691
+ new_P = [w for w in P if self.G.has_edge(v, w)]
692
+ self.expand(new_C, new_C_weight, new_P)
693
+
694
+ def find_max_weight_clique(self):
695
+ """Find a maximum weight clique."""
696
+ # Sort nodes in reverse order of degree for speed
697
+ nodes = sorted(self.G.nodes(), key=lambda v: self.G.degree(v), reverse=True)
698
+ nodes = [v for v in nodes if self.node_weights[v] > 0]
699
+ self.expand([], 0, nodes)
700
+
701
+
702
+ @not_implemented_for("directed")
703
+ @nx._dispatchable(node_attrs="weight")
704
+ def max_weight_clique(G, weight="weight"):
705
+ """Find a maximum weight clique in G.
706
+
707
+ A *clique* in a graph is a set of nodes such that every two distinct nodes
708
+ are adjacent. The *weight* of a clique is the sum of the weights of its
709
+ nodes. A *maximum weight clique* of graph G is a clique C in G such that
710
+ no clique in G has weight greater than the weight of C.
711
+
712
+ Parameters
713
+ ----------
714
+ G : NetworkX graph
715
+ Undirected graph
716
+ weight : string or None, optional (default='weight')
717
+ The node attribute that holds the integer value used as a weight.
718
+ If None, then each node has weight 1.
719
+
720
+ Returns
721
+ -------
722
+ clique : list
723
+ the nodes of a maximum weight clique
724
+ weight : int
725
+ the weight of a maximum weight clique
726
+
727
+ Notes
728
+ -----
729
+ The implementation is recursive, and therefore it may run into recursion
730
+ depth issues if G contains a clique whose number of nodes is close to the
731
+ recursion depth limit.
732
+
733
+ At each search node, the algorithm greedily constructs a weighted
734
+ independent set cover of part of the graph in order to find a small set of
735
+ nodes on which to branch. The algorithm is very similar to the algorithm
736
+ of Tavares et al. [1]_, other than the fact that the NetworkX version does
737
+ not use bitsets. This style of algorithm for maximum weight clique (and
738
+ maximum weight independent set, which is the same problem but on the
739
+ complement graph) has a decades-long history. See Algorithm B of Warren
740
+ and Hicks [2]_ and the references in that paper.
741
+
742
+ References
743
+ ----------
744
+ .. [1] Tavares, W.A., Neto, M.B.C., Rodrigues, C.D., Michelon, P.: Um
745
+ algoritmo de branch and bound para o problema da clique máxima
746
+ ponderada. Proceedings of XLVII SBPO 1 (2015).
747
+
748
+ .. [2] Warren, Jeffrey S, Hicks, Illya V.: Combinatorial Branch-and-Bound
749
+ for the Maximum Weight Independent Set Problem. Technical Report,
750
+ Texas A&M University (2016).
751
+ """
752
+
753
+ mwc = MaxWeightClique(G, weight)
754
+ mwc.find_max_weight_clique()
755
+ return mwc.incumbent_nodes, mwc.incumbent_weight
.venv/lib/python3.11/site-packages/networkx/algorithms/cluster.py ADDED
@@ -0,0 +1,609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Algorithms to characterize the number of triangles in a graph."""
2
+
3
+ from collections import Counter
4
+ from itertools import chain, combinations
5
+
6
+ import networkx as nx
7
+ from networkx.utils import not_implemented_for
8
+
9
+ __all__ = [
10
+ "triangles",
11
+ "average_clustering",
12
+ "clustering",
13
+ "transitivity",
14
+ "square_clustering",
15
+ "generalized_degree",
16
+ ]
17
+
18
+
19
+ @not_implemented_for("directed")
20
+ @nx._dispatchable
21
+ def triangles(G, nodes=None):
22
+ """Compute the number of triangles.
23
+
24
+ Finds the number of triangles that include a node as one vertex.
25
+
26
+ Parameters
27
+ ----------
28
+ G : graph
29
+ A networkx graph
30
+
31
+ nodes : node, iterable of nodes, or None (default=None)
32
+ If a singleton node, return the number of triangles for that node.
33
+ If an iterable, compute the number of triangles for each of those nodes.
34
+ If `None` (the default) compute the number of triangles for all nodes in `G`.
35
+
36
+ Returns
37
+ -------
38
+ out : dict or int
39
+ If `nodes` is a container of nodes, returns number of triangles keyed by node (dict).
40
+ If `nodes` is a specific node, returns number of triangles for the node (int).
41
+
42
+ Examples
43
+ --------
44
+ >>> G = nx.complete_graph(5)
45
+ >>> print(nx.triangles(G, 0))
46
+ 6
47
+ >>> print(nx.triangles(G))
48
+ {0: 6, 1: 6, 2: 6, 3: 6, 4: 6}
49
+ >>> print(list(nx.triangles(G, [0, 1]).values()))
50
+ [6, 6]
51
+
52
+ Notes
53
+ -----
54
+ Self loops are ignored.
55
+
56
+ """
57
+ if nodes is not None:
58
+ # If `nodes` represents a single node, return only its number of triangles
59
+ if nodes in G:
60
+ return next(_triangles_and_degree_iter(G, nodes))[2] // 2
61
+
62
+ # if `nodes` is a container of nodes, then return a
63
+ # dictionary mapping node to number of triangles.
64
+ return {v: t // 2 for v, d, t, _ in _triangles_and_degree_iter(G, nodes)}
65
+
66
+ # if nodes is None, then compute triangles for the complete graph
67
+
68
+ # dict used to avoid visiting the same nodes twice
69
+ # this allows calculating/counting each triangle only once
70
+ later_nbrs = {}
71
+
72
+ # iterate over the nodes in a graph
73
+ for node, neighbors in G.adjacency():
74
+ later_nbrs[node] = {n for n in neighbors if n not in later_nbrs and n != node}
75
+
76
+ # instantiate Counter for each node to include isolated nodes
77
+ # add 1 to the count if a nodes neighbor's neighbor is also a neighbor
78
+ triangle_counts = Counter(dict.fromkeys(G, 0))
79
+ for node1, neighbors in later_nbrs.items():
80
+ for node2 in neighbors:
81
+ third_nodes = neighbors & later_nbrs[node2]
82
+ m = len(third_nodes)
83
+ triangle_counts[node1] += m
84
+ triangle_counts[node2] += m
85
+ triangle_counts.update(third_nodes)
86
+
87
+ return dict(triangle_counts)
88
+
89
+
90
+ @not_implemented_for("multigraph")
91
+ def _triangles_and_degree_iter(G, nodes=None):
92
+ """Return an iterator of (node, degree, triangles, generalized degree).
93
+
94
+ This double counts triangles so you may want to divide by 2.
95
+ See degree(), triangles() and generalized_degree() for definitions
96
+ and details.
97
+
98
+ """
99
+ if nodes is None:
100
+ nodes_nbrs = G.adj.items()
101
+ else:
102
+ nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
103
+
104
+ for v, v_nbrs in nodes_nbrs:
105
+ vs = set(v_nbrs) - {v}
106
+ gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs)
107
+ ntriangles = sum(k * val for k, val in gen_degree.items())
108
+ yield (v, len(vs), ntriangles, gen_degree)
109
+
110
+
111
+ @not_implemented_for("multigraph")
112
+ def _weighted_triangles_and_degree_iter(G, nodes=None, weight="weight"):
113
+ """Return an iterator of (node, degree, weighted_triangles).
114
+
115
+ Used for weighted clustering.
116
+ Note: this returns the geometric average weight of edges in the triangle.
117
+ Also, each triangle is counted twice (each direction).
118
+ So you may want to divide by 2.
119
+
120
+ """
121
+ import numpy as np
122
+
123
+ if weight is None or G.number_of_edges() == 0:
124
+ max_weight = 1
125
+ else:
126
+ max_weight = max(d.get(weight, 1) for u, v, d in G.edges(data=True))
127
+ if nodes is None:
128
+ nodes_nbrs = G.adj.items()
129
+ else:
130
+ nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
131
+
132
+ def wt(u, v):
133
+ return G[u][v].get(weight, 1) / max_weight
134
+
135
+ for i, nbrs in nodes_nbrs:
136
+ inbrs = set(nbrs) - {i}
137
+ weighted_triangles = 0
138
+ seen = set()
139
+ for j in inbrs:
140
+ seen.add(j)
141
+ # This avoids counting twice -- we double at the end.
142
+ jnbrs = set(G[j]) - seen
143
+ # Only compute the edge weight once, before the inner inner
144
+ # loop.
145
+ wij = wt(i, j)
146
+ weighted_triangles += np.cbrt(
147
+ [(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs]
148
+ ).sum()
149
+ yield (i, len(inbrs), 2 * float(weighted_triangles))
150
+
151
+
152
+ @not_implemented_for("multigraph")
153
+ def _directed_triangles_and_degree_iter(G, nodes=None):
154
+ """Return an iterator of
155
+ (node, total_degree, reciprocal_degree, directed_triangles).
156
+
157
+ Used for directed clustering.
158
+ Note that unlike `_triangles_and_degree_iter()`, this function counts
159
+ directed triangles so does not count triangles twice.
160
+
161
+ """
162
+ nodes_nbrs = ((n, G._pred[n], G._succ[n]) for n in G.nbunch_iter(nodes))
163
+
164
+ for i, preds, succs in nodes_nbrs:
165
+ ipreds = set(preds) - {i}
166
+ isuccs = set(succs) - {i}
167
+
168
+ directed_triangles = 0
169
+ for j in chain(ipreds, isuccs):
170
+ jpreds = set(G._pred[j]) - {j}
171
+ jsuccs = set(G._succ[j]) - {j}
172
+ directed_triangles += sum(
173
+ 1
174
+ for k in chain(
175
+ (ipreds & jpreds),
176
+ (ipreds & jsuccs),
177
+ (isuccs & jpreds),
178
+ (isuccs & jsuccs),
179
+ )
180
+ )
181
+ dtotal = len(ipreds) + len(isuccs)
182
+ dbidirectional = len(ipreds & isuccs)
183
+ yield (i, dtotal, dbidirectional, directed_triangles)
184
+
185
+
186
+ @not_implemented_for("multigraph")
187
+ def _directed_weighted_triangles_and_degree_iter(G, nodes=None, weight="weight"):
188
+ """Return an iterator of
189
+ (node, total_degree, reciprocal_degree, directed_weighted_triangles).
190
+
191
+ Used for directed weighted clustering.
192
+ Note that unlike `_weighted_triangles_and_degree_iter()`, this function counts
193
+ directed triangles so does not count triangles twice.
194
+
195
+ """
196
+ import numpy as np
197
+
198
+ if weight is None or G.number_of_edges() == 0:
199
+ max_weight = 1
200
+ else:
201
+ max_weight = max(d.get(weight, 1) for u, v, d in G.edges(data=True))
202
+
203
+ nodes_nbrs = ((n, G._pred[n], G._succ[n]) for n in G.nbunch_iter(nodes))
204
+
205
+ def wt(u, v):
206
+ return G[u][v].get(weight, 1) / max_weight
207
+
208
+ for i, preds, succs in nodes_nbrs:
209
+ ipreds = set(preds) - {i}
210
+ isuccs = set(succs) - {i}
211
+
212
+ directed_triangles = 0
213
+ for j in ipreds:
214
+ jpreds = set(G._pred[j]) - {j}
215
+ jsuccs = set(G._succ[j]) - {j}
216
+ directed_triangles += np.cbrt(
217
+ [(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds]
218
+ ).sum()
219
+ directed_triangles += np.cbrt(
220
+ [(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs]
221
+ ).sum()
222
+ directed_triangles += np.cbrt(
223
+ [(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds]
224
+ ).sum()
225
+ directed_triangles += np.cbrt(
226
+ [(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs]
227
+ ).sum()
228
+
229
+ for j in isuccs:
230
+ jpreds = set(G._pred[j]) - {j}
231
+ jsuccs = set(G._succ[j]) - {j}
232
+ directed_triangles += np.cbrt(
233
+ [(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds]
234
+ ).sum()
235
+ directed_triangles += np.cbrt(
236
+ [(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs]
237
+ ).sum()
238
+ directed_triangles += np.cbrt(
239
+ [(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds]
240
+ ).sum()
241
+ directed_triangles += np.cbrt(
242
+ [(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs]
243
+ ).sum()
244
+
245
+ dtotal = len(ipreds) + len(isuccs)
246
+ dbidirectional = len(ipreds & isuccs)
247
+ yield (i, dtotal, dbidirectional, float(directed_triangles))
248
+
249
+
250
+ @nx._dispatchable(edge_attrs="weight")
251
+ def average_clustering(G, nodes=None, weight=None, count_zeros=True):
252
+ r"""Compute the average clustering coefficient for the graph G.
253
+
254
+ The clustering coefficient for the graph is the average,
255
+
256
+ .. math::
257
+
258
+ C = \frac{1}{n}\sum_{v \in G} c_v,
259
+
260
+ where :math:`n` is the number of nodes in `G`.
261
+
262
+ Parameters
263
+ ----------
264
+ G : graph
265
+
266
+ nodes : container of nodes, optional (default=all nodes in G)
267
+ Compute average clustering for nodes in this container.
268
+
269
+ weight : string or None, optional (default=None)
270
+ The edge attribute that holds the numerical value used as a weight.
271
+ If None, then each edge has weight 1.
272
+
273
+ count_zeros : bool
274
+ If False include only the nodes with nonzero clustering in the average.
275
+
276
+ Returns
277
+ -------
278
+ avg : float
279
+ Average clustering
280
+
281
+ Examples
282
+ --------
283
+ >>> G = nx.complete_graph(5)
284
+ >>> print(nx.average_clustering(G))
285
+ 1.0
286
+
287
+ Notes
288
+ -----
289
+ This is a space saving routine; it might be faster
290
+ to use the clustering function to get a list and then take the average.
291
+
292
+ Self loops are ignored.
293
+
294
+ References
295
+ ----------
296
+ .. [1] Generalizations of the clustering coefficient to weighted
297
+ complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela,
298
+ K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007).
299
+ http://jponnela.com/web_documents/a9.pdf
300
+ .. [2] Marcus Kaiser, Mean clustering coefficients: the role of isolated
301
+ nodes and leafs on clustering measures for small-world networks.
302
+ https://arxiv.org/abs/0802.2512
303
+ """
304
+ c = clustering(G, nodes, weight=weight).values()
305
+ if not count_zeros:
306
+ c = [v for v in c if abs(v) > 0]
307
+ return sum(c) / len(c)
308
+
309
+
310
+ @nx._dispatchable(edge_attrs="weight")
311
+ def clustering(G, nodes=None, weight=None):
312
+ r"""Compute the clustering coefficient for nodes.
313
+
314
+ For unweighted graphs, the clustering of a node :math:`u`
315
+ is the fraction of possible triangles through that node that exist,
316
+
317
+ .. math::
318
+
319
+ c_u = \frac{2 T(u)}{deg(u)(deg(u)-1)},
320
+
321
+ where :math:`T(u)` is the number of triangles through node :math:`u` and
322
+ :math:`deg(u)` is the degree of :math:`u`.
323
+
324
+ For weighted graphs, there are several ways to define clustering [1]_.
325
+ the one used here is defined
326
+ as the geometric average of the subgraph edge weights [2]_,
327
+
328
+ .. math::
329
+
330
+ c_u = \frac{1}{deg(u)(deg(u)-1))}
331
+ \sum_{vw} (\hat{w}_{uv} \hat{w}_{uw} \hat{w}_{vw})^{1/3}.
332
+
333
+ The edge weights :math:`\hat{w}_{uv}` are normalized by the maximum weight
334
+ in the network :math:`\hat{w}_{uv} = w_{uv}/\max(w)`.
335
+
336
+ The value of :math:`c_u` is assigned to 0 if :math:`deg(u) < 2`.
337
+
338
+ Additionally, this weighted definition has been generalized to support negative edge weights [3]_.
339
+
340
+ For directed graphs, the clustering is similarly defined as the fraction
341
+ of all possible directed triangles or geometric average of the subgraph
342
+ edge weights for unweighted and weighted directed graph respectively [4]_.
343
+
344
+ .. math::
345
+
346
+ c_u = \frac{T(u)}{2(deg^{tot}(u)(deg^{tot}(u)-1) - 2deg^{\leftrightarrow}(u))},
347
+
348
+ where :math:`T(u)` is the number of directed triangles through node
349
+ :math:`u`, :math:`deg^{tot}(u)` is the sum of in degree and out degree of
350
+ :math:`u` and :math:`deg^{\leftrightarrow}(u)` is the reciprocal degree of
351
+ :math:`u`.
352
+
353
+
354
+ Parameters
355
+ ----------
356
+ G : graph
357
+
358
+ nodes : node, iterable of nodes, or None (default=None)
359
+ If a singleton node, return the number of triangles for that node.
360
+ If an iterable, compute the number of triangles for each of those nodes.
361
+ If `None` (the default) compute the number of triangles for all nodes in `G`.
362
+
363
+ weight : string or None, optional (default=None)
364
+ The edge attribute that holds the numerical value used as a weight.
365
+ If None, then each edge has weight 1.
366
+
367
+ Returns
368
+ -------
369
+ out : float, or dictionary
370
+ Clustering coefficient at specified nodes
371
+
372
+ Examples
373
+ --------
374
+ >>> G = nx.complete_graph(5)
375
+ >>> print(nx.clustering(G, 0))
376
+ 1.0
377
+ >>> print(nx.clustering(G))
378
+ {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0}
379
+
380
+ Notes
381
+ -----
382
+ Self loops are ignored.
383
+
384
+ References
385
+ ----------
386
+ .. [1] Generalizations of the clustering coefficient to weighted
387
+ complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela,
388
+ K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007).
389
+ http://jponnela.com/web_documents/a9.pdf
390
+ .. [2] Intensity and coherence of motifs in weighted complex
391
+ networks by J. P. Onnela, J. Saramäki, J. Kertész, and K. Kaski,
392
+ Physical Review E, 71(6), 065103 (2005).
393
+ .. [3] Generalization of Clustering Coefficients to Signed Correlation Networks
394
+ by G. Costantini and M. Perugini, PloS one, 9(2), e88669 (2014).
395
+ .. [4] Clustering in complex directed networks by G. Fagiolo,
396
+ Physical Review E, 76(2), 026107 (2007).
397
+ """
398
+ if G.is_directed():
399
+ if weight is not None:
400
+ td_iter = _directed_weighted_triangles_and_degree_iter(G, nodes, weight)
401
+ clusterc = {
402
+ v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2)
403
+ for v, dt, db, t in td_iter
404
+ }
405
+ else:
406
+ td_iter = _directed_triangles_and_degree_iter(G, nodes)
407
+ clusterc = {
408
+ v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2)
409
+ for v, dt, db, t in td_iter
410
+ }
411
+ else:
412
+ # The formula 2*T/(d*(d-1)) from docs is t/(d*(d-1)) here b/c t==2*T
413
+ if weight is not None:
414
+ td_iter = _weighted_triangles_and_degree_iter(G, nodes, weight)
415
+ clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t in td_iter}
416
+ else:
417
+ td_iter = _triangles_and_degree_iter(G, nodes)
418
+ clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t, _ in td_iter}
419
+ if nodes in G:
420
+ # Return the value of the sole entry in the dictionary.
421
+ return clusterc[nodes]
422
+ return clusterc
423
+
424
+
425
+ @nx._dispatchable
426
+ def transitivity(G):
427
+ r"""Compute graph transitivity, the fraction of all possible triangles
428
+ present in G.
429
+
430
+ Possible triangles are identified by the number of "triads"
431
+ (two edges with a shared vertex).
432
+
433
+ The transitivity is
434
+
435
+ .. math::
436
+
437
+ T = 3\frac{\#triangles}{\#triads}.
438
+
439
+ Parameters
440
+ ----------
441
+ G : graph
442
+
443
+ Returns
444
+ -------
445
+ out : float
446
+ Transitivity
447
+
448
+ Notes
449
+ -----
450
+ Self loops are ignored.
451
+
452
+ Examples
453
+ --------
454
+ >>> G = nx.complete_graph(5)
455
+ >>> print(nx.transitivity(G))
456
+ 1.0
457
+ """
458
+ triangles_contri = [
459
+ (t, d * (d - 1)) for v, d, t, _ in _triangles_and_degree_iter(G)
460
+ ]
461
+ # If the graph is empty
462
+ if len(triangles_contri) == 0:
463
+ return 0
464
+ triangles, contri = map(sum, zip(*triangles_contri))
465
+ return 0 if triangles == 0 else triangles / contri
466
+
467
+
468
+ @nx._dispatchable
469
+ def square_clustering(G, nodes=None):
470
+ r"""Compute the squares clustering coefficient for nodes.
471
+
472
+ For each node return the fraction of possible squares that exist at
473
+ the node [1]_
474
+
475
+ .. math::
476
+ C_4(v) = \frac{ \sum_{u=1}^{k_v}
477
+ \sum_{w=u+1}^{k_v} q_v(u,w) }{ \sum_{u=1}^{k_v}
478
+ \sum_{w=u+1}^{k_v} [a_v(u,w) + q_v(u,w)]},
479
+
480
+ where :math:`q_v(u,w)` are the number of common neighbors of :math:`u` and
481
+ :math:`w` other than :math:`v` (ie squares), and :math:`a_v(u,w) = (k_u -
482
+ (1+q_v(u,w)+\theta_{uv})) + (k_w - (1+q_v(u,w)+\theta_{uw}))`, where
483
+ :math:`\theta_{uw} = 1` if :math:`u` and :math:`w` are connected and 0
484
+ otherwise. [2]_
485
+
486
+ Parameters
487
+ ----------
488
+ G : graph
489
+
490
+ nodes : container of nodes, optional (default=all nodes in G)
491
+ Compute clustering for nodes in this container.
492
+
493
+ Returns
494
+ -------
495
+ c4 : dictionary
496
+ A dictionary keyed by node with the square clustering coefficient value.
497
+
498
+ Examples
499
+ --------
500
+ >>> G = nx.complete_graph(5)
501
+ >>> print(nx.square_clustering(G, 0))
502
+ 1.0
503
+ >>> print(nx.square_clustering(G))
504
+ {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0}
505
+
506
+ Notes
507
+ -----
508
+ While :math:`C_3(v)` (triangle clustering) gives the probability that
509
+ two neighbors of node v are connected with each other, :math:`C_4(v)` is
510
+ the probability that two neighbors of node v share a common
511
+ neighbor different from v. This algorithm can be applied to both
512
+ bipartite and unipartite networks.
513
+
514
+ References
515
+ ----------
516
+ .. [1] Pedro G. Lind, Marta C. González, and Hans J. Herrmann. 2005
517
+ Cycles and clustering in bipartite networks.
518
+ Physical Review E (72) 056127.
519
+ .. [2] Zhang, Peng et al. Clustering Coefficient and Community Structure of
520
+ Bipartite Networks. Physica A: Statistical Mechanics and its Applications 387.27 (2008): 6869–6875.
521
+ https://arxiv.org/abs/0710.0117v1
522
+ """
523
+ if nodes is None:
524
+ node_iter = G
525
+ else:
526
+ node_iter = G.nbunch_iter(nodes)
527
+ clustering = {}
528
+ for v in node_iter:
529
+ clustering[v] = 0
530
+ potential = 0
531
+ for u, w in combinations(G[v], 2):
532
+ squares = len((set(G[u]) & set(G[w])) - {v})
533
+ clustering[v] += squares
534
+ degm = squares + 1
535
+ if w in G[u]:
536
+ degm += 1
537
+ potential += (len(G[u]) - degm) + (len(G[w]) - degm) + squares
538
+ if potential > 0:
539
+ clustering[v] /= potential
540
+ if nodes in G:
541
+ # Return the value of the sole entry in the dictionary.
542
+ return clustering[nodes]
543
+ return clustering
544
+
545
+
546
+ @not_implemented_for("directed")
547
+ @nx._dispatchable
548
+ def generalized_degree(G, nodes=None):
549
+ r"""Compute the generalized degree for nodes.
550
+
551
+ For each node, the generalized degree shows how many edges of given
552
+ triangle multiplicity the node is connected to. The triangle multiplicity
553
+ of an edge is the number of triangles an edge participates in. The
554
+ generalized degree of node :math:`i` can be written as a vector
555
+ :math:`\mathbf{k}_i=(k_i^{(0)}, \dotsc, k_i^{(N-2)})` where
556
+ :math:`k_i^{(j)}` is the number of edges attached to node :math:`i` that
557
+ participate in :math:`j` triangles.
558
+
559
+ Parameters
560
+ ----------
561
+ G : graph
562
+
563
+ nodes : container of nodes, optional (default=all nodes in G)
564
+ Compute the generalized degree for nodes in this container.
565
+
566
+ Returns
567
+ -------
568
+ out : Counter, or dictionary of Counters
569
+ Generalized degree of specified nodes. The Counter is keyed by edge
570
+ triangle multiplicity.
571
+
572
+ Examples
573
+ --------
574
+ >>> G = nx.complete_graph(5)
575
+ >>> print(nx.generalized_degree(G, 0))
576
+ Counter({3: 4})
577
+ >>> print(nx.generalized_degree(G))
578
+ {0: Counter({3: 4}), 1: Counter({3: 4}), 2: Counter({3: 4}), 3: Counter({3: 4}), 4: Counter({3: 4})}
579
+
580
+ To recover the number of triangles attached to a node:
581
+
582
+ >>> k1 = nx.generalized_degree(G, 0)
583
+ >>> sum([k * v for k, v in k1.items()]) / 2 == nx.triangles(G, 0)
584
+ True
585
+
586
+ Notes
587
+ -----
588
+ Self loops are ignored.
589
+
590
+ In a network of N nodes, the highest triangle multiplicity an edge can have
591
+ is N-2.
592
+
593
+ The return value does not include a `zero` entry if no edges of a
594
+ particular triangle multiplicity are present.
595
+
596
+ The number of triangles node :math:`i` is attached to can be recovered from
597
+ the generalized degree :math:`\mathbf{k}_i=(k_i^{(0)}, \dotsc,
598
+ k_i^{(N-2)})` by :math:`(k_i^{(1)}+2k_i^{(2)}+\dotsc +(N-2)k_i^{(N-2)})/2`.
599
+
600
+ References
601
+ ----------
602
+ .. [1] Networks with arbitrary edge multiplicities by V. Zlatić,
603
+ D. Garlaschelli and G. Caldarelli, EPL (Europhysics Letters),
604
+ Volume 97, Number 2 (2012).
605
+ https://iopscience.iop.org/article/10.1209/0295-5075/97/28005
606
+ """
607
+ if nodes in G:
608
+ return next(_triangles_and_degree_iter(G, nodes))[3]
609
+ return {v: gd for v, d, t, gd in _triangles_and_degree_iter(G, nodes)}
.venv/lib/python3.11/site-packages/networkx/algorithms/communicability_alg.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Communicability.
3
+ """
4
+
5
+ import networkx as nx
6
+ from networkx.utils import not_implemented_for
7
+
8
+ __all__ = ["communicability", "communicability_exp"]
9
+
10
+
11
+ @not_implemented_for("directed")
12
+ @not_implemented_for("multigraph")
13
+ @nx._dispatchable
14
+ def communicability(G):
15
+ r"""Returns communicability between all pairs of nodes in G.
16
+
17
+ The communicability between pairs of nodes in G is the sum of
18
+ walks of different lengths starting at node u and ending at node v.
19
+
20
+ Parameters
21
+ ----------
22
+ G: graph
23
+
24
+ Returns
25
+ -------
26
+ comm: dictionary of dictionaries
27
+ Dictionary of dictionaries keyed by nodes with communicability
28
+ as the value.
29
+
30
+ Raises
31
+ ------
32
+ NetworkXError
33
+ If the graph is not undirected and simple.
34
+
35
+ See Also
36
+ --------
37
+ communicability_exp:
38
+ Communicability between all pairs of nodes in G using spectral
39
+ decomposition.
40
+ communicability_betweenness_centrality:
41
+ Communicability betweenness centrality for each node in G.
42
+
43
+ Notes
44
+ -----
45
+ This algorithm uses a spectral decomposition of the adjacency matrix.
46
+ Let G=(V,E) be a simple undirected graph. Using the connection between
47
+ the powers of the adjacency matrix and the number of walks in the graph,
48
+ the communicability between nodes `u` and `v` based on the graph spectrum
49
+ is [1]_
50
+
51
+ .. math::
52
+ C(u,v)=\sum_{j=1}^{n}\phi_{j}(u)\phi_{j}(v)e^{\lambda_{j}},
53
+
54
+ where `\phi_{j}(u)` is the `u\rm{th}` element of the `j\rm{th}` orthonormal
55
+ eigenvector of the adjacency matrix associated with the eigenvalue
56
+ `\lambda_{j}`.
57
+
58
+ References
59
+ ----------
60
+ .. [1] Ernesto Estrada, Naomichi Hatano,
61
+ "Communicability in complex networks",
62
+ Phys. Rev. E 77, 036111 (2008).
63
+ https://arxiv.org/abs/0707.0756
64
+
65
+ Examples
66
+ --------
67
+ >>> G = nx.Graph([(0, 1), (1, 2), (1, 5), (5, 4), (2, 4), (2, 3), (4, 3), (3, 6)])
68
+ >>> c = nx.communicability(G)
69
+ """
70
+ import numpy as np
71
+
72
+ nodelist = list(G) # ordering of nodes in matrix
73
+ A = nx.to_numpy_array(G, nodelist)
74
+ # convert to 0-1 matrix
75
+ A[A != 0.0] = 1
76
+ w, vec = np.linalg.eigh(A)
77
+ expw = np.exp(w)
78
+ mapping = dict(zip(nodelist, range(len(nodelist))))
79
+ c = {}
80
+ # computing communicabilities
81
+ for u in G:
82
+ c[u] = {}
83
+ for v in G:
84
+ s = 0
85
+ p = mapping[u]
86
+ q = mapping[v]
87
+ for j in range(len(nodelist)):
88
+ s += vec[:, j][p] * vec[:, j][q] * expw[j]
89
+ c[u][v] = float(s)
90
+ return c
91
+
92
+
93
+ @not_implemented_for("directed")
94
+ @not_implemented_for("multigraph")
95
+ @nx._dispatchable
96
+ def communicability_exp(G):
97
+ r"""Returns communicability between all pairs of nodes in G.
98
+
99
+ Communicability between pair of node (u,v) of node in G is the sum of
100
+ walks of different lengths starting at node u and ending at node v.
101
+
102
+ Parameters
103
+ ----------
104
+ G: graph
105
+
106
+ Returns
107
+ -------
108
+ comm: dictionary of dictionaries
109
+ Dictionary of dictionaries keyed by nodes with communicability
110
+ as the value.
111
+
112
+ Raises
113
+ ------
114
+ NetworkXError
115
+ If the graph is not undirected and simple.
116
+
117
+ See Also
118
+ --------
119
+ communicability:
120
+ Communicability between pairs of nodes in G.
121
+ communicability_betweenness_centrality:
122
+ Communicability betweenness centrality for each node in G.
123
+
124
+ Notes
125
+ -----
126
+ This algorithm uses matrix exponentiation of the adjacency matrix.
127
+
128
+ Let G=(V,E) be a simple undirected graph. Using the connection between
129
+ the powers of the adjacency matrix and the number of walks in the graph,
130
+ the communicability between nodes u and v is [1]_,
131
+
132
+ .. math::
133
+ C(u,v) = (e^A)_{uv},
134
+
135
+ where `A` is the adjacency matrix of G.
136
+
137
+ References
138
+ ----------
139
+ .. [1] Ernesto Estrada, Naomichi Hatano,
140
+ "Communicability in complex networks",
141
+ Phys. Rev. E 77, 036111 (2008).
142
+ https://arxiv.org/abs/0707.0756
143
+
144
+ Examples
145
+ --------
146
+ >>> G = nx.Graph([(0, 1), (1, 2), (1, 5), (5, 4), (2, 4), (2, 3), (4, 3), (3, 6)])
147
+ >>> c = nx.communicability_exp(G)
148
+ """
149
+ import scipy as sp
150
+
151
+ nodelist = list(G) # ordering of nodes in matrix
152
+ A = nx.to_numpy_array(G, nodelist)
153
+ # convert to 0-1 matrix
154
+ A[A != 0.0] = 1
155
+ # communicability matrix
156
+ expA = sp.linalg.expm(A)
157
+ mapping = dict(zip(nodelist, range(len(nodelist))))
158
+ c = {}
159
+ for u in G:
160
+ c[u] = {}
161
+ for v in G:
162
+ c[u][v] = float(expA[mapping[u], mapping[v]])
163
+ return c
.venv/lib/python3.11/site-packages/networkx/algorithms/core.py ADDED
@@ -0,0 +1,649 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Find the k-cores of a graph.
3
+
4
+ The k-core is found by recursively pruning nodes with degrees less than k.
5
+
6
+ See the following references for details:
7
+
8
+ An O(m) Algorithm for Cores Decomposition of Networks
9
+ Vladimir Batagelj and Matjaz Zaversnik, 2003.
10
+ https://arxiv.org/abs/cs.DS/0310049
11
+
12
+ Generalized Cores
13
+ Vladimir Batagelj and Matjaz Zaversnik, 2002.
14
+ https://arxiv.org/pdf/cs/0202039
15
+
16
+ For directed graphs a more general notion is that of D-cores which
17
+ looks at (k, l) restrictions on (in, out) degree. The (k, k) D-core
18
+ is the k-core.
19
+
20
+ D-cores: Measuring Collaboration of Directed Graphs Based on Degeneracy
21
+ Christos Giatsidis, Dimitrios M. Thilikos, Michalis Vazirgiannis, ICDM 2011.
22
+ http://www.graphdegeneracy.org/dcores_ICDM_2011.pdf
23
+
24
+ Multi-scale structure and topological anomaly detection via a new network \
25
+ statistic: The onion decomposition
26
+ L. Hébert-Dufresne, J. A. Grochow, and A. Allard
27
+ Scientific Reports 6, 31708 (2016)
28
+ http://doi.org/10.1038/srep31708
29
+
30
+ """
31
+
32
+ import networkx as nx
33
+
34
+ __all__ = [
35
+ "core_number",
36
+ "k_core",
37
+ "k_shell",
38
+ "k_crust",
39
+ "k_corona",
40
+ "k_truss",
41
+ "onion_layers",
42
+ ]
43
+
44
+
45
+ @nx.utils.not_implemented_for("multigraph")
46
+ @nx._dispatchable
47
+ def core_number(G):
48
+ """Returns the core number for each node.
49
+
50
+ A k-core is a maximal subgraph that contains nodes of degree k or more.
51
+
52
+ The core number of a node is the largest value k of a k-core containing
53
+ that node.
54
+
55
+ Parameters
56
+ ----------
57
+ G : NetworkX graph
58
+ An undirected or directed graph
59
+
60
+ Returns
61
+ -------
62
+ core_number : dictionary
63
+ A dictionary keyed by node to the core number.
64
+
65
+ Raises
66
+ ------
67
+ NetworkXNotImplemented
68
+ If `G` is a multigraph or contains self loops.
69
+
70
+ Notes
71
+ -----
72
+ For directed graphs the node degree is defined to be the
73
+ in-degree + out-degree.
74
+
75
+ Examples
76
+ --------
77
+ >>> degrees = [0, 1, 2, 2, 2, 2, 3]
78
+ >>> H = nx.havel_hakimi_graph(degrees)
79
+ >>> nx.core_number(H)
80
+ {0: 1, 1: 2, 2: 2, 3: 2, 4: 1, 5: 2, 6: 0}
81
+ >>> G = nx.DiGraph()
82
+ >>> G.add_edges_from([(1, 2), (2, 1), (2, 3), (2, 4), (3, 4), (4, 3)])
83
+ >>> nx.core_number(G)
84
+ {1: 2, 2: 2, 3: 2, 4: 2}
85
+
86
+ References
87
+ ----------
88
+ .. [1] An O(m) Algorithm for Cores Decomposition of Networks
89
+ Vladimir Batagelj and Matjaz Zaversnik, 2003.
90
+ https://arxiv.org/abs/cs.DS/0310049
91
+ """
92
+ if nx.number_of_selfloops(G) > 0:
93
+ msg = (
94
+ "Input graph has self loops which is not permitted; "
95
+ "Consider using G.remove_edges_from(nx.selfloop_edges(G))."
96
+ )
97
+ raise nx.NetworkXNotImplemented(msg)
98
+ degrees = dict(G.degree())
99
+ # Sort nodes by degree.
100
+ nodes = sorted(degrees, key=degrees.get)
101
+ bin_boundaries = [0]
102
+ curr_degree = 0
103
+ for i, v in enumerate(nodes):
104
+ if degrees[v] > curr_degree:
105
+ bin_boundaries.extend([i] * (degrees[v] - curr_degree))
106
+ curr_degree = degrees[v]
107
+ node_pos = {v: pos for pos, v in enumerate(nodes)}
108
+ # The initial guess for the core number of a node is its degree.
109
+ core = degrees
110
+ nbrs = {v: list(nx.all_neighbors(G, v)) for v in G}
111
+ for v in nodes:
112
+ for u in nbrs[v]:
113
+ if core[u] > core[v]:
114
+ nbrs[u].remove(v)
115
+ pos = node_pos[u]
116
+ bin_start = bin_boundaries[core[u]]
117
+ node_pos[u] = bin_start
118
+ node_pos[nodes[bin_start]] = pos
119
+ nodes[bin_start], nodes[pos] = nodes[pos], nodes[bin_start]
120
+ bin_boundaries[core[u]] += 1
121
+ core[u] -= 1
122
+ return core
123
+
124
+
125
+ def _core_subgraph(G, k_filter, k=None, core=None):
126
+ """Returns the subgraph induced by nodes passing filter `k_filter`.
127
+
128
+ Parameters
129
+ ----------
130
+ G : NetworkX graph
131
+ The graph or directed graph to process
132
+ k_filter : filter function
133
+ This function filters the nodes chosen. It takes three inputs:
134
+ A node of G, the filter's cutoff, and the core dict of the graph.
135
+ The function should return a Boolean value.
136
+ k : int, optional
137
+ The order of the core. If not specified use the max core number.
138
+ This value is used as the cutoff for the filter.
139
+ core : dict, optional
140
+ Precomputed core numbers keyed by node for the graph `G`.
141
+ If not specified, the core numbers will be computed from `G`.
142
+
143
+ """
144
+ if core is None:
145
+ core = core_number(G)
146
+ if k is None:
147
+ k = max(core.values())
148
+ nodes = (v for v in core if k_filter(v, k, core))
149
+ return G.subgraph(nodes).copy()
150
+
151
+
152
+ @nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
153
+ def k_core(G, k=None, core_number=None):
154
+ """Returns the k-core of G.
155
+
156
+ A k-core is a maximal subgraph that contains nodes of degree `k` or more.
157
+
158
+ .. deprecated:: 3.3
159
+ `k_core` will not accept `MultiGraph` objects in version 3.5.
160
+
161
+ Parameters
162
+ ----------
163
+ G : NetworkX graph
164
+ A graph or directed graph
165
+ k : int, optional
166
+ The order of the core. If not specified return the main core.
167
+ core_number : dictionary, optional
168
+ Precomputed core numbers for the graph G.
169
+
170
+ Returns
171
+ -------
172
+ G : NetworkX graph
173
+ The k-core subgraph
174
+
175
+ Raises
176
+ ------
177
+ NetworkXNotImplemented
178
+ The k-core is not defined for multigraphs or graphs with self loops.
179
+
180
+ Notes
181
+ -----
182
+ The main core is the core with `k` as the largest core_number.
183
+
184
+ For directed graphs the node degree is defined to be the
185
+ in-degree + out-degree.
186
+
187
+ Graph, node, and edge attributes are copied to the subgraph.
188
+
189
+ Examples
190
+ --------
191
+ >>> degrees = [0, 1, 2, 2, 2, 2, 3]
192
+ >>> H = nx.havel_hakimi_graph(degrees)
193
+ >>> H.degree
194
+ DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
195
+ >>> nx.k_core(H).nodes
196
+ NodeView((1, 2, 3, 5))
197
+
198
+ See Also
199
+ --------
200
+ core_number
201
+
202
+ References
203
+ ----------
204
+ .. [1] An O(m) Algorithm for Cores Decomposition of Networks
205
+ Vladimir Batagelj and Matjaz Zaversnik, 2003.
206
+ https://arxiv.org/abs/cs.DS/0310049
207
+ """
208
+
209
+ import warnings
210
+
211
+ if G.is_multigraph():
212
+ warnings.warn(
213
+ (
214
+ "\n\n`k_core` will not accept `MultiGraph` objects in version 3.5.\n"
215
+ "Convert it to an undirected graph instead, using::\n\n"
216
+ "\tG = nx.Graph(G)\n"
217
+ ),
218
+ category=DeprecationWarning,
219
+ stacklevel=5,
220
+ )
221
+
222
+ def k_filter(v, k, c):
223
+ return c[v] >= k
224
+
225
+ return _core_subgraph(G, k_filter, k, core_number)
226
+
227
+
228
+ @nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
229
+ def k_shell(G, k=None, core_number=None):
230
+ """Returns the k-shell of G.
231
+
232
+ The k-shell is the subgraph induced by nodes with core number k.
233
+ That is, nodes in the k-core that are not in the (k+1)-core.
234
+
235
+ .. deprecated:: 3.3
236
+ `k_shell` will not accept `MultiGraph` objects in version 3.5.
237
+
238
+ Parameters
239
+ ----------
240
+ G : NetworkX graph
241
+ A graph or directed graph.
242
+ k : int, optional
243
+ The order of the shell. If not specified return the outer shell.
244
+ core_number : dictionary, optional
245
+ Precomputed core numbers for the graph G.
246
+
247
+
248
+ Returns
249
+ -------
250
+ G : NetworkX graph
251
+ The k-shell subgraph
252
+
253
+ Raises
254
+ ------
255
+ NetworkXNotImplemented
256
+ The k-shell is not implemented for multigraphs or graphs with self loops.
257
+
258
+ Notes
259
+ -----
260
+ This is similar to k_corona but in that case only neighbors in the
261
+ k-core are considered.
262
+
263
+ For directed graphs the node degree is defined to be the
264
+ in-degree + out-degree.
265
+
266
+ Graph, node, and edge attributes are copied to the subgraph.
267
+
268
+ Examples
269
+ --------
270
+ >>> degrees = [0, 1, 2, 2, 2, 2, 3]
271
+ >>> H = nx.havel_hakimi_graph(degrees)
272
+ >>> H.degree
273
+ DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
274
+ >>> nx.k_shell(H, k=1).nodes
275
+ NodeView((0, 4))
276
+
277
+ See Also
278
+ --------
279
+ core_number
280
+ k_corona
281
+
282
+
283
+ References
284
+ ----------
285
+ .. [1] A model of Internet topology using k-shell decomposition
286
+ Shai Carmi, Shlomo Havlin, Scott Kirkpatrick, Yuval Shavitt,
287
+ and Eran Shir, PNAS July 3, 2007 vol. 104 no. 27 11150-11154
288
+ http://www.pnas.org/content/104/27/11150.full
289
+ """
290
+
291
+ import warnings
292
+
293
+ if G.is_multigraph():
294
+ warnings.warn(
295
+ (
296
+ "\n\n`k_shell` will not accept `MultiGraph` objects in version 3.5.\n"
297
+ "Convert it to an undirected graph instead, using::\n\n"
298
+ "\tG = nx.Graph(G)\n"
299
+ ),
300
+ category=DeprecationWarning,
301
+ stacklevel=5,
302
+ )
303
+
304
+ def k_filter(v, k, c):
305
+ return c[v] == k
306
+
307
+ return _core_subgraph(G, k_filter, k, core_number)
308
+
309
+
310
+ @nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
311
+ def k_crust(G, k=None, core_number=None):
312
+ """Returns the k-crust of G.
313
+
314
+ The k-crust is the graph G with the edges of the k-core removed
315
+ and isolated nodes found after the removal of edges are also removed.
316
+
317
+ .. deprecated:: 3.3
318
+ `k_crust` will not accept `MultiGraph` objects in version 3.5.
319
+
320
+ Parameters
321
+ ----------
322
+ G : NetworkX graph
323
+ A graph or directed graph.
324
+ k : int, optional
325
+ The order of the shell. If not specified return the main crust.
326
+ core_number : dictionary, optional
327
+ Precomputed core numbers for the graph G.
328
+
329
+ Returns
330
+ -------
331
+ G : NetworkX graph
332
+ The k-crust subgraph
333
+
334
+ Raises
335
+ ------
336
+ NetworkXNotImplemented
337
+ The k-crust is not implemented for multigraphs or graphs with self loops.
338
+
339
+ Notes
340
+ -----
341
+ This definition of k-crust is different than the definition in [1]_.
342
+ The k-crust in [1]_ is equivalent to the k+1 crust of this algorithm.
343
+
344
+ For directed graphs the node degree is defined to be the
345
+ in-degree + out-degree.
346
+
347
+ Graph, node, and edge attributes are copied to the subgraph.
348
+
349
+ Examples
350
+ --------
351
+ >>> degrees = [0, 1, 2, 2, 2, 2, 3]
352
+ >>> H = nx.havel_hakimi_graph(degrees)
353
+ >>> H.degree
354
+ DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
355
+ >>> nx.k_crust(H, k=1).nodes
356
+ NodeView((0, 4, 6))
357
+
358
+ See Also
359
+ --------
360
+ core_number
361
+
362
+ References
363
+ ----------
364
+ .. [1] A model of Internet topology using k-shell decomposition
365
+ Shai Carmi, Shlomo Havlin, Scott Kirkpatrick, Yuval Shavitt,
366
+ and Eran Shir, PNAS July 3, 2007 vol. 104 no. 27 11150-11154
367
+ http://www.pnas.org/content/104/27/11150.full
368
+ """
369
+
370
+ import warnings
371
+
372
+ if G.is_multigraph():
373
+ warnings.warn(
374
+ (
375
+ "\n\n`k_crust` will not accept `MultiGraph` objects in version 3.5.\n"
376
+ "Convert it to an undirected graph instead, using::\n\n"
377
+ "\tG = nx.Graph(G)\n"
378
+ ),
379
+ category=DeprecationWarning,
380
+ stacklevel=5,
381
+ )
382
+
383
+ # Default for k is one less than in _core_subgraph, so just inline.
384
+ # Filter is c[v] <= k
385
+ if core_number is None:
386
+ core_number = nx.core_number(G)
387
+ if k is None:
388
+ k = max(core_number.values()) - 1
389
+ nodes = (v for v in core_number if core_number[v] <= k)
390
+ return G.subgraph(nodes).copy()
391
+
392
+
393
+ @nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
394
+ def k_corona(G, k, core_number=None):
395
+ """Returns the k-corona of G.
396
+
397
+ The k-corona is the subgraph of nodes in the k-core which have
398
+ exactly k neighbors in the k-core.
399
+
400
+ .. deprecated:: 3.3
401
+ `k_corona` will not accept `MultiGraph` objects in version 3.5.
402
+
403
+ Parameters
404
+ ----------
405
+ G : NetworkX graph
406
+ A graph or directed graph
407
+ k : int
408
+ The order of the corona.
409
+ core_number : dictionary, optional
410
+ Precomputed core numbers for the graph G.
411
+
412
+ Returns
413
+ -------
414
+ G : NetworkX graph
415
+ The k-corona subgraph
416
+
417
+ Raises
418
+ ------
419
+ NetworkXNotImplemented
420
+ The k-corona is not defined for multigraphs or graphs with self loops.
421
+
422
+ Notes
423
+ -----
424
+ For directed graphs the node degree is defined to be the
425
+ in-degree + out-degree.
426
+
427
+ Graph, node, and edge attributes are copied to the subgraph.
428
+
429
+ Examples
430
+ --------
431
+ >>> degrees = [0, 1, 2, 2, 2, 2, 3]
432
+ >>> H = nx.havel_hakimi_graph(degrees)
433
+ >>> H.degree
434
+ DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
435
+ >>> nx.k_corona(H, k=2).nodes
436
+ NodeView((1, 2, 3, 5))
437
+
438
+ See Also
439
+ --------
440
+ core_number
441
+
442
+ References
443
+ ----------
444
+ .. [1] k -core (bootstrap) percolation on complex networks:
445
+ Critical phenomena and nonlocal effects,
446
+ A. V. Goltsev, S. N. Dorogovtsev, and J. F. F. Mendes,
447
+ Phys. Rev. E 73, 056101 (2006)
448
+ http://link.aps.org/doi/10.1103/PhysRevE.73.056101
449
+ """
450
+
451
+ import warnings
452
+
453
+ if G.is_multigraph():
454
+ warnings.warn(
455
+ (
456
+ "\n\n`k_corona` will not accept `MultiGraph` objects in version 3.5.\n"
457
+ "Convert it to an undirected graph instead, using::\n\n"
458
+ "\tG = nx.Graph(G)\n"
459
+ ),
460
+ category=DeprecationWarning,
461
+ stacklevel=5,
462
+ )
463
+
464
+ def func(v, k, c):
465
+ return c[v] == k and k == sum(1 for w in G[v] if c[w] >= k)
466
+
467
+ return _core_subgraph(G, func, k, core_number)
468
+
469
+
470
+ @nx.utils.not_implemented_for("directed")
471
+ @nx.utils.not_implemented_for("multigraph")
472
+ @nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
473
+ def k_truss(G, k):
474
+ """Returns the k-truss of `G`.
475
+
476
+ The k-truss is the maximal induced subgraph of `G` which contains at least
477
+ three vertices where every edge is incident to at least `k-2` triangles.
478
+
479
+ Parameters
480
+ ----------
481
+ G : NetworkX graph
482
+ An undirected graph
483
+ k : int
484
+ The order of the truss
485
+
486
+ Returns
487
+ -------
488
+ H : NetworkX graph
489
+ The k-truss subgraph
490
+
491
+ Raises
492
+ ------
493
+ NetworkXNotImplemented
494
+ If `G` is a multigraph or directed graph or if it contains self loops.
495
+
496
+ Notes
497
+ -----
498
+ A k-clique is a (k-2)-truss and a k-truss is a (k+1)-core.
499
+
500
+ Graph, node, and edge attributes are copied to the subgraph.
501
+
502
+ K-trusses were originally defined in [2] which states that the k-truss
503
+ is the maximal induced subgraph where each edge belongs to at least
504
+ `k-2` triangles. A more recent paper, [1], uses a slightly different
505
+ definition requiring that each edge belong to at least `k` triangles.
506
+ This implementation uses the original definition of `k-2` triangles.
507
+
508
+ Examples
509
+ --------
510
+ >>> degrees = [0, 1, 2, 2, 2, 2, 3]
511
+ >>> H = nx.havel_hakimi_graph(degrees)
512
+ >>> H.degree
513
+ DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
514
+ >>> nx.k_truss(H, k=2).nodes
515
+ NodeView((0, 1, 2, 3, 4, 5))
516
+
517
+ References
518
+ ----------
519
+ .. [1] Bounds and Algorithms for k-truss. Paul Burkhardt, Vance Faber,
520
+ David G. Harris, 2018. https://arxiv.org/abs/1806.05523v2
521
+ .. [2] Trusses: Cohesive Subgraphs for Social Network Analysis. Jonathan
522
+ Cohen, 2005.
523
+ """
524
+ if nx.number_of_selfloops(G) > 0:
525
+ msg = (
526
+ "Input graph has self loops which is not permitted; "
527
+ "Consider using G.remove_edges_from(nx.selfloop_edges(G))."
528
+ )
529
+ raise nx.NetworkXNotImplemented(msg)
530
+
531
+ H = G.copy()
532
+
533
+ n_dropped = 1
534
+ while n_dropped > 0:
535
+ n_dropped = 0
536
+ to_drop = []
537
+ seen = set()
538
+ for u in H:
539
+ nbrs_u = set(H[u])
540
+ seen.add(u)
541
+ new_nbrs = [v for v in nbrs_u if v not in seen]
542
+ for v in new_nbrs:
543
+ if len(nbrs_u & set(H[v])) < (k - 2):
544
+ to_drop.append((u, v))
545
+ H.remove_edges_from(to_drop)
546
+ n_dropped = len(to_drop)
547
+ H.remove_nodes_from(list(nx.isolates(H)))
548
+
549
+ return H
550
+
551
+
552
+ @nx.utils.not_implemented_for("multigraph")
553
+ @nx.utils.not_implemented_for("directed")
554
+ @nx._dispatchable
555
+ def onion_layers(G):
556
+ """Returns the layer of each vertex in an onion decomposition of the graph.
557
+
558
+ The onion decomposition refines the k-core decomposition by providing
559
+ information on the internal organization of each k-shell. It is usually
560
+ used alongside the `core numbers`.
561
+
562
+ Parameters
563
+ ----------
564
+ G : NetworkX graph
565
+ An undirected graph without self loops.
566
+
567
+ Returns
568
+ -------
569
+ od_layers : dictionary
570
+ A dictionary keyed by node to the onion layer. The layers are
571
+ contiguous integers starting at 1.
572
+
573
+ Raises
574
+ ------
575
+ NetworkXNotImplemented
576
+ If `G` is a multigraph or directed graph or if it contains self loops.
577
+
578
+ Examples
579
+ --------
580
+ >>> degrees = [0, 1, 2, 2, 2, 2, 3]
581
+ >>> H = nx.havel_hakimi_graph(degrees)
582
+ >>> H.degree
583
+ DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
584
+ >>> nx.onion_layers(H)
585
+ {6: 1, 0: 2, 4: 3, 1: 4, 2: 4, 3: 4, 5: 4}
586
+
587
+ See Also
588
+ --------
589
+ core_number
590
+
591
+ References
592
+ ----------
593
+ .. [1] Multi-scale structure and topological anomaly detection via a new
594
+ network statistic: The onion decomposition
595
+ L. Hébert-Dufresne, J. A. Grochow, and A. Allard
596
+ Scientific Reports 6, 31708 (2016)
597
+ http://doi.org/10.1038/srep31708
598
+ .. [2] Percolation and the effective structure of complex networks
599
+ A. Allard and L. Hébert-Dufresne
600
+ Physical Review X 9, 011023 (2019)
601
+ http://doi.org/10.1103/PhysRevX.9.011023
602
+ """
603
+ if nx.number_of_selfloops(G) > 0:
604
+ msg = (
605
+ "Input graph contains self loops which is not permitted; "
606
+ "Consider using G.remove_edges_from(nx.selfloop_edges(G))."
607
+ )
608
+ raise nx.NetworkXNotImplemented(msg)
609
+ # Dictionaries to register the k-core/onion decompositions.
610
+ od_layers = {}
611
+ # Adjacency list
612
+ neighbors = {v: list(nx.all_neighbors(G, v)) for v in G}
613
+ # Effective degree of nodes.
614
+ degrees = dict(G.degree())
615
+ # Performs the onion decomposition.
616
+ current_core = 1
617
+ current_layer = 1
618
+ # Sets vertices of degree 0 to layer 1, if any.
619
+ isolated_nodes = list(nx.isolates(G))
620
+ if len(isolated_nodes) > 0:
621
+ for v in isolated_nodes:
622
+ od_layers[v] = current_layer
623
+ degrees.pop(v)
624
+ current_layer = 2
625
+ # Finds the layer for the remaining nodes.
626
+ while len(degrees) > 0:
627
+ # Sets the order for looking at nodes.
628
+ nodes = sorted(degrees, key=degrees.get)
629
+ # Sets properly the current core.
630
+ min_degree = degrees[nodes[0]]
631
+ if min_degree > current_core:
632
+ current_core = min_degree
633
+ # Identifies vertices in the current layer.
634
+ this_layer = []
635
+ for n in nodes:
636
+ if degrees[n] > current_core:
637
+ break
638
+ this_layer.append(n)
639
+ # Identifies the core/layer of the vertices in the current layer.
640
+ for v in this_layer:
641
+ od_layers[v] = current_layer
642
+ for n in neighbors[v]:
643
+ neighbors[n].remove(v)
644
+ degrees[n] = degrees[n] - 1
645
+ degrees.pop(v)
646
+ # Updates the layer count.
647
+ current_layer = current_layer + 1
648
+ # Returns the dictionaries containing the onion layer of each vertices.
649
+ return od_layers
.venv/lib/python3.11/site-packages/networkx/algorithms/covering.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions related to graph covers."""
2
+
3
+ from functools import partial
4
+ from itertools import chain
5
+
6
+ import networkx as nx
7
+ from networkx.utils import arbitrary_element, not_implemented_for
8
+
9
+ __all__ = ["min_edge_cover", "is_edge_cover"]
10
+
11
+
12
+ @not_implemented_for("directed")
13
+ @not_implemented_for("multigraph")
14
+ @nx._dispatchable
15
+ def min_edge_cover(G, matching_algorithm=None):
16
+ """Returns the min cardinality edge cover of the graph as a set of edges.
17
+
18
+ A smallest edge cover can be found in polynomial time by finding
19
+ a maximum matching and extending it greedily so that all nodes
20
+ are covered. This function follows that process. A maximum matching
21
+ algorithm can be specified for the first step of the algorithm.
22
+ The resulting set may return a set with one 2-tuple for each edge,
23
+ (the usual case) or with both 2-tuples `(u, v)` and `(v, u)` for
24
+ each edge. The latter is only done when a bipartite matching algorithm
25
+ is specified as `matching_algorithm`.
26
+
27
+ Parameters
28
+ ----------
29
+ G : NetworkX graph
30
+ An undirected graph.
31
+
32
+ matching_algorithm : function
33
+ A function that returns a maximum cardinality matching for `G`.
34
+ The function must take one input, the graph `G`, and return
35
+ either a set of edges (with only one direction for the pair of nodes)
36
+ or a dictionary mapping each node to its mate. If not specified,
37
+ :func:`~networkx.algorithms.matching.max_weight_matching` is used.
38
+ Common bipartite matching functions include
39
+ :func:`~networkx.algorithms.bipartite.matching.hopcroft_karp_matching`
40
+ or
41
+ :func:`~networkx.algorithms.bipartite.matching.eppstein_matching`.
42
+
43
+ Returns
44
+ -------
45
+ min_cover : set
46
+
47
+ A set of the edges in a minimum edge cover in the form of tuples.
48
+ It contains only one of the equivalent 2-tuples `(u, v)` and `(v, u)`
49
+ for each edge. If a bipartite method is used to compute the matching,
50
+ the returned set contains both the 2-tuples `(u, v)` and `(v, u)`
51
+ for each edge of a minimum edge cover.
52
+
53
+ Examples
54
+ --------
55
+ >>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
56
+ >>> sorted(nx.min_edge_cover(G))
57
+ [(2, 1), (3, 0)]
58
+
59
+ Notes
60
+ -----
61
+ An edge cover of a graph is a set of edges such that every node of
62
+ the graph is incident to at least one edge of the set.
63
+ The minimum edge cover is an edge covering of smallest cardinality.
64
+
65
+ Due to its implementation, the worst-case running time of this algorithm
66
+ is bounded by the worst-case running time of the function
67
+ ``matching_algorithm``.
68
+
69
+ Minimum edge cover for `G` can also be found using the `min_edge_covering`
70
+ function in :mod:`networkx.algorithms.bipartite.covering` which is
71
+ simply this function with a default matching algorithm of
72
+ :func:`~networkx.algorithms.bipartite.matching.hopcraft_karp_matching`
73
+ """
74
+ if len(G) == 0:
75
+ return set()
76
+ if nx.number_of_isolates(G) > 0:
77
+ # ``min_cover`` does not exist as there is an isolated node
78
+ raise nx.NetworkXException(
79
+ "Graph has a node with no edge incident on it, so no edge cover exists."
80
+ )
81
+ if matching_algorithm is None:
82
+ matching_algorithm = partial(nx.max_weight_matching, maxcardinality=True)
83
+ maximum_matching = matching_algorithm(G)
84
+ # ``min_cover`` is superset of ``maximum_matching``
85
+ try:
86
+ # bipartite matching algs return dict so convert if needed
87
+ min_cover = set(maximum_matching.items())
88
+ bipartite_cover = True
89
+ except AttributeError:
90
+ min_cover = maximum_matching
91
+ bipartite_cover = False
92
+ # iterate for uncovered nodes
93
+ uncovered_nodes = set(G) - {v for u, v in min_cover} - {u for u, v in min_cover}
94
+ for v in uncovered_nodes:
95
+ # Since `v` is uncovered, each edge incident to `v` will join it
96
+ # with a covered node (otherwise, if there were an edge joining
97
+ # uncovered nodes `u` and `v`, the maximum matching algorithm
98
+ # would have found it), so we can choose an arbitrary edge
99
+ # incident to `v`. (This applies only in a simple graph, not a
100
+ # multigraph.)
101
+ u = arbitrary_element(G[v])
102
+ min_cover.add((u, v))
103
+ if bipartite_cover:
104
+ min_cover.add((v, u))
105
+ return min_cover
106
+
107
+
108
+ @not_implemented_for("directed")
109
+ @nx._dispatchable
110
+ def is_edge_cover(G, cover):
111
+ """Decides whether a set of edges is a valid edge cover of the graph.
112
+
113
+ Given a set of edges, whether it is an edge covering can
114
+ be decided if we just check whether all nodes of the graph
115
+ has an edge from the set, incident on it.
116
+
117
+ Parameters
118
+ ----------
119
+ G : NetworkX graph
120
+ An undirected bipartite graph.
121
+
122
+ cover : set
123
+ Set of edges to be checked.
124
+
125
+ Returns
126
+ -------
127
+ bool
128
+ Whether the set of edges is a valid edge cover of the graph.
129
+
130
+ Examples
131
+ --------
132
+ >>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
133
+ >>> cover = {(2, 1), (3, 0)}
134
+ >>> nx.is_edge_cover(G, cover)
135
+ True
136
+
137
+ Notes
138
+ -----
139
+ An edge cover of a graph is a set of edges such that every node of
140
+ the graph is incident to at least one edge of the set.
141
+ """
142
+ return set(G) <= set(chain.from_iterable(cover))
.venv/lib/python3.11/site-packages/networkx/algorithms/cuts.py ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions for finding and evaluating cuts in a graph."""
2
+
3
+ from itertools import chain
4
+
5
+ import networkx as nx
6
+
7
+ __all__ = [
8
+ "boundary_expansion",
9
+ "conductance",
10
+ "cut_size",
11
+ "edge_expansion",
12
+ "mixing_expansion",
13
+ "node_expansion",
14
+ "normalized_cut_size",
15
+ "volume",
16
+ ]
17
+
18
+
19
+ # TODO STILL NEED TO UPDATE ALL THE DOCUMENTATION!
20
+
21
+
22
+ @nx._dispatchable(edge_attrs="weight")
23
+ def cut_size(G, S, T=None, weight=None):
24
+ """Returns the size of the cut between two sets of nodes.
25
+
26
+ A *cut* is a partition of the nodes of a graph into two sets. The
27
+ *cut size* is the sum of the weights of the edges "between" the two
28
+ sets of nodes.
29
+
30
+ Parameters
31
+ ----------
32
+ G : NetworkX graph
33
+
34
+ S : collection
35
+ A collection of nodes in `G`.
36
+
37
+ T : collection
38
+ A collection of nodes in `G`. If not specified, this is taken to
39
+ be the set complement of `S`.
40
+
41
+ weight : object
42
+ Edge attribute key to use as weight. If not specified, edges
43
+ have weight one.
44
+
45
+ Returns
46
+ -------
47
+ number
48
+ Total weight of all edges from nodes in set `S` to nodes in
49
+ set `T` (and, in the case of directed graphs, all edges from
50
+ nodes in `T` to nodes in `S`).
51
+
52
+ Examples
53
+ --------
54
+ In the graph with two cliques joined by a single edges, the natural
55
+ bipartition of the graph into two blocks, one for each clique,
56
+ yields a cut of weight one::
57
+
58
+ >>> G = nx.barbell_graph(3, 0)
59
+ >>> S = {0, 1, 2}
60
+ >>> T = {3, 4, 5}
61
+ >>> nx.cut_size(G, S, T)
62
+ 1
63
+
64
+ Each parallel edge in a multigraph is counted when determining the
65
+ cut size::
66
+
67
+ >>> G = nx.MultiGraph(["ab", "ab"])
68
+ >>> S = {"a"}
69
+ >>> T = {"b"}
70
+ >>> nx.cut_size(G, S, T)
71
+ 2
72
+
73
+ Notes
74
+ -----
75
+ In a multigraph, the cut size is the total weight of edges including
76
+ multiplicity.
77
+
78
+ """
79
+ edges = nx.edge_boundary(G, S, T, data=weight, default=1)
80
+ if G.is_directed():
81
+ edges = chain(edges, nx.edge_boundary(G, T, S, data=weight, default=1))
82
+ return sum(weight for u, v, weight in edges)
83
+
84
+
85
+ @nx._dispatchable(edge_attrs="weight")
86
+ def volume(G, S, weight=None):
87
+ """Returns the volume of a set of nodes.
88
+
89
+ The *volume* of a set *S* is the sum of the (out-)degrees of nodes
90
+ in *S* (taking into account parallel edges in multigraphs). [1]
91
+
92
+ Parameters
93
+ ----------
94
+ G : NetworkX graph
95
+
96
+ S : collection
97
+ A collection of nodes in `G`.
98
+
99
+ weight : object
100
+ Edge attribute key to use as weight. If not specified, edges
101
+ have weight one.
102
+
103
+ Returns
104
+ -------
105
+ number
106
+ The volume of the set of nodes represented by `S` in the graph
107
+ `G`.
108
+
109
+ See also
110
+ --------
111
+ conductance
112
+ cut_size
113
+ edge_expansion
114
+ edge_boundary
115
+ normalized_cut_size
116
+
117
+ References
118
+ ----------
119
+ .. [1] David Gleich.
120
+ *Hierarchical Directed Spectral Graph Partitioning*.
121
+ <https://www.cs.purdue.edu/homes/dgleich/publications/Gleich%202005%20-%20hierarchical%20directed%20spectral.pdf>
122
+
123
+ """
124
+ degree = G.out_degree if G.is_directed() else G.degree
125
+ return sum(d for v, d in degree(S, weight=weight))
126
+
127
+
128
+ @nx._dispatchable(edge_attrs="weight")
129
+ def normalized_cut_size(G, S, T=None, weight=None):
130
+ """Returns the normalized size of the cut between two sets of nodes.
131
+
132
+ The *normalized cut size* is the cut size times the sum of the
133
+ reciprocal sizes of the volumes of the two sets. [1]
134
+
135
+ Parameters
136
+ ----------
137
+ G : NetworkX graph
138
+
139
+ S : collection
140
+ A collection of nodes in `G`.
141
+
142
+ T : collection
143
+ A collection of nodes in `G`.
144
+
145
+ weight : object
146
+ Edge attribute key to use as weight. If not specified, edges
147
+ have weight one.
148
+
149
+ Returns
150
+ -------
151
+ number
152
+ The normalized cut size between the two sets `S` and `T`.
153
+
154
+ Notes
155
+ -----
156
+ In a multigraph, the cut size is the total weight of edges including
157
+ multiplicity.
158
+
159
+ See also
160
+ --------
161
+ conductance
162
+ cut_size
163
+ edge_expansion
164
+ volume
165
+
166
+ References
167
+ ----------
168
+ .. [1] David Gleich.
169
+ *Hierarchical Directed Spectral Graph Partitioning*.
170
+ <https://www.cs.purdue.edu/homes/dgleich/publications/Gleich%202005%20-%20hierarchical%20directed%20spectral.pdf>
171
+
172
+ """
173
+ if T is None:
174
+ T = set(G) - set(S)
175
+ num_cut_edges = cut_size(G, S, T=T, weight=weight)
176
+ volume_S = volume(G, S, weight=weight)
177
+ volume_T = volume(G, T, weight=weight)
178
+ return num_cut_edges * ((1 / volume_S) + (1 / volume_T))
179
+
180
+
181
+ @nx._dispatchable(edge_attrs="weight")
182
+ def conductance(G, S, T=None, weight=None):
183
+ """Returns the conductance of two sets of nodes.
184
+
185
+ The *conductance* is the quotient of the cut size and the smaller of
186
+ the volumes of the two sets. [1]
187
+
188
+ Parameters
189
+ ----------
190
+ G : NetworkX graph
191
+
192
+ S : collection
193
+ A collection of nodes in `G`.
194
+
195
+ T : collection
196
+ A collection of nodes in `G`.
197
+
198
+ weight : object
199
+ Edge attribute key to use as weight. If not specified, edges
200
+ have weight one.
201
+
202
+ Returns
203
+ -------
204
+ number
205
+ The conductance between the two sets `S` and `T`.
206
+
207
+ See also
208
+ --------
209
+ cut_size
210
+ edge_expansion
211
+ normalized_cut_size
212
+ volume
213
+
214
+ References
215
+ ----------
216
+ .. [1] David Gleich.
217
+ *Hierarchical Directed Spectral Graph Partitioning*.
218
+ <https://www.cs.purdue.edu/homes/dgleich/publications/Gleich%202005%20-%20hierarchical%20directed%20spectral.pdf>
219
+
220
+ """
221
+ if T is None:
222
+ T = set(G) - set(S)
223
+ num_cut_edges = cut_size(G, S, T, weight=weight)
224
+ volume_S = volume(G, S, weight=weight)
225
+ volume_T = volume(G, T, weight=weight)
226
+ return num_cut_edges / min(volume_S, volume_T)
227
+
228
+
229
+ @nx._dispatchable(edge_attrs="weight")
230
+ def edge_expansion(G, S, T=None, weight=None):
231
+ """Returns the edge expansion between two node sets.
232
+
233
+ The *edge expansion* is the quotient of the cut size and the smaller
234
+ of the cardinalities of the two sets. [1]
235
+
236
+ Parameters
237
+ ----------
238
+ G : NetworkX graph
239
+
240
+ S : collection
241
+ A collection of nodes in `G`.
242
+
243
+ T : collection
244
+ A collection of nodes in `G`.
245
+
246
+ weight : object
247
+ Edge attribute key to use as weight. If not specified, edges
248
+ have weight one.
249
+
250
+ Returns
251
+ -------
252
+ number
253
+ The edge expansion between the two sets `S` and `T`.
254
+
255
+ See also
256
+ --------
257
+ boundary_expansion
258
+ mixing_expansion
259
+ node_expansion
260
+
261
+ References
262
+ ----------
263
+ .. [1] Fan Chung.
264
+ *Spectral Graph Theory*.
265
+ (CBMS Regional Conference Series in Mathematics, No. 92),
266
+ American Mathematical Society, 1997, ISBN 0-8218-0315-8
267
+ <http://www.math.ucsd.edu/~fan/research/revised.html>
268
+
269
+ """
270
+ if T is None:
271
+ T = set(G) - set(S)
272
+ num_cut_edges = cut_size(G, S, T=T, weight=weight)
273
+ return num_cut_edges / min(len(S), len(T))
274
+
275
+
276
+ @nx._dispatchable(edge_attrs="weight")
277
+ def mixing_expansion(G, S, T=None, weight=None):
278
+ """Returns the mixing expansion between two node sets.
279
+
280
+ The *mixing expansion* is the quotient of the cut size and twice the
281
+ number of edges in the graph. [1]
282
+
283
+ Parameters
284
+ ----------
285
+ G : NetworkX graph
286
+
287
+ S : collection
288
+ A collection of nodes in `G`.
289
+
290
+ T : collection
291
+ A collection of nodes in `G`.
292
+
293
+ weight : object
294
+ Edge attribute key to use as weight. If not specified, edges
295
+ have weight one.
296
+
297
+ Returns
298
+ -------
299
+ number
300
+ The mixing expansion between the two sets `S` and `T`.
301
+
302
+ See also
303
+ --------
304
+ boundary_expansion
305
+ edge_expansion
306
+ node_expansion
307
+
308
+ References
309
+ ----------
310
+ .. [1] Vadhan, Salil P.
311
+ "Pseudorandomness."
312
+ *Foundations and Trends
313
+ in Theoretical Computer Science* 7.1–3 (2011): 1–336.
314
+ <https://doi.org/10.1561/0400000010>
315
+
316
+ """
317
+ num_cut_edges = cut_size(G, S, T=T, weight=weight)
318
+ num_total_edges = G.number_of_edges()
319
+ return num_cut_edges / (2 * num_total_edges)
320
+
321
+
322
+ # TODO What is the generalization to two arguments, S and T? Does the
323
+ # denominator become `min(len(S), len(T))`?
324
+ @nx._dispatchable
325
+ def node_expansion(G, S):
326
+ """Returns the node expansion of the set `S`.
327
+
328
+ The *node expansion* is the quotient of the size of the node
329
+ boundary of *S* and the cardinality of *S*. [1]
330
+
331
+ Parameters
332
+ ----------
333
+ G : NetworkX graph
334
+
335
+ S : collection
336
+ A collection of nodes in `G`.
337
+
338
+ Returns
339
+ -------
340
+ number
341
+ The node expansion of the set `S`.
342
+
343
+ See also
344
+ --------
345
+ boundary_expansion
346
+ edge_expansion
347
+ mixing_expansion
348
+
349
+ References
350
+ ----------
351
+ .. [1] Vadhan, Salil P.
352
+ "Pseudorandomness."
353
+ *Foundations and Trends
354
+ in Theoretical Computer Science* 7.1–3 (2011): 1–336.
355
+ <https://doi.org/10.1561/0400000010>
356
+
357
+ """
358
+ neighborhood = set(chain.from_iterable(G.neighbors(v) for v in S))
359
+ return len(neighborhood) / len(S)
360
+
361
+
362
+ # TODO What is the generalization to two arguments, S and T? Does the
363
+ # denominator become `min(len(S), len(T))`?
364
+ @nx._dispatchable
365
+ def boundary_expansion(G, S):
366
+ """Returns the boundary expansion of the set `S`.
367
+
368
+ The *boundary expansion* is the quotient of the size
369
+ of the node boundary and the cardinality of *S*. [1]
370
+
371
+ Parameters
372
+ ----------
373
+ G : NetworkX graph
374
+
375
+ S : collection
376
+ A collection of nodes in `G`.
377
+
378
+ Returns
379
+ -------
380
+ number
381
+ The boundary expansion of the set `S`.
382
+
383
+ See also
384
+ --------
385
+ edge_expansion
386
+ mixing_expansion
387
+ node_expansion
388
+
389
+ References
390
+ ----------
391
+ .. [1] Vadhan, Salil P.
392
+ "Pseudorandomness."
393
+ *Foundations and Trends in Theoretical Computer Science*
394
+ 7.1–3 (2011): 1–336.
395
+ <https://doi.org/10.1561/0400000010>
396
+
397
+ """
398
+ return len(nx.node_boundary(G, S)) / len(S)
.venv/lib/python3.11/site-packages/networkx/algorithms/cycles.py ADDED
@@ -0,0 +1,1230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ========================
3
+ Cycle finding algorithms
4
+ ========================
5
+ """
6
+
7
+ from collections import Counter, defaultdict
8
+ from itertools import combinations, product
9
+ from math import inf
10
+
11
+ import networkx as nx
12
+ from networkx.utils import not_implemented_for, pairwise
13
+
14
+ __all__ = [
15
+ "cycle_basis",
16
+ "simple_cycles",
17
+ "recursive_simple_cycles",
18
+ "find_cycle",
19
+ "minimum_cycle_basis",
20
+ "chordless_cycles",
21
+ "girth",
22
+ ]
23
+
24
+
25
+ @not_implemented_for("directed")
26
+ @not_implemented_for("multigraph")
27
+ @nx._dispatchable
28
+ def cycle_basis(G, root=None):
29
+ """Returns a list of cycles which form a basis for cycles of G.
30
+
31
+ A basis for cycles of a network is a minimal collection of
32
+ cycles such that any cycle in the network can be written
33
+ as a sum of cycles in the basis. Here summation of cycles
34
+ is defined as "exclusive or" of the edges. Cycle bases are
35
+ useful, e.g. when deriving equations for electric circuits
36
+ using Kirchhoff's Laws.
37
+
38
+ Parameters
39
+ ----------
40
+ G : NetworkX Graph
41
+ root : node, optional
42
+ Specify starting node for basis.
43
+
44
+ Returns
45
+ -------
46
+ A list of cycle lists. Each cycle list is a list of nodes
47
+ which forms a cycle (loop) in G.
48
+
49
+ Examples
50
+ --------
51
+ >>> G = nx.Graph()
52
+ >>> nx.add_cycle(G, [0, 1, 2, 3])
53
+ >>> nx.add_cycle(G, [0, 3, 4, 5])
54
+ >>> nx.cycle_basis(G, 0)
55
+ [[3, 4, 5, 0], [1, 2, 3, 0]]
56
+
57
+ Notes
58
+ -----
59
+ This is adapted from algorithm CACM 491 [1]_.
60
+
61
+ References
62
+ ----------
63
+ .. [1] Paton, K. An algorithm for finding a fundamental set of
64
+ cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518.
65
+
66
+ See Also
67
+ --------
68
+ simple_cycles
69
+ minimum_cycle_basis
70
+ """
71
+ gnodes = dict.fromkeys(G) # set-like object that maintains node order
72
+ cycles = []
73
+ while gnodes: # loop over connected components
74
+ if root is None:
75
+ root = gnodes.popitem()[0]
76
+ stack = [root]
77
+ pred = {root: root}
78
+ used = {root: set()}
79
+ while stack: # walk the spanning tree finding cycles
80
+ z = stack.pop() # use last-in so cycles easier to find
81
+ zused = used[z]
82
+ for nbr in G[z]:
83
+ if nbr not in used: # new node
84
+ pred[nbr] = z
85
+ stack.append(nbr)
86
+ used[nbr] = {z}
87
+ elif nbr == z: # self loops
88
+ cycles.append([z])
89
+ elif nbr not in zused: # found a cycle
90
+ pn = used[nbr]
91
+ cycle = [nbr, z]
92
+ p = pred[z]
93
+ while p not in pn:
94
+ cycle.append(p)
95
+ p = pred[p]
96
+ cycle.append(p)
97
+ cycles.append(cycle)
98
+ used[nbr].add(z)
99
+ for node in pred:
100
+ gnodes.pop(node, None)
101
+ root = None
102
+ return cycles
103
+
104
+
105
+ @nx._dispatchable
106
+ def simple_cycles(G, length_bound=None):
107
+ """Find simple cycles (elementary circuits) of a graph.
108
+
109
+ A "simple cycle", or "elementary circuit", is a closed path where
110
+ no node appears twice. In a directed graph, two simple cycles are distinct
111
+ if they are not cyclic permutations of each other. In an undirected graph,
112
+ two simple cycles are distinct if they are not cyclic permutations of each
113
+ other nor of the other's reversal.
114
+
115
+ Optionally, the cycles are bounded in length. In the unbounded case, we use
116
+ a nonrecursive, iterator/generator version of Johnson's algorithm [1]_. In
117
+ the bounded case, we use a version of the algorithm of Gupta and
118
+ Suzumura [2]_. There may be better algorithms for some cases [3]_ [4]_ [5]_.
119
+
120
+ The algorithms of Johnson, and Gupta and Suzumura, are enhanced by some
121
+ well-known preprocessing techniques. When `G` is directed, we restrict our
122
+ attention to strongly connected components of `G`, generate all simple cycles
123
+ containing a certain node, remove that node, and further decompose the
124
+ remainder into strongly connected components. When `G` is undirected, we
125
+ restrict our attention to biconnected components, generate all simple cycles
126
+ containing a particular edge, remove that edge, and further decompose the
127
+ remainder into biconnected components.
128
+
129
+ Note that multigraphs are supported by this function -- and in undirected
130
+ multigraphs, a pair of parallel edges is considered a cycle of length 2.
131
+ Likewise, self-loops are considered to be cycles of length 1. We define
132
+ cycles as sequences of nodes; so the presence of loops and parallel edges
133
+ does not change the number of simple cycles in a graph.
134
+
135
+ Parameters
136
+ ----------
137
+ G : NetworkX Graph
138
+ A networkx graph. Undirected, directed, and multigraphs are all supported.
139
+
140
+ length_bound : int or None, optional (default=None)
141
+ If `length_bound` is an int, generate all simple cycles of `G` with length at
142
+ most `length_bound`. Otherwise, generate all simple cycles of `G`.
143
+
144
+ Yields
145
+ ------
146
+ list of nodes
147
+ Each cycle is represented by a list of nodes along the cycle.
148
+
149
+ Examples
150
+ --------
151
+ >>> G = nx.DiGraph([(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)])
152
+ >>> sorted(nx.simple_cycles(G))
153
+ [[0], [0, 1, 2], [0, 2], [1, 2], [2]]
154
+
155
+ To filter the cycles so that they don't include certain nodes or edges,
156
+ copy your graph and eliminate those nodes or edges before calling.
157
+ For example, to exclude self-loops from the above example:
158
+
159
+ >>> H = G.copy()
160
+ >>> H.remove_edges_from(nx.selfloop_edges(G))
161
+ >>> sorted(nx.simple_cycles(H))
162
+ [[0, 1, 2], [0, 2], [1, 2]]
163
+
164
+ Notes
165
+ -----
166
+ When `length_bound` is None, the time complexity is $O((n+e)(c+1))$ for $n$
167
+ nodes, $e$ edges and $c$ simple circuits. Otherwise, when ``length_bound > 1``,
168
+ the time complexity is $O((c+n)(k-1)d^k)$ where $d$ is the average degree of
169
+ the nodes of `G` and $k$ = `length_bound`.
170
+
171
+ Raises
172
+ ------
173
+ ValueError
174
+ when ``length_bound < 0``.
175
+
176
+ References
177
+ ----------
178
+ .. [1] Finding all the elementary circuits of a directed graph.
179
+ D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
180
+ https://doi.org/10.1137/0204007
181
+ .. [2] Finding All Bounded-Length Simple Cycles in a Directed Graph
182
+ A. Gupta and T. Suzumura https://arxiv.org/abs/2105.10094
183
+ .. [3] Enumerating the cycles of a digraph: a new preprocessing strategy.
184
+ G. Loizou and P. Thanish, Information Sciences, v. 27, 163-182, 1982.
185
+ .. [4] A search strategy for the elementary cycles of a directed graph.
186
+ J.L. Szwarcfiter and P.E. Lauer, BIT NUMERICAL MATHEMATICS,
187
+ v. 16, no. 2, 192-204, 1976.
188
+ .. [5] Optimal Listing of Cycles and st-Paths in Undirected Graphs
189
+ R. Ferreira and R. Grossi and A. Marino and N. Pisanti and R. Rizzi and
190
+ G. Sacomoto https://arxiv.org/abs/1205.2766
191
+
192
+ See Also
193
+ --------
194
+ cycle_basis
195
+ chordless_cycles
196
+ """
197
+
198
+ if length_bound is not None:
199
+ if length_bound == 0:
200
+ return
201
+ elif length_bound < 0:
202
+ raise ValueError("length bound must be non-negative")
203
+
204
+ directed = G.is_directed()
205
+ yield from ([v] for v, Gv in G.adj.items() if v in Gv)
206
+
207
+ if length_bound is not None and length_bound == 1:
208
+ return
209
+
210
+ if G.is_multigraph() and not directed:
211
+ visited = set()
212
+ for u, Gu in G.adj.items():
213
+ multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited)
214
+ yield from ([u, v] for v, m in multiplicity if m > 1)
215
+ visited.add(u)
216
+
217
+ # explicitly filter out loops; implicitly filter out parallel edges
218
+ if directed:
219
+ G = nx.DiGraph((u, v) for u, Gu in G.adj.items() for v in Gu if v != u)
220
+ else:
221
+ G = nx.Graph((u, v) for u, Gu in G.adj.items() for v in Gu if v != u)
222
+
223
+ # this case is not strictly necessary but improves performance
224
+ if length_bound is not None and length_bound == 2:
225
+ if directed:
226
+ visited = set()
227
+ for u, Gu in G.adj.items():
228
+ yield from (
229
+ [v, u] for v in visited.intersection(Gu) if G.has_edge(v, u)
230
+ )
231
+ visited.add(u)
232
+ return
233
+
234
+ if directed:
235
+ yield from _directed_cycle_search(G, length_bound)
236
+ else:
237
+ yield from _undirected_cycle_search(G, length_bound)
238
+
239
+
240
+ def _directed_cycle_search(G, length_bound):
241
+ """A dispatch function for `simple_cycles` for directed graphs.
242
+
243
+ We generate all cycles of G through binary partition.
244
+
245
+ 1. Pick a node v in G which belongs to at least one cycle
246
+ a. Generate all cycles of G which contain the node v.
247
+ b. Recursively generate all cycles of G \\ v.
248
+
249
+ This is accomplished through the following:
250
+
251
+ 1. Compute the strongly connected components SCC of G.
252
+ 2. Select and remove a biconnected component C from BCC. Select a
253
+ non-tree edge (u, v) of a depth-first search of G[C].
254
+ 3. For each simple cycle P containing v in G[C], yield P.
255
+ 4. Add the biconnected components of G[C \\ v] to BCC.
256
+
257
+ If the parameter length_bound is not None, then step 3 will be limited to
258
+ simple cycles of length at most length_bound.
259
+
260
+ Parameters
261
+ ----------
262
+ G : NetworkX DiGraph
263
+ A directed graph
264
+
265
+ length_bound : int or None
266
+ If length_bound is an int, generate all simple cycles of G with length at most length_bound.
267
+ Otherwise, generate all simple cycles of G.
268
+
269
+ Yields
270
+ ------
271
+ list of nodes
272
+ Each cycle is represented by a list of nodes along the cycle.
273
+ """
274
+
275
+ scc = nx.strongly_connected_components
276
+ components = [c for c in scc(G) if len(c) >= 2]
277
+ while components:
278
+ c = components.pop()
279
+ Gc = G.subgraph(c)
280
+ v = next(iter(c))
281
+ if length_bound is None:
282
+ yield from _johnson_cycle_search(Gc, [v])
283
+ else:
284
+ yield from _bounded_cycle_search(Gc, [v], length_bound)
285
+ # delete v after searching G, to make sure we can find v
286
+ G.remove_node(v)
287
+ components.extend(c for c in scc(Gc) if len(c) >= 2)
288
+
289
+
290
+ def _undirected_cycle_search(G, length_bound):
291
+ """A dispatch function for `simple_cycles` for undirected graphs.
292
+
293
+ We generate all cycles of G through binary partition.
294
+
295
+ 1. Pick an edge (u, v) in G which belongs to at least one cycle
296
+ a. Generate all cycles of G which contain the edge (u, v)
297
+ b. Recursively generate all cycles of G \\ (u, v)
298
+
299
+ This is accomplished through the following:
300
+
301
+ 1. Compute the biconnected components BCC of G.
302
+ 2. Select and remove a biconnected component C from BCC. Select a
303
+ non-tree edge (u, v) of a depth-first search of G[C].
304
+ 3. For each (v -> u) path P remaining in G[C] \\ (u, v), yield P.
305
+ 4. Add the biconnected components of G[C] \\ (u, v) to BCC.
306
+
307
+ If the parameter length_bound is not None, then step 3 will be limited to simple paths
308
+ of length at most length_bound.
309
+
310
+ Parameters
311
+ ----------
312
+ G : NetworkX Graph
313
+ An undirected graph
314
+
315
+ length_bound : int or None
316
+ If length_bound is an int, generate all simple cycles of G with length at most length_bound.
317
+ Otherwise, generate all simple cycles of G.
318
+
319
+ Yields
320
+ ------
321
+ list of nodes
322
+ Each cycle is represented by a list of nodes along the cycle.
323
+ """
324
+
325
+ bcc = nx.biconnected_components
326
+ components = [c for c in bcc(G) if len(c) >= 3]
327
+ while components:
328
+ c = components.pop()
329
+ Gc = G.subgraph(c)
330
+ uv = list(next(iter(Gc.edges)))
331
+ G.remove_edge(*uv)
332
+ # delete (u, v) before searching G, to avoid fake 3-cycles [u, v, u]
333
+ if length_bound is None:
334
+ yield from _johnson_cycle_search(Gc, uv)
335
+ else:
336
+ yield from _bounded_cycle_search(Gc, uv, length_bound)
337
+ components.extend(c for c in bcc(Gc) if len(c) >= 3)
338
+
339
+
340
+ class _NeighborhoodCache(dict):
341
+ """Very lightweight graph wrapper which caches neighborhoods as list.
342
+
343
+ This dict subclass uses the __missing__ functionality to query graphs for
344
+ their neighborhoods, and store the result as a list. This is used to avoid
345
+ the performance penalty incurred by subgraph views.
346
+ """
347
+
348
+ def __init__(self, G):
349
+ self.G = G
350
+
351
+ def __missing__(self, v):
352
+ Gv = self[v] = list(self.G[v])
353
+ return Gv
354
+
355
+
356
+ def _johnson_cycle_search(G, path):
357
+ """The main loop of the cycle-enumeration algorithm of Johnson.
358
+
359
+ Parameters
360
+ ----------
361
+ G : NetworkX Graph or DiGraph
362
+ A graph
363
+
364
+ path : list
365
+ A cycle prefix. All cycles generated will begin with this prefix.
366
+
367
+ Yields
368
+ ------
369
+ list of nodes
370
+ Each cycle is represented by a list of nodes along the cycle.
371
+
372
+ References
373
+ ----------
374
+ .. [1] Finding all the elementary circuits of a directed graph.
375
+ D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
376
+ https://doi.org/10.1137/0204007
377
+
378
+ """
379
+
380
+ G = _NeighborhoodCache(G)
381
+ blocked = set(path)
382
+ B = defaultdict(set) # graph portions that yield no elementary circuit
383
+ start = path[0]
384
+ stack = [iter(G[path[-1]])]
385
+ closed = [False]
386
+ while stack:
387
+ nbrs = stack[-1]
388
+ for w in nbrs:
389
+ if w == start:
390
+ yield path[:]
391
+ closed[-1] = True
392
+ elif w not in blocked:
393
+ path.append(w)
394
+ closed.append(False)
395
+ stack.append(iter(G[w]))
396
+ blocked.add(w)
397
+ break
398
+ else: # no more nbrs
399
+ stack.pop()
400
+ v = path.pop()
401
+ if closed.pop():
402
+ if closed:
403
+ closed[-1] = True
404
+ unblock_stack = {v}
405
+ while unblock_stack:
406
+ u = unblock_stack.pop()
407
+ if u in blocked:
408
+ blocked.remove(u)
409
+ unblock_stack.update(B[u])
410
+ B[u].clear()
411
+ else:
412
+ for w in G[v]:
413
+ B[w].add(v)
414
+
415
+
416
+ def _bounded_cycle_search(G, path, length_bound):
417
+ """The main loop of the cycle-enumeration algorithm of Gupta and Suzumura.
418
+
419
+ Parameters
420
+ ----------
421
+ G : NetworkX Graph or DiGraph
422
+ A graph
423
+
424
+ path : list
425
+ A cycle prefix. All cycles generated will begin with this prefix.
426
+
427
+ length_bound: int
428
+ A length bound. All cycles generated will have length at most length_bound.
429
+
430
+ Yields
431
+ ------
432
+ list of nodes
433
+ Each cycle is represented by a list of nodes along the cycle.
434
+
435
+ References
436
+ ----------
437
+ .. [1] Finding All Bounded-Length Simple Cycles in a Directed Graph
438
+ A. Gupta and T. Suzumura https://arxiv.org/abs/2105.10094
439
+
440
+ """
441
+ G = _NeighborhoodCache(G)
442
+ lock = {v: 0 for v in path}
443
+ B = defaultdict(set)
444
+ start = path[0]
445
+ stack = [iter(G[path[-1]])]
446
+ blen = [length_bound]
447
+ while stack:
448
+ nbrs = stack[-1]
449
+ for w in nbrs:
450
+ if w == start:
451
+ yield path[:]
452
+ blen[-1] = 1
453
+ elif len(path) < lock.get(w, length_bound):
454
+ path.append(w)
455
+ blen.append(length_bound)
456
+ lock[w] = len(path)
457
+ stack.append(iter(G[w]))
458
+ break
459
+ else:
460
+ stack.pop()
461
+ v = path.pop()
462
+ bl = blen.pop()
463
+ if blen:
464
+ blen[-1] = min(blen[-1], bl)
465
+ if bl < length_bound:
466
+ relax_stack = [(bl, v)]
467
+ while relax_stack:
468
+ bl, u = relax_stack.pop()
469
+ if lock.get(u, length_bound) < length_bound - bl + 1:
470
+ lock[u] = length_bound - bl + 1
471
+ relax_stack.extend((bl + 1, w) for w in B[u].difference(path))
472
+ else:
473
+ for w in G[v]:
474
+ B[w].add(v)
475
+
476
+
477
+ @nx._dispatchable
478
+ def chordless_cycles(G, length_bound=None):
479
+ """Find simple chordless cycles of a graph.
480
+
481
+ A `simple cycle` is a closed path where no node appears twice. In a simple
482
+ cycle, a `chord` is an additional edge between two nodes in the cycle. A
483
+ `chordless cycle` is a simple cycle without chords. Said differently, a
484
+ chordless cycle is a cycle C in a graph G where the number of edges in the
485
+ induced graph G[C] is equal to the length of `C`.
486
+
487
+ Note that some care must be taken in the case that G is not a simple graph
488
+ nor a simple digraph. Some authors limit the definition of chordless cycles
489
+ to have a prescribed minimum length; we do not.
490
+
491
+ 1. We interpret self-loops to be chordless cycles, except in multigraphs
492
+ with multiple loops in parallel. Likewise, in a chordless cycle of
493
+ length greater than 1, there can be no nodes with self-loops.
494
+
495
+ 2. We interpret directed two-cycles to be chordless cycles, except in
496
+ multi-digraphs when any edge in a two-cycle has a parallel copy.
497
+
498
+ 3. We interpret parallel pairs of undirected edges as two-cycles, except
499
+ when a third (or more) parallel edge exists between the two nodes.
500
+
501
+ 4. Generalizing the above, edges with parallel clones may not occur in
502
+ chordless cycles.
503
+
504
+ In a directed graph, two chordless cycles are distinct if they are not
505
+ cyclic permutations of each other. In an undirected graph, two chordless
506
+ cycles are distinct if they are not cyclic permutations of each other nor of
507
+ the other's reversal.
508
+
509
+ Optionally, the cycles are bounded in length.
510
+
511
+ We use an algorithm strongly inspired by that of Dias et al [1]_. It has
512
+ been modified in the following ways:
513
+
514
+ 1. Recursion is avoided, per Python's limitations
515
+
516
+ 2. The labeling function is not necessary, because the starting paths
517
+ are chosen (and deleted from the host graph) to prevent multiple
518
+ occurrences of the same path
519
+
520
+ 3. The search is optionally bounded at a specified length
521
+
522
+ 4. Support for directed graphs is provided by extending cycles along
523
+ forward edges, and blocking nodes along forward and reverse edges
524
+
525
+ 5. Support for multigraphs is provided by omitting digons from the set
526
+ of forward edges
527
+
528
+ Parameters
529
+ ----------
530
+ G : NetworkX DiGraph
531
+ A directed graph
532
+
533
+ length_bound : int or None, optional (default=None)
534
+ If length_bound is an int, generate all simple cycles of G with length at
535
+ most length_bound. Otherwise, generate all simple cycles of G.
536
+
537
+ Yields
538
+ ------
539
+ list of nodes
540
+ Each cycle is represented by a list of nodes along the cycle.
541
+
542
+ Examples
543
+ --------
544
+ >>> sorted(list(nx.chordless_cycles(nx.complete_graph(4))))
545
+ [[1, 0, 2], [1, 0, 3], [2, 0, 3], [2, 1, 3]]
546
+
547
+ Notes
548
+ -----
549
+ When length_bound is None, and the graph is simple, the time complexity is
550
+ $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ chordless cycles.
551
+
552
+ Raises
553
+ ------
554
+ ValueError
555
+ when length_bound < 0.
556
+
557
+ References
558
+ ----------
559
+ .. [1] Efficient enumeration of chordless cycles
560
+ E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi
561
+ https://arxiv.org/abs/1309.1051
562
+
563
+ See Also
564
+ --------
565
+ simple_cycles
566
+ """
567
+
568
+ if length_bound is not None:
569
+ if length_bound == 0:
570
+ return
571
+ elif length_bound < 0:
572
+ raise ValueError("length bound must be non-negative")
573
+
574
+ directed = G.is_directed()
575
+ multigraph = G.is_multigraph()
576
+
577
+ if multigraph:
578
+ yield from ([v] for v, Gv in G.adj.items() if len(Gv.get(v, ())) == 1)
579
+ else:
580
+ yield from ([v] for v, Gv in G.adj.items() if v in Gv)
581
+
582
+ if length_bound is not None and length_bound == 1:
583
+ return
584
+
585
+ # Nodes with loops cannot belong to longer cycles. Let's delete them here.
586
+ # also, we implicitly reduce the multiplicity of edges down to 1 in the case
587
+ # of multiedges.
588
+ if directed:
589
+ F = nx.DiGraph((u, v) for u, Gu in G.adj.items() if u not in Gu for v in Gu)
590
+ B = F.to_undirected(as_view=False)
591
+ else:
592
+ F = nx.Graph((u, v) for u, Gu in G.adj.items() if u not in Gu for v in Gu)
593
+ B = None
594
+
595
+ # If we're given a multigraph, we have a few cases to consider with parallel
596
+ # edges.
597
+ #
598
+ # 1. If we have 2 or more edges in parallel between the nodes (u, v), we
599
+ # must not construct longer cycles along (u, v).
600
+ # 2. If G is not directed, then a pair of parallel edges between (u, v) is a
601
+ # chordless cycle unless there exists a third (or more) parallel edge.
602
+ # 3. If G is directed, then parallel edges do not form cycles, but do
603
+ # preclude back-edges from forming cycles (handled in the next section),
604
+ # Thus, if an edge (u, v) is duplicated and the reverse (v, u) is also
605
+ # present, then we remove both from F.
606
+ #
607
+ # In directed graphs, we need to consider both directions that edges can
608
+ # take, so iterate over all edges (u, v) and possibly (v, u). In undirected
609
+ # graphs, we need to be a little careful to only consider every edge once,
610
+ # so we use a "visited" set to emulate node-order comparisons.
611
+
612
+ if multigraph:
613
+ if not directed:
614
+ B = F.copy()
615
+ visited = set()
616
+ for u, Gu in G.adj.items():
617
+ if directed:
618
+ multiplicity = ((v, len(Guv)) for v, Guv in Gu.items())
619
+ for v, m in multiplicity:
620
+ if m > 1:
621
+ F.remove_edges_from(((u, v), (v, u)))
622
+ else:
623
+ multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited)
624
+ for v, m in multiplicity:
625
+ if m == 2:
626
+ yield [u, v]
627
+ if m > 1:
628
+ F.remove_edge(u, v)
629
+ visited.add(u)
630
+
631
+ # If we're given a directed graphs, we need to think about digons. If we
632
+ # have two edges (u, v) and (v, u), then that's a two-cycle. If either edge
633
+ # was duplicated above, then we removed both from F. So, any digons we find
634
+ # here are chordless. After finding digons, we remove their edges from F
635
+ # to avoid traversing them in the search for chordless cycles.
636
+ if directed:
637
+ for u, Fu in F.adj.items():
638
+ digons = [[u, v] for v in Fu if F.has_edge(v, u)]
639
+ yield from digons
640
+ F.remove_edges_from(digons)
641
+ F.remove_edges_from(e[::-1] for e in digons)
642
+
643
+ if length_bound is not None and length_bound == 2:
644
+ return
645
+
646
+ # Now, we prepare to search for cycles. We have removed all cycles of
647
+ # lengths 1 and 2, so F is a simple graph or simple digraph. We repeatedly
648
+ # separate digraphs into their strongly connected components, and undirected
649
+ # graphs into their biconnected components. For each component, we pick a
650
+ # node v, search for chordless cycles based at each "stem" (u, v, w), and
651
+ # then remove v from that component before separating the graph again.
652
+ if directed:
653
+ separate = nx.strongly_connected_components
654
+
655
+ # Directed stems look like (u -> v -> w), so we use the product of
656
+ # predecessors of v with successors of v.
657
+ def stems(C, v):
658
+ for u, w in product(C.pred[v], C.succ[v]):
659
+ if not G.has_edge(u, w): # omit stems with acyclic chords
660
+ yield [u, v, w], F.has_edge(w, u)
661
+
662
+ else:
663
+ separate = nx.biconnected_components
664
+
665
+ # Undirected stems look like (u ~ v ~ w), but we must not also search
666
+ # (w ~ v ~ u), so we use combinations of v's neighbors of length 2.
667
+ def stems(C, v):
668
+ yield from (([u, v, w], F.has_edge(w, u)) for u, w in combinations(C[v], 2))
669
+
670
+ components = [c for c in separate(F) if len(c) > 2]
671
+ while components:
672
+ c = components.pop()
673
+ v = next(iter(c))
674
+ Fc = F.subgraph(c)
675
+ Fcc = Bcc = None
676
+ for S, is_triangle in stems(Fc, v):
677
+ if is_triangle:
678
+ yield S
679
+ else:
680
+ if Fcc is None:
681
+ Fcc = _NeighborhoodCache(Fc)
682
+ Bcc = Fcc if B is None else _NeighborhoodCache(B.subgraph(c))
683
+ yield from _chordless_cycle_search(Fcc, Bcc, S, length_bound)
684
+
685
+ components.extend(c for c in separate(F.subgraph(c - {v})) if len(c) > 2)
686
+
687
+
688
+ def _chordless_cycle_search(F, B, path, length_bound):
689
+ """The main loop for chordless cycle enumeration.
690
+
691
+ This algorithm is strongly inspired by that of Dias et al [1]_. It has been
692
+ modified in the following ways:
693
+
694
+ 1. Recursion is avoided, per Python's limitations
695
+
696
+ 2. The labeling function is not necessary, because the starting paths
697
+ are chosen (and deleted from the host graph) to prevent multiple
698
+ occurrences of the same path
699
+
700
+ 3. The search is optionally bounded at a specified length
701
+
702
+ 4. Support for directed graphs is provided by extending cycles along
703
+ forward edges, and blocking nodes along forward and reverse edges
704
+
705
+ 5. Support for multigraphs is provided by omitting digons from the set
706
+ of forward edges
707
+
708
+ Parameters
709
+ ----------
710
+ F : _NeighborhoodCache
711
+ A graph of forward edges to follow in constructing cycles
712
+
713
+ B : _NeighborhoodCache
714
+ A graph of blocking edges to prevent the production of chordless cycles
715
+
716
+ path : list
717
+ A cycle prefix. All cycles generated will begin with this prefix.
718
+
719
+ length_bound : int
720
+ A length bound. All cycles generated will have length at most length_bound.
721
+
722
+
723
+ Yields
724
+ ------
725
+ list of nodes
726
+ Each cycle is represented by a list of nodes along the cycle.
727
+
728
+ References
729
+ ----------
730
+ .. [1] Efficient enumeration of chordless cycles
731
+ E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi
732
+ https://arxiv.org/abs/1309.1051
733
+
734
+ """
735
+ blocked = defaultdict(int)
736
+ target = path[0]
737
+ blocked[path[1]] = 1
738
+ for w in path[1:]:
739
+ for v in B[w]:
740
+ blocked[v] += 1
741
+
742
+ stack = [iter(F[path[2]])]
743
+ while stack:
744
+ nbrs = stack[-1]
745
+ for w in nbrs:
746
+ if blocked[w] == 1 and (length_bound is None or len(path) < length_bound):
747
+ Fw = F[w]
748
+ if target in Fw:
749
+ yield path + [w]
750
+ else:
751
+ Bw = B[w]
752
+ if target in Bw:
753
+ continue
754
+ for v in Bw:
755
+ blocked[v] += 1
756
+ path.append(w)
757
+ stack.append(iter(Fw))
758
+ break
759
+ else:
760
+ stack.pop()
761
+ for v in B[path.pop()]:
762
+ blocked[v] -= 1
763
+
764
+
765
+ @not_implemented_for("undirected")
766
+ @nx._dispatchable(mutates_input=True)
767
+ def recursive_simple_cycles(G):
768
+ """Find simple cycles (elementary circuits) of a directed graph.
769
+
770
+ A `simple cycle`, or `elementary circuit`, is a closed path where
771
+ no node appears twice. Two elementary circuits are distinct if they
772
+ are not cyclic permutations of each other.
773
+
774
+ This version uses a recursive algorithm to build a list of cycles.
775
+ You should probably use the iterator version called simple_cycles().
776
+ Warning: This recursive version uses lots of RAM!
777
+ It appears in NetworkX for pedagogical value.
778
+
779
+ Parameters
780
+ ----------
781
+ G : NetworkX DiGraph
782
+ A directed graph
783
+
784
+ Returns
785
+ -------
786
+ A list of cycles, where each cycle is represented by a list of nodes
787
+ along the cycle.
788
+
789
+ Example:
790
+
791
+ >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
792
+ >>> G = nx.DiGraph(edges)
793
+ >>> nx.recursive_simple_cycles(G)
794
+ [[0], [2], [0, 1, 2], [0, 2], [1, 2]]
795
+
796
+ Notes
797
+ -----
798
+ The implementation follows pp. 79-80 in [1]_.
799
+
800
+ The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$
801
+ elementary circuits.
802
+
803
+ References
804
+ ----------
805
+ .. [1] Finding all the elementary circuits of a directed graph.
806
+ D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
807
+ https://doi.org/10.1137/0204007
808
+
809
+ See Also
810
+ --------
811
+ simple_cycles, cycle_basis
812
+ """
813
+
814
+ # Jon Olav Vik, 2010-08-09
815
+ def _unblock(thisnode):
816
+ """Recursively unblock and remove nodes from B[thisnode]."""
817
+ if blocked[thisnode]:
818
+ blocked[thisnode] = False
819
+ while B[thisnode]:
820
+ _unblock(B[thisnode].pop())
821
+
822
+ def circuit(thisnode, startnode, component):
823
+ closed = False # set to True if elementary path is closed
824
+ path.append(thisnode)
825
+ blocked[thisnode] = True
826
+ for nextnode in component[thisnode]: # direct successors of thisnode
827
+ if nextnode == startnode:
828
+ result.append(path[:])
829
+ closed = True
830
+ elif not blocked[nextnode]:
831
+ if circuit(nextnode, startnode, component):
832
+ closed = True
833
+ if closed:
834
+ _unblock(thisnode)
835
+ else:
836
+ for nextnode in component[thisnode]:
837
+ if thisnode not in B[nextnode]: # TODO: use set for speedup?
838
+ B[nextnode].append(thisnode)
839
+ path.pop() # remove thisnode from path
840
+ return closed
841
+
842
+ path = [] # stack of nodes in current path
843
+ blocked = defaultdict(bool) # vertex: blocked from search?
844
+ B = defaultdict(list) # graph portions that yield no elementary circuit
845
+ result = [] # list to accumulate the circuits found
846
+
847
+ # Johnson's algorithm exclude self cycle edges like (v, v)
848
+ # To be backward compatible, we record those cycles in advance
849
+ # and then remove from subG
850
+ for v in G:
851
+ if G.has_edge(v, v):
852
+ result.append([v])
853
+ G.remove_edge(v, v)
854
+
855
+ # Johnson's algorithm requires some ordering of the nodes.
856
+ # They might not be sortable so we assign an arbitrary ordering.
857
+ ordering = dict(zip(G, range(len(G))))
858
+ for s in ordering:
859
+ # Build the subgraph induced by s and following nodes in the ordering
860
+ subgraph = G.subgraph(node for node in G if ordering[node] >= ordering[s])
861
+ # Find the strongly connected component in the subgraph
862
+ # that contains the least node according to the ordering
863
+ strongcomp = nx.strongly_connected_components(subgraph)
864
+ mincomp = min(strongcomp, key=lambda ns: min(ordering[n] for n in ns))
865
+ component = G.subgraph(mincomp)
866
+ if len(component) > 1:
867
+ # smallest node in the component according to the ordering
868
+ startnode = min(component, key=ordering.__getitem__)
869
+ for node in component:
870
+ blocked[node] = False
871
+ B[node][:] = []
872
+ dummy = circuit(startnode, startnode, component)
873
+ return result
874
+
875
+
876
+ @nx._dispatchable
877
+ def find_cycle(G, source=None, orientation=None):
878
+ """Returns a cycle found via depth-first traversal.
879
+
880
+ The cycle is a list of edges indicating the cyclic path.
881
+ Orientation of directed edges is controlled by `orientation`.
882
+
883
+ Parameters
884
+ ----------
885
+ G : graph
886
+ A directed/undirected graph/multigraph.
887
+
888
+ source : node, list of nodes
889
+ The node from which the traversal begins. If None, then a source
890
+ is chosen arbitrarily and repeatedly until all edges from each node in
891
+ the graph are searched.
892
+
893
+ orientation : None | 'original' | 'reverse' | 'ignore' (default: None)
894
+ For directed graphs and directed multigraphs, edge traversals need not
895
+ respect the original orientation of the edges.
896
+ When set to 'reverse' every edge is traversed in the reverse direction.
897
+ When set to 'ignore', every edge is treated as undirected.
898
+ When set to 'original', every edge is treated as directed.
899
+ In all three cases, the yielded edge tuples add a last entry to
900
+ indicate the direction in which that edge was traversed.
901
+ If orientation is None, the yielded edge has no direction indicated.
902
+ The direction is respected, but not reported.
903
+
904
+ Returns
905
+ -------
906
+ edges : directed edges
907
+ A list of directed edges indicating the path taken for the loop.
908
+ If no cycle is found, then an exception is raised.
909
+ For graphs, an edge is of the form `(u, v)` where `u` and `v`
910
+ are the tail and head of the edge as determined by the traversal.
911
+ For multigraphs, an edge is of the form `(u, v, key)`, where `key` is
912
+ the key of the edge. When the graph is directed, then `u` and `v`
913
+ are always in the order of the actual directed edge.
914
+ If orientation is not None then the edge tuple is extended to include
915
+ the direction of traversal ('forward' or 'reverse') on that edge.
916
+
917
+ Raises
918
+ ------
919
+ NetworkXNoCycle
920
+ If no cycle was found.
921
+
922
+ Examples
923
+ --------
924
+ In this example, we construct a DAG and find, in the first call, that there
925
+ are no directed cycles, and so an exception is raised. In the second call,
926
+ we ignore edge orientations and find that there is an undirected cycle.
927
+ Note that the second call finds a directed cycle while effectively
928
+ traversing an undirected graph, and so, we found an "undirected cycle".
929
+ This means that this DAG structure does not form a directed tree (which
930
+ is also known as a polytree).
931
+
932
+ >>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2)])
933
+ >>> nx.find_cycle(G, orientation="original")
934
+ Traceback (most recent call last):
935
+ ...
936
+ networkx.exception.NetworkXNoCycle: No cycle found.
937
+ >>> list(nx.find_cycle(G, orientation="ignore"))
938
+ [(0, 1, 'forward'), (1, 2, 'forward'), (0, 2, 'reverse')]
939
+
940
+ See Also
941
+ --------
942
+ simple_cycles
943
+ """
944
+ if not G.is_directed() or orientation in (None, "original"):
945
+
946
+ def tailhead(edge):
947
+ return edge[:2]
948
+
949
+ elif orientation == "reverse":
950
+
951
+ def tailhead(edge):
952
+ return edge[1], edge[0]
953
+
954
+ elif orientation == "ignore":
955
+
956
+ def tailhead(edge):
957
+ if edge[-1] == "reverse":
958
+ return edge[1], edge[0]
959
+ return edge[:2]
960
+
961
+ explored = set()
962
+ cycle = []
963
+ final_node = None
964
+ for start_node in G.nbunch_iter(source):
965
+ if start_node in explored:
966
+ # No loop is possible.
967
+ continue
968
+
969
+ edges = []
970
+ # All nodes seen in this iteration of edge_dfs
971
+ seen = {start_node}
972
+ # Nodes in active path.
973
+ active_nodes = {start_node}
974
+ previous_head = None
975
+
976
+ for edge in nx.edge_dfs(G, start_node, orientation):
977
+ # Determine if this edge is a continuation of the active path.
978
+ tail, head = tailhead(edge)
979
+ if head in explored:
980
+ # Then we've already explored it. No loop is possible.
981
+ continue
982
+ if previous_head is not None and tail != previous_head:
983
+ # This edge results from backtracking.
984
+ # Pop until we get a node whose head equals the current tail.
985
+ # So for example, we might have:
986
+ # (0, 1), (1, 2), (2, 3), (1, 4)
987
+ # which must become:
988
+ # (0, 1), (1, 4)
989
+ while True:
990
+ try:
991
+ popped_edge = edges.pop()
992
+ except IndexError:
993
+ edges = []
994
+ active_nodes = {tail}
995
+ break
996
+ else:
997
+ popped_head = tailhead(popped_edge)[1]
998
+ active_nodes.remove(popped_head)
999
+
1000
+ if edges:
1001
+ last_head = tailhead(edges[-1])[1]
1002
+ if tail == last_head:
1003
+ break
1004
+ edges.append(edge)
1005
+
1006
+ if head in active_nodes:
1007
+ # We have a loop!
1008
+ cycle.extend(edges)
1009
+ final_node = head
1010
+ break
1011
+ else:
1012
+ seen.add(head)
1013
+ active_nodes.add(head)
1014
+ previous_head = head
1015
+
1016
+ if cycle:
1017
+ break
1018
+ else:
1019
+ explored.update(seen)
1020
+
1021
+ else:
1022
+ assert len(cycle) == 0
1023
+ raise nx.exception.NetworkXNoCycle("No cycle found.")
1024
+
1025
+ # We now have a list of edges which ends on a cycle.
1026
+ # So we need to remove from the beginning edges that are not relevant.
1027
+
1028
+ for i, edge in enumerate(cycle):
1029
+ tail, head = tailhead(edge)
1030
+ if tail == final_node:
1031
+ break
1032
+
1033
+ return cycle[i:]
1034
+
1035
+
1036
+ @not_implemented_for("directed")
1037
+ @not_implemented_for("multigraph")
1038
+ @nx._dispatchable(edge_attrs="weight")
1039
+ def minimum_cycle_basis(G, weight=None):
1040
+ """Returns a minimum weight cycle basis for G
1041
+
1042
+ Minimum weight means a cycle basis for which the total weight
1043
+ (length for unweighted graphs) of all the cycles is minimum.
1044
+
1045
+ Parameters
1046
+ ----------
1047
+ G : NetworkX Graph
1048
+ weight: string
1049
+ name of the edge attribute to use for edge weights
1050
+
1051
+ Returns
1052
+ -------
1053
+ A list of cycle lists. Each cycle list is a list of nodes
1054
+ which forms a cycle (loop) in G. Note that the nodes are not
1055
+ necessarily returned in a order by which they appear in the cycle
1056
+
1057
+ Examples
1058
+ --------
1059
+ >>> G = nx.Graph()
1060
+ >>> nx.add_cycle(G, [0, 1, 2, 3])
1061
+ >>> nx.add_cycle(G, [0, 3, 4, 5])
1062
+ >>> nx.minimum_cycle_basis(G)
1063
+ [[5, 4, 3, 0], [3, 2, 1, 0]]
1064
+
1065
+ References:
1066
+ [1] Kavitha, Telikepalli, et al. "An O(m^2n) Algorithm for
1067
+ Minimum Cycle Basis of Graphs."
1068
+ http://link.springer.com/article/10.1007/s00453-007-9064-z
1069
+ [2] de Pina, J. 1995. Applications of shortest path methods.
1070
+ Ph.D. thesis, University of Amsterdam, Netherlands
1071
+
1072
+ See Also
1073
+ --------
1074
+ simple_cycles, cycle_basis
1075
+ """
1076
+ # We first split the graph in connected subgraphs
1077
+ return sum(
1078
+ (_min_cycle_basis(G.subgraph(c), weight) for c in nx.connected_components(G)),
1079
+ [],
1080
+ )
1081
+
1082
+
1083
+ def _min_cycle_basis(G, weight):
1084
+ cb = []
1085
+ # We extract the edges not in a spanning tree. We do not really need a
1086
+ # *minimum* spanning tree. That is why we call the next function with
1087
+ # weight=None. Depending on implementation, it may be faster as well
1088
+ tree_edges = list(nx.minimum_spanning_edges(G, weight=None, data=False))
1089
+ chords = G.edges - tree_edges - {(v, u) for u, v in tree_edges}
1090
+
1091
+ # We maintain a set of vectors orthogonal to sofar found cycles
1092
+ set_orth = [{edge} for edge in chords]
1093
+ while set_orth:
1094
+ base = set_orth.pop()
1095
+ # kth cycle is "parallel" to kth vector in set_orth
1096
+ cycle_edges = _min_cycle(G, base, weight)
1097
+ cb.append([v for u, v in cycle_edges])
1098
+
1099
+ # now update set_orth so that k+1,k+2... th elements are
1100
+ # orthogonal to the newly found cycle, as per [p. 336, 1]
1101
+ set_orth = [
1102
+ (
1103
+ {e for e in orth if e not in base if e[::-1] not in base}
1104
+ | {e for e in base if e not in orth if e[::-1] not in orth}
1105
+ )
1106
+ if sum((e in orth or e[::-1] in orth) for e in cycle_edges) % 2
1107
+ else orth
1108
+ for orth in set_orth
1109
+ ]
1110
+ return cb
1111
+
1112
+
1113
+ def _min_cycle(G, orth, weight):
1114
+ """
1115
+ Computes the minimum weight cycle in G,
1116
+ orthogonal to the vector orth as per [p. 338, 1]
1117
+ Use (u, 1) to indicate the lifted copy of u (denoted u' in paper).
1118
+ """
1119
+ Gi = nx.Graph()
1120
+
1121
+ # Add 2 copies of each edge in G to Gi.
1122
+ # If edge is in orth, add cross edge; otherwise in-plane edge
1123
+ for u, v, wt in G.edges(data=weight, default=1):
1124
+ if (u, v) in orth or (v, u) in orth:
1125
+ Gi.add_edges_from([(u, (v, 1)), ((u, 1), v)], Gi_weight=wt)
1126
+ else:
1127
+ Gi.add_edges_from([(u, v), ((u, 1), (v, 1))], Gi_weight=wt)
1128
+
1129
+ # find the shortest length in Gi between n and (n, 1) for each n
1130
+ # Note: Use "Gi_weight" for name of weight attribute
1131
+ spl = nx.shortest_path_length
1132
+ lift = {n: spl(Gi, source=n, target=(n, 1), weight="Gi_weight") for n in G}
1133
+
1134
+ # Now compute that short path in Gi, which translates to a cycle in G
1135
+ start = min(lift, key=lift.get)
1136
+ end = (start, 1)
1137
+ min_path_i = nx.shortest_path(Gi, source=start, target=end, weight="Gi_weight")
1138
+
1139
+ # Now we obtain the actual path, re-map nodes in Gi to those in G
1140
+ min_path = [n if n in G else n[0] for n in min_path_i]
1141
+
1142
+ # Now remove the edges that occur two times
1143
+ # two passes: flag which edges get kept, then build it
1144
+ edgelist = list(pairwise(min_path))
1145
+ edgeset = set()
1146
+ for e in edgelist:
1147
+ if e in edgeset:
1148
+ edgeset.remove(e)
1149
+ elif e[::-1] in edgeset:
1150
+ edgeset.remove(e[::-1])
1151
+ else:
1152
+ edgeset.add(e)
1153
+
1154
+ min_edgelist = []
1155
+ for e in edgelist:
1156
+ if e in edgeset:
1157
+ min_edgelist.append(e)
1158
+ edgeset.remove(e)
1159
+ elif e[::-1] in edgeset:
1160
+ min_edgelist.append(e[::-1])
1161
+ edgeset.remove(e[::-1])
1162
+
1163
+ return min_edgelist
1164
+
1165
+
1166
+ @not_implemented_for("directed")
1167
+ @not_implemented_for("multigraph")
1168
+ @nx._dispatchable
1169
+ def girth(G):
1170
+ """Returns the girth of the graph.
1171
+
1172
+ The girth of a graph is the length of its shortest cycle, or infinity if
1173
+ the graph is acyclic. The algorithm follows the description given on the
1174
+ Wikipedia page [1]_, and runs in time O(mn) on a graph with m edges and n
1175
+ nodes.
1176
+
1177
+ Parameters
1178
+ ----------
1179
+ G : NetworkX Graph
1180
+
1181
+ Returns
1182
+ -------
1183
+ int or math.inf
1184
+
1185
+ Examples
1186
+ --------
1187
+ All examples below (except P_5) can easily be checked using Wikipedia,
1188
+ which has a page for each of these famous graphs.
1189
+
1190
+ >>> nx.girth(nx.chvatal_graph())
1191
+ 4
1192
+ >>> nx.girth(nx.tutte_graph())
1193
+ 4
1194
+ >>> nx.girth(nx.petersen_graph())
1195
+ 5
1196
+ >>> nx.girth(nx.heawood_graph())
1197
+ 6
1198
+ >>> nx.girth(nx.pappus_graph())
1199
+ 6
1200
+ >>> nx.girth(nx.path_graph(5))
1201
+ inf
1202
+
1203
+ References
1204
+ ----------
1205
+ .. [1] `Wikipedia: Girth <https://en.wikipedia.org/wiki/Girth_(graph_theory)>`_
1206
+
1207
+ """
1208
+ girth = depth_limit = inf
1209
+ tree_edge = nx.algorithms.traversal.breadth_first_search.TREE_EDGE
1210
+ level_edge = nx.algorithms.traversal.breadth_first_search.LEVEL_EDGE
1211
+ for n in G:
1212
+ # run a BFS from source n, keeping track of distances; since we want
1213
+ # the shortest cycle, no need to explore beyond the current minimum length
1214
+ depth = {n: 0}
1215
+ for u, v, label in nx.bfs_labeled_edges(G, n):
1216
+ du = depth[u]
1217
+ if du > depth_limit:
1218
+ break
1219
+ if label is tree_edge:
1220
+ depth[v] = du + 1
1221
+ else:
1222
+ # if (u, v) is a level edge, the length is du + du + 1 (odd)
1223
+ # otherwise, it's a forward edge; length is du + (du + 1) + 1 (even)
1224
+ delta = label is level_edge
1225
+ length = du + du + 2 - delta
1226
+ if length < girth:
1227
+ girth = length
1228
+ depth_limit = du - delta
1229
+
1230
+ return girth
.venv/lib/python3.11/site-packages/networkx/algorithms/d_separation.py ADDED
@@ -0,0 +1,722 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Algorithm for testing d-separation in DAGs.
3
+
4
+ *d-separation* is a test for conditional independence in probability
5
+ distributions that can be factorized using DAGs. It is a purely
6
+ graphical test that uses the underlying graph and makes no reference
7
+ to the actual distribution parameters. See [1]_ for a formal
8
+ definition.
9
+
10
+ The implementation is based on the conceptually simple linear time
11
+ algorithm presented in [2]_. Refer to [3]_, [4]_ for a couple of
12
+ alternative algorithms.
13
+
14
+ The functional interface in NetworkX consists of three functions:
15
+
16
+ - `find_minimal_d_separator` returns a minimal d-separator set ``z``.
17
+ That is, removing any node or nodes from it makes it no longer a d-separator.
18
+ - `is_d_separator` checks if a given set is a d-separator.
19
+ - `is_minimal_d_separator` checks if a given set is a minimal d-separator.
20
+
21
+ D-separators
22
+ ------------
23
+
24
+ Here, we provide a brief overview of d-separation and related concepts that
25
+ are relevant for understanding it:
26
+
27
+ The ideas of d-separation and d-connection relate to paths being open or blocked.
28
+
29
+ - A "path" is a sequence of nodes connected in order by edges. Unlike for most
30
+ graph theory analysis, the direction of the edges is ignored. Thus the path
31
+ can be thought of as a traditional path on the undirected version of the graph.
32
+ - A "candidate d-separator" ``z`` is a set of nodes being considered as
33
+ possibly blocking all paths between two prescribed sets ``x`` and ``y`` of nodes.
34
+ We refer to each node in the candidate d-separator as "known".
35
+ - A "collider" node on a path is a node that is a successor of its two neighbor
36
+ nodes on the path. That is, ``c`` is a collider if the edge directions
37
+ along the path look like ``... u -> c <- v ...``.
38
+ - If a collider node or any of its descendants are "known", the collider
39
+ is called an "open collider". Otherwise it is a "blocking collider".
40
+ - Any path can be "blocked" in two ways. If the path contains a "known" node
41
+ that is not a collider, the path is blocked. Also, if the path contains a
42
+ collider that is not a "known" node, the path is blocked.
43
+ - A path is "open" if it is not blocked. That is, it is open if every node is
44
+ either an open collider or not a "known". Said another way, every
45
+ "known" in the path is a collider and every collider is open (has a
46
+ "known" as a inclusive descendant). The concept of "open path" is meant to
47
+ demonstrate a probabilistic conditional dependence between two nodes given
48
+ prescribed knowledge ("known" nodes).
49
+ - Two sets ``x`` and ``y`` of nodes are "d-separated" by a set of nodes ``z``
50
+ if all paths between nodes in ``x`` and nodes in ``y`` are blocked. That is,
51
+ if there are no open paths from any node in ``x`` to any node in ``y``.
52
+ Such a set ``z`` is a "d-separator" of ``x`` and ``y``.
53
+ - A "minimal d-separator" is a d-separator ``z`` for which no node or subset
54
+ of nodes can be removed with it still being a d-separator.
55
+
56
+ The d-separator blocks some paths between ``x`` and ``y`` but opens others.
57
+ Nodes in the d-separator block paths if the nodes are not colliders.
58
+ But if a collider or its descendant nodes are in the d-separation set, the
59
+ colliders are open, allowing a path through that collider.
60
+
61
+ Illustration of D-separation with examples
62
+ ------------------------------------------
63
+
64
+ A pair of two nodes, ``u`` and ``v``, are d-connected if there is a path
65
+ from ``u`` to ``v`` that is not blocked. That means, there is an open
66
+ path from ``u`` to ``v``.
67
+
68
+ For example, if the d-separating set is the empty set, then the following paths are
69
+ open between ``u`` and ``v``:
70
+
71
+ - u <- n -> v
72
+ - u -> w -> ... -> n -> v
73
+
74
+ If on the other hand, ``n`` is in the d-separating set, then ``n`` blocks
75
+ those paths between ``u`` and ``v``.
76
+
77
+ Colliders block a path if they and their descendants are not included
78
+ in the d-separating set. An example of a path that is blocked when the
79
+ d-separating set is empty is:
80
+
81
+ - u -> w -> ... -> n <- v
82
+
83
+ The node ``n`` is a collider in this path and is not in the d-separating set.
84
+ So ``n`` blocks this path. However, if ``n`` or a descendant of ``n`` is
85
+ included in the d-separating set, then the path through the collider
86
+ at ``n`` (... -> n <- ...) is "open".
87
+
88
+ D-separation is concerned with blocking all paths between nodes from ``x`` to ``y``.
89
+ A d-separating set between ``x`` and ``y`` is one where all paths are blocked.
90
+
91
+ D-separation and its applications in probability
92
+ ------------------------------------------------
93
+
94
+ D-separation is commonly used in probabilistic causal-graph models. D-separation
95
+ connects the idea of probabilistic "dependence" with separation in a graph. If
96
+ one assumes the causal Markov condition [5]_, (every node is conditionally
97
+ independent of its non-descendants, given its parents) then d-separation implies
98
+ conditional independence in probability distributions.
99
+ Symmetrically, d-connection implies dependence.
100
+
101
+ The intuition is as follows. The edges on a causal graph indicate which nodes
102
+ influence the outcome of other nodes directly. An edge from u to v
103
+ implies that the outcome of event ``u`` influences the probabilities for
104
+ the outcome of event ``v``. Certainly knowing ``u`` changes predictions for ``v``.
105
+ But also knowing ``v`` changes predictions for ``u``. The outcomes are dependent.
106
+ Furthermore, an edge from ``v`` to ``w`` would mean that ``w`` and ``v`` are dependent
107
+ and thus that ``u`` could indirectly influence ``w``.
108
+
109
+ Without any knowledge about the system (candidate d-separating set is empty)
110
+ a causal graph ``u -> v -> w`` allows all three nodes to be dependent. But
111
+ if we know the outcome of ``v``, the conditional probabilities of outcomes for
112
+ ``u`` and ``w`` are independent of each other. That is, once we know the outcome
113
+ for ```v`, the probabilities for ``w`` do not depend on the outcome for ``u``.
114
+ This is the idea behind ``v`` blocking the path if it is "known" (in the candidate
115
+ d-separating set).
116
+
117
+ The same argument works whether the direction of the edges are both
118
+ left-going and when both arrows head out from the middle. Having a "known"
119
+ node on a path blocks the collider-free path because those relationships
120
+ make the conditional probabilities independent.
121
+
122
+ The direction of the causal edges does impact dependence precisely in the
123
+ case of a collider e.g. ``u -> v <- w``. In that situation, both ``u`` and ``w``
124
+ influence ``v```. But they do not directly influence each other. So without any
125
+ knowledge of any outcomes, ``u`` and ``w`` are independent. That is the idea behind
126
+ colliders blocking the path. But, if ``v`` is known, the conditional probabilities
127
+ of ``u`` and ``w`` can be dependent. This is the heart of Berkson's Paradox [6]_.
128
+ For example, suppose ``u`` and ``w`` are boolean events (they either happen or do not)
129
+ and ``v`` represents the outcome "at least one of ``u`` and ``w`` occur". Then knowing
130
+ ``v`` is true makes the conditional probabilities of ``u`` and ``w`` dependent.
131
+ Essentially, knowing that at least one of them is true raises the probability of
132
+ each. But further knowledge that ``w`` is true (or false) change the conditional
133
+ probability of ``u`` to either the original value or 1. So the conditional
134
+ probability of ``u`` depends on the outcome of ``w`` even though there is no
135
+ causal relationship between them. When a collider is known, dependence can
136
+ occur across paths through that collider. This is the reason open colliders
137
+ do not block paths.
138
+
139
+ Furthermore, even if ``v`` is not "known", if one of its descendants is "known"
140
+ we can use that information to know more about ``v`` which again makes
141
+ ``u`` and ``w`` potentially dependent. Suppose the chance of ``n`` occurring
142
+ is much higher when ``v`` occurs ("at least one of ``u`` and ``w`` occur").
143
+ Then if we know ``n`` occurred, it is more likely that ``v`` occurred and that
144
+ makes the chance of ``u`` and ``w`` dependent. This is the idea behind why
145
+ a collider does no block a path if any descendant of the collider is "known".
146
+
147
+ When two sets of nodes ``x`` and ``y`` are d-separated by a set ``z``,
148
+ it means that given the outcomes of the nodes in ``z``, the probabilities
149
+ of outcomes of the nodes in ``x`` are independent of the outcomes of the
150
+ nodes in ``y`` and vice versa.
151
+
152
+ Examples
153
+ --------
154
+ A Hidden Markov Model with 5 observed states and 5 hidden states
155
+ where the hidden states have causal relationships resulting in
156
+ a path results in the following causal network. We check that
157
+ early states along the path are separated from late state in
158
+ the path by the d-separator of the middle hidden state.
159
+ Thus if we condition on the middle hidden state, the early
160
+ state probabilities are independent of the late state outcomes.
161
+
162
+ >>> G = nx.DiGraph()
163
+ >>> G.add_edges_from(
164
+ ... [
165
+ ... ("H1", "H2"),
166
+ ... ("H2", "H3"),
167
+ ... ("H3", "H4"),
168
+ ... ("H4", "H5"),
169
+ ... ("H1", "O1"),
170
+ ... ("H2", "O2"),
171
+ ... ("H3", "O3"),
172
+ ... ("H4", "O4"),
173
+ ... ("H5", "O5"),
174
+ ... ]
175
+ ... )
176
+ >>> x, y, z = ({"H1", "O1"}, {"H5", "O5"}, {"H3"})
177
+ >>> nx.is_d_separator(G, x, y, z)
178
+ True
179
+ >>> nx.is_minimal_d_separator(G, x, y, z)
180
+ True
181
+ >>> nx.is_minimal_d_separator(G, x, y, z | {"O3"})
182
+ False
183
+ >>> z = nx.find_minimal_d_separator(G, x | y, {"O2", "O3", "O4"})
184
+ >>> z == {"H2", "H4"}
185
+ True
186
+
187
+ If no minimal_d_separator exists, `None` is returned
188
+
189
+ >>> other_z = nx.find_minimal_d_separator(G, x | y, {"H2", "H3"})
190
+ >>> other_z is None
191
+ True
192
+
193
+
194
+ References
195
+ ----------
196
+
197
+ .. [1] Pearl, J. (2009). Causality. Cambridge: Cambridge University Press.
198
+
199
+ .. [2] Darwiche, A. (2009). Modeling and reasoning with Bayesian networks.
200
+ Cambridge: Cambridge University Press.
201
+
202
+ .. [3] Shachter, Ross D. "Bayes-ball: The rational pastime (for
203
+ determining irrelevance and requisite information in belief networks
204
+ and influence diagrams)." In Proceedings of the Fourteenth Conference
205
+ on Uncertainty in Artificial Intelligence (UAI), (pp. 480–487). 1998.
206
+
207
+ .. [4] Koller, D., & Friedman, N. (2009).
208
+ Probabilistic graphical models: principles and techniques. The MIT Press.
209
+
210
+ .. [5] https://en.wikipedia.org/wiki/Causal_Markov_condition
211
+
212
+ .. [6] https://en.wikipedia.org/wiki/Berkson%27s_paradox
213
+
214
+ """
215
+
216
+ from collections import deque
217
+ from itertools import chain
218
+
219
+ import networkx as nx
220
+ from networkx.utils import UnionFind, not_implemented_for
221
+
222
+ __all__ = [
223
+ "is_d_separator",
224
+ "is_minimal_d_separator",
225
+ "find_minimal_d_separator",
226
+ "d_separated",
227
+ "minimal_d_separator",
228
+ ]
229
+
230
+
231
+ @not_implemented_for("undirected")
232
+ @nx._dispatchable
233
+ def is_d_separator(G, x, y, z):
234
+ """Return whether node sets `x` and `y` are d-separated by `z`.
235
+
236
+ Parameters
237
+ ----------
238
+ G : nx.DiGraph
239
+ A NetworkX DAG.
240
+
241
+ x : node or set of nodes
242
+ First node or set of nodes in `G`.
243
+
244
+ y : node or set of nodes
245
+ Second node or set of nodes in `G`.
246
+
247
+ z : node or set of nodes
248
+ Potential separator (set of conditioning nodes in `G`). Can be empty set.
249
+
250
+ Returns
251
+ -------
252
+ b : bool
253
+ A boolean that is true if `x` is d-separated from `y` given `z` in `G`.
254
+
255
+ Raises
256
+ ------
257
+ NetworkXError
258
+ The *d-separation* test is commonly used on disjoint sets of
259
+ nodes in acyclic directed graphs. Accordingly, the algorithm
260
+ raises a :exc:`NetworkXError` if the node sets are not
261
+ disjoint or if the input graph is not a DAG.
262
+
263
+ NodeNotFound
264
+ If any of the input nodes are not found in the graph,
265
+ a :exc:`NodeNotFound` exception is raised
266
+
267
+ Notes
268
+ -----
269
+ A d-separating set in a DAG is a set of nodes that
270
+ blocks all paths between the two sets. Nodes in `z`
271
+ block a path if they are part of the path and are not a collider,
272
+ or a descendant of a collider. Also colliders that are not in `z`
273
+ block a path. A collider structure along a path
274
+ is ``... -> c <- ...`` where ``c`` is the collider node.
275
+
276
+ https://en.wikipedia.org/wiki/Bayesian_network#d-separation
277
+ """
278
+ try:
279
+ x = {x} if x in G else x
280
+ y = {y} if y in G else y
281
+ z = {z} if z in G else z
282
+
283
+ intersection = x & y or x & z or y & z
284
+ if intersection:
285
+ raise nx.NetworkXError(
286
+ f"The sets are not disjoint, with intersection {intersection}"
287
+ )
288
+
289
+ set_v = x | y | z
290
+ if set_v - G.nodes:
291
+ raise nx.NodeNotFound(f"The node(s) {set_v - G.nodes} are not found in G")
292
+ except TypeError:
293
+ raise nx.NodeNotFound("One of x, y, or z is not a node or a set of nodes in G")
294
+
295
+ if not nx.is_directed_acyclic_graph(G):
296
+ raise nx.NetworkXError("graph should be directed acyclic")
297
+
298
+ # contains -> and <-> edges from starting node T
299
+ forward_deque = deque([])
300
+ forward_visited = set()
301
+
302
+ # contains <- and - edges from starting node T
303
+ backward_deque = deque(x)
304
+ backward_visited = set()
305
+
306
+ ancestors_or_z = set().union(*[nx.ancestors(G, node) for node in x]) | z | x
307
+
308
+ while forward_deque or backward_deque:
309
+ if backward_deque:
310
+ node = backward_deque.popleft()
311
+ backward_visited.add(node)
312
+ if node in y:
313
+ return False
314
+ if node in z:
315
+ continue
316
+
317
+ # add <- edges to backward deque
318
+ backward_deque.extend(G.pred[node].keys() - backward_visited)
319
+ # add -> edges to forward deque
320
+ forward_deque.extend(G.succ[node].keys() - forward_visited)
321
+
322
+ if forward_deque:
323
+ node = forward_deque.popleft()
324
+ forward_visited.add(node)
325
+ if node in y:
326
+ return False
327
+
328
+ # Consider if -> node <- is opened due to ancestor of node in z
329
+ if node in ancestors_or_z:
330
+ # add <- edges to backward deque
331
+ backward_deque.extend(G.pred[node].keys() - backward_visited)
332
+ if node not in z:
333
+ # add -> edges to forward deque
334
+ forward_deque.extend(G.succ[node].keys() - forward_visited)
335
+
336
+ return True
337
+
338
+
339
+ @not_implemented_for("undirected")
340
+ @nx._dispatchable
341
+ def find_minimal_d_separator(G, x, y, *, included=None, restricted=None):
342
+ """Returns a minimal d-separating set between `x` and `y` if possible
343
+
344
+ A d-separating set in a DAG is a set of nodes that blocks all
345
+ paths between the two sets of nodes, `x` and `y`. This function
346
+ constructs a d-separating set that is "minimal", meaning no nodes can
347
+ be removed without it losing the d-separating property for `x` and `y`.
348
+ If no d-separating sets exist for `x` and `y`, this returns `None`.
349
+
350
+ In a DAG there may be more than one minimal d-separator between two
351
+ sets of nodes. Minimal d-separators are not always unique. This function
352
+ returns one minimal d-separator, or `None` if no d-separator exists.
353
+
354
+ Uses the algorithm presented in [1]_. The complexity of the algorithm
355
+ is :math:`O(m)`, where :math:`m` stands for the number of edges in
356
+ the subgraph of G consisting of only the ancestors of `x` and `y`.
357
+ For full details, see [1]_.
358
+
359
+ Parameters
360
+ ----------
361
+ G : graph
362
+ A networkx DAG.
363
+ x : set | node
364
+ A node or set of nodes in the graph.
365
+ y : set | node
366
+ A node or set of nodes in the graph.
367
+ included : set | node | None
368
+ A node or set of nodes which must be included in the found separating set,
369
+ default is None, which means the empty set.
370
+ restricted : set | node | None
371
+ Restricted node or set of nodes to consider. Only these nodes can be in
372
+ the found separating set, default is None meaning all nodes in ``G``.
373
+
374
+ Returns
375
+ -------
376
+ z : set | None
377
+ The minimal d-separating set, if at least one d-separating set exists,
378
+ otherwise None.
379
+
380
+ Raises
381
+ ------
382
+ NetworkXError
383
+ Raises a :exc:`NetworkXError` if the input graph is not a DAG
384
+ or if node sets `x`, `y`, and `included` are not disjoint.
385
+
386
+ NodeNotFound
387
+ If any of the input nodes are not found in the graph,
388
+ a :exc:`NodeNotFound` exception is raised.
389
+
390
+ References
391
+ ----------
392
+ .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
393
+ minimal d-separators in linear time and applications." In
394
+ Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
395
+ """
396
+ if not nx.is_directed_acyclic_graph(G):
397
+ raise nx.NetworkXError("graph should be directed acyclic")
398
+
399
+ try:
400
+ x = {x} if x in G else x
401
+ y = {y} if y in G else y
402
+
403
+ if included is None:
404
+ included = set()
405
+ elif included in G:
406
+ included = {included}
407
+
408
+ if restricted is None:
409
+ restricted = set(G)
410
+ elif restricted in G:
411
+ restricted = {restricted}
412
+
413
+ set_y = x | y | included | restricted
414
+ if set_y - G.nodes:
415
+ raise nx.NodeNotFound(f"The node(s) {set_y - G.nodes} are not found in G")
416
+ except TypeError:
417
+ raise nx.NodeNotFound(
418
+ "One of x, y, included or restricted is not a node or set of nodes in G"
419
+ )
420
+
421
+ if not included <= restricted:
422
+ raise nx.NetworkXError(
423
+ f"Included nodes {included} must be in restricted nodes {restricted}"
424
+ )
425
+
426
+ intersection = x & y or x & included or y & included
427
+ if intersection:
428
+ raise nx.NetworkXError(
429
+ f"The sets x, y, included are not disjoint. Overlap: {intersection}"
430
+ )
431
+
432
+ nodeset = x | y | included
433
+ ancestors_x_y_included = nodeset.union(*[nx.ancestors(G, node) for node in nodeset])
434
+
435
+ z_init = restricted & (ancestors_x_y_included - (x | y))
436
+
437
+ x_closure = _reachable(G, x, ancestors_x_y_included, z_init)
438
+ if x_closure & y:
439
+ return None
440
+
441
+ z_updated = z_init & (x_closure | included)
442
+ y_closure = _reachable(G, y, ancestors_x_y_included, z_updated)
443
+ return z_updated & (y_closure | included)
444
+
445
+
446
+ @not_implemented_for("undirected")
447
+ @nx._dispatchable
448
+ def is_minimal_d_separator(G, x, y, z, *, included=None, restricted=None):
449
+ """Determine if `z` is a minimal d-separator for `x` and `y`.
450
+
451
+ A d-separator, `z`, in a DAG is a set of nodes that blocks
452
+ all paths from nodes in set `x` to nodes in set `y`.
453
+ A minimal d-separator is a d-separator `z` such that removing
454
+ any subset of nodes makes it no longer a d-separator.
455
+
456
+ Note: This function checks whether `z` is a d-separator AND is
457
+ minimal. One can use the function `is_d_separator` to only check if
458
+ `z` is a d-separator. See examples below.
459
+
460
+ Parameters
461
+ ----------
462
+ G : nx.DiGraph
463
+ A NetworkX DAG.
464
+ x : node | set
465
+ A node or set of nodes in the graph.
466
+ y : node | set
467
+ A node or set of nodes in the graph.
468
+ z : node | set
469
+ The node or set of nodes to check if it is a minimal d-separating set.
470
+ The function :func:`is_d_separator` is called inside this function
471
+ to verify that `z` is in fact a d-separator.
472
+ included : set | node | None
473
+ A node or set of nodes which must be included in the found separating set,
474
+ default is ``None``, which means the empty set.
475
+ restricted : set | node | None
476
+ Restricted node or set of nodes to consider. Only these nodes can be in
477
+ the found separating set, default is ``None`` meaning all nodes in ``G``.
478
+
479
+ Returns
480
+ -------
481
+ bool
482
+ Whether or not the set `z` is a minimal d-separator subject to
483
+ `restricted` nodes and `included` node constraints.
484
+
485
+ Examples
486
+ --------
487
+ >>> G = nx.path_graph([0, 1, 2, 3], create_using=nx.DiGraph)
488
+ >>> G.add_node(4)
489
+ >>> nx.is_minimal_d_separator(G, 0, 2, {1})
490
+ True
491
+ >>> # since {1} is the minimal d-separator, {1, 3, 4} is not minimal
492
+ >>> nx.is_minimal_d_separator(G, 0, 2, {1, 3, 4})
493
+ False
494
+ >>> # alternatively, if we only want to check that {1, 3, 4} is a d-separator
495
+ >>> nx.is_d_separator(G, 0, 2, {1, 3, 4})
496
+ True
497
+
498
+ Raises
499
+ ------
500
+ NetworkXError
501
+ Raises a :exc:`NetworkXError` if the input graph is not a DAG.
502
+
503
+ NodeNotFound
504
+ If any of the input nodes are not found in the graph,
505
+ a :exc:`NodeNotFound` exception is raised.
506
+
507
+ References
508
+ ----------
509
+ .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
510
+ minimal d-separators in linear time and applications." In
511
+ Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
512
+
513
+ Notes
514
+ -----
515
+ This function works on verifying that a set is minimal and
516
+ d-separating between two nodes. Uses criterion (a), (b), (c) on
517
+ page 4 of [1]_. a) closure(`x`) and `y` are disjoint. b) `z` contains
518
+ all nodes from `included` and is contained in the `restricted`
519
+ nodes and in the union of ancestors of `x`, `y`, and `included`.
520
+ c) the nodes in `z` not in `included` are contained in both
521
+ closure(x) and closure(y). The closure of a set is the set of nodes
522
+ connected to the set by a directed path in G.
523
+
524
+ The complexity is :math:`O(m)`, where :math:`m` stands for the
525
+ number of edges in the subgraph of G consisting of only the
526
+ ancestors of `x` and `y`.
527
+
528
+ For full details, see [1]_.
529
+ """
530
+ if not nx.is_directed_acyclic_graph(G):
531
+ raise nx.NetworkXError("graph should be directed acyclic")
532
+
533
+ try:
534
+ x = {x} if x in G else x
535
+ y = {y} if y in G else y
536
+ z = {z} if z in G else z
537
+
538
+ if included is None:
539
+ included = set()
540
+ elif included in G:
541
+ included = {included}
542
+
543
+ if restricted is None:
544
+ restricted = set(G)
545
+ elif restricted in G:
546
+ restricted = {restricted}
547
+
548
+ set_y = x | y | included | restricted
549
+ if set_y - G.nodes:
550
+ raise nx.NodeNotFound(f"The node(s) {set_y - G.nodes} are not found in G")
551
+ except TypeError:
552
+ raise nx.NodeNotFound(
553
+ "One of x, y, z, included or restricted is not a node or set of nodes in G"
554
+ )
555
+
556
+ if not included <= z:
557
+ raise nx.NetworkXError(
558
+ f"Included nodes {included} must be in proposed separating set z {x}"
559
+ )
560
+ if not z <= restricted:
561
+ raise nx.NetworkXError(
562
+ f"Separating set {z} must be contained in restricted set {restricted}"
563
+ )
564
+
565
+ intersection = x.intersection(y) or x.intersection(z) or y.intersection(z)
566
+ if intersection:
567
+ raise nx.NetworkXError(
568
+ f"The sets are not disjoint, with intersection {intersection}"
569
+ )
570
+
571
+ nodeset = x | y | included
572
+ ancestors_x_y_included = nodeset.union(*[nx.ancestors(G, n) for n in nodeset])
573
+
574
+ # criterion (a) -- check that z is actually a separator
575
+ x_closure = _reachable(G, x, ancestors_x_y_included, z)
576
+ if x_closure & y:
577
+ return False
578
+
579
+ # criterion (b) -- basic constraint; included and restricted already checked above
580
+ if not (z <= ancestors_x_y_included):
581
+ return False
582
+
583
+ # criterion (c) -- check that z is minimal
584
+ y_closure = _reachable(G, y, ancestors_x_y_included, z)
585
+ if not ((z - included) <= (x_closure & y_closure)):
586
+ return False
587
+ return True
588
+
589
+
590
+ @not_implemented_for("undirected")
591
+ def _reachable(G, x, a, z):
592
+ """Modified Bayes-Ball algorithm for finding d-connected nodes.
593
+
594
+ Find all nodes in `a` that are d-connected to those in `x` by
595
+ those in `z`. This is an implementation of the function
596
+ `REACHABLE` in [1]_ (which is itself a modification of the
597
+ Bayes-Ball algorithm [2]_) when restricted to DAGs.
598
+
599
+ Parameters
600
+ ----------
601
+ G : nx.DiGraph
602
+ A NetworkX DAG.
603
+ x : node | set
604
+ A node in the DAG, or a set of nodes.
605
+ a : node | set
606
+ A (set of) node(s) in the DAG containing the ancestors of `x`.
607
+ z : node | set
608
+ The node or set of nodes conditioned on when checking d-connectedness.
609
+
610
+ Returns
611
+ -------
612
+ w : set
613
+ The closure of `x` in `a` with respect to d-connectedness
614
+ given `z`.
615
+
616
+ References
617
+ ----------
618
+ .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
619
+ minimal d-separators in linear time and applications." In
620
+ Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
621
+
622
+ .. [2] Shachter, Ross D. "Bayes-ball: The rational pastime
623
+ (for determining irrelevance and requisite information in
624
+ belief networks and influence diagrams)." In Proceedings of the
625
+ Fourteenth Conference on Uncertainty in Artificial Intelligence
626
+ (UAI), (pp. 480–487). 1998.
627
+ """
628
+
629
+ def _pass(e, v, f, n):
630
+ """Whether a ball entering node `v` along edge `e` passes to `n` along `f`.
631
+
632
+ Boolean function defined on page 6 of [1]_.
633
+
634
+ Parameters
635
+ ----------
636
+ e : bool
637
+ Directed edge by which the ball got to node `v`; `True` iff directed into `v`.
638
+ v : node
639
+ Node where the ball is.
640
+ f : bool
641
+ Directed edge connecting nodes `v` and `n`; `True` iff directed `n`.
642
+ n : node
643
+ Checking whether the ball passes to this node.
644
+
645
+ Returns
646
+ -------
647
+ b : bool
648
+ Whether the ball passes or not.
649
+
650
+ References
651
+ ----------
652
+ .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding
653
+ minimal d-separators in linear time and applications." In
654
+ Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020.
655
+ """
656
+ is_element_of_A = n in a
657
+ # almost_definite_status = True # always true for DAGs; not so for RCGs
658
+ collider_if_in_Z = v not in z or (e and not f)
659
+ return is_element_of_A and collider_if_in_Z # and almost_definite_status
660
+
661
+ queue = deque([])
662
+ for node in x:
663
+ if bool(G.pred[node]):
664
+ queue.append((True, node))
665
+ if bool(G.succ[node]):
666
+ queue.append((False, node))
667
+ processed = queue.copy()
668
+
669
+ while any(queue):
670
+ e, v = queue.popleft()
671
+ preds = ((False, n) for n in G.pred[v])
672
+ succs = ((True, n) for n in G.succ[v])
673
+ f_n_pairs = chain(preds, succs)
674
+ for f, n in f_n_pairs:
675
+ if (f, n) not in processed and _pass(e, v, f, n):
676
+ queue.append((f, n))
677
+ processed.append((f, n))
678
+
679
+ return {w for (_, w) in processed}
680
+
681
+
682
+ # Deprecated functions:
683
+ def d_separated(G, x, y, z):
684
+ """Return whether nodes sets ``x`` and ``y`` are d-separated by ``z``.
685
+
686
+ .. deprecated:: 3.3
687
+
688
+ This function is deprecated and will be removed in NetworkX v3.5.
689
+ Please use `is_d_separator(G, x, y, z)`.
690
+
691
+ """
692
+ import warnings
693
+
694
+ warnings.warn(
695
+ "d_separated is deprecated and will be removed in NetworkX v3.5."
696
+ "Please use `is_d_separator(G, x, y, z)`.",
697
+ category=DeprecationWarning,
698
+ stacklevel=2,
699
+ )
700
+ return nx.is_d_separator(G, x, y, z)
701
+
702
+
703
+ def minimal_d_separator(G, u, v):
704
+ """Returns a minimal_d-separating set between `x` and `y` if possible
705
+
706
+ .. deprecated:: 3.3
707
+
708
+ minimal_d_separator is deprecated and will be removed in NetworkX v3.5.
709
+ Please use `find_minimal_d_separator(G, x, y)`.
710
+
711
+ """
712
+ import warnings
713
+
714
+ warnings.warn(
715
+ (
716
+ "This function is deprecated and will be removed in NetworkX v3.5."
717
+ "Please use `is_d_separator(G, x, y)`."
718
+ ),
719
+ category=DeprecationWarning,
720
+ stacklevel=2,
721
+ )
722
+ return nx.find_minimal_d_separator(G, u, v)
.venv/lib/python3.11/site-packages/networkx/algorithms/dag.py ADDED
@@ -0,0 +1,1418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Algorithms for directed acyclic graphs (DAGs).
2
+
3
+ Note that most of these functions are only guaranteed to work for DAGs.
4
+ In general, these functions do not check for acyclic-ness, so it is up
5
+ to the user to check for that.
6
+ """
7
+
8
+ import heapq
9
+ from collections import deque
10
+ from functools import partial
11
+ from itertools import chain, combinations, product, starmap
12
+ from math import gcd
13
+
14
+ import networkx as nx
15
+ from networkx.utils import arbitrary_element, not_implemented_for, pairwise
16
+
17
+ __all__ = [
18
+ "descendants",
19
+ "ancestors",
20
+ "topological_sort",
21
+ "lexicographical_topological_sort",
22
+ "all_topological_sorts",
23
+ "topological_generations",
24
+ "is_directed_acyclic_graph",
25
+ "is_aperiodic",
26
+ "transitive_closure",
27
+ "transitive_closure_dag",
28
+ "transitive_reduction",
29
+ "antichains",
30
+ "dag_longest_path",
31
+ "dag_longest_path_length",
32
+ "dag_to_branching",
33
+ "compute_v_structures",
34
+ ]
35
+
36
+ chaini = chain.from_iterable
37
+
38
+
39
+ @nx._dispatchable
40
+ def descendants(G, source):
41
+ """Returns all nodes reachable from `source` in `G`.
42
+
43
+ Parameters
44
+ ----------
45
+ G : NetworkX Graph
46
+ source : node in `G`
47
+
48
+ Returns
49
+ -------
50
+ set()
51
+ The descendants of `source` in `G`
52
+
53
+ Raises
54
+ ------
55
+ NetworkXError
56
+ If node `source` is not in `G`.
57
+
58
+ Examples
59
+ --------
60
+ >>> DG = nx.path_graph(5, create_using=nx.DiGraph)
61
+ >>> sorted(nx.descendants(DG, 2))
62
+ [3, 4]
63
+
64
+ The `source` node is not a descendant of itself, but can be included manually:
65
+
66
+ >>> sorted(nx.descendants(DG, 2) | {2})
67
+ [2, 3, 4]
68
+
69
+ See also
70
+ --------
71
+ ancestors
72
+ """
73
+ return {child for parent, child in nx.bfs_edges(G, source)}
74
+
75
+
76
+ @nx._dispatchable
77
+ def ancestors(G, source):
78
+ """Returns all nodes having a path to `source` in `G`.
79
+
80
+ Parameters
81
+ ----------
82
+ G : NetworkX Graph
83
+ source : node in `G`
84
+
85
+ Returns
86
+ -------
87
+ set()
88
+ The ancestors of `source` in `G`
89
+
90
+ Raises
91
+ ------
92
+ NetworkXError
93
+ If node `source` is not in `G`.
94
+
95
+ Examples
96
+ --------
97
+ >>> DG = nx.path_graph(5, create_using=nx.DiGraph)
98
+ >>> sorted(nx.ancestors(DG, 2))
99
+ [0, 1]
100
+
101
+ The `source` node is not an ancestor of itself, but can be included manually:
102
+
103
+ >>> sorted(nx.ancestors(DG, 2) | {2})
104
+ [0, 1, 2]
105
+
106
+ See also
107
+ --------
108
+ descendants
109
+ """
110
+ return {child for parent, child in nx.bfs_edges(G, source, reverse=True)}
111
+
112
+
113
+ @nx._dispatchable
114
+ def has_cycle(G):
115
+ """Decides whether the directed graph has a cycle."""
116
+ try:
117
+ # Feed the entire iterator into a zero-length deque.
118
+ deque(topological_sort(G), maxlen=0)
119
+ except nx.NetworkXUnfeasible:
120
+ return True
121
+ else:
122
+ return False
123
+
124
+
125
+ @nx._dispatchable
126
+ def is_directed_acyclic_graph(G):
127
+ """Returns True if the graph `G` is a directed acyclic graph (DAG) or
128
+ False if not.
129
+
130
+ Parameters
131
+ ----------
132
+ G : NetworkX graph
133
+
134
+ Returns
135
+ -------
136
+ bool
137
+ True if `G` is a DAG, False otherwise
138
+
139
+ Examples
140
+ --------
141
+ Undirected graph::
142
+
143
+ >>> G = nx.Graph([(1, 2), (2, 3)])
144
+ >>> nx.is_directed_acyclic_graph(G)
145
+ False
146
+
147
+ Directed graph with cycle::
148
+
149
+ >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
150
+ >>> nx.is_directed_acyclic_graph(G)
151
+ False
152
+
153
+ Directed acyclic graph::
154
+
155
+ >>> G = nx.DiGraph([(1, 2), (2, 3)])
156
+ >>> nx.is_directed_acyclic_graph(G)
157
+ True
158
+
159
+ See also
160
+ --------
161
+ topological_sort
162
+ """
163
+ return G.is_directed() and not has_cycle(G)
164
+
165
+
166
+ @nx._dispatchable
167
+ def topological_generations(G):
168
+ """Stratifies a DAG into generations.
169
+
170
+ A topological generation is node collection in which ancestors of a node in each
171
+ generation are guaranteed to be in a previous generation, and any descendants of
172
+ a node are guaranteed to be in a following generation. Nodes are guaranteed to
173
+ be in the earliest possible generation that they can belong to.
174
+
175
+ Parameters
176
+ ----------
177
+ G : NetworkX digraph
178
+ A directed acyclic graph (DAG)
179
+
180
+ Yields
181
+ ------
182
+ sets of nodes
183
+ Yields sets of nodes representing each generation.
184
+
185
+ Raises
186
+ ------
187
+ NetworkXError
188
+ Generations are defined for directed graphs only. If the graph
189
+ `G` is undirected, a :exc:`NetworkXError` is raised.
190
+
191
+ NetworkXUnfeasible
192
+ If `G` is not a directed acyclic graph (DAG) no topological generations
193
+ exist and a :exc:`NetworkXUnfeasible` exception is raised. This can also
194
+ be raised if `G` is changed while the returned iterator is being processed
195
+
196
+ RuntimeError
197
+ If `G` is changed while the returned iterator is being processed.
198
+
199
+ Examples
200
+ --------
201
+ >>> DG = nx.DiGraph([(2, 1), (3, 1)])
202
+ >>> [sorted(generation) for generation in nx.topological_generations(DG)]
203
+ [[2, 3], [1]]
204
+
205
+ Notes
206
+ -----
207
+ The generation in which a node resides can also be determined by taking the
208
+ max-path-distance from the node to the farthest leaf node. That value can
209
+ be obtained with this function using `enumerate(topological_generations(G))`.
210
+
211
+ See also
212
+ --------
213
+ topological_sort
214
+ """
215
+ if not G.is_directed():
216
+ raise nx.NetworkXError("Topological sort not defined on undirected graphs.")
217
+
218
+ multigraph = G.is_multigraph()
219
+ indegree_map = {v: d for v, d in G.in_degree() if d > 0}
220
+ zero_indegree = [v for v, d in G.in_degree() if d == 0]
221
+
222
+ while zero_indegree:
223
+ this_generation = zero_indegree
224
+ zero_indegree = []
225
+ for node in this_generation:
226
+ if node not in G:
227
+ raise RuntimeError("Graph changed during iteration")
228
+ for child in G.neighbors(node):
229
+ try:
230
+ indegree_map[child] -= len(G[node][child]) if multigraph else 1
231
+ except KeyError as err:
232
+ raise RuntimeError("Graph changed during iteration") from err
233
+ if indegree_map[child] == 0:
234
+ zero_indegree.append(child)
235
+ del indegree_map[child]
236
+ yield this_generation
237
+
238
+ if indegree_map:
239
+ raise nx.NetworkXUnfeasible(
240
+ "Graph contains a cycle or graph changed during iteration"
241
+ )
242
+
243
+
244
+ @nx._dispatchable
245
+ def topological_sort(G):
246
+ """Returns a generator of nodes in topologically sorted order.
247
+
248
+ A topological sort is a nonunique permutation of the nodes of a
249
+ directed graph such that an edge from u to v implies that u
250
+ appears before v in the topological sort order. This ordering is
251
+ valid only if the graph has no directed cycles.
252
+
253
+ Parameters
254
+ ----------
255
+ G : NetworkX digraph
256
+ A directed acyclic graph (DAG)
257
+
258
+ Yields
259
+ ------
260
+ nodes
261
+ Yields the nodes in topological sorted order.
262
+
263
+ Raises
264
+ ------
265
+ NetworkXError
266
+ Topological sort is defined for directed graphs only. If the graph `G`
267
+ is undirected, a :exc:`NetworkXError` is raised.
268
+
269
+ NetworkXUnfeasible
270
+ If `G` is not a directed acyclic graph (DAG) no topological sort exists
271
+ and a :exc:`NetworkXUnfeasible` exception is raised. This can also be
272
+ raised if `G` is changed while the returned iterator is being processed
273
+
274
+ RuntimeError
275
+ If `G` is changed while the returned iterator is being processed.
276
+
277
+ Examples
278
+ --------
279
+ To get the reverse order of the topological sort:
280
+
281
+ >>> DG = nx.DiGraph([(1, 2), (2, 3)])
282
+ >>> list(reversed(list(nx.topological_sort(DG))))
283
+ [3, 2, 1]
284
+
285
+ If your DiGraph naturally has the edges representing tasks/inputs
286
+ and nodes representing people/processes that initiate tasks, then
287
+ topological_sort is not quite what you need. You will have to change
288
+ the tasks to nodes with dependence reflected by edges. The result is
289
+ a kind of topological sort of the edges. This can be done
290
+ with :func:`networkx.line_graph` as follows:
291
+
292
+ >>> list(nx.topological_sort(nx.line_graph(DG)))
293
+ [(1, 2), (2, 3)]
294
+
295
+ Notes
296
+ -----
297
+ This algorithm is based on a description and proof in
298
+ "Introduction to Algorithms: A Creative Approach" [1]_ .
299
+
300
+ See also
301
+ --------
302
+ is_directed_acyclic_graph, lexicographical_topological_sort
303
+
304
+ References
305
+ ----------
306
+ .. [1] Manber, U. (1989).
307
+ *Introduction to Algorithms - A Creative Approach.* Addison-Wesley.
308
+ """
309
+ for generation in nx.topological_generations(G):
310
+ yield from generation
311
+
312
+
313
+ @nx._dispatchable
314
+ def lexicographical_topological_sort(G, key=None):
315
+ """Generate the nodes in the unique lexicographical topological sort order.
316
+
317
+ Generates a unique ordering of nodes by first sorting topologically (for which there are often
318
+ multiple valid orderings) and then additionally by sorting lexicographically.
319
+
320
+ A topological sort arranges the nodes of a directed graph so that the
321
+ upstream node of each directed edge precedes the downstream node.
322
+ It is always possible to find a solution for directed graphs that have no cycles.
323
+ There may be more than one valid solution.
324
+
325
+ Lexicographical sorting is just sorting alphabetically. It is used here to break ties in the
326
+ topological sort and to determine a single, unique ordering. This can be useful in comparing
327
+ sort results.
328
+
329
+ The lexicographical order can be customized by providing a function to the `key=` parameter.
330
+ The definition of the key function is the same as used in python's built-in `sort()`.
331
+ The function takes a single argument and returns a key to use for sorting purposes.
332
+
333
+ Lexicographical sorting can fail if the node names are un-sortable. See the example below.
334
+ The solution is to provide a function to the `key=` argument that returns sortable keys.
335
+
336
+
337
+ Parameters
338
+ ----------
339
+ G : NetworkX digraph
340
+ A directed acyclic graph (DAG)
341
+
342
+ key : function, optional
343
+ A function of one argument that converts a node name to a comparison key.
344
+ It defines and resolves ambiguities in the sort order. Defaults to the identity function.
345
+
346
+ Yields
347
+ ------
348
+ nodes
349
+ Yields the nodes of G in lexicographical topological sort order.
350
+
351
+ Raises
352
+ ------
353
+ NetworkXError
354
+ Topological sort is defined for directed graphs only. If the graph `G`
355
+ is undirected, a :exc:`NetworkXError` is raised.
356
+
357
+ NetworkXUnfeasible
358
+ If `G` is not a directed acyclic graph (DAG) no topological sort exists
359
+ and a :exc:`NetworkXUnfeasible` exception is raised. This can also be
360
+ raised if `G` is changed while the returned iterator is being processed
361
+
362
+ RuntimeError
363
+ If `G` is changed while the returned iterator is being processed.
364
+
365
+ TypeError
366
+ Results from un-sortable node names.
367
+ Consider using `key=` parameter to resolve ambiguities in the sort order.
368
+
369
+ Examples
370
+ --------
371
+ >>> DG = nx.DiGraph([(2, 1), (2, 5), (1, 3), (1, 4), (5, 4)])
372
+ >>> list(nx.lexicographical_topological_sort(DG))
373
+ [2, 1, 3, 5, 4]
374
+ >>> list(nx.lexicographical_topological_sort(DG, key=lambda x: -x))
375
+ [2, 5, 1, 4, 3]
376
+
377
+ The sort will fail for any graph with integer and string nodes. Comparison of integer to strings
378
+ is not defined in python. Is 3 greater or less than 'red'?
379
+
380
+ >>> DG = nx.DiGraph([(1, "red"), (3, "red"), (1, "green"), (2, "blue")])
381
+ >>> list(nx.lexicographical_topological_sort(DG))
382
+ Traceback (most recent call last):
383
+ ...
384
+ TypeError: '<' not supported between instances of 'str' and 'int'
385
+ ...
386
+
387
+ Incomparable nodes can be resolved using a `key` function. This example function
388
+ allows comparison of integers and strings by returning a tuple where the first
389
+ element is True for `str`, False otherwise. The second element is the node name.
390
+ This groups the strings and integers separately so they can be compared only among themselves.
391
+
392
+ >>> key = lambda node: (isinstance(node, str), node)
393
+ >>> list(nx.lexicographical_topological_sort(DG, key=key))
394
+ [1, 2, 3, 'blue', 'green', 'red']
395
+
396
+ Notes
397
+ -----
398
+ This algorithm is based on a description and proof in
399
+ "Introduction to Algorithms: A Creative Approach" [1]_ .
400
+
401
+ See also
402
+ --------
403
+ topological_sort
404
+
405
+ References
406
+ ----------
407
+ .. [1] Manber, U. (1989).
408
+ *Introduction to Algorithms - A Creative Approach.* Addison-Wesley.
409
+ """
410
+ if not G.is_directed():
411
+ msg = "Topological sort not defined on undirected graphs."
412
+ raise nx.NetworkXError(msg)
413
+
414
+ if key is None:
415
+
416
+ def key(node):
417
+ return node
418
+
419
+ nodeid_map = {n: i for i, n in enumerate(G)}
420
+
421
+ def create_tuple(node):
422
+ return key(node), nodeid_map[node], node
423
+
424
+ indegree_map = {v: d for v, d in G.in_degree() if d > 0}
425
+ # These nodes have zero indegree and ready to be returned.
426
+ zero_indegree = [create_tuple(v) for v, d in G.in_degree() if d == 0]
427
+ heapq.heapify(zero_indegree)
428
+
429
+ while zero_indegree:
430
+ _, _, node = heapq.heappop(zero_indegree)
431
+
432
+ if node not in G:
433
+ raise RuntimeError("Graph changed during iteration")
434
+ for _, child in G.edges(node):
435
+ try:
436
+ indegree_map[child] -= 1
437
+ except KeyError as err:
438
+ raise RuntimeError("Graph changed during iteration") from err
439
+ if indegree_map[child] == 0:
440
+ try:
441
+ heapq.heappush(zero_indegree, create_tuple(child))
442
+ except TypeError as err:
443
+ raise TypeError(
444
+ f"{err}\nConsider using `key=` parameter to resolve ambiguities in the sort order."
445
+ )
446
+ del indegree_map[child]
447
+
448
+ yield node
449
+
450
+ if indegree_map:
451
+ msg = "Graph contains a cycle or graph changed during iteration"
452
+ raise nx.NetworkXUnfeasible(msg)
453
+
454
+
455
+ @not_implemented_for("undirected")
456
+ @nx._dispatchable
457
+ def all_topological_sorts(G):
458
+ """Returns a generator of _all_ topological sorts of the directed graph G.
459
+
460
+ A topological sort is a nonunique permutation of the nodes such that an
461
+ edge from u to v implies that u appears before v in the topological sort
462
+ order.
463
+
464
+ Parameters
465
+ ----------
466
+ G : NetworkX DiGraph
467
+ A directed graph
468
+
469
+ Yields
470
+ ------
471
+ topological_sort_order : list
472
+ a list of nodes in `G`, representing one of the topological sort orders
473
+
474
+ Raises
475
+ ------
476
+ NetworkXNotImplemented
477
+ If `G` is not directed
478
+ NetworkXUnfeasible
479
+ If `G` is not acyclic
480
+
481
+ Examples
482
+ --------
483
+ To enumerate all topological sorts of directed graph:
484
+
485
+ >>> DG = nx.DiGraph([(1, 2), (2, 3), (2, 4)])
486
+ >>> list(nx.all_topological_sorts(DG))
487
+ [[1, 2, 4, 3], [1, 2, 3, 4]]
488
+
489
+ Notes
490
+ -----
491
+ Implements an iterative version of the algorithm given in [1].
492
+
493
+ References
494
+ ----------
495
+ .. [1] Knuth, Donald E., Szwarcfiter, Jayme L. (1974).
496
+ "A Structured Program to Generate All Topological Sorting Arrangements"
497
+ Information Processing Letters, Volume 2, Issue 6, 1974, Pages 153-157,
498
+ ISSN 0020-0190,
499
+ https://doi.org/10.1016/0020-0190(74)90001-5.
500
+ Elsevier (North-Holland), Amsterdam
501
+ """
502
+ if not G.is_directed():
503
+ raise nx.NetworkXError("Topological sort not defined on undirected graphs.")
504
+
505
+ # the names of count and D are chosen to match the global variables in [1]
506
+ # number of edges originating in a vertex v
507
+ count = dict(G.in_degree())
508
+ # vertices with indegree 0
509
+ D = deque([v for v, d in G.in_degree() if d == 0])
510
+ # stack of first value chosen at a position k in the topological sort
511
+ bases = []
512
+ current_sort = []
513
+
514
+ # do-while construct
515
+ while True:
516
+ assert all(count[v] == 0 for v in D)
517
+
518
+ if len(current_sort) == len(G):
519
+ yield list(current_sort)
520
+
521
+ # clean-up stack
522
+ while len(current_sort) > 0:
523
+ assert len(bases) == len(current_sort)
524
+ q = current_sort.pop()
525
+
526
+ # "restores" all edges (q, x)
527
+ # NOTE: it is important to iterate over edges instead
528
+ # of successors, so count is updated correctly in multigraphs
529
+ for _, j in G.out_edges(q):
530
+ count[j] += 1
531
+ assert count[j] >= 0
532
+ # remove entries from D
533
+ while len(D) > 0 and count[D[-1]] > 0:
534
+ D.pop()
535
+
536
+ # corresponds to a circular shift of the values in D
537
+ # if the first value chosen (the base) is in the first
538
+ # position of D again, we are done and need to consider the
539
+ # previous condition
540
+ D.appendleft(q)
541
+ if D[-1] == bases[-1]:
542
+ # all possible values have been chosen at current position
543
+ # remove corresponding marker
544
+ bases.pop()
545
+ else:
546
+ # there are still elements that have not been fixed
547
+ # at the current position in the topological sort
548
+ # stop removing elements, escape inner loop
549
+ break
550
+
551
+ else:
552
+ if len(D) == 0:
553
+ raise nx.NetworkXUnfeasible("Graph contains a cycle.")
554
+
555
+ # choose next node
556
+ q = D.pop()
557
+ # "erase" all edges (q, x)
558
+ # NOTE: it is important to iterate over edges instead
559
+ # of successors, so count is updated correctly in multigraphs
560
+ for _, j in G.out_edges(q):
561
+ count[j] -= 1
562
+ assert count[j] >= 0
563
+ if count[j] == 0:
564
+ D.append(j)
565
+ current_sort.append(q)
566
+
567
+ # base for current position might _not_ be fixed yet
568
+ if len(bases) < len(current_sort):
569
+ bases.append(q)
570
+
571
+ if len(bases) == 0:
572
+ break
573
+
574
+
575
+ @nx._dispatchable
576
+ def is_aperiodic(G):
577
+ """Returns True if `G` is aperiodic.
578
+
579
+ A directed graph is aperiodic if there is no integer k > 1 that
580
+ divides the length of every cycle in the graph.
581
+
582
+ Parameters
583
+ ----------
584
+ G : NetworkX DiGraph
585
+ A directed graph
586
+
587
+ Returns
588
+ -------
589
+ bool
590
+ True if the graph is aperiodic False otherwise
591
+
592
+ Raises
593
+ ------
594
+ NetworkXError
595
+ If `G` is not directed
596
+
597
+ Examples
598
+ --------
599
+ A graph consisting of one cycle, the length of which is 2. Therefore ``k = 2``
600
+ divides the length of every cycle in the graph and thus the graph
601
+ is *not aperiodic*::
602
+
603
+ >>> DG = nx.DiGraph([(1, 2), (2, 1)])
604
+ >>> nx.is_aperiodic(DG)
605
+ False
606
+
607
+ A graph consisting of two cycles: one of length 2 and the other of length 3.
608
+ The cycle lengths are coprime, so there is no single value of k where ``k > 1``
609
+ that divides each cycle length and therefore the graph is *aperiodic*::
610
+
611
+ >>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1), (1, 4), (4, 1)])
612
+ >>> nx.is_aperiodic(DG)
613
+ True
614
+
615
+ A graph consisting of two cycles: one of length 2 and the other of length 4.
616
+ The lengths of the cycles share a common factor ``k = 2``, and therefore
617
+ the graph is *not aperiodic*::
618
+
619
+ >>> DG = nx.DiGraph([(1, 2), (2, 1), (3, 4), (4, 5), (5, 6), (6, 3)])
620
+ >>> nx.is_aperiodic(DG)
621
+ False
622
+
623
+ An acyclic graph, therefore the graph is *not aperiodic*::
624
+
625
+ >>> DG = nx.DiGraph([(1, 2), (2, 3)])
626
+ >>> nx.is_aperiodic(DG)
627
+ False
628
+
629
+ Notes
630
+ -----
631
+ This uses the method outlined in [1]_, which runs in $O(m)$ time
632
+ given $m$ edges in `G`. Note that a graph is not aperiodic if it is
633
+ acyclic as every integer trivial divides length 0 cycles.
634
+
635
+ References
636
+ ----------
637
+ .. [1] Jarvis, J. P.; Shier, D. R. (1996),
638
+ "Graph-theoretic analysis of finite Markov chains,"
639
+ in Shier, D. R.; Wallenius, K. T., Applied Mathematical Modeling:
640
+ A Multidisciplinary Approach, CRC Press.
641
+ """
642
+ if not G.is_directed():
643
+ raise nx.NetworkXError("is_aperiodic not defined for undirected graphs")
644
+ if len(G) == 0:
645
+ raise nx.NetworkXPointlessConcept("Graph has no nodes.")
646
+ s = arbitrary_element(G)
647
+ levels = {s: 0}
648
+ this_level = [s]
649
+ g = 0
650
+ lev = 1
651
+ while this_level:
652
+ next_level = []
653
+ for u in this_level:
654
+ for v in G[u]:
655
+ if v in levels: # Non-Tree Edge
656
+ g = gcd(g, levels[u] - levels[v] + 1)
657
+ else: # Tree Edge
658
+ next_level.append(v)
659
+ levels[v] = lev
660
+ this_level = next_level
661
+ lev += 1
662
+ if len(levels) == len(G): # All nodes in tree
663
+ return g == 1
664
+ else:
665
+ return g == 1 and nx.is_aperiodic(G.subgraph(set(G) - set(levels)))
666
+
667
+
668
+ @nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
669
+ def transitive_closure(G, reflexive=False):
670
+ """Returns transitive closure of a graph
671
+
672
+ The transitive closure of G = (V,E) is a graph G+ = (V,E+) such that
673
+ for all v, w in V there is an edge (v, w) in E+ if and only if there
674
+ is a path from v to w in G.
675
+
676
+ Handling of paths from v to v has some flexibility within this definition.
677
+ A reflexive transitive closure creates a self-loop for the path
678
+ from v to v of length 0. The usual transitive closure creates a
679
+ self-loop only if a cycle exists (a path from v to v with length > 0).
680
+ We also allow an option for no self-loops.
681
+
682
+ Parameters
683
+ ----------
684
+ G : NetworkX Graph
685
+ A directed/undirected graph/multigraph.
686
+ reflexive : Bool or None, optional (default: False)
687
+ Determines when cycles create self-loops in the Transitive Closure.
688
+ If True, trivial cycles (length 0) create self-loops. The result
689
+ is a reflexive transitive closure of G.
690
+ If False (the default) non-trivial cycles create self-loops.
691
+ If None, self-loops are not created.
692
+
693
+ Returns
694
+ -------
695
+ NetworkX graph
696
+ The transitive closure of `G`
697
+
698
+ Raises
699
+ ------
700
+ NetworkXError
701
+ If `reflexive` not in `{None, True, False}`
702
+
703
+ Examples
704
+ --------
705
+ The treatment of trivial (i.e. length 0) cycles is controlled by the
706
+ `reflexive` parameter.
707
+
708
+ Trivial (i.e. length 0) cycles do not create self-loops when
709
+ ``reflexive=False`` (the default)::
710
+
711
+ >>> DG = nx.DiGraph([(1, 2), (2, 3)])
712
+ >>> TC = nx.transitive_closure(DG, reflexive=False)
713
+ >>> TC.edges()
714
+ OutEdgeView([(1, 2), (1, 3), (2, 3)])
715
+
716
+ However, nontrivial (i.e. length greater than 0) cycles create self-loops
717
+ when ``reflexive=False`` (the default)::
718
+
719
+ >>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
720
+ >>> TC = nx.transitive_closure(DG, reflexive=False)
721
+ >>> TC.edges()
722
+ OutEdgeView([(1, 2), (1, 3), (1, 1), (2, 3), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3)])
723
+
724
+ Trivial cycles (length 0) create self-loops when ``reflexive=True``::
725
+
726
+ >>> DG = nx.DiGraph([(1, 2), (2, 3)])
727
+ >>> TC = nx.transitive_closure(DG, reflexive=True)
728
+ >>> TC.edges()
729
+ OutEdgeView([(1, 2), (1, 1), (1, 3), (2, 3), (2, 2), (3, 3)])
730
+
731
+ And the third option is not to create self-loops at all when ``reflexive=None``::
732
+
733
+ >>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
734
+ >>> TC = nx.transitive_closure(DG, reflexive=None)
735
+ >>> TC.edges()
736
+ OutEdgeView([(1, 2), (1, 3), (2, 3), (2, 1), (3, 1), (3, 2)])
737
+
738
+ References
739
+ ----------
740
+ .. [1] https://www.ics.uci.edu/~eppstein/PADS/PartialOrder.py
741
+ """
742
+ TC = G.copy()
743
+
744
+ if reflexive not in {None, True, False}:
745
+ raise nx.NetworkXError("Incorrect value for the parameter `reflexive`")
746
+
747
+ for v in G:
748
+ if reflexive is None:
749
+ TC.add_edges_from((v, u) for u in nx.descendants(G, v) if u not in TC[v])
750
+ elif reflexive is True:
751
+ TC.add_edges_from(
752
+ (v, u) for u in nx.descendants(G, v) | {v} if u not in TC[v]
753
+ )
754
+ elif reflexive is False:
755
+ TC.add_edges_from((v, e[1]) for e in nx.edge_bfs(G, v) if e[1] not in TC[v])
756
+
757
+ return TC
758
+
759
+
760
+ @not_implemented_for("undirected")
761
+ @nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
762
+ def transitive_closure_dag(G, topo_order=None):
763
+ """Returns the transitive closure of a directed acyclic graph.
764
+
765
+ This function is faster than the function `transitive_closure`, but fails
766
+ if the graph has a cycle.
767
+
768
+ The transitive closure of G = (V,E) is a graph G+ = (V,E+) such that
769
+ for all v, w in V there is an edge (v, w) in E+ if and only if there
770
+ is a non-null path from v to w in G.
771
+
772
+ Parameters
773
+ ----------
774
+ G : NetworkX DiGraph
775
+ A directed acyclic graph (DAG)
776
+
777
+ topo_order: list or tuple, optional
778
+ A topological order for G (if None, the function will compute one)
779
+
780
+ Returns
781
+ -------
782
+ NetworkX DiGraph
783
+ The transitive closure of `G`
784
+
785
+ Raises
786
+ ------
787
+ NetworkXNotImplemented
788
+ If `G` is not directed
789
+ NetworkXUnfeasible
790
+ If `G` has a cycle
791
+
792
+ Examples
793
+ --------
794
+ >>> DG = nx.DiGraph([(1, 2), (2, 3)])
795
+ >>> TC = nx.transitive_closure_dag(DG)
796
+ >>> TC.edges()
797
+ OutEdgeView([(1, 2), (1, 3), (2, 3)])
798
+
799
+ Notes
800
+ -----
801
+ This algorithm is probably simple enough to be well-known but I didn't find
802
+ a mention in the literature.
803
+ """
804
+ if topo_order is None:
805
+ topo_order = list(topological_sort(G))
806
+
807
+ TC = G.copy()
808
+
809
+ # idea: traverse vertices following a reverse topological order, connecting
810
+ # each vertex to its descendants at distance 2 as we go
811
+ for v in reversed(topo_order):
812
+ TC.add_edges_from((v, u) for u in nx.descendants_at_distance(TC, v, 2))
813
+
814
+ return TC
815
+
816
+
817
+ @not_implemented_for("undirected")
818
+ @nx._dispatchable(returns_graph=True)
819
+ def transitive_reduction(G):
820
+ """Returns transitive reduction of a directed graph
821
+
822
+ The transitive reduction of G = (V,E) is a graph G- = (V,E-) such that
823
+ for all v,w in V there is an edge (v,w) in E- if and only if (v,w) is
824
+ in E and there is no path from v to w in G with length greater than 1.
825
+
826
+ Parameters
827
+ ----------
828
+ G : NetworkX DiGraph
829
+ A directed acyclic graph (DAG)
830
+
831
+ Returns
832
+ -------
833
+ NetworkX DiGraph
834
+ The transitive reduction of `G`
835
+
836
+ Raises
837
+ ------
838
+ NetworkXError
839
+ If `G` is not a directed acyclic graph (DAG) transitive reduction is
840
+ not uniquely defined and a :exc:`NetworkXError` exception is raised.
841
+
842
+ Examples
843
+ --------
844
+ To perform transitive reduction on a DiGraph:
845
+
846
+ >>> DG = nx.DiGraph([(1, 2), (2, 3), (1, 3)])
847
+ >>> TR = nx.transitive_reduction(DG)
848
+ >>> list(TR.edges)
849
+ [(1, 2), (2, 3)]
850
+
851
+ To avoid unnecessary data copies, this implementation does not return a
852
+ DiGraph with node/edge data.
853
+ To perform transitive reduction on a DiGraph and transfer node/edge data:
854
+
855
+ >>> DG = nx.DiGraph()
856
+ >>> DG.add_edges_from([(1, 2), (2, 3), (1, 3)], color="red")
857
+ >>> TR = nx.transitive_reduction(DG)
858
+ >>> TR.add_nodes_from(DG.nodes(data=True))
859
+ >>> TR.add_edges_from((u, v, DG.edges[u, v]) for u, v in TR.edges)
860
+ >>> list(TR.edges(data=True))
861
+ [(1, 2, {'color': 'red'}), (2, 3, {'color': 'red'})]
862
+
863
+ References
864
+ ----------
865
+ https://en.wikipedia.org/wiki/Transitive_reduction
866
+
867
+ """
868
+ if not is_directed_acyclic_graph(G):
869
+ msg = "Directed Acyclic Graph required for transitive_reduction"
870
+ raise nx.NetworkXError(msg)
871
+ TR = nx.DiGraph()
872
+ TR.add_nodes_from(G.nodes())
873
+ descendants = {}
874
+ # count before removing set stored in descendants
875
+ check_count = dict(G.in_degree)
876
+ for u in G:
877
+ u_nbrs = set(G[u])
878
+ for v in G[u]:
879
+ if v in u_nbrs:
880
+ if v not in descendants:
881
+ descendants[v] = {y for x, y in nx.dfs_edges(G, v)}
882
+ u_nbrs -= descendants[v]
883
+ check_count[v] -= 1
884
+ if check_count[v] == 0:
885
+ del descendants[v]
886
+ TR.add_edges_from((u, v) for v in u_nbrs)
887
+ return TR
888
+
889
+
890
+ @not_implemented_for("undirected")
891
+ @nx._dispatchable
892
+ def antichains(G, topo_order=None):
893
+ """Generates antichains from a directed acyclic graph (DAG).
894
+
895
+ An antichain is a subset of a partially ordered set such that any
896
+ two elements in the subset are incomparable.
897
+
898
+ Parameters
899
+ ----------
900
+ G : NetworkX DiGraph
901
+ A directed acyclic graph (DAG)
902
+
903
+ topo_order: list or tuple, optional
904
+ A topological order for G (if None, the function will compute one)
905
+
906
+ Yields
907
+ ------
908
+ antichain : list
909
+ a list of nodes in `G` representing an antichain
910
+
911
+ Raises
912
+ ------
913
+ NetworkXNotImplemented
914
+ If `G` is not directed
915
+
916
+ NetworkXUnfeasible
917
+ If `G` contains a cycle
918
+
919
+ Examples
920
+ --------
921
+ >>> DG = nx.DiGraph([(1, 2), (1, 3)])
922
+ >>> list(nx.antichains(DG))
923
+ [[], [3], [2], [2, 3], [1]]
924
+
925
+ Notes
926
+ -----
927
+ This function was originally developed by Peter Jipsen and Franco Saliola
928
+ for the SAGE project. It's included in NetworkX with permission from the
929
+ authors. Original SAGE code at:
930
+
931
+ https://github.com/sagemath/sage/blob/master/src/sage/combinat/posets/hasse_diagram.py
932
+
933
+ References
934
+ ----------
935
+ .. [1] Free Lattices, by R. Freese, J. Jezek and J. B. Nation,
936
+ AMS, Vol 42, 1995, p. 226.
937
+ """
938
+ if topo_order is None:
939
+ topo_order = list(nx.topological_sort(G))
940
+
941
+ TC = nx.transitive_closure_dag(G, topo_order)
942
+ antichains_stacks = [([], list(reversed(topo_order)))]
943
+
944
+ while antichains_stacks:
945
+ (antichain, stack) = antichains_stacks.pop()
946
+ # Invariant:
947
+ # - the elements of antichain are independent
948
+ # - the elements of stack are independent from those of antichain
949
+ yield antichain
950
+ while stack:
951
+ x = stack.pop()
952
+ new_antichain = antichain + [x]
953
+ new_stack = [t for t in stack if not ((t in TC[x]) or (x in TC[t]))]
954
+ antichains_stacks.append((new_antichain, new_stack))
955
+
956
+
957
+ @not_implemented_for("undirected")
958
+ @nx._dispatchable(edge_attrs={"weight": "default_weight"})
959
+ def dag_longest_path(G, weight="weight", default_weight=1, topo_order=None):
960
+ """Returns the longest path in a directed acyclic graph (DAG).
961
+
962
+ If `G` has edges with `weight` attribute the edge data are used as
963
+ weight values.
964
+
965
+ Parameters
966
+ ----------
967
+ G : NetworkX DiGraph
968
+ A directed acyclic graph (DAG)
969
+
970
+ weight : str, optional
971
+ Edge data key to use for weight
972
+
973
+ default_weight : int, optional
974
+ The weight of edges that do not have a weight attribute
975
+
976
+ topo_order: list or tuple, optional
977
+ A topological order for `G` (if None, the function will compute one)
978
+
979
+ Returns
980
+ -------
981
+ list
982
+ Longest path
983
+
984
+ Raises
985
+ ------
986
+ NetworkXNotImplemented
987
+ If `G` is not directed
988
+
989
+ Examples
990
+ --------
991
+ >>> DG = nx.DiGraph(
992
+ ... [(0, 1, {"cost": 1}), (1, 2, {"cost": 1}), (0, 2, {"cost": 42})]
993
+ ... )
994
+ >>> list(nx.all_simple_paths(DG, 0, 2))
995
+ [[0, 1, 2], [0, 2]]
996
+ >>> nx.dag_longest_path(DG)
997
+ [0, 1, 2]
998
+ >>> nx.dag_longest_path(DG, weight="cost")
999
+ [0, 2]
1000
+
1001
+ In the case where multiple valid topological orderings exist, `topo_order`
1002
+ can be used to specify a specific ordering:
1003
+
1004
+ >>> DG = nx.DiGraph([(0, 1), (0, 2)])
1005
+ >>> sorted(nx.all_topological_sorts(DG)) # Valid topological orderings
1006
+ [[0, 1, 2], [0, 2, 1]]
1007
+ >>> nx.dag_longest_path(DG, topo_order=[0, 1, 2])
1008
+ [0, 1]
1009
+ >>> nx.dag_longest_path(DG, topo_order=[0, 2, 1])
1010
+ [0, 2]
1011
+
1012
+ See also
1013
+ --------
1014
+ dag_longest_path_length
1015
+
1016
+ """
1017
+ if not G:
1018
+ return []
1019
+
1020
+ if topo_order is None:
1021
+ topo_order = nx.topological_sort(G)
1022
+
1023
+ dist = {} # stores {v : (length, u)}
1024
+ for v in topo_order:
1025
+ us = [
1026
+ (
1027
+ dist[u][0]
1028
+ + (
1029
+ max(data.values(), key=lambda x: x.get(weight, default_weight))
1030
+ if G.is_multigraph()
1031
+ else data
1032
+ ).get(weight, default_weight),
1033
+ u,
1034
+ )
1035
+ for u, data in G.pred[v].items()
1036
+ ]
1037
+
1038
+ # Use the best predecessor if there is one and its distance is
1039
+ # non-negative, otherwise terminate.
1040
+ maxu = max(us, key=lambda x: x[0]) if us else (0, v)
1041
+ dist[v] = maxu if maxu[0] >= 0 else (0, v)
1042
+
1043
+ u = None
1044
+ v = max(dist, key=lambda x: dist[x][0])
1045
+ path = []
1046
+ while u != v:
1047
+ path.append(v)
1048
+ u = v
1049
+ v = dist[v][1]
1050
+
1051
+ path.reverse()
1052
+ return path
1053
+
1054
+
1055
+ @not_implemented_for("undirected")
1056
+ @nx._dispatchable(edge_attrs={"weight": "default_weight"})
1057
+ def dag_longest_path_length(G, weight="weight", default_weight=1):
1058
+ """Returns the longest path length in a DAG
1059
+
1060
+ Parameters
1061
+ ----------
1062
+ G : NetworkX DiGraph
1063
+ A directed acyclic graph (DAG)
1064
+
1065
+ weight : string, optional
1066
+ Edge data key to use for weight
1067
+
1068
+ default_weight : int, optional
1069
+ The weight of edges that do not have a weight attribute
1070
+
1071
+ Returns
1072
+ -------
1073
+ int
1074
+ Longest path length
1075
+
1076
+ Raises
1077
+ ------
1078
+ NetworkXNotImplemented
1079
+ If `G` is not directed
1080
+
1081
+ Examples
1082
+ --------
1083
+ >>> DG = nx.DiGraph(
1084
+ ... [(0, 1, {"cost": 1}), (1, 2, {"cost": 1}), (0, 2, {"cost": 42})]
1085
+ ... )
1086
+ >>> list(nx.all_simple_paths(DG, 0, 2))
1087
+ [[0, 1, 2], [0, 2]]
1088
+ >>> nx.dag_longest_path_length(DG)
1089
+ 2
1090
+ >>> nx.dag_longest_path_length(DG, weight="cost")
1091
+ 42
1092
+
1093
+ See also
1094
+ --------
1095
+ dag_longest_path
1096
+ """
1097
+ path = nx.dag_longest_path(G, weight, default_weight)
1098
+ path_length = 0
1099
+ if G.is_multigraph():
1100
+ for u, v in pairwise(path):
1101
+ i = max(G[u][v], key=lambda x: G[u][v][x].get(weight, default_weight))
1102
+ path_length += G[u][v][i].get(weight, default_weight)
1103
+ else:
1104
+ for u, v in pairwise(path):
1105
+ path_length += G[u][v].get(weight, default_weight)
1106
+
1107
+ return path_length
1108
+
1109
+
1110
+ @nx._dispatchable
1111
+ def root_to_leaf_paths(G):
1112
+ """Yields root-to-leaf paths in a directed acyclic graph.
1113
+
1114
+ `G` must be a directed acyclic graph. If not, the behavior of this
1115
+ function is undefined. A "root" in this graph is a node of in-degree
1116
+ zero and a "leaf" a node of out-degree zero.
1117
+
1118
+ When invoked, this function iterates over each path from any root to
1119
+ any leaf. A path is a list of nodes.
1120
+
1121
+ """
1122
+ roots = (v for v, d in G.in_degree() if d == 0)
1123
+ leaves = (v for v, d in G.out_degree() if d == 0)
1124
+ all_paths = partial(nx.all_simple_paths, G)
1125
+ # TODO In Python 3, this would be better as `yield from ...`.
1126
+ return chaini(starmap(all_paths, product(roots, leaves)))
1127
+
1128
+
1129
+ @not_implemented_for("multigraph")
1130
+ @not_implemented_for("undirected")
1131
+ @nx._dispatchable(returns_graph=True)
1132
+ def dag_to_branching(G):
1133
+ """Returns a branching representing all (overlapping) paths from
1134
+ root nodes to leaf nodes in the given directed acyclic graph.
1135
+
1136
+ As described in :mod:`networkx.algorithms.tree.recognition`, a
1137
+ *branching* is a directed forest in which each node has at most one
1138
+ parent. In other words, a branching is a disjoint union of
1139
+ *arborescences*. For this function, each node of in-degree zero in
1140
+ `G` becomes a root of one of the arborescences, and there will be
1141
+ one leaf node for each distinct path from that root to a leaf node
1142
+ in `G`.
1143
+
1144
+ Each node `v` in `G` with *k* parents becomes *k* distinct nodes in
1145
+ the returned branching, one for each parent, and the sub-DAG rooted
1146
+ at `v` is duplicated for each copy. The algorithm then recurses on
1147
+ the children of each copy of `v`.
1148
+
1149
+ Parameters
1150
+ ----------
1151
+ G : NetworkX graph
1152
+ A directed acyclic graph.
1153
+
1154
+ Returns
1155
+ -------
1156
+ DiGraph
1157
+ The branching in which there is a bijection between root-to-leaf
1158
+ paths in `G` (in which multiple paths may share the same leaf)
1159
+ and root-to-leaf paths in the branching (in which there is a
1160
+ unique path from a root to a leaf).
1161
+
1162
+ Each node has an attribute 'source' whose value is the original
1163
+ node to which this node corresponds. No other graph, node, or
1164
+ edge attributes are copied into this new graph.
1165
+
1166
+ Raises
1167
+ ------
1168
+ NetworkXNotImplemented
1169
+ If `G` is not directed, or if `G` is a multigraph.
1170
+
1171
+ HasACycle
1172
+ If `G` is not acyclic.
1173
+
1174
+ Examples
1175
+ --------
1176
+ To examine which nodes in the returned branching were produced by
1177
+ which original node in the directed acyclic graph, we can collect
1178
+ the mapping from source node to new nodes into a dictionary. For
1179
+ example, consider the directed diamond graph::
1180
+
1181
+ >>> from collections import defaultdict
1182
+ >>> from operator import itemgetter
1183
+ >>>
1184
+ >>> G = nx.DiGraph(nx.utils.pairwise("abd"))
1185
+ >>> G.add_edges_from(nx.utils.pairwise("acd"))
1186
+ >>> B = nx.dag_to_branching(G)
1187
+ >>>
1188
+ >>> sources = defaultdict(set)
1189
+ >>> for v, source in B.nodes(data="source"):
1190
+ ... sources[source].add(v)
1191
+ >>> len(sources["a"])
1192
+ 1
1193
+ >>> len(sources["d"])
1194
+ 2
1195
+
1196
+ To copy node attributes from the original graph to the new graph,
1197
+ you can use a dictionary like the one constructed in the above
1198
+ example::
1199
+
1200
+ >>> for source, nodes in sources.items():
1201
+ ... for v in nodes:
1202
+ ... B.nodes[v].update(G.nodes[source])
1203
+
1204
+ Notes
1205
+ -----
1206
+ This function is not idempotent in the sense that the node labels in
1207
+ the returned branching may be uniquely generated each time the
1208
+ function is invoked. In fact, the node labels may not be integers;
1209
+ in order to relabel the nodes to be more readable, you can use the
1210
+ :func:`networkx.convert_node_labels_to_integers` function.
1211
+
1212
+ The current implementation of this function uses
1213
+ :func:`networkx.prefix_tree`, so it is subject to the limitations of
1214
+ that function.
1215
+
1216
+ """
1217
+ if has_cycle(G):
1218
+ msg = "dag_to_branching is only defined for acyclic graphs"
1219
+ raise nx.HasACycle(msg)
1220
+ paths = root_to_leaf_paths(G)
1221
+ B = nx.prefix_tree(paths)
1222
+ # Remove the synthetic `root`(0) and `NIL`(-1) nodes from the tree
1223
+ B.remove_node(0)
1224
+ B.remove_node(-1)
1225
+ return B
1226
+
1227
+
1228
+ @not_implemented_for("undirected")
1229
+ @nx._dispatchable
1230
+ def compute_v_structures(G):
1231
+ """Yields 3-node tuples that represent the v-structures in `G`.
1232
+
1233
+ .. deprecated:: 3.4
1234
+
1235
+ `compute_v_structures` actually yields colliders. It will be removed in
1236
+ version 3.6. Use `nx.dag.v_structures` or `nx.dag.colliders` instead.
1237
+
1238
+ Colliders are triples in the directed acyclic graph (DAG) where two parent nodes
1239
+ point to the same child node. V-structures are colliders where the two parent
1240
+ nodes are not adjacent. In a causal graph setting, the parents do not directly
1241
+ depend on each other, but conditioning on the child node provides an association.
1242
+
1243
+ Parameters
1244
+ ----------
1245
+ G : graph
1246
+ A networkx `~networkx.DiGraph`.
1247
+
1248
+ Yields
1249
+ ------
1250
+ A 3-tuple representation of a v-structure
1251
+ Each v-structure is a 3-tuple with the parent, collider, and other parent.
1252
+
1253
+ Raises
1254
+ ------
1255
+ NetworkXNotImplemented
1256
+ If `G` is an undirected graph.
1257
+
1258
+ Examples
1259
+ --------
1260
+ >>> G = nx.DiGraph([(1, 2), (0, 4), (3, 1), (2, 4), (0, 5), (4, 5), (1, 5)])
1261
+ >>> nx.is_directed_acyclic_graph(G)
1262
+ True
1263
+ >>> list(nx.compute_v_structures(G))
1264
+ [(0, 4, 2), (0, 5, 4), (0, 5, 1), (4, 5, 1)]
1265
+
1266
+ See Also
1267
+ --------
1268
+ v_structures
1269
+ colliders
1270
+
1271
+ Notes
1272
+ -----
1273
+ This function was written to be used on DAGs, however it works on cyclic graphs
1274
+ too. Since colliders are referred to in the cyclic causal graph literature
1275
+ [2]_ we allow cyclic graphs in this function. It is suggested that you test if
1276
+ your input graph is acyclic as in the example if you want that property.
1277
+
1278
+ References
1279
+ ----------
1280
+ .. [1] `Pearl's PRIMER <https://bayes.cs.ucla.edu/PRIMER/primer-ch2.pdf>`_
1281
+ Ch-2 page 50: v-structures def.
1282
+ .. [2] A Hyttinen, P.O. Hoyer, F. Eberhardt, M J ̈arvisalo, (2013)
1283
+ "Discovering cyclic causal models with latent variables:
1284
+ a general SAT-based procedure", UAI'13: Proceedings of the Twenty-Ninth
1285
+ Conference on Uncertainty in Artificial Intelligence, pg 301–310,
1286
+ `doi:10.5555/3023638.3023669 <https://dl.acm.org/doi/10.5555/3023638.3023669>`_
1287
+ """
1288
+ import warnings
1289
+
1290
+ warnings.warn(
1291
+ (
1292
+ "\n\n`compute_v_structures` actually yields colliders. It will be\n"
1293
+ "removed in version 3.6. Use `nx.dag.v_structures` or `nx.dag.colliders`\n"
1294
+ "instead.\n"
1295
+ ),
1296
+ category=DeprecationWarning,
1297
+ stacklevel=5,
1298
+ )
1299
+
1300
+ return colliders(G)
1301
+
1302
+
1303
+ @not_implemented_for("undirected")
1304
+ @nx._dispatchable
1305
+ def v_structures(G):
1306
+ """Yields 3-node tuples that represent the v-structures in `G`.
1307
+
1308
+ Colliders are triples in the directed acyclic graph (DAG) where two parent nodes
1309
+ point to the same child node. V-structures are colliders where the two parent
1310
+ nodes are not adjacent. In a causal graph setting, the parents do not directly
1311
+ depend on each other, but conditioning on the child node provides an association.
1312
+
1313
+ Parameters
1314
+ ----------
1315
+ G : graph
1316
+ A networkx `~networkx.DiGraph`.
1317
+
1318
+ Yields
1319
+ ------
1320
+ A 3-tuple representation of a v-structure
1321
+ Each v-structure is a 3-tuple with the parent, collider, and other parent.
1322
+
1323
+ Raises
1324
+ ------
1325
+ NetworkXNotImplemented
1326
+ If `G` is an undirected graph.
1327
+
1328
+ Examples
1329
+ --------
1330
+ >>> G = nx.DiGraph([(1, 2), (0, 4), (3, 1), (2, 4), (0, 5), (4, 5), (1, 5)])
1331
+ >>> nx.is_directed_acyclic_graph(G)
1332
+ True
1333
+ >>> list(nx.dag.v_structures(G))
1334
+ [(0, 4, 2), (0, 5, 1), (4, 5, 1)]
1335
+
1336
+ See Also
1337
+ --------
1338
+ colliders
1339
+
1340
+ Notes
1341
+ -----
1342
+ This function was written to be used on DAGs, however it works on cyclic graphs
1343
+ too. Since colliders are referred to in the cyclic causal graph literature
1344
+ [2]_ we allow cyclic graphs in this function. It is suggested that you test if
1345
+ your input graph is acyclic as in the example if you want that property.
1346
+
1347
+ References
1348
+ ----------
1349
+ .. [1] `Pearl's PRIMER <https://bayes.cs.ucla.edu/PRIMER/primer-ch2.pdf>`_
1350
+ Ch-2 page 50: v-structures def.
1351
+ .. [2] A Hyttinen, P.O. Hoyer, F. Eberhardt, M J ̈arvisalo, (2013)
1352
+ "Discovering cyclic causal models with latent variables:
1353
+ a general SAT-based procedure", UAI'13: Proceedings of the Twenty-Ninth
1354
+ Conference on Uncertainty in Artificial Intelligence, pg 301–310,
1355
+ `doi:10.5555/3023638.3023669 <https://dl.acm.org/doi/10.5555/3023638.3023669>`_
1356
+ """
1357
+ for p1, c, p2 in colliders(G):
1358
+ if not (G.has_edge(p1, p2) or G.has_edge(p2, p1)):
1359
+ yield (p1, c, p2)
1360
+
1361
+
1362
+ @not_implemented_for("undirected")
1363
+ @nx._dispatchable
1364
+ def colliders(G):
1365
+ """Yields 3-node tuples that represent the colliders in `G`.
1366
+
1367
+ In a Directed Acyclic Graph (DAG), if you have three nodes A, B, and C, and
1368
+ there are edges from A to C and from B to C, then C is a collider [1]_ . In
1369
+ a causal graph setting, this means that both events A and B are "causing" C,
1370
+ and conditioning on C provide an association between A and B even if
1371
+ no direct causal relationship exists between A and B.
1372
+
1373
+ Parameters
1374
+ ----------
1375
+ G : graph
1376
+ A networkx `~networkx.DiGraph`.
1377
+
1378
+ Yields
1379
+ ------
1380
+ A 3-tuple representation of a collider
1381
+ Each collider is a 3-tuple with the parent, collider, and other parent.
1382
+
1383
+ Raises
1384
+ ------
1385
+ NetworkXNotImplemented
1386
+ If `G` is an undirected graph.
1387
+
1388
+ Examples
1389
+ --------
1390
+ >>> G = nx.DiGraph([(1, 2), (0, 4), (3, 1), (2, 4), (0, 5), (4, 5), (1, 5)])
1391
+ >>> nx.is_directed_acyclic_graph(G)
1392
+ True
1393
+ >>> list(nx.dag.colliders(G))
1394
+ [(0, 4, 2), (0, 5, 4), (0, 5, 1), (4, 5, 1)]
1395
+
1396
+ See Also
1397
+ --------
1398
+ v_structures
1399
+
1400
+ Notes
1401
+ -----
1402
+ This function was written to be used on DAGs, however it works on cyclic graphs
1403
+ too. Since colliders are referred to in the cyclic causal graph literature
1404
+ [2]_ we allow cyclic graphs in this function. It is suggested that you test if
1405
+ your input graph is acyclic as in the example if you want that property.
1406
+
1407
+ References
1408
+ ----------
1409
+ .. [1] `Wikipedia: Collider in causal graphs <https://en.wikipedia.org/wiki/Collider_(statistics)>`_
1410
+ .. [2] A Hyttinen, P.O. Hoyer, F. Eberhardt, M J ̈arvisalo, (2013)
1411
+ "Discovering cyclic causal models with latent variables:
1412
+ a general SAT-based procedure", UAI'13: Proceedings of the Twenty-Ninth
1413
+ Conference on Uncertainty in Artificial Intelligence, pg 301–310,
1414
+ `doi:10.5555/3023638.3023669 <https://dl.acm.org/doi/10.5555/3023638.3023669>`_
1415
+ """
1416
+ for node in G.nodes:
1417
+ for p1, p2 in combinations(G.predecessors(node), 2):
1418
+ yield (p1, node, p2)
.venv/lib/python3.11/site-packages/networkx/algorithms/distance_measures.py ADDED
@@ -0,0 +1,1022 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Graph diameter, radius, eccentricity and other properties."""
2
+
3
+ import math
4
+
5
+ import networkx as nx
6
+ from networkx.utils import not_implemented_for
7
+
8
+ __all__ = [
9
+ "eccentricity",
10
+ "diameter",
11
+ "harmonic_diameter",
12
+ "radius",
13
+ "periphery",
14
+ "center",
15
+ "barycenter",
16
+ "resistance_distance",
17
+ "kemeny_constant",
18
+ "effective_graph_resistance",
19
+ ]
20
+
21
+
22
+ def _extrema_bounding(G, compute="diameter", weight=None):
23
+ """Compute requested extreme distance metric of undirected graph G
24
+
25
+ Computation is based on smart lower and upper bounds, and in practice
26
+ linear in the number of nodes, rather than quadratic (except for some
27
+ border cases such as complete graphs or circle shaped graphs).
28
+
29
+ Parameters
30
+ ----------
31
+ G : NetworkX graph
32
+ An undirected graph
33
+
34
+ compute : string denoting the requesting metric
35
+ "diameter" for the maximal eccentricity value,
36
+ "radius" for the minimal eccentricity value,
37
+ "periphery" for the set of nodes with eccentricity equal to the diameter,
38
+ "center" for the set of nodes with eccentricity equal to the radius,
39
+ "eccentricities" for the maximum distance from each node to all other nodes in G
40
+
41
+ weight : string, function, or None
42
+ If this is a string, then edge weights will be accessed via the
43
+ edge attribute with this key (that is, the weight of the edge
44
+ joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
45
+ such edge attribute exists, the weight of the edge is assumed to
46
+ be one.
47
+
48
+ If this is a function, the weight of an edge is the value
49
+ returned by the function. The function must accept exactly three
50
+ positional arguments: the two endpoints of an edge and the
51
+ dictionary of edge attributes for that edge. The function must
52
+ return a number.
53
+
54
+ If this is None, every edge has weight/distance/cost 1.
55
+
56
+ Weights stored as floating point values can lead to small round-off
57
+ errors in distances. Use integer weights to avoid this.
58
+
59
+ Weights should be positive, since they are distances.
60
+
61
+ Returns
62
+ -------
63
+ value : value of the requested metric
64
+ int for "diameter" and "radius" or
65
+ list of nodes for "center" and "periphery" or
66
+ dictionary of eccentricity values keyed by node for "eccentricities"
67
+
68
+ Raises
69
+ ------
70
+ NetworkXError
71
+ If the graph consists of multiple components
72
+ ValueError
73
+ If `compute` is not one of "diameter", "radius", "periphery", "center", or "eccentricities".
74
+
75
+ Notes
76
+ -----
77
+ This algorithm was proposed in [1]_ and discussed further in [2]_ and [3]_.
78
+
79
+ References
80
+ ----------
81
+ .. [1] F. W. Takes, W. A. Kosters,
82
+ "Determining the diameter of small world networks."
83
+ Proceedings of the 20th ACM international conference on Information and knowledge management, 2011
84
+ https://dl.acm.org/doi/abs/10.1145/2063576.2063748
85
+ .. [2] F. W. Takes, W. A. Kosters,
86
+ "Computing the Eccentricity Distribution of Large Graphs."
87
+ Algorithms, 2013
88
+ https://www.mdpi.com/1999-4893/6/1/100
89
+ .. [3] M. Borassi, P. Crescenzi, M. Habib, W. A. Kosters, A. Marino, F. W. Takes,
90
+ "Fast diameter and radius BFS-based computation in (weakly connected) real-world graphs: With an application to the six degrees of separation games. "
91
+ Theoretical Computer Science, 2015
92
+ https://www.sciencedirect.com/science/article/pii/S0304397515001644
93
+ """
94
+ # init variables
95
+ degrees = dict(G.degree()) # start with the highest degree node
96
+ minlowernode = max(degrees, key=degrees.get)
97
+ N = len(degrees) # number of nodes
98
+ # alternate between smallest lower and largest upper bound
99
+ high = False
100
+ # status variables
101
+ ecc_lower = dict.fromkeys(G, 0)
102
+ ecc_upper = dict.fromkeys(G, N)
103
+ candidates = set(G)
104
+
105
+ # (re)set bound extremes
106
+ minlower = N
107
+ maxlower = 0
108
+ minupper = N
109
+ maxupper = 0
110
+
111
+ # repeat the following until there are no more candidates
112
+ while candidates:
113
+ if high:
114
+ current = maxuppernode # select node with largest upper bound
115
+ else:
116
+ current = minlowernode # select node with smallest lower bound
117
+ high = not high
118
+
119
+ # get distances from/to current node and derive eccentricity
120
+ dist = nx.shortest_path_length(G, source=current, weight=weight)
121
+
122
+ if len(dist) != N:
123
+ msg = "Cannot compute metric because graph is not connected."
124
+ raise nx.NetworkXError(msg)
125
+ current_ecc = max(dist.values())
126
+
127
+ # print status update
128
+ # print ("ecc of " + str(current) + " (" + str(ecc_lower[current]) + "/"
129
+ # + str(ecc_upper[current]) + ", deg: " + str(dist[current]) + ") is "
130
+ # + str(current_ecc))
131
+ # print(ecc_upper)
132
+
133
+ # (re)set bound extremes
134
+ maxuppernode = None
135
+ minlowernode = None
136
+
137
+ # update node bounds
138
+ for i in candidates:
139
+ # update eccentricity bounds
140
+ d = dist[i]
141
+ ecc_lower[i] = low = max(ecc_lower[i], max(d, (current_ecc - d)))
142
+ ecc_upper[i] = upp = min(ecc_upper[i], current_ecc + d)
143
+
144
+ # update min/max values of lower and upper bounds
145
+ minlower = min(ecc_lower[i], minlower)
146
+ maxlower = max(ecc_lower[i], maxlower)
147
+ minupper = min(ecc_upper[i], minupper)
148
+ maxupper = max(ecc_upper[i], maxupper)
149
+
150
+ # update candidate set
151
+ if compute == "diameter":
152
+ ruled_out = {
153
+ i
154
+ for i in candidates
155
+ if ecc_upper[i] <= maxlower and 2 * ecc_lower[i] >= maxupper
156
+ }
157
+ elif compute == "radius":
158
+ ruled_out = {
159
+ i
160
+ for i in candidates
161
+ if ecc_lower[i] >= minupper and ecc_upper[i] + 1 <= 2 * minlower
162
+ }
163
+ elif compute == "periphery":
164
+ ruled_out = {
165
+ i
166
+ for i in candidates
167
+ if ecc_upper[i] < maxlower
168
+ and (maxlower == maxupper or ecc_lower[i] > maxupper)
169
+ }
170
+ elif compute == "center":
171
+ ruled_out = {
172
+ i
173
+ for i in candidates
174
+ if ecc_lower[i] > minupper
175
+ and (minlower == minupper or ecc_upper[i] + 1 < 2 * minlower)
176
+ }
177
+ elif compute == "eccentricities":
178
+ ruled_out = set()
179
+ else:
180
+ msg = "compute must be one of 'diameter', 'radius', 'periphery', 'center', 'eccentricities'"
181
+ raise ValueError(msg)
182
+
183
+ ruled_out.update(i for i in candidates if ecc_lower[i] == ecc_upper[i])
184
+ candidates -= ruled_out
185
+
186
+ # for i in ruled_out:
187
+ # print("removing %g: ecc_u: %g maxl: %g ecc_l: %g maxu: %g"%
188
+ # (i,ecc_upper[i],maxlower,ecc_lower[i],maxupper))
189
+ # print("node %g: ecc_u: %g maxl: %g ecc_l: %g maxu: %g"%
190
+ # (4,ecc_upper[4],maxlower,ecc_lower[4],maxupper))
191
+ # print("NODE 4: %g"%(ecc_upper[4] <= maxlower))
192
+ # print("NODE 4: %g"%(2 * ecc_lower[4] >= maxupper))
193
+ # print("NODE 4: %g"%(ecc_upper[4] <= maxlower
194
+ # and 2 * ecc_lower[4] >= maxupper))
195
+
196
+ # updating maxuppernode and minlowernode for selection in next round
197
+ for i in candidates:
198
+ if (
199
+ minlowernode is None
200
+ or (
201
+ ecc_lower[i] == ecc_lower[minlowernode]
202
+ and degrees[i] > degrees[minlowernode]
203
+ )
204
+ or (ecc_lower[i] < ecc_lower[minlowernode])
205
+ ):
206
+ minlowernode = i
207
+
208
+ if (
209
+ maxuppernode is None
210
+ or (
211
+ ecc_upper[i] == ecc_upper[maxuppernode]
212
+ and degrees[i] > degrees[maxuppernode]
213
+ )
214
+ or (ecc_upper[i] > ecc_upper[maxuppernode])
215
+ ):
216
+ maxuppernode = i
217
+
218
+ # print status update
219
+ # print (" min=" + str(minlower) + "/" + str(minupper) +
220
+ # " max=" + str(maxlower) + "/" + str(maxupper) +
221
+ # " candidates: " + str(len(candidates)))
222
+ # print("cand:",candidates)
223
+ # print("ecc_l",ecc_lower)
224
+ # print("ecc_u",ecc_upper)
225
+ # wait = input("press Enter to continue")
226
+
227
+ # return the correct value of the requested metric
228
+ if compute == "diameter":
229
+ return maxlower
230
+ if compute == "radius":
231
+ return minupper
232
+ if compute == "periphery":
233
+ p = [v for v in G if ecc_lower[v] == maxlower]
234
+ return p
235
+ if compute == "center":
236
+ c = [v for v in G if ecc_upper[v] == minupper]
237
+ return c
238
+ if compute == "eccentricities":
239
+ return ecc_lower
240
+ return None
241
+
242
+
243
+ @nx._dispatchable(edge_attrs="weight")
244
+ def eccentricity(G, v=None, sp=None, weight=None):
245
+ """Returns the eccentricity of nodes in G.
246
+
247
+ The eccentricity of a node v is the maximum distance from v to
248
+ all other nodes in G.
249
+
250
+ Parameters
251
+ ----------
252
+ G : NetworkX graph
253
+ A graph
254
+
255
+ v : node, optional
256
+ Return value of specified node
257
+
258
+ sp : dict of dicts, optional
259
+ All pairs shortest path lengths as a dictionary of dictionaries
260
+
261
+ weight : string, function, or None (default=None)
262
+ If this is a string, then edge weights will be accessed via the
263
+ edge attribute with this key (that is, the weight of the edge
264
+ joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
265
+ such edge attribute exists, the weight of the edge is assumed to
266
+ be one.
267
+
268
+ If this is a function, the weight of an edge is the value
269
+ returned by the function. The function must accept exactly three
270
+ positional arguments: the two endpoints of an edge and the
271
+ dictionary of edge attributes for that edge. The function must
272
+ return a number.
273
+
274
+ If this is None, every edge has weight/distance/cost 1.
275
+
276
+ Weights stored as floating point values can lead to small round-off
277
+ errors in distances. Use integer weights to avoid this.
278
+
279
+ Weights should be positive, since they are distances.
280
+
281
+ Returns
282
+ -------
283
+ ecc : dictionary
284
+ A dictionary of eccentricity values keyed by node.
285
+
286
+ Examples
287
+ --------
288
+ >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
289
+ >>> dict(nx.eccentricity(G))
290
+ {1: 2, 2: 3, 3: 2, 4: 2, 5: 3}
291
+
292
+ >>> dict(
293
+ ... nx.eccentricity(G, v=[1, 5])
294
+ ... ) # This returns the eccentricity of node 1 & 5
295
+ {1: 2, 5: 3}
296
+
297
+ """
298
+ # if v is None: # none, use entire graph
299
+ # nodes=G.nodes()
300
+ # elif v in G: # is v a single node
301
+ # nodes=[v]
302
+ # else: # assume v is a container of nodes
303
+ # nodes=v
304
+ order = G.order()
305
+ e = {}
306
+ for n in G.nbunch_iter(v):
307
+ if sp is None:
308
+ length = nx.shortest_path_length(G, source=n, weight=weight)
309
+
310
+ L = len(length)
311
+ else:
312
+ try:
313
+ length = sp[n]
314
+ L = len(length)
315
+ except TypeError as err:
316
+ raise nx.NetworkXError('Format of "sp" is invalid.') from err
317
+ if L != order:
318
+ if G.is_directed():
319
+ msg = (
320
+ "Found infinite path length because the digraph is not"
321
+ " strongly connected"
322
+ )
323
+ else:
324
+ msg = "Found infinite path length because the graph is not" " connected"
325
+ raise nx.NetworkXError(msg)
326
+
327
+ e[n] = max(length.values())
328
+
329
+ if v in G:
330
+ return e[v] # return single value
331
+ return e
332
+
333
+
334
+ @nx._dispatchable(edge_attrs="weight")
335
+ def diameter(G, e=None, usebounds=False, weight=None):
336
+ """Returns the diameter of the graph G.
337
+
338
+ The diameter is the maximum eccentricity.
339
+
340
+ Parameters
341
+ ----------
342
+ G : NetworkX graph
343
+ A graph
344
+
345
+ e : eccentricity dictionary, optional
346
+ A precomputed dictionary of eccentricities.
347
+
348
+ weight : string, function, or None
349
+ If this is a string, then edge weights will be accessed via the
350
+ edge attribute with this key (that is, the weight of the edge
351
+ joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
352
+ such edge attribute exists, the weight of the edge is assumed to
353
+ be one.
354
+
355
+ If this is a function, the weight of an edge is the value
356
+ returned by the function. The function must accept exactly three
357
+ positional arguments: the two endpoints of an edge and the
358
+ dictionary of edge attributes for that edge. The function must
359
+ return a number.
360
+
361
+ If this is None, every edge has weight/distance/cost 1.
362
+
363
+ Weights stored as floating point values can lead to small round-off
364
+ errors in distances. Use integer weights to avoid this.
365
+
366
+ Weights should be positive, since they are distances.
367
+
368
+ Returns
369
+ -------
370
+ d : integer
371
+ Diameter of graph
372
+
373
+ Examples
374
+ --------
375
+ >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
376
+ >>> nx.diameter(G)
377
+ 3
378
+
379
+ See Also
380
+ --------
381
+ eccentricity
382
+ """
383
+ if usebounds is True and e is None and not G.is_directed():
384
+ return _extrema_bounding(G, compute="diameter", weight=weight)
385
+ if e is None:
386
+ e = eccentricity(G, weight=weight)
387
+ return max(e.values())
388
+
389
+
390
+ @nx._dispatchable
391
+ def harmonic_diameter(G, sp=None):
392
+ """Returns the harmonic diameter of the graph G.
393
+
394
+ The harmonic diameter of a graph is the harmonic mean of the distances
395
+ between all pairs of distinct vertices. Graphs that are not strongly
396
+ connected have infinite diameter and mean distance, making such
397
+ measures not useful. Restricting the diameter or mean distance to
398
+ finite distances yields paradoxical values (e.g., a perfect match
399
+ would have diameter one). The harmonic mean handles gracefully
400
+ infinite distances (e.g., a perfect match has harmonic diameter equal
401
+ to the number of vertices minus one), making it possible to assign a
402
+ meaningful value to all graphs.
403
+
404
+ Note that in [1] the harmonic diameter is called "connectivity length":
405
+ however, "harmonic diameter" is a more standard name from the
406
+ theory of metric spaces. The name "harmonic mean distance" is perhaps
407
+ a more descriptive name, but is not used in the literature, so we use the
408
+ name "harmonic diameter" here.
409
+
410
+ Parameters
411
+ ----------
412
+ G : NetworkX graph
413
+ A graph
414
+
415
+ sp : dict of dicts, optional
416
+ All-pairs shortest path lengths as a dictionary of dictionaries
417
+
418
+ Returns
419
+ -------
420
+ hd : float
421
+ Harmonic diameter of graph
422
+
423
+ References
424
+ ----------
425
+ .. [1] Massimo Marchiori and Vito Latora, "Harmony in the small-world".
426
+ *Physica A: Statistical Mechanics and Its Applications*
427
+ 285(3-4), pages 539-546, 2000.
428
+ <https://doi.org/10.1016/S0378-4371(00)00311-3>
429
+ """
430
+ order = G.order()
431
+
432
+ sum_invd = 0
433
+ for n in G:
434
+ if sp is None:
435
+ length = nx.single_source_shortest_path_length(G, n)
436
+ else:
437
+ try:
438
+ length = sp[n]
439
+ L = len(length)
440
+ except TypeError as err:
441
+ raise nx.NetworkXError('Format of "sp" is invalid.') from err
442
+
443
+ for d in length.values():
444
+ # Note that this will skip the zero distance from n to itself,
445
+ # as it should be, but also zero-weight paths in weighted graphs.
446
+ if d != 0:
447
+ sum_invd += 1 / d
448
+
449
+ if sum_invd != 0:
450
+ return order * (order - 1) / sum_invd
451
+ if order > 1:
452
+ return math.inf
453
+ return math.nan
454
+
455
+
456
+ @nx._dispatchable(edge_attrs="weight")
457
+ def periphery(G, e=None, usebounds=False, weight=None):
458
+ """Returns the periphery of the graph G.
459
+
460
+ The periphery is the set of nodes with eccentricity equal to the diameter.
461
+
462
+ Parameters
463
+ ----------
464
+ G : NetworkX graph
465
+ A graph
466
+
467
+ e : eccentricity dictionary, optional
468
+ A precomputed dictionary of eccentricities.
469
+
470
+ weight : string, function, or None
471
+ If this is a string, then edge weights will be accessed via the
472
+ edge attribute with this key (that is, the weight of the edge
473
+ joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
474
+ such edge attribute exists, the weight of the edge is assumed to
475
+ be one.
476
+
477
+ If this is a function, the weight of an edge is the value
478
+ returned by the function. The function must accept exactly three
479
+ positional arguments: the two endpoints of an edge and the
480
+ dictionary of edge attributes for that edge. The function must
481
+ return a number.
482
+
483
+ If this is None, every edge has weight/distance/cost 1.
484
+
485
+ Weights stored as floating point values can lead to small round-off
486
+ errors in distances. Use integer weights to avoid this.
487
+
488
+ Weights should be positive, since they are distances.
489
+
490
+ Returns
491
+ -------
492
+ p : list
493
+ List of nodes in periphery
494
+
495
+ Examples
496
+ --------
497
+ >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
498
+ >>> nx.periphery(G)
499
+ [2, 5]
500
+
501
+ See Also
502
+ --------
503
+ barycenter
504
+ center
505
+ """
506
+ if usebounds is True and e is None and not G.is_directed():
507
+ return _extrema_bounding(G, compute="periphery", weight=weight)
508
+ if e is None:
509
+ e = eccentricity(G, weight=weight)
510
+ diameter = max(e.values())
511
+ p = [v for v in e if e[v] == diameter]
512
+ return p
513
+
514
+
515
+ @nx._dispatchable(edge_attrs="weight")
516
+ def radius(G, e=None, usebounds=False, weight=None):
517
+ """Returns the radius of the graph G.
518
+
519
+ The radius is the minimum eccentricity.
520
+
521
+ Parameters
522
+ ----------
523
+ G : NetworkX graph
524
+ A graph
525
+
526
+ e : eccentricity dictionary, optional
527
+ A precomputed dictionary of eccentricities.
528
+
529
+ weight : string, function, or None
530
+ If this is a string, then edge weights will be accessed via the
531
+ edge attribute with this key (that is, the weight of the edge
532
+ joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
533
+ such edge attribute exists, the weight of the edge is assumed to
534
+ be one.
535
+
536
+ If this is a function, the weight of an edge is the value
537
+ returned by the function. The function must accept exactly three
538
+ positional arguments: the two endpoints of an edge and the
539
+ dictionary of edge attributes for that edge. The function must
540
+ return a number.
541
+
542
+ If this is None, every edge has weight/distance/cost 1.
543
+
544
+ Weights stored as floating point values can lead to small round-off
545
+ errors in distances. Use integer weights to avoid this.
546
+
547
+ Weights should be positive, since they are distances.
548
+
549
+ Returns
550
+ -------
551
+ r : integer
552
+ Radius of graph
553
+
554
+ Examples
555
+ --------
556
+ >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
557
+ >>> nx.radius(G)
558
+ 2
559
+
560
+ """
561
+ if usebounds is True and e is None and not G.is_directed():
562
+ return _extrema_bounding(G, compute="radius", weight=weight)
563
+ if e is None:
564
+ e = eccentricity(G, weight=weight)
565
+ return min(e.values())
566
+
567
+
568
+ @nx._dispatchable(edge_attrs="weight")
569
+ def center(G, e=None, usebounds=False, weight=None):
570
+ """Returns the center of the graph G.
571
+
572
+ The center is the set of nodes with eccentricity equal to radius.
573
+
574
+ Parameters
575
+ ----------
576
+ G : NetworkX graph
577
+ A graph
578
+
579
+ e : eccentricity dictionary, optional
580
+ A precomputed dictionary of eccentricities.
581
+
582
+ weight : string, function, or None
583
+ If this is a string, then edge weights will be accessed via the
584
+ edge attribute with this key (that is, the weight of the edge
585
+ joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
586
+ such edge attribute exists, the weight of the edge is assumed to
587
+ be one.
588
+
589
+ If this is a function, the weight of an edge is the value
590
+ returned by the function. The function must accept exactly three
591
+ positional arguments: the two endpoints of an edge and the
592
+ dictionary of edge attributes for that edge. The function must
593
+ return a number.
594
+
595
+ If this is None, every edge has weight/distance/cost 1.
596
+
597
+ Weights stored as floating point values can lead to small round-off
598
+ errors in distances. Use integer weights to avoid this.
599
+
600
+ Weights should be positive, since they are distances.
601
+
602
+ Returns
603
+ -------
604
+ c : list
605
+ List of nodes in center
606
+
607
+ Examples
608
+ --------
609
+ >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
610
+ >>> list(nx.center(G))
611
+ [1, 3, 4]
612
+
613
+ See Also
614
+ --------
615
+ barycenter
616
+ periphery
617
+ """
618
+ if usebounds is True and e is None and not G.is_directed():
619
+ return _extrema_bounding(G, compute="center", weight=weight)
620
+ if e is None:
621
+ e = eccentricity(G, weight=weight)
622
+ radius = min(e.values())
623
+ p = [v for v in e if e[v] == radius]
624
+ return p
625
+
626
+
627
+ @nx._dispatchable(edge_attrs="weight", mutates_input={"attr": 2})
628
+ def barycenter(G, weight=None, attr=None, sp=None):
629
+ r"""Calculate barycenter of a connected graph, optionally with edge weights.
630
+
631
+ The :dfn:`barycenter` a
632
+ :func:`connected <networkx.algorithms.components.is_connected>` graph
633
+ :math:`G` is the subgraph induced by the set of its nodes :math:`v`
634
+ minimizing the objective function
635
+
636
+ .. math::
637
+
638
+ \sum_{u \in V(G)} d_G(u, v),
639
+
640
+ where :math:`d_G` is the (possibly weighted) :func:`path length
641
+ <networkx.algorithms.shortest_paths.generic.shortest_path_length>`.
642
+ The barycenter is also called the :dfn:`median`. See [West01]_, p. 78.
643
+
644
+ Parameters
645
+ ----------
646
+ G : :class:`networkx.Graph`
647
+ The connected graph :math:`G`.
648
+ weight : :class:`str`, optional
649
+ Passed through to
650
+ :func:`~networkx.algorithms.shortest_paths.generic.shortest_path_length`.
651
+ attr : :class:`str`, optional
652
+ If given, write the value of the objective function to each node's
653
+ `attr` attribute. Otherwise do not store the value.
654
+ sp : dict of dicts, optional
655
+ All pairs shortest path lengths as a dictionary of dictionaries
656
+
657
+ Returns
658
+ -------
659
+ list
660
+ Nodes of `G` that induce the barycenter of `G`.
661
+
662
+ Raises
663
+ ------
664
+ NetworkXNoPath
665
+ If `G` is disconnected. `G` may appear disconnected to
666
+ :func:`barycenter` if `sp` is given but is missing shortest path
667
+ lengths for any pairs.
668
+ ValueError
669
+ If `sp` and `weight` are both given.
670
+
671
+ Examples
672
+ --------
673
+ >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
674
+ >>> nx.barycenter(G)
675
+ [1, 3, 4]
676
+
677
+ See Also
678
+ --------
679
+ center
680
+ periphery
681
+ """
682
+ if sp is None:
683
+ sp = nx.shortest_path_length(G, weight=weight)
684
+ else:
685
+ sp = sp.items()
686
+ if weight is not None:
687
+ raise ValueError("Cannot use both sp, weight arguments together")
688
+ smallest, barycenter_vertices, n = float("inf"), [], len(G)
689
+ for v, dists in sp:
690
+ if len(dists) < n:
691
+ raise nx.NetworkXNoPath(
692
+ f"Input graph {G} is disconnected, so every induced subgraph "
693
+ "has infinite barycentricity."
694
+ )
695
+ barycentricity = sum(dists.values())
696
+ if attr is not None:
697
+ G.nodes[v][attr] = barycentricity
698
+ if barycentricity < smallest:
699
+ smallest = barycentricity
700
+ barycenter_vertices = [v]
701
+ elif barycentricity == smallest:
702
+ barycenter_vertices.append(v)
703
+ if attr is not None:
704
+ nx._clear_cache(G)
705
+ return barycenter_vertices
706
+
707
+
708
+ @not_implemented_for("directed")
709
+ @nx._dispatchable(edge_attrs="weight")
710
+ def resistance_distance(G, nodeA=None, nodeB=None, weight=None, invert_weight=True):
711
+ """Returns the resistance distance between pairs of nodes in graph G.
712
+
713
+ The resistance distance between two nodes of a graph is akin to treating
714
+ the graph as a grid of resistors with a resistance equal to the provided
715
+ weight [1]_, [2]_.
716
+
717
+ If weight is not provided, then a weight of 1 is used for all edges.
718
+
719
+ If two nodes are the same, the resistance distance is zero.
720
+
721
+ Parameters
722
+ ----------
723
+ G : NetworkX graph
724
+ A graph
725
+
726
+ nodeA : node or None, optional (default=None)
727
+ A node within graph G.
728
+ If None, compute resistance distance using all nodes as source nodes.
729
+
730
+ nodeB : node or None, optional (default=None)
731
+ A node within graph G.
732
+ If None, compute resistance distance using all nodes as target nodes.
733
+
734
+ weight : string or None, optional (default=None)
735
+ The edge data key used to compute the resistance distance.
736
+ If None, then each edge has weight 1.
737
+
738
+ invert_weight : boolean (default=True)
739
+ Proper calculation of resistance distance requires building the
740
+ Laplacian matrix with the reciprocal of the weight. Not required
741
+ if the weight is already inverted. Weight cannot be zero.
742
+
743
+ Returns
744
+ -------
745
+ rd : dict or float
746
+ If `nodeA` and `nodeB` are given, resistance distance between `nodeA`
747
+ and `nodeB`. If `nodeA` or `nodeB` is unspecified (the default), a
748
+ dictionary of nodes with resistance distances as the value.
749
+
750
+ Raises
751
+ ------
752
+ NetworkXNotImplemented
753
+ If `G` is a directed graph.
754
+
755
+ NetworkXError
756
+ If `G` is not connected, or contains no nodes,
757
+ or `nodeA` is not in `G` or `nodeB` is not in `G`.
758
+
759
+ Examples
760
+ --------
761
+ >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
762
+ >>> round(nx.resistance_distance(G, 1, 3), 10)
763
+ 0.625
764
+
765
+ Notes
766
+ -----
767
+ The implementation is based on Theorem A in [2]_. Self-loops are ignored.
768
+ Multi-edges are contracted in one edge with weight equal to the harmonic sum of the weights.
769
+
770
+ References
771
+ ----------
772
+ .. [1] Wikipedia
773
+ "Resistance distance."
774
+ https://en.wikipedia.org/wiki/Resistance_distance
775
+ .. [2] D. J. Klein and M. Randic.
776
+ Resistance distance.
777
+ J. of Math. Chem. 12:81-95, 1993.
778
+ """
779
+ import numpy as np
780
+
781
+ if len(G) == 0:
782
+ raise nx.NetworkXError("Graph G must contain at least one node.")
783
+ if not nx.is_connected(G):
784
+ raise nx.NetworkXError("Graph G must be strongly connected.")
785
+ if nodeA is not None and nodeA not in G:
786
+ raise nx.NetworkXError("Node A is not in graph G.")
787
+ if nodeB is not None and nodeB not in G:
788
+ raise nx.NetworkXError("Node B is not in graph G.")
789
+
790
+ G = G.copy()
791
+ node_list = list(G)
792
+
793
+ # Invert weights
794
+ if invert_weight and weight is not None:
795
+ if G.is_multigraph():
796
+ for u, v, k, d in G.edges(keys=True, data=True):
797
+ d[weight] = 1 / d[weight]
798
+ else:
799
+ for u, v, d in G.edges(data=True):
800
+ d[weight] = 1 / d[weight]
801
+
802
+ # Compute resistance distance using the Pseudo-inverse of the Laplacian
803
+ # Self-loops are ignored
804
+ L = nx.laplacian_matrix(G, weight=weight).todense()
805
+ Linv = np.linalg.pinv(L, hermitian=True)
806
+
807
+ # Return relevant distances
808
+ if nodeA is not None and nodeB is not None:
809
+ i = node_list.index(nodeA)
810
+ j = node_list.index(nodeB)
811
+ return Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i)
812
+
813
+ elif nodeA is not None:
814
+ i = node_list.index(nodeA)
815
+ d = {}
816
+ for n in G:
817
+ j = node_list.index(n)
818
+ d[n] = Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i)
819
+ return d
820
+
821
+ elif nodeB is not None:
822
+ j = node_list.index(nodeB)
823
+ d = {}
824
+ for n in G:
825
+ i = node_list.index(n)
826
+ d[n] = Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i)
827
+ return d
828
+
829
+ else:
830
+ d = {}
831
+ for n in G:
832
+ i = node_list.index(n)
833
+ d[n] = {}
834
+ for n2 in G:
835
+ j = node_list.index(n2)
836
+ d[n][n2] = (
837
+ Linv.item(i, i)
838
+ + Linv.item(j, j)
839
+ - Linv.item(i, j)
840
+ - Linv.item(j, i)
841
+ )
842
+ return d
843
+
844
+
845
+ @not_implemented_for("directed")
846
+ @nx._dispatchable(edge_attrs="weight")
847
+ def effective_graph_resistance(G, weight=None, invert_weight=True):
848
+ """Returns the Effective graph resistance of G.
849
+
850
+ Also known as the Kirchhoff index.
851
+
852
+ The effective graph resistance is defined as the sum
853
+ of the resistance distance of every node pair in G [1]_.
854
+
855
+ If weight is not provided, then a weight of 1 is used for all edges.
856
+
857
+ The effective graph resistance of a disconnected graph is infinite.
858
+
859
+ Parameters
860
+ ----------
861
+ G : NetworkX graph
862
+ A graph
863
+
864
+ weight : string or None, optional (default=None)
865
+ The edge data key used to compute the effective graph resistance.
866
+ If None, then each edge has weight 1.
867
+
868
+ invert_weight : boolean (default=True)
869
+ Proper calculation of resistance distance requires building the
870
+ Laplacian matrix with the reciprocal of the weight. Not required
871
+ if the weight is already inverted. Weight cannot be zero.
872
+
873
+ Returns
874
+ -------
875
+ RG : float
876
+ The effective graph resistance of `G`.
877
+
878
+ Raises
879
+ ------
880
+ NetworkXNotImplemented
881
+ If `G` is a directed graph.
882
+
883
+ NetworkXError
884
+ If `G` does not contain any nodes.
885
+
886
+ Examples
887
+ --------
888
+ >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
889
+ >>> round(nx.effective_graph_resistance(G), 10)
890
+ 10.25
891
+
892
+ Notes
893
+ -----
894
+ The implementation is based on Theorem 2.2 in [2]_. Self-loops are ignored.
895
+ Multi-edges are contracted in one edge with weight equal to the harmonic sum of the weights.
896
+
897
+ References
898
+ ----------
899
+ .. [1] Wolfram
900
+ "Kirchhoff Index."
901
+ https://mathworld.wolfram.com/KirchhoffIndex.html
902
+ .. [2] W. Ellens, F. M. Spieksma, P. Van Mieghem, A. Jamakovic, R. E. Kooij.
903
+ Effective graph resistance.
904
+ Lin. Alg. Appl. 435:2491-2506, 2011.
905
+ """
906
+ import numpy as np
907
+
908
+ if len(G) == 0:
909
+ raise nx.NetworkXError("Graph G must contain at least one node.")
910
+
911
+ # Disconnected graphs have infinite Effective graph resistance
912
+ if not nx.is_connected(G):
913
+ return float("inf")
914
+
915
+ # Invert weights
916
+ G = G.copy()
917
+ if invert_weight and weight is not None:
918
+ if G.is_multigraph():
919
+ for u, v, k, d in G.edges(keys=True, data=True):
920
+ d[weight] = 1 / d[weight]
921
+ else:
922
+ for u, v, d in G.edges(data=True):
923
+ d[weight] = 1 / d[weight]
924
+
925
+ # Get Laplacian eigenvalues
926
+ mu = np.sort(nx.laplacian_spectrum(G, weight=weight))
927
+
928
+ # Compute Effective graph resistance based on spectrum of the Laplacian
929
+ # Self-loops are ignored
930
+ return float(np.sum(1 / mu[1:]) * G.number_of_nodes())
931
+
932
+
933
+ @nx.utils.not_implemented_for("directed")
934
+ @nx._dispatchable(edge_attrs="weight")
935
+ def kemeny_constant(G, *, weight=None):
936
+ """Returns the Kemeny constant of the given graph.
937
+
938
+ The *Kemeny constant* (or Kemeny's constant) of a graph `G`
939
+ can be computed by regarding the graph as a Markov chain.
940
+ The Kemeny constant is then the expected number of time steps
941
+ to transition from a starting state i to a random destination state
942
+ sampled from the Markov chain's stationary distribution.
943
+ The Kemeny constant is independent of the chosen initial state [1]_.
944
+
945
+ The Kemeny constant measures the time needed for spreading
946
+ across a graph. Low values indicate a closely connected graph
947
+ whereas high values indicate a spread-out graph.
948
+
949
+ If weight is not provided, then a weight of 1 is used for all edges.
950
+
951
+ Since `G` represents a Markov chain, the weights must be positive.
952
+
953
+ Parameters
954
+ ----------
955
+ G : NetworkX graph
956
+
957
+ weight : string or None, optional (default=None)
958
+ The edge data key used to compute the Kemeny constant.
959
+ If None, then each edge has weight 1.
960
+
961
+ Returns
962
+ -------
963
+ float
964
+ The Kemeny constant of the graph `G`.
965
+
966
+ Raises
967
+ ------
968
+ NetworkXNotImplemented
969
+ If the graph `G` is directed.
970
+
971
+ NetworkXError
972
+ If the graph `G` is not connected, or contains no nodes,
973
+ or has edges with negative weights.
974
+
975
+ Examples
976
+ --------
977
+ >>> G = nx.complete_graph(5)
978
+ >>> round(nx.kemeny_constant(G), 10)
979
+ 3.2
980
+
981
+ Notes
982
+ -----
983
+ The implementation is based on equation (3.3) in [2]_.
984
+ Self-loops are allowed and indicate a Markov chain where
985
+ the state can remain the same. Multi-edges are contracted
986
+ in one edge with weight equal to the sum of the weights.
987
+
988
+ References
989
+ ----------
990
+ .. [1] Wikipedia
991
+ "Kemeny's constant."
992
+ https://en.wikipedia.org/wiki/Kemeny%27s_constant
993
+ .. [2] Lovász L.
994
+ Random walks on graphs: A survey.
995
+ Paul Erdös is Eighty, vol. 2, Bolyai Society,
996
+ Mathematical Studies, Keszthely, Hungary (1993), pp. 1-46
997
+ """
998
+ import numpy as np
999
+ import scipy as sp
1000
+
1001
+ if len(G) == 0:
1002
+ raise nx.NetworkXError("Graph G must contain at least one node.")
1003
+ if not nx.is_connected(G):
1004
+ raise nx.NetworkXError("Graph G must be connected.")
1005
+ if nx.is_negatively_weighted(G, weight=weight):
1006
+ raise nx.NetworkXError("The weights of graph G must be nonnegative.")
1007
+
1008
+ # Compute matrix H = D^-1/2 A D^-1/2
1009
+ A = nx.adjacency_matrix(G, weight=weight)
1010
+ n, m = A.shape
1011
+ diags = A.sum(axis=1)
1012
+ with np.errstate(divide="ignore"):
1013
+ diags_sqrt = 1.0 / np.sqrt(diags)
1014
+ diags_sqrt[np.isinf(diags_sqrt)] = 0
1015
+ DH = sp.sparse.csr_array(sp.sparse.spdiags(diags_sqrt, 0, m, n, format="csr"))
1016
+ H = DH @ (A @ DH)
1017
+
1018
+ # Compute eigenvalues of H
1019
+ eig = np.sort(sp.linalg.eigvalsh(H.todense()))
1020
+
1021
+ # Compute the Kemeny constant
1022
+ return float(np.sum(1 / (1 - eig[:-1])))
.venv/lib/python3.11/site-packages/networkx/algorithms/distance_regular.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ =======================
3
+ Distance-regular graphs
4
+ =======================
5
+ """
6
+
7
+ import networkx as nx
8
+ from networkx.utils import not_implemented_for
9
+
10
+ from .distance_measures import diameter
11
+
12
+ __all__ = [
13
+ "is_distance_regular",
14
+ "is_strongly_regular",
15
+ "intersection_array",
16
+ "global_parameters",
17
+ ]
18
+
19
+
20
+ @nx._dispatchable
21
+ def is_distance_regular(G):
22
+ """Returns True if the graph is distance regular, False otherwise.
23
+
24
+ A connected graph G is distance-regular if for any nodes x,y
25
+ and any integers i,j=0,1,...,d (where d is the graph
26
+ diameter), the number of vertices at distance i from x and
27
+ distance j from y depends only on i,j and the graph distance
28
+ between x and y, independently of the choice of x and y.
29
+
30
+ Parameters
31
+ ----------
32
+ G: Networkx graph (undirected)
33
+
34
+ Returns
35
+ -------
36
+ bool
37
+ True if the graph is Distance Regular, False otherwise
38
+
39
+ Examples
40
+ --------
41
+ >>> G = nx.hypercube_graph(6)
42
+ >>> nx.is_distance_regular(G)
43
+ True
44
+
45
+ See Also
46
+ --------
47
+ intersection_array, global_parameters
48
+
49
+ Notes
50
+ -----
51
+ For undirected and simple graphs only
52
+
53
+ References
54
+ ----------
55
+ .. [1] Brouwer, A. E.; Cohen, A. M.; and Neumaier, A.
56
+ Distance-Regular Graphs. New York: Springer-Verlag, 1989.
57
+ .. [2] Weisstein, Eric W. "Distance-Regular Graph."
58
+ http://mathworld.wolfram.com/Distance-RegularGraph.html
59
+
60
+ """
61
+ try:
62
+ intersection_array(G)
63
+ return True
64
+ except nx.NetworkXError:
65
+ return False
66
+
67
+
68
+ def global_parameters(b, c):
69
+ """Returns global parameters for a given intersection array.
70
+
71
+ Given a distance-regular graph G with integers b_i, c_i,i = 0,....,d
72
+ such that for any 2 vertices x,y in G at a distance i=d(x,y), there
73
+ are exactly c_i neighbors of y at a distance of i-1 from x and b_i
74
+ neighbors of y at a distance of i+1 from x.
75
+
76
+ Thus, a distance regular graph has the global parameters,
77
+ [[c_0,a_0,b_0],[c_1,a_1,b_1],......,[c_d,a_d,b_d]] for the
78
+ intersection array [b_0,b_1,.....b_{d-1};c_1,c_2,.....c_d]
79
+ where a_i+b_i+c_i=k , k= degree of every vertex.
80
+
81
+ Parameters
82
+ ----------
83
+ b : list
84
+
85
+ c : list
86
+
87
+ Returns
88
+ -------
89
+ iterable
90
+ An iterable over three tuples.
91
+
92
+ Examples
93
+ --------
94
+ >>> G = nx.dodecahedral_graph()
95
+ >>> b, c = nx.intersection_array(G)
96
+ >>> list(nx.global_parameters(b, c))
97
+ [(0, 0, 3), (1, 0, 2), (1, 1, 1), (1, 1, 1), (2, 0, 1), (3, 0, 0)]
98
+
99
+ References
100
+ ----------
101
+ .. [1] Weisstein, Eric W. "Global Parameters."
102
+ From MathWorld--A Wolfram Web Resource.
103
+ http://mathworld.wolfram.com/GlobalParameters.html
104
+
105
+ See Also
106
+ --------
107
+ intersection_array
108
+ """
109
+ return ((y, b[0] - x - y, x) for x, y in zip(b + [0], [0] + c))
110
+
111
+
112
+ @not_implemented_for("directed")
113
+ @not_implemented_for("multigraph")
114
+ @nx._dispatchable
115
+ def intersection_array(G):
116
+ """Returns the intersection array of a distance-regular graph.
117
+
118
+ Given a distance-regular graph G with integers b_i, c_i,i = 0,....,d
119
+ such that for any 2 vertices x,y in G at a distance i=d(x,y), there
120
+ are exactly c_i neighbors of y at a distance of i-1 from x and b_i
121
+ neighbors of y at a distance of i+1 from x.
122
+
123
+ A distance regular graph's intersection array is given by,
124
+ [b_0,b_1,.....b_{d-1};c_1,c_2,.....c_d]
125
+
126
+ Parameters
127
+ ----------
128
+ G: Networkx graph (undirected)
129
+
130
+ Returns
131
+ -------
132
+ b,c: tuple of lists
133
+
134
+ Examples
135
+ --------
136
+ >>> G = nx.icosahedral_graph()
137
+ >>> nx.intersection_array(G)
138
+ ([5, 2, 1], [1, 2, 5])
139
+
140
+ References
141
+ ----------
142
+ .. [1] Weisstein, Eric W. "Intersection Array."
143
+ From MathWorld--A Wolfram Web Resource.
144
+ http://mathworld.wolfram.com/IntersectionArray.html
145
+
146
+ See Also
147
+ --------
148
+ global_parameters
149
+ """
150
+ # test for regular graph (all degrees must be equal)
151
+ if len(G) == 0:
152
+ raise nx.NetworkXPointlessConcept("Graph has no nodes.")
153
+ degree = iter(G.degree())
154
+ (_, k) = next(degree)
155
+ for _, knext in degree:
156
+ if knext != k:
157
+ raise nx.NetworkXError("Graph is not distance regular.")
158
+ k = knext
159
+ path_length = dict(nx.all_pairs_shortest_path_length(G))
160
+ diameter = max(max(path_length[n].values()) for n in path_length)
161
+ bint = {} # 'b' intersection array
162
+ cint = {} # 'c' intersection array
163
+ for u in G:
164
+ for v in G:
165
+ try:
166
+ i = path_length[u][v]
167
+ except KeyError as err: # graph must be connected
168
+ raise nx.NetworkXError("Graph is not distance regular.") from err
169
+ # number of neighbors of v at a distance of i-1 from u
170
+ c = len([n for n in G[v] if path_length[n][u] == i - 1])
171
+ # number of neighbors of v at a distance of i+1 from u
172
+ b = len([n for n in G[v] if path_length[n][u] == i + 1])
173
+ # b,c are independent of u and v
174
+ if cint.get(i, c) != c or bint.get(i, b) != b:
175
+ raise nx.NetworkXError("Graph is not distance regular")
176
+ bint[i] = b
177
+ cint[i] = c
178
+ return (
179
+ [bint.get(j, 0) for j in range(diameter)],
180
+ [cint.get(j + 1, 0) for j in range(diameter)],
181
+ )
182
+
183
+
184
+ # TODO There is a definition for directed strongly regular graphs.
185
+ @not_implemented_for("directed")
186
+ @not_implemented_for("multigraph")
187
+ @nx._dispatchable
188
+ def is_strongly_regular(G):
189
+ """Returns True if and only if the given graph is strongly
190
+ regular.
191
+
192
+ An undirected graph is *strongly regular* if
193
+
194
+ * it is regular,
195
+ * each pair of adjacent vertices has the same number of neighbors in
196
+ common,
197
+ * each pair of nonadjacent vertices has the same number of neighbors
198
+ in common.
199
+
200
+ Each strongly regular graph is a distance-regular graph.
201
+ Conversely, if a distance-regular graph has diameter two, then it is
202
+ a strongly regular graph. For more information on distance-regular
203
+ graphs, see :func:`is_distance_regular`.
204
+
205
+ Parameters
206
+ ----------
207
+ G : NetworkX graph
208
+ An undirected graph.
209
+
210
+ Returns
211
+ -------
212
+ bool
213
+ Whether `G` is strongly regular.
214
+
215
+ Examples
216
+ --------
217
+
218
+ The cycle graph on five vertices is strongly regular. It is
219
+ two-regular, each pair of adjacent vertices has no shared neighbors,
220
+ and each pair of nonadjacent vertices has one shared neighbor::
221
+
222
+ >>> G = nx.cycle_graph(5)
223
+ >>> nx.is_strongly_regular(G)
224
+ True
225
+
226
+ """
227
+ # Here is an alternate implementation based directly on the
228
+ # definition of strongly regular graphs:
229
+ #
230
+ # return (all_equal(G.degree().values())
231
+ # and all_equal(len(common_neighbors(G, u, v))
232
+ # for u, v in G.edges())
233
+ # and all_equal(len(common_neighbors(G, u, v))
234
+ # for u, v in non_edges(G)))
235
+ #
236
+ # We instead use the fact that a distance-regular graph of diameter
237
+ # two is strongly regular.
238
+ return is_distance_regular(G) and diameter(G) == 2
.venv/lib/python3.11/site-packages/networkx/algorithms/dominance.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dominance algorithms.
3
+ """
4
+
5
+ from functools import reduce
6
+
7
+ import networkx as nx
8
+ from networkx.utils import not_implemented_for
9
+
10
+ __all__ = ["immediate_dominators", "dominance_frontiers"]
11
+
12
+
13
+ @not_implemented_for("undirected")
14
+ @nx._dispatchable
15
+ def immediate_dominators(G, start):
16
+ """Returns the immediate dominators of all nodes of a directed graph.
17
+
18
+ Parameters
19
+ ----------
20
+ G : a DiGraph or MultiDiGraph
21
+ The graph where dominance is to be computed.
22
+
23
+ start : node
24
+ The start node of dominance computation.
25
+
26
+ Returns
27
+ -------
28
+ idom : dict keyed by nodes
29
+ A dict containing the immediate dominators of each node reachable from
30
+ `start`.
31
+
32
+ Raises
33
+ ------
34
+ NetworkXNotImplemented
35
+ If `G` is undirected.
36
+
37
+ NetworkXError
38
+ If `start` is not in `G`.
39
+
40
+ Notes
41
+ -----
42
+ Except for `start`, the immediate dominators are the parents of their
43
+ corresponding nodes in the dominator tree.
44
+
45
+ Examples
46
+ --------
47
+ >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 5), (3, 4), (4, 5)])
48
+ >>> sorted(nx.immediate_dominators(G, 1).items())
49
+ [(1, 1), (2, 1), (3, 1), (4, 3), (5, 1)]
50
+
51
+ References
52
+ ----------
53
+ .. [1] Cooper, Keith D., Harvey, Timothy J. and Kennedy, Ken.
54
+ "A simple, fast dominance algorithm." (2006).
55
+ https://hdl.handle.net/1911/96345
56
+ """
57
+ if start not in G:
58
+ raise nx.NetworkXError("start is not in G")
59
+
60
+ idom = {start: start}
61
+
62
+ order = list(nx.dfs_postorder_nodes(G, start))
63
+ dfn = {u: i for i, u in enumerate(order)}
64
+ order.pop()
65
+ order.reverse()
66
+
67
+ def intersect(u, v):
68
+ while u != v:
69
+ while dfn[u] < dfn[v]:
70
+ u = idom[u]
71
+ while dfn[u] > dfn[v]:
72
+ v = idom[v]
73
+ return u
74
+
75
+ changed = True
76
+ while changed:
77
+ changed = False
78
+ for u in order:
79
+ new_idom = reduce(intersect, (v for v in G.pred[u] if v in idom))
80
+ if u not in idom or idom[u] != new_idom:
81
+ idom[u] = new_idom
82
+ changed = True
83
+
84
+ return idom
85
+
86
+
87
+ @nx._dispatchable
88
+ def dominance_frontiers(G, start):
89
+ """Returns the dominance frontiers of all nodes of a directed graph.
90
+
91
+ Parameters
92
+ ----------
93
+ G : a DiGraph or MultiDiGraph
94
+ The graph where dominance is to be computed.
95
+
96
+ start : node
97
+ The start node of dominance computation.
98
+
99
+ Returns
100
+ -------
101
+ df : dict keyed by nodes
102
+ A dict containing the dominance frontiers of each node reachable from
103
+ `start` as lists.
104
+
105
+ Raises
106
+ ------
107
+ NetworkXNotImplemented
108
+ If `G` is undirected.
109
+
110
+ NetworkXError
111
+ If `start` is not in `G`.
112
+
113
+ Examples
114
+ --------
115
+ >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 5), (3, 4), (4, 5)])
116
+ >>> sorted((u, sorted(df)) for u, df in nx.dominance_frontiers(G, 1).items())
117
+ [(1, []), (2, [5]), (3, [5]), (4, [5]), (5, [])]
118
+
119
+ References
120
+ ----------
121
+ .. [1] Cooper, Keith D., Harvey, Timothy J. and Kennedy, Ken.
122
+ "A simple, fast dominance algorithm." (2006).
123
+ https://hdl.handle.net/1911/96345
124
+ """
125
+ idom = nx.immediate_dominators(G, start)
126
+
127
+ df = {u: set() for u in idom}
128
+ for u in idom:
129
+ if len(G.pred[u]) >= 2:
130
+ for v in G.pred[u]:
131
+ if v in idom:
132
+ while v != idom[u]:
133
+ df[v].add(u)
134
+ v = idom[v]
135
+ return df
.venv/lib/python3.11/site-packages/networkx/algorithms/efficiency_measures.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Provides functions for computing the efficiency of nodes and graphs."""
2
+
3
+ import networkx as nx
4
+ from networkx.exception import NetworkXNoPath
5
+
6
+ from ..utils import not_implemented_for
7
+
8
+ __all__ = ["efficiency", "local_efficiency", "global_efficiency"]
9
+
10
+
11
+ @not_implemented_for("directed")
12
+ @nx._dispatchable
13
+ def efficiency(G, u, v):
14
+ """Returns the efficiency of a pair of nodes in a graph.
15
+
16
+ The *efficiency* of a pair of nodes is the multiplicative inverse of the
17
+ shortest path distance between the nodes [1]_. Returns 0 if no path
18
+ between nodes.
19
+
20
+ Parameters
21
+ ----------
22
+ G : :class:`networkx.Graph`
23
+ An undirected graph for which to compute the average local efficiency.
24
+ u, v : node
25
+ Nodes in the graph ``G``.
26
+
27
+ Returns
28
+ -------
29
+ float
30
+ Multiplicative inverse of the shortest path distance between the nodes.
31
+
32
+ Examples
33
+ --------
34
+ >>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
35
+ >>> nx.efficiency(G, 2, 3) # this gives efficiency for node 2 and 3
36
+ 0.5
37
+
38
+ Notes
39
+ -----
40
+ Edge weights are ignored when computing the shortest path distances.
41
+
42
+ See also
43
+ --------
44
+ local_efficiency
45
+ global_efficiency
46
+
47
+ References
48
+ ----------
49
+ .. [1] Latora, Vito, and Massimo Marchiori.
50
+ "Efficient behavior of small-world networks."
51
+ *Physical Review Letters* 87.19 (2001): 198701.
52
+ <https://doi.org/10.1103/PhysRevLett.87.198701>
53
+
54
+ """
55
+ try:
56
+ eff = 1 / nx.shortest_path_length(G, u, v)
57
+ except NetworkXNoPath:
58
+ eff = 0
59
+ return eff
60
+
61
+
62
+ @not_implemented_for("directed")
63
+ @nx._dispatchable
64
+ def global_efficiency(G):
65
+ """Returns the average global efficiency of the graph.
66
+
67
+ The *efficiency* of a pair of nodes in a graph is the multiplicative
68
+ inverse of the shortest path distance between the nodes. The *average
69
+ global efficiency* of a graph is the average efficiency of all pairs of
70
+ nodes [1]_.
71
+
72
+ Parameters
73
+ ----------
74
+ G : :class:`networkx.Graph`
75
+ An undirected graph for which to compute the average global efficiency.
76
+
77
+ Returns
78
+ -------
79
+ float
80
+ The average global efficiency of the graph.
81
+
82
+ Examples
83
+ --------
84
+ >>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
85
+ >>> round(nx.global_efficiency(G), 12)
86
+ 0.916666666667
87
+
88
+ Notes
89
+ -----
90
+ Edge weights are ignored when computing the shortest path distances.
91
+
92
+ See also
93
+ --------
94
+ local_efficiency
95
+
96
+ References
97
+ ----------
98
+ .. [1] Latora, Vito, and Massimo Marchiori.
99
+ "Efficient behavior of small-world networks."
100
+ *Physical Review Letters* 87.19 (2001): 198701.
101
+ <https://doi.org/10.1103/PhysRevLett.87.198701>
102
+
103
+ """
104
+ n = len(G)
105
+ denom = n * (n - 1)
106
+ if denom != 0:
107
+ lengths = nx.all_pairs_shortest_path_length(G)
108
+ g_eff = 0
109
+ for source, targets in lengths:
110
+ for target, distance in targets.items():
111
+ if distance > 0:
112
+ g_eff += 1 / distance
113
+ g_eff /= denom
114
+ # g_eff = sum(1 / d for s, tgts in lengths
115
+ # for t, d in tgts.items() if d > 0) / denom
116
+ else:
117
+ g_eff = 0
118
+ # TODO This can be made more efficient by computing all pairs shortest
119
+ # path lengths in parallel.
120
+ return g_eff
121
+
122
+
123
+ @not_implemented_for("directed")
124
+ @nx._dispatchable
125
+ def local_efficiency(G):
126
+ """Returns the average local efficiency of the graph.
127
+
128
+ The *efficiency* of a pair of nodes in a graph is the multiplicative
129
+ inverse of the shortest path distance between the nodes. The *local
130
+ efficiency* of a node in the graph is the average global efficiency of the
131
+ subgraph induced by the neighbors of the node. The *average local
132
+ efficiency* is the average of the local efficiencies of each node [1]_.
133
+
134
+ Parameters
135
+ ----------
136
+ G : :class:`networkx.Graph`
137
+ An undirected graph for which to compute the average local efficiency.
138
+
139
+ Returns
140
+ -------
141
+ float
142
+ The average local efficiency of the graph.
143
+
144
+ Examples
145
+ --------
146
+ >>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
147
+ >>> nx.local_efficiency(G)
148
+ 0.9166666666666667
149
+
150
+ Notes
151
+ -----
152
+ Edge weights are ignored when computing the shortest path distances.
153
+
154
+ See also
155
+ --------
156
+ global_efficiency
157
+
158
+ References
159
+ ----------
160
+ .. [1] Latora, Vito, and Massimo Marchiori.
161
+ "Efficient behavior of small-world networks."
162
+ *Physical Review Letters* 87.19 (2001): 198701.
163
+ <https://doi.org/10.1103/PhysRevLett.87.198701>
164
+
165
+ """
166
+ efficiency_list = (global_efficiency(G.subgraph(G[v])) for v in G)
167
+ return sum(efficiency_list) / len(G)
.venv/lib/python3.11/site-packages/networkx/algorithms/euler.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Eulerian circuits and graphs.
3
+ """
4
+
5
+ from itertools import combinations
6
+
7
+ import networkx as nx
8
+
9
+ from ..utils import arbitrary_element, not_implemented_for
10
+
11
+ __all__ = [
12
+ "is_eulerian",
13
+ "eulerian_circuit",
14
+ "eulerize",
15
+ "is_semieulerian",
16
+ "has_eulerian_path",
17
+ "eulerian_path",
18
+ ]
19
+
20
+
21
+ @nx._dispatchable
22
+ def is_eulerian(G):
23
+ """Returns True if and only if `G` is Eulerian.
24
+
25
+ A graph is *Eulerian* if it has an Eulerian circuit. An *Eulerian
26
+ circuit* is a closed walk that includes each edge of a graph exactly
27
+ once.
28
+
29
+ Graphs with isolated vertices (i.e. vertices with zero degree) are not
30
+ considered to have Eulerian circuits. Therefore, if the graph is not
31
+ connected (or not strongly connected, for directed graphs), this function
32
+ returns False.
33
+
34
+ Parameters
35
+ ----------
36
+ G : NetworkX graph
37
+ A graph, either directed or undirected.
38
+
39
+ Examples
40
+ --------
41
+ >>> nx.is_eulerian(nx.DiGraph({0: [3], 1: [2], 2: [3], 3: [0, 1]}))
42
+ True
43
+ >>> nx.is_eulerian(nx.complete_graph(5))
44
+ True
45
+ >>> nx.is_eulerian(nx.petersen_graph())
46
+ False
47
+
48
+ If you prefer to allow graphs with isolated vertices to have Eulerian circuits,
49
+ you can first remove such vertices and then call `is_eulerian` as below example shows.
50
+
51
+ >>> G = nx.Graph([(0, 1), (1, 2), (0, 2)])
52
+ >>> G.add_node(3)
53
+ >>> nx.is_eulerian(G)
54
+ False
55
+
56
+ >>> G.remove_nodes_from(list(nx.isolates(G)))
57
+ >>> nx.is_eulerian(G)
58
+ True
59
+
60
+
61
+ """
62
+ if G.is_directed():
63
+ # Every node must have equal in degree and out degree and the
64
+ # graph must be strongly connected
65
+ return all(
66
+ G.in_degree(n) == G.out_degree(n) for n in G
67
+ ) and nx.is_strongly_connected(G)
68
+ # An undirected Eulerian graph has no vertices of odd degree and
69
+ # must be connected.
70
+ return all(d % 2 == 0 for v, d in G.degree()) and nx.is_connected(G)
71
+
72
+
73
+ @nx._dispatchable
74
+ def is_semieulerian(G):
75
+ """Return True iff `G` is semi-Eulerian.
76
+
77
+ G is semi-Eulerian if it has an Eulerian path but no Eulerian circuit.
78
+
79
+ See Also
80
+ --------
81
+ has_eulerian_path
82
+ is_eulerian
83
+ """
84
+ return has_eulerian_path(G) and not is_eulerian(G)
85
+
86
+
87
+ def _find_path_start(G):
88
+ """Return a suitable starting vertex for an Eulerian path.
89
+
90
+ If no path exists, return None.
91
+ """
92
+ if not has_eulerian_path(G):
93
+ return None
94
+
95
+ if is_eulerian(G):
96
+ return arbitrary_element(G)
97
+
98
+ if G.is_directed():
99
+ v1, v2 = (v for v in G if G.in_degree(v) != G.out_degree(v))
100
+ # Determines which is the 'start' node (as opposed to the 'end')
101
+ if G.out_degree(v1) > G.in_degree(v1):
102
+ return v1
103
+ else:
104
+ return v2
105
+
106
+ else:
107
+ # In an undirected graph randomly choose one of the possibilities
108
+ start = [v for v in G if G.degree(v) % 2 != 0][0]
109
+ return start
110
+
111
+
112
+ def _simplegraph_eulerian_circuit(G, source):
113
+ if G.is_directed():
114
+ degree = G.out_degree
115
+ edges = G.out_edges
116
+ else:
117
+ degree = G.degree
118
+ edges = G.edges
119
+ vertex_stack = [source]
120
+ last_vertex = None
121
+ while vertex_stack:
122
+ current_vertex = vertex_stack[-1]
123
+ if degree(current_vertex) == 0:
124
+ if last_vertex is not None:
125
+ yield (last_vertex, current_vertex)
126
+ last_vertex = current_vertex
127
+ vertex_stack.pop()
128
+ else:
129
+ _, next_vertex = arbitrary_element(edges(current_vertex))
130
+ vertex_stack.append(next_vertex)
131
+ G.remove_edge(current_vertex, next_vertex)
132
+
133
+
134
+ def _multigraph_eulerian_circuit(G, source):
135
+ if G.is_directed():
136
+ degree = G.out_degree
137
+ edges = G.out_edges
138
+ else:
139
+ degree = G.degree
140
+ edges = G.edges
141
+ vertex_stack = [(source, None)]
142
+ last_vertex = None
143
+ last_key = None
144
+ while vertex_stack:
145
+ current_vertex, current_key = vertex_stack[-1]
146
+ if degree(current_vertex) == 0:
147
+ if last_vertex is not None:
148
+ yield (last_vertex, current_vertex, last_key)
149
+ last_vertex, last_key = current_vertex, current_key
150
+ vertex_stack.pop()
151
+ else:
152
+ triple = arbitrary_element(edges(current_vertex, keys=True))
153
+ _, next_vertex, next_key = triple
154
+ vertex_stack.append((next_vertex, next_key))
155
+ G.remove_edge(current_vertex, next_vertex, next_key)
156
+
157
+
158
+ @nx._dispatchable
159
+ def eulerian_circuit(G, source=None, keys=False):
160
+ """Returns an iterator over the edges of an Eulerian circuit in `G`.
161
+
162
+ An *Eulerian circuit* is a closed walk that includes each edge of a
163
+ graph exactly once.
164
+
165
+ Parameters
166
+ ----------
167
+ G : NetworkX graph
168
+ A graph, either directed or undirected.
169
+
170
+ source : node, optional
171
+ Starting node for circuit.
172
+
173
+ keys : bool
174
+ If False, edges generated by this function will be of the form
175
+ ``(u, v)``. Otherwise, edges will be of the form ``(u, v, k)``.
176
+ This option is ignored unless `G` is a multigraph.
177
+
178
+ Returns
179
+ -------
180
+ edges : iterator
181
+ An iterator over edges in the Eulerian circuit.
182
+
183
+ Raises
184
+ ------
185
+ NetworkXError
186
+ If the graph is not Eulerian.
187
+
188
+ See Also
189
+ --------
190
+ is_eulerian
191
+
192
+ Notes
193
+ -----
194
+ This is a linear time implementation of an algorithm adapted from [1]_.
195
+
196
+ For general information about Euler tours, see [2]_.
197
+
198
+ References
199
+ ----------
200
+ .. [1] J. Edmonds, E. L. Johnson.
201
+ Matching, Euler tours and the Chinese postman.
202
+ Mathematical programming, Volume 5, Issue 1 (1973), 111-114.
203
+ .. [2] https://en.wikipedia.org/wiki/Eulerian_path
204
+
205
+ Examples
206
+ --------
207
+ To get an Eulerian circuit in an undirected graph::
208
+
209
+ >>> G = nx.complete_graph(3)
210
+ >>> list(nx.eulerian_circuit(G))
211
+ [(0, 2), (2, 1), (1, 0)]
212
+ >>> list(nx.eulerian_circuit(G, source=1))
213
+ [(1, 2), (2, 0), (0, 1)]
214
+
215
+ To get the sequence of vertices in an Eulerian circuit::
216
+
217
+ >>> [u for u, v in nx.eulerian_circuit(G)]
218
+ [0, 2, 1]
219
+
220
+ """
221
+ if not is_eulerian(G):
222
+ raise nx.NetworkXError("G is not Eulerian.")
223
+ if G.is_directed():
224
+ G = G.reverse()
225
+ else:
226
+ G = G.copy()
227
+ if source is None:
228
+ source = arbitrary_element(G)
229
+ if G.is_multigraph():
230
+ for u, v, k in _multigraph_eulerian_circuit(G, source):
231
+ if keys:
232
+ yield u, v, k
233
+ else:
234
+ yield u, v
235
+ else:
236
+ yield from _simplegraph_eulerian_circuit(G, source)
237
+
238
+
239
+ @nx._dispatchable
240
+ def has_eulerian_path(G, source=None):
241
+ """Return True iff `G` has an Eulerian path.
242
+
243
+ An Eulerian path is a path in a graph which uses each edge of a graph
244
+ exactly once. If `source` is specified, then this function checks
245
+ whether an Eulerian path that starts at node `source` exists.
246
+
247
+ A directed graph has an Eulerian path iff:
248
+ - at most one vertex has out_degree - in_degree = 1,
249
+ - at most one vertex has in_degree - out_degree = 1,
250
+ - every other vertex has equal in_degree and out_degree,
251
+ - and all of its vertices belong to a single connected
252
+ component of the underlying undirected graph.
253
+
254
+ If `source` is not None, an Eulerian path starting at `source` exists if no
255
+ other node has out_degree - in_degree = 1. This is equivalent to either
256
+ there exists an Eulerian circuit or `source` has out_degree - in_degree = 1
257
+ and the conditions above hold.
258
+
259
+ An undirected graph has an Eulerian path iff:
260
+ - exactly zero or two vertices have odd degree,
261
+ - and all of its vertices belong to a single connected component.
262
+
263
+ If `source` is not None, an Eulerian path starting at `source` exists if
264
+ either there exists an Eulerian circuit or `source` has an odd degree and the
265
+ conditions above hold.
266
+
267
+ Graphs with isolated vertices (i.e. vertices with zero degree) are not considered
268
+ to have an Eulerian path. Therefore, if the graph is not connected (or not strongly
269
+ connected, for directed graphs), this function returns False.
270
+
271
+ Parameters
272
+ ----------
273
+ G : NetworkX Graph
274
+ The graph to find an euler path in.
275
+
276
+ source : node, optional
277
+ Starting node for path.
278
+
279
+ Returns
280
+ -------
281
+ Bool : True if G has an Eulerian path.
282
+
283
+ Examples
284
+ --------
285
+ If you prefer to allow graphs with isolated vertices to have Eulerian path,
286
+ you can first remove such vertices and then call `has_eulerian_path` as below example shows.
287
+
288
+ >>> G = nx.Graph([(0, 1), (1, 2), (0, 2)])
289
+ >>> G.add_node(3)
290
+ >>> nx.has_eulerian_path(G)
291
+ False
292
+
293
+ >>> G.remove_nodes_from(list(nx.isolates(G)))
294
+ >>> nx.has_eulerian_path(G)
295
+ True
296
+
297
+ See Also
298
+ --------
299
+ is_eulerian
300
+ eulerian_path
301
+ """
302
+ if nx.is_eulerian(G):
303
+ return True
304
+
305
+ if G.is_directed():
306
+ ins = G.in_degree
307
+ outs = G.out_degree
308
+ # Since we know it is not eulerian, outs - ins must be 1 for source
309
+ if source is not None and outs[source] - ins[source] != 1:
310
+ return False
311
+
312
+ unbalanced_ins = 0
313
+ unbalanced_outs = 0
314
+ for v in G:
315
+ if ins[v] - outs[v] == 1:
316
+ unbalanced_ins += 1
317
+ elif outs[v] - ins[v] == 1:
318
+ unbalanced_outs += 1
319
+ elif ins[v] != outs[v]:
320
+ return False
321
+
322
+ return (
323
+ unbalanced_ins <= 1 and unbalanced_outs <= 1 and nx.is_weakly_connected(G)
324
+ )
325
+ else:
326
+ # We know it is not eulerian, so degree of source must be odd.
327
+ if source is not None and G.degree[source] % 2 != 1:
328
+ return False
329
+
330
+ # Sum is 2 since we know it is not eulerian (which implies sum is 0)
331
+ return sum(d % 2 == 1 for v, d in G.degree()) == 2 and nx.is_connected(G)
332
+
333
+
334
+ @nx._dispatchable
335
+ def eulerian_path(G, source=None, keys=False):
336
+ """Return an iterator over the edges of an Eulerian path in `G`.
337
+
338
+ Parameters
339
+ ----------
340
+ G : NetworkX Graph
341
+ The graph in which to look for an eulerian path.
342
+ source : node or None (default: None)
343
+ The node at which to start the search. None means search over all
344
+ starting nodes.
345
+ keys : Bool (default: False)
346
+ Indicates whether to yield edge 3-tuples (u, v, edge_key).
347
+ The default yields edge 2-tuples
348
+
349
+ Yields
350
+ ------
351
+ Edge tuples along the eulerian path.
352
+
353
+ Warning: If `source` provided is not the start node of an Euler path
354
+ will raise error even if an Euler Path exists.
355
+ """
356
+ if not has_eulerian_path(G, source):
357
+ raise nx.NetworkXError("Graph has no Eulerian paths.")
358
+ if G.is_directed():
359
+ G = G.reverse()
360
+ if source is None or nx.is_eulerian(G) is False:
361
+ source = _find_path_start(G)
362
+ if G.is_multigraph():
363
+ for u, v, k in _multigraph_eulerian_circuit(G, source):
364
+ if keys:
365
+ yield u, v, k
366
+ else:
367
+ yield u, v
368
+ else:
369
+ yield from _simplegraph_eulerian_circuit(G, source)
370
+ else:
371
+ G = G.copy()
372
+ if source is None:
373
+ source = _find_path_start(G)
374
+ if G.is_multigraph():
375
+ if keys:
376
+ yield from reversed(
377
+ [(v, u, k) for u, v, k in _multigraph_eulerian_circuit(G, source)]
378
+ )
379
+ else:
380
+ yield from reversed(
381
+ [(v, u) for u, v, k in _multigraph_eulerian_circuit(G, source)]
382
+ )
383
+ else:
384
+ yield from reversed(
385
+ [(v, u) for u, v in _simplegraph_eulerian_circuit(G, source)]
386
+ )
387
+
388
+
389
+ @not_implemented_for("directed")
390
+ @nx._dispatchable(returns_graph=True)
391
+ def eulerize(G):
392
+ """Transforms a graph into an Eulerian graph.
393
+
394
+ If `G` is Eulerian the result is `G` as a MultiGraph, otherwise the result is a smallest
395
+ (in terms of the number of edges) multigraph whose underlying simple graph is `G`.
396
+
397
+ Parameters
398
+ ----------
399
+ G : NetworkX graph
400
+ An undirected graph
401
+
402
+ Returns
403
+ -------
404
+ G : NetworkX multigraph
405
+
406
+ Raises
407
+ ------
408
+ NetworkXError
409
+ If the graph is not connected.
410
+
411
+ See Also
412
+ --------
413
+ is_eulerian
414
+ eulerian_circuit
415
+
416
+ References
417
+ ----------
418
+ .. [1] J. Edmonds, E. L. Johnson.
419
+ Matching, Euler tours and the Chinese postman.
420
+ Mathematical programming, Volume 5, Issue 1 (1973), 111-114.
421
+ .. [2] https://en.wikipedia.org/wiki/Eulerian_path
422
+ .. [3] http://web.math.princeton.edu/math_alive/5/Notes1.pdf
423
+
424
+ Examples
425
+ --------
426
+ >>> G = nx.complete_graph(10)
427
+ >>> H = nx.eulerize(G)
428
+ >>> nx.is_eulerian(H)
429
+ True
430
+
431
+ """
432
+ if G.order() == 0:
433
+ raise nx.NetworkXPointlessConcept("Cannot Eulerize null graph")
434
+ if not nx.is_connected(G):
435
+ raise nx.NetworkXError("G is not connected")
436
+ odd_degree_nodes = [n for n, d in G.degree() if d % 2 == 1]
437
+ G = nx.MultiGraph(G)
438
+ if len(odd_degree_nodes) == 0:
439
+ return G
440
+
441
+ # get all shortest paths between vertices of odd degree
442
+ odd_deg_pairs_paths = [
443
+ (m, {n: nx.shortest_path(G, source=m, target=n)})
444
+ for m, n in combinations(odd_degree_nodes, 2)
445
+ ]
446
+
447
+ # use the number of vertices in a graph + 1 as an upper bound on
448
+ # the maximum length of a path in G
449
+ upper_bound_on_max_path_length = len(G) + 1
450
+
451
+ # use "len(G) + 1 - len(P)",
452
+ # where P is a shortest path between vertices n and m,
453
+ # as edge-weights in a new graph
454
+ # store the paths in the graph for easy indexing later
455
+ Gp = nx.Graph()
456
+ for n, Ps in odd_deg_pairs_paths:
457
+ for m, P in Ps.items():
458
+ if n != m:
459
+ Gp.add_edge(
460
+ m, n, weight=upper_bound_on_max_path_length - len(P), path=P
461
+ )
462
+
463
+ # find the minimum weight matching of edges in the weighted graph
464
+ best_matching = nx.Graph(list(nx.max_weight_matching(Gp)))
465
+
466
+ # duplicate each edge along each path in the set of paths in Gp
467
+ for m, n in best_matching.edges():
468
+ path = Gp[m][n]["path"]
469
+ G.add_edges_from(nx.utils.pairwise(path))
470
+ return G
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .maxflow import *
2
+ from .mincost import *
3
+ from .boykovkolmogorov import *
4
+ from .dinitz_alg import *
5
+ from .edmondskarp import *
6
+ from .gomory_hu import *
7
+ from .preflowpush import *
8
+ from .shortestaugmentingpath import *
9
+ from .capacityscaling import *
10
+ from .networksimplex import *
11
+ from .utils import build_flow_dict, build_residual_network
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/boykovkolmogorov.py ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Boykov-Kolmogorov algorithm for maximum flow problems.
3
+ """
4
+
5
+ from collections import deque
6
+ from operator import itemgetter
7
+
8
+ import networkx as nx
9
+ from networkx.algorithms.flow.utils import build_residual_network
10
+
11
+ __all__ = ["boykov_kolmogorov"]
12
+
13
+
14
+ @nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True)
15
+ def boykov_kolmogorov(
16
+ G, s, t, capacity="capacity", residual=None, value_only=False, cutoff=None
17
+ ):
18
+ r"""Find a maximum single-commodity flow using Boykov-Kolmogorov algorithm.
19
+
20
+ This function returns the residual network resulting after computing
21
+ the maximum flow. See below for details about the conventions
22
+ NetworkX uses for defining residual networks.
23
+
24
+ This algorithm has worse case complexity $O(n^2 m |C|)$ for $n$ nodes, $m$
25
+ edges, and $|C|$ the cost of the minimum cut [1]_. This implementation
26
+ uses the marking heuristic defined in [2]_ which improves its running
27
+ time in many practical problems.
28
+
29
+ Parameters
30
+ ----------
31
+ G : NetworkX graph
32
+ Edges of the graph are expected to have an attribute called
33
+ 'capacity'. If this attribute is not present, the edge is
34
+ considered to have infinite capacity.
35
+
36
+ s : node
37
+ Source node for the flow.
38
+
39
+ t : node
40
+ Sink node for the flow.
41
+
42
+ capacity : string
43
+ Edges of the graph G are expected to have an attribute capacity
44
+ that indicates how much flow the edge can support. If this
45
+ attribute is not present, the edge is considered to have
46
+ infinite capacity. Default value: 'capacity'.
47
+
48
+ residual : NetworkX graph
49
+ Residual network on which the algorithm is to be executed. If None, a
50
+ new residual network is created. Default value: None.
51
+
52
+ value_only : bool
53
+ If True compute only the value of the maximum flow. This parameter
54
+ will be ignored by this algorithm because it is not applicable.
55
+
56
+ cutoff : integer, float
57
+ If specified, the algorithm will terminate when the flow value reaches
58
+ or exceeds the cutoff. In this case, it may be unable to immediately
59
+ determine a minimum cut. Default value: None.
60
+
61
+ Returns
62
+ -------
63
+ R : NetworkX DiGraph
64
+ Residual network after computing the maximum flow.
65
+
66
+ Raises
67
+ ------
68
+ NetworkXError
69
+ The algorithm does not support MultiGraph and MultiDiGraph. If
70
+ the input graph is an instance of one of these two classes, a
71
+ NetworkXError is raised.
72
+
73
+ NetworkXUnbounded
74
+ If the graph has a path of infinite capacity, the value of a
75
+ feasible flow on the graph is unbounded above and the function
76
+ raises a NetworkXUnbounded.
77
+
78
+ See also
79
+ --------
80
+ :meth:`maximum_flow`
81
+ :meth:`minimum_cut`
82
+ :meth:`preflow_push`
83
+ :meth:`shortest_augmenting_path`
84
+
85
+ Notes
86
+ -----
87
+ The residual network :samp:`R` from an input graph :samp:`G` has the
88
+ same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
89
+ of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
90
+ self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
91
+ in :samp:`G`.
92
+
93
+ For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
94
+ is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
95
+ in :samp:`G` or zero otherwise. If the capacity is infinite,
96
+ :samp:`R[u][v]['capacity']` will have a high arbitrary finite value
97
+ that does not affect the solution of the problem. This value is stored in
98
+ :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
99
+ :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
100
+ satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
101
+
102
+ The flow value, defined as the total flow into :samp:`t`, the sink, is
103
+ stored in :samp:`R.graph['flow_value']`. If :samp:`cutoff` is not
104
+ specified, reachability to :samp:`t` using only edges :samp:`(u, v)` such
105
+ that :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
106
+ :samp:`s`-:samp:`t` cut.
107
+
108
+ Examples
109
+ --------
110
+ >>> from networkx.algorithms.flow import boykov_kolmogorov
111
+
112
+ The functions that implement flow algorithms and output a residual
113
+ network, such as this one, are not imported to the base NetworkX
114
+ namespace, so you have to explicitly import them from the flow package.
115
+
116
+ >>> G = nx.DiGraph()
117
+ >>> G.add_edge("x", "a", capacity=3.0)
118
+ >>> G.add_edge("x", "b", capacity=1.0)
119
+ >>> G.add_edge("a", "c", capacity=3.0)
120
+ >>> G.add_edge("b", "c", capacity=5.0)
121
+ >>> G.add_edge("b", "d", capacity=4.0)
122
+ >>> G.add_edge("d", "e", capacity=2.0)
123
+ >>> G.add_edge("c", "y", capacity=2.0)
124
+ >>> G.add_edge("e", "y", capacity=3.0)
125
+ >>> R = boykov_kolmogorov(G, "x", "y")
126
+ >>> flow_value = nx.maximum_flow_value(G, "x", "y")
127
+ >>> flow_value
128
+ 3.0
129
+ >>> flow_value == R.graph["flow_value"]
130
+ True
131
+
132
+ A nice feature of the Boykov-Kolmogorov algorithm is that a partition
133
+ of the nodes that defines a minimum cut can be easily computed based
134
+ on the search trees used during the algorithm. These trees are stored
135
+ in the graph attribute `trees` of the residual network.
136
+
137
+ >>> source_tree, target_tree = R.graph["trees"]
138
+ >>> partition = (set(source_tree), set(G) - set(source_tree))
139
+
140
+ Or equivalently:
141
+
142
+ >>> partition = (set(G) - set(target_tree), set(target_tree))
143
+
144
+ References
145
+ ----------
146
+ .. [1] Boykov, Y., & Kolmogorov, V. (2004). An experimental comparison
147
+ of min-cut/max-flow algorithms for energy minimization in vision.
148
+ Pattern Analysis and Machine Intelligence, IEEE Transactions on,
149
+ 26(9), 1124-1137.
150
+ https://doi.org/10.1109/TPAMI.2004.60
151
+
152
+ .. [2] Vladimir Kolmogorov. Graph-based Algorithms for Multi-camera
153
+ Reconstruction Problem. PhD thesis, Cornell University, CS Department,
154
+ 2003. pp. 109-114.
155
+ https://web.archive.org/web/20170809091249/https://pub.ist.ac.at/~vnk/papers/thesis.pdf
156
+
157
+ """
158
+ R = boykov_kolmogorov_impl(G, s, t, capacity, residual, cutoff)
159
+ R.graph["algorithm"] = "boykov_kolmogorov"
160
+ nx._clear_cache(R)
161
+ return R
162
+
163
+
164
+ def boykov_kolmogorov_impl(G, s, t, capacity, residual, cutoff):
165
+ if s not in G:
166
+ raise nx.NetworkXError(f"node {str(s)} not in graph")
167
+ if t not in G:
168
+ raise nx.NetworkXError(f"node {str(t)} not in graph")
169
+ if s == t:
170
+ raise nx.NetworkXError("source and sink are the same node")
171
+
172
+ if residual is None:
173
+ R = build_residual_network(G, capacity)
174
+ else:
175
+ R = residual
176
+
177
+ # Initialize/reset the residual network.
178
+ # This is way too slow
179
+ # nx.set_edge_attributes(R, 0, 'flow')
180
+ for u in R:
181
+ for e in R[u].values():
182
+ e["flow"] = 0
183
+
184
+ # Use an arbitrary high value as infinite. It is computed
185
+ # when building the residual network.
186
+ INF = R.graph["inf"]
187
+
188
+ if cutoff is None:
189
+ cutoff = INF
190
+
191
+ R_succ = R.succ
192
+ R_pred = R.pred
193
+
194
+ def grow():
195
+ """Bidirectional breadth-first search for the growth stage.
196
+
197
+ Returns a connecting edge, that is and edge that connects
198
+ a node from the source search tree with a node from the
199
+ target search tree.
200
+ The first node in the connecting edge is always from the
201
+ source tree and the last node from the target tree.
202
+ """
203
+ while active:
204
+ u = active[0]
205
+ if u in source_tree:
206
+ this_tree = source_tree
207
+ other_tree = target_tree
208
+ neighbors = R_succ
209
+ else:
210
+ this_tree = target_tree
211
+ other_tree = source_tree
212
+ neighbors = R_pred
213
+ for v, attr in neighbors[u].items():
214
+ if attr["capacity"] - attr["flow"] > 0:
215
+ if v not in this_tree:
216
+ if v in other_tree:
217
+ return (u, v) if this_tree is source_tree else (v, u)
218
+ this_tree[v] = u
219
+ dist[v] = dist[u] + 1
220
+ timestamp[v] = timestamp[u]
221
+ active.append(v)
222
+ elif v in this_tree and _is_closer(u, v):
223
+ this_tree[v] = u
224
+ dist[v] = dist[u] + 1
225
+ timestamp[v] = timestamp[u]
226
+ _ = active.popleft()
227
+ return None, None
228
+
229
+ def augment(u, v):
230
+ """Augmentation stage.
231
+
232
+ Reconstruct path and determine its residual capacity.
233
+ We start from a connecting edge, which links a node
234
+ from the source tree to a node from the target tree.
235
+ The connecting edge is the output of the grow function
236
+ and the input of this function.
237
+ """
238
+ attr = R_succ[u][v]
239
+ flow = min(INF, attr["capacity"] - attr["flow"])
240
+ path = [u]
241
+ # Trace a path from u to s in source_tree.
242
+ w = u
243
+ while w != s:
244
+ n = w
245
+ w = source_tree[n]
246
+ attr = R_pred[n][w]
247
+ flow = min(flow, attr["capacity"] - attr["flow"])
248
+ path.append(w)
249
+ path.reverse()
250
+ # Trace a path from v to t in target_tree.
251
+ path.append(v)
252
+ w = v
253
+ while w != t:
254
+ n = w
255
+ w = target_tree[n]
256
+ attr = R_succ[n][w]
257
+ flow = min(flow, attr["capacity"] - attr["flow"])
258
+ path.append(w)
259
+ # Augment flow along the path and check for saturated edges.
260
+ it = iter(path)
261
+ u = next(it)
262
+ these_orphans = []
263
+ for v in it:
264
+ R_succ[u][v]["flow"] += flow
265
+ R_succ[v][u]["flow"] -= flow
266
+ if R_succ[u][v]["flow"] == R_succ[u][v]["capacity"]:
267
+ if v in source_tree:
268
+ source_tree[v] = None
269
+ these_orphans.append(v)
270
+ if u in target_tree:
271
+ target_tree[u] = None
272
+ these_orphans.append(u)
273
+ u = v
274
+ orphans.extend(sorted(these_orphans, key=dist.get))
275
+ return flow
276
+
277
+ def adopt():
278
+ """Adoption stage.
279
+
280
+ Reconstruct search trees by adopting or discarding orphans.
281
+ During augmentation stage some edges got saturated and thus
282
+ the source and target search trees broke down to forests, with
283
+ orphans as roots of some of its trees. We have to reconstruct
284
+ the search trees rooted to source and target before we can grow
285
+ them again.
286
+ """
287
+ while orphans:
288
+ u = orphans.popleft()
289
+ if u in source_tree:
290
+ tree = source_tree
291
+ neighbors = R_pred
292
+ else:
293
+ tree = target_tree
294
+ neighbors = R_succ
295
+ nbrs = ((n, attr, dist[n]) for n, attr in neighbors[u].items() if n in tree)
296
+ for v, attr, d in sorted(nbrs, key=itemgetter(2)):
297
+ if attr["capacity"] - attr["flow"] > 0:
298
+ if _has_valid_root(v, tree):
299
+ tree[u] = v
300
+ dist[u] = dist[v] + 1
301
+ timestamp[u] = time
302
+ break
303
+ else:
304
+ nbrs = (
305
+ (n, attr, dist[n]) for n, attr in neighbors[u].items() if n in tree
306
+ )
307
+ for v, attr, d in sorted(nbrs, key=itemgetter(2)):
308
+ if attr["capacity"] - attr["flow"] > 0:
309
+ if v not in active:
310
+ active.append(v)
311
+ if tree[v] == u:
312
+ tree[v] = None
313
+ orphans.appendleft(v)
314
+ if u in active:
315
+ active.remove(u)
316
+ del tree[u]
317
+
318
+ def _has_valid_root(n, tree):
319
+ path = []
320
+ v = n
321
+ while v is not None:
322
+ path.append(v)
323
+ if v in (s, t):
324
+ base_dist = 0
325
+ break
326
+ elif timestamp[v] == time:
327
+ base_dist = dist[v]
328
+ break
329
+ v = tree[v]
330
+ else:
331
+ return False
332
+ length = len(path)
333
+ for i, u in enumerate(path, 1):
334
+ dist[u] = base_dist + length - i
335
+ timestamp[u] = time
336
+ return True
337
+
338
+ def _is_closer(u, v):
339
+ return timestamp[v] <= timestamp[u] and dist[v] > dist[u] + 1
340
+
341
+ source_tree = {s: None}
342
+ target_tree = {t: None}
343
+ active = deque([s, t])
344
+ orphans = deque()
345
+ flow_value = 0
346
+ # data structures for the marking heuristic
347
+ time = 1
348
+ timestamp = {s: time, t: time}
349
+ dist = {s: 0, t: 0}
350
+ while flow_value < cutoff:
351
+ # Growth stage
352
+ u, v = grow()
353
+ if u is None:
354
+ break
355
+ time += 1
356
+ # Augmentation stage
357
+ flow_value += augment(u, v)
358
+ # Adoption stage
359
+ adopt()
360
+
361
+ if flow_value * 2 > INF:
362
+ raise nx.NetworkXUnbounded("Infinite capacity path, flow unbounded above.")
363
+
364
+ # Add source and target tree in a graph attribute.
365
+ # A partition that defines a minimum cut can be directly
366
+ # computed from the search trees as explained in the docstrings.
367
+ R.graph["trees"] = (source_tree, target_tree)
368
+ # Add the standard flow_value graph attribute.
369
+ R.graph["flow_value"] = flow_value
370
+ return R
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/capacityscaling.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Capacity scaling minimum cost flow algorithm.
3
+ """
4
+
5
+ __all__ = ["capacity_scaling"]
6
+
7
+ from itertools import chain
8
+ from math import log
9
+
10
+ import networkx as nx
11
+
12
+ from ...utils import BinaryHeap, arbitrary_element, not_implemented_for
13
+
14
+
15
+ def _detect_unboundedness(R):
16
+ """Detect infinite-capacity negative cycles."""
17
+ G = nx.DiGraph()
18
+ G.add_nodes_from(R)
19
+
20
+ # Value simulating infinity.
21
+ inf = R.graph["inf"]
22
+ # True infinity.
23
+ f_inf = float("inf")
24
+ for u in R:
25
+ for v, e in R[u].items():
26
+ # Compute the minimum weight of infinite-capacity (u, v) edges.
27
+ w = f_inf
28
+ for k, e in e.items():
29
+ if e["capacity"] == inf:
30
+ w = min(w, e["weight"])
31
+ if w != f_inf:
32
+ G.add_edge(u, v, weight=w)
33
+
34
+ if nx.negative_edge_cycle(G):
35
+ raise nx.NetworkXUnbounded(
36
+ "Negative cost cycle of infinite capacity found. "
37
+ "Min cost flow may be unbounded below."
38
+ )
39
+
40
+
41
+ @not_implemented_for("undirected")
42
+ def _build_residual_network(G, demand, capacity, weight):
43
+ """Build a residual network and initialize a zero flow."""
44
+ if sum(G.nodes[u].get(demand, 0) for u in G) != 0:
45
+ raise nx.NetworkXUnfeasible("Sum of the demands should be 0.")
46
+
47
+ R = nx.MultiDiGraph()
48
+ R.add_nodes_from(
49
+ (u, {"excess": -G.nodes[u].get(demand, 0), "potential": 0}) for u in G
50
+ )
51
+
52
+ inf = float("inf")
53
+ # Detect selfloops with infinite capacities and negative weights.
54
+ for u, v, e in nx.selfloop_edges(G, data=True):
55
+ if e.get(weight, 0) < 0 and e.get(capacity, inf) == inf:
56
+ raise nx.NetworkXUnbounded(
57
+ "Negative cost cycle of infinite capacity found. "
58
+ "Min cost flow may be unbounded below."
59
+ )
60
+
61
+ # Extract edges with positive capacities. Self loops excluded.
62
+ if G.is_multigraph():
63
+ edge_list = [
64
+ (u, v, k, e)
65
+ for u, v, k, e in G.edges(data=True, keys=True)
66
+ if u != v and e.get(capacity, inf) > 0
67
+ ]
68
+ else:
69
+ edge_list = [
70
+ (u, v, 0, e)
71
+ for u, v, e in G.edges(data=True)
72
+ if u != v and e.get(capacity, inf) > 0
73
+ ]
74
+ # Simulate infinity with the larger of the sum of absolute node imbalances
75
+ # the sum of finite edge capacities or any positive value if both sums are
76
+ # zero. This allows the infinite-capacity edges to be distinguished for
77
+ # unboundedness detection and directly participate in residual capacity
78
+ # calculation.
79
+ inf = (
80
+ max(
81
+ sum(abs(R.nodes[u]["excess"]) for u in R),
82
+ 2
83
+ * sum(
84
+ e[capacity]
85
+ for u, v, k, e in edge_list
86
+ if capacity in e and e[capacity] != inf
87
+ ),
88
+ )
89
+ or 1
90
+ )
91
+ for u, v, k, e in edge_list:
92
+ r = min(e.get(capacity, inf), inf)
93
+ w = e.get(weight, 0)
94
+ # Add both (u, v) and (v, u) into the residual network marked with the
95
+ # original key. (key[1] == True) indicates the (u, v) is in the
96
+ # original network.
97
+ R.add_edge(u, v, key=(k, True), capacity=r, weight=w, flow=0)
98
+ R.add_edge(v, u, key=(k, False), capacity=0, weight=-w, flow=0)
99
+
100
+ # Record the value simulating infinity.
101
+ R.graph["inf"] = inf
102
+
103
+ _detect_unboundedness(R)
104
+
105
+ return R
106
+
107
+
108
+ def _build_flow_dict(G, R, capacity, weight):
109
+ """Build a flow dictionary from a residual network."""
110
+ inf = float("inf")
111
+ flow_dict = {}
112
+ if G.is_multigraph():
113
+ for u in G:
114
+ flow_dict[u] = {}
115
+ for v, es in G[u].items():
116
+ flow_dict[u][v] = {
117
+ # Always saturate negative selfloops.
118
+ k: (
119
+ 0
120
+ if (
121
+ u != v or e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0
122
+ )
123
+ else e[capacity]
124
+ )
125
+ for k, e in es.items()
126
+ }
127
+ for v, es in R[u].items():
128
+ if v in flow_dict[u]:
129
+ flow_dict[u][v].update(
130
+ (k[0], e["flow"]) for k, e in es.items() if e["flow"] > 0
131
+ )
132
+ else:
133
+ for u in G:
134
+ flow_dict[u] = {
135
+ # Always saturate negative selfloops.
136
+ v: (
137
+ 0
138
+ if (u != v or e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0)
139
+ else e[capacity]
140
+ )
141
+ for v, e in G[u].items()
142
+ }
143
+ flow_dict[u].update(
144
+ (v, e["flow"])
145
+ for v, es in R[u].items()
146
+ for e in es.values()
147
+ if e["flow"] > 0
148
+ )
149
+ return flow_dict
150
+
151
+
152
+ @nx._dispatchable(
153
+ node_attrs="demand", edge_attrs={"capacity": float("inf"), "weight": 0}
154
+ )
155
+ def capacity_scaling(
156
+ G, demand="demand", capacity="capacity", weight="weight", heap=BinaryHeap
157
+ ):
158
+ r"""Find a minimum cost flow satisfying all demands in digraph G.
159
+
160
+ This is a capacity scaling successive shortest augmenting path algorithm.
161
+
162
+ G is a digraph with edge costs and capacities and in which nodes
163
+ have demand, i.e., they want to send or receive some amount of
164
+ flow. A negative demand means that the node wants to send flow, a
165
+ positive demand means that the node want to receive flow. A flow on
166
+ the digraph G satisfies all demand if the net flow into each node
167
+ is equal to the demand of that node.
168
+
169
+ Parameters
170
+ ----------
171
+ G : NetworkX graph
172
+ DiGraph or MultiDiGraph on which a minimum cost flow satisfying all
173
+ demands is to be found.
174
+
175
+ demand : string
176
+ Nodes of the graph G are expected to have an attribute demand
177
+ that indicates how much flow a node wants to send (negative
178
+ demand) or receive (positive demand). Note that the sum of the
179
+ demands should be 0 otherwise the problem in not feasible. If
180
+ this attribute is not present, a node is considered to have 0
181
+ demand. Default value: 'demand'.
182
+
183
+ capacity : string
184
+ Edges of the graph G are expected to have an attribute capacity
185
+ that indicates how much flow the edge can support. If this
186
+ attribute is not present, the edge is considered to have
187
+ infinite capacity. Default value: 'capacity'.
188
+
189
+ weight : string
190
+ Edges of the graph G are expected to have an attribute weight
191
+ that indicates the cost incurred by sending one unit of flow on
192
+ that edge. If not present, the weight is considered to be 0.
193
+ Default value: 'weight'.
194
+
195
+ heap : class
196
+ Type of heap to be used in the algorithm. It should be a subclass of
197
+ :class:`MinHeap` or implement a compatible interface.
198
+
199
+ If a stock heap implementation is to be used, :class:`BinaryHeap` is
200
+ recommended over :class:`PairingHeap` for Python implementations without
201
+ optimized attribute accesses (e.g., CPython) despite a slower
202
+ asymptotic running time. For Python implementations with optimized
203
+ attribute accesses (e.g., PyPy), :class:`PairingHeap` provides better
204
+ performance. Default value: :class:`BinaryHeap`.
205
+
206
+ Returns
207
+ -------
208
+ flowCost : integer
209
+ Cost of a minimum cost flow satisfying all demands.
210
+
211
+ flowDict : dictionary
212
+ If G is a digraph, a dict-of-dicts keyed by nodes such that
213
+ flowDict[u][v] is the flow on edge (u, v).
214
+ If G is a MultiDiGraph, a dict-of-dicts-of-dicts keyed by nodes
215
+ so that flowDict[u][v][key] is the flow on edge (u, v, key).
216
+
217
+ Raises
218
+ ------
219
+ NetworkXError
220
+ This exception is raised if the input graph is not directed,
221
+ not connected.
222
+
223
+ NetworkXUnfeasible
224
+ This exception is raised in the following situations:
225
+
226
+ * The sum of the demands is not zero. Then, there is no
227
+ flow satisfying all demands.
228
+ * There is no flow satisfying all demand.
229
+
230
+ NetworkXUnbounded
231
+ This exception is raised if the digraph G has a cycle of
232
+ negative cost and infinite capacity. Then, the cost of a flow
233
+ satisfying all demands is unbounded below.
234
+
235
+ Notes
236
+ -----
237
+ This algorithm does not work if edge weights are floating-point numbers.
238
+
239
+ See also
240
+ --------
241
+ :meth:`network_simplex`
242
+
243
+ Examples
244
+ --------
245
+ A simple example of a min cost flow problem.
246
+
247
+ >>> G = nx.DiGraph()
248
+ >>> G.add_node("a", demand=-5)
249
+ >>> G.add_node("d", demand=5)
250
+ >>> G.add_edge("a", "b", weight=3, capacity=4)
251
+ >>> G.add_edge("a", "c", weight=6, capacity=10)
252
+ >>> G.add_edge("b", "d", weight=1, capacity=9)
253
+ >>> G.add_edge("c", "d", weight=2, capacity=5)
254
+ >>> flowCost, flowDict = nx.capacity_scaling(G)
255
+ >>> flowCost
256
+ 24
257
+ >>> flowDict
258
+ {'a': {'b': 4, 'c': 1}, 'd': {}, 'b': {'d': 4}, 'c': {'d': 1}}
259
+
260
+ It is possible to change the name of the attributes used for the
261
+ algorithm.
262
+
263
+ >>> G = nx.DiGraph()
264
+ >>> G.add_node("p", spam=-4)
265
+ >>> G.add_node("q", spam=2)
266
+ >>> G.add_node("a", spam=-2)
267
+ >>> G.add_node("d", spam=-1)
268
+ >>> G.add_node("t", spam=2)
269
+ >>> G.add_node("w", spam=3)
270
+ >>> G.add_edge("p", "q", cost=7, vacancies=5)
271
+ >>> G.add_edge("p", "a", cost=1, vacancies=4)
272
+ >>> G.add_edge("q", "d", cost=2, vacancies=3)
273
+ >>> G.add_edge("t", "q", cost=1, vacancies=2)
274
+ >>> G.add_edge("a", "t", cost=2, vacancies=4)
275
+ >>> G.add_edge("d", "w", cost=3, vacancies=4)
276
+ >>> G.add_edge("t", "w", cost=4, vacancies=1)
277
+ >>> flowCost, flowDict = nx.capacity_scaling(
278
+ ... G, demand="spam", capacity="vacancies", weight="cost"
279
+ ... )
280
+ >>> flowCost
281
+ 37
282
+ >>> flowDict
283
+ {'p': {'q': 2, 'a': 2}, 'q': {'d': 1}, 'a': {'t': 4}, 'd': {'w': 2}, 't': {'q': 1, 'w': 1}, 'w': {}}
284
+ """
285
+ R = _build_residual_network(G, demand, capacity, weight)
286
+
287
+ inf = float("inf")
288
+ # Account cost of negative selfloops.
289
+ flow_cost = sum(
290
+ 0
291
+ if e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0
292
+ else e[capacity] * e[weight]
293
+ for u, v, e in nx.selfloop_edges(G, data=True)
294
+ )
295
+
296
+ # Determine the maximum edge capacity.
297
+ wmax = max(chain([-inf], (e["capacity"] for u, v, e in R.edges(data=True))))
298
+ if wmax == -inf:
299
+ # Residual network has no edges.
300
+ return flow_cost, _build_flow_dict(G, R, capacity, weight)
301
+
302
+ R_nodes = R.nodes
303
+ R_succ = R.succ
304
+
305
+ delta = 2 ** int(log(wmax, 2))
306
+ while delta >= 1:
307
+ # Saturate Δ-residual edges with negative reduced costs to achieve
308
+ # Δ-optimality.
309
+ for u in R:
310
+ p_u = R_nodes[u]["potential"]
311
+ for v, es in R_succ[u].items():
312
+ for k, e in es.items():
313
+ flow = e["capacity"] - e["flow"]
314
+ if e["weight"] - p_u + R_nodes[v]["potential"] < 0:
315
+ flow = e["capacity"] - e["flow"]
316
+ if flow >= delta:
317
+ e["flow"] += flow
318
+ R_succ[v][u][(k[0], not k[1])]["flow"] -= flow
319
+ R_nodes[u]["excess"] -= flow
320
+ R_nodes[v]["excess"] += flow
321
+ # Determine the Δ-active nodes.
322
+ S = set()
323
+ T = set()
324
+ S_add = S.add
325
+ S_remove = S.remove
326
+ T_add = T.add
327
+ T_remove = T.remove
328
+ for u in R:
329
+ excess = R_nodes[u]["excess"]
330
+ if excess >= delta:
331
+ S_add(u)
332
+ elif excess <= -delta:
333
+ T_add(u)
334
+ # Repeatedly augment flow from S to T along shortest paths until
335
+ # Δ-feasibility is achieved.
336
+ while S and T:
337
+ s = arbitrary_element(S)
338
+ t = None
339
+ # Search for a shortest path in terms of reduce costs from s to
340
+ # any t in T in the Δ-residual network.
341
+ d = {}
342
+ pred = {s: None}
343
+ h = heap()
344
+ h_insert = h.insert
345
+ h_get = h.get
346
+ h_insert(s, 0)
347
+ while h:
348
+ u, d_u = h.pop()
349
+ d[u] = d_u
350
+ if u in T:
351
+ # Path found.
352
+ t = u
353
+ break
354
+ p_u = R_nodes[u]["potential"]
355
+ for v, es in R_succ[u].items():
356
+ if v in d:
357
+ continue
358
+ wmin = inf
359
+ # Find the minimum-weighted (u, v) Δ-residual edge.
360
+ for k, e in es.items():
361
+ if e["capacity"] - e["flow"] >= delta:
362
+ w = e["weight"]
363
+ if w < wmin:
364
+ wmin = w
365
+ kmin = k
366
+ emin = e
367
+ if wmin == inf:
368
+ continue
369
+ # Update the distance label of v.
370
+ d_v = d_u + wmin - p_u + R_nodes[v]["potential"]
371
+ if h_insert(v, d_v):
372
+ pred[v] = (u, kmin, emin)
373
+ if t is not None:
374
+ # Augment Δ units of flow from s to t.
375
+ while u != s:
376
+ v = u
377
+ u, k, e = pred[v]
378
+ e["flow"] += delta
379
+ R_succ[v][u][(k[0], not k[1])]["flow"] -= delta
380
+ # Account node excess and deficit.
381
+ R_nodes[s]["excess"] -= delta
382
+ R_nodes[t]["excess"] += delta
383
+ if R_nodes[s]["excess"] < delta:
384
+ S_remove(s)
385
+ if R_nodes[t]["excess"] > -delta:
386
+ T_remove(t)
387
+ # Update node potentials.
388
+ d_t = d[t]
389
+ for u, d_u in d.items():
390
+ R_nodes[u]["potential"] -= d_u - d_t
391
+ else:
392
+ # Path not found.
393
+ S_remove(s)
394
+ delta //= 2
395
+
396
+ if any(R.nodes[u]["excess"] != 0 for u in R):
397
+ raise nx.NetworkXUnfeasible("No flow satisfying all demands.")
398
+
399
+ # Calculate the flow cost.
400
+ for u in R:
401
+ for v, es in R_succ[u].items():
402
+ for e in es.values():
403
+ flow = e["flow"]
404
+ if flow > 0:
405
+ flow_cost += flow * e["weight"]
406
+
407
+ return flow_cost, _build_flow_dict(G, R, capacity, weight)
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/dinitz_alg.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dinitz' algorithm for maximum flow problems.
3
+ """
4
+
5
+ from collections import deque
6
+
7
+ import networkx as nx
8
+ from networkx.algorithms.flow.utils import build_residual_network
9
+ from networkx.utils import pairwise
10
+
11
+ __all__ = ["dinitz"]
12
+
13
+
14
+ @nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True)
15
+ def dinitz(G, s, t, capacity="capacity", residual=None, value_only=False, cutoff=None):
16
+ """Find a maximum single-commodity flow using Dinitz' algorithm.
17
+
18
+ This function returns the residual network resulting after computing
19
+ the maximum flow. See below for details about the conventions
20
+ NetworkX uses for defining residual networks.
21
+
22
+ This algorithm has a running time of $O(n^2 m)$ for $n$ nodes and $m$
23
+ edges [1]_.
24
+
25
+
26
+ Parameters
27
+ ----------
28
+ G : NetworkX graph
29
+ Edges of the graph are expected to have an attribute called
30
+ 'capacity'. If this attribute is not present, the edge is
31
+ considered to have infinite capacity.
32
+
33
+ s : node
34
+ Source node for the flow.
35
+
36
+ t : node
37
+ Sink node for the flow.
38
+
39
+ capacity : string
40
+ Edges of the graph G are expected to have an attribute capacity
41
+ that indicates how much flow the edge can support. If this
42
+ attribute is not present, the edge is considered to have
43
+ infinite capacity. Default value: 'capacity'.
44
+
45
+ residual : NetworkX graph
46
+ Residual network on which the algorithm is to be executed. If None, a
47
+ new residual network is created. Default value: None.
48
+
49
+ value_only : bool
50
+ If True compute only the value of the maximum flow. This parameter
51
+ will be ignored by this algorithm because it is not applicable.
52
+
53
+ cutoff : integer, float
54
+ If specified, the algorithm will terminate when the flow value reaches
55
+ or exceeds the cutoff. In this case, it may be unable to immediately
56
+ determine a minimum cut. Default value: None.
57
+
58
+ Returns
59
+ -------
60
+ R : NetworkX DiGraph
61
+ Residual network after computing the maximum flow.
62
+
63
+ Raises
64
+ ------
65
+ NetworkXError
66
+ The algorithm does not support MultiGraph and MultiDiGraph. If
67
+ the input graph is an instance of one of these two classes, a
68
+ NetworkXError is raised.
69
+
70
+ NetworkXUnbounded
71
+ If the graph has a path of infinite capacity, the value of a
72
+ feasible flow on the graph is unbounded above and the function
73
+ raises a NetworkXUnbounded.
74
+
75
+ See also
76
+ --------
77
+ :meth:`maximum_flow`
78
+ :meth:`minimum_cut`
79
+ :meth:`preflow_push`
80
+ :meth:`shortest_augmenting_path`
81
+
82
+ Notes
83
+ -----
84
+ The residual network :samp:`R` from an input graph :samp:`G` has the
85
+ same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
86
+ of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
87
+ self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
88
+ in :samp:`G`.
89
+
90
+ For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
91
+ is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
92
+ in :samp:`G` or zero otherwise. If the capacity is infinite,
93
+ :samp:`R[u][v]['capacity']` will have a high arbitrary finite value
94
+ that does not affect the solution of the problem. This value is stored in
95
+ :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
96
+ :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
97
+ satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
98
+
99
+ The flow value, defined as the total flow into :samp:`t`, the sink, is
100
+ stored in :samp:`R.graph['flow_value']`. If :samp:`cutoff` is not
101
+ specified, reachability to :samp:`t` using only edges :samp:`(u, v)` such
102
+ that :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
103
+ :samp:`s`-:samp:`t` cut.
104
+
105
+ Examples
106
+ --------
107
+ >>> from networkx.algorithms.flow import dinitz
108
+
109
+ The functions that implement flow algorithms and output a residual
110
+ network, such as this one, are not imported to the base NetworkX
111
+ namespace, so you have to explicitly import them from the flow package.
112
+
113
+ >>> G = nx.DiGraph()
114
+ >>> G.add_edge("x", "a", capacity=3.0)
115
+ >>> G.add_edge("x", "b", capacity=1.0)
116
+ >>> G.add_edge("a", "c", capacity=3.0)
117
+ >>> G.add_edge("b", "c", capacity=5.0)
118
+ >>> G.add_edge("b", "d", capacity=4.0)
119
+ >>> G.add_edge("d", "e", capacity=2.0)
120
+ >>> G.add_edge("c", "y", capacity=2.0)
121
+ >>> G.add_edge("e", "y", capacity=3.0)
122
+ >>> R = dinitz(G, "x", "y")
123
+ >>> flow_value = nx.maximum_flow_value(G, "x", "y")
124
+ >>> flow_value
125
+ 3.0
126
+ >>> flow_value == R.graph["flow_value"]
127
+ True
128
+
129
+ References
130
+ ----------
131
+ .. [1] Dinitz' Algorithm: The Original Version and Even's Version.
132
+ 2006. Yefim Dinitz. In Theoretical Computer Science. Lecture
133
+ Notes in Computer Science. Volume 3895. pp 218-240.
134
+ https://doi.org/10.1007/11685654_10
135
+
136
+ """
137
+ R = dinitz_impl(G, s, t, capacity, residual, cutoff)
138
+ R.graph["algorithm"] = "dinitz"
139
+ nx._clear_cache(R)
140
+ return R
141
+
142
+
143
+ def dinitz_impl(G, s, t, capacity, residual, cutoff):
144
+ if s not in G:
145
+ raise nx.NetworkXError(f"node {str(s)} not in graph")
146
+ if t not in G:
147
+ raise nx.NetworkXError(f"node {str(t)} not in graph")
148
+ if s == t:
149
+ raise nx.NetworkXError("source and sink are the same node")
150
+
151
+ if residual is None:
152
+ R = build_residual_network(G, capacity)
153
+ else:
154
+ R = residual
155
+
156
+ # Initialize/reset the residual network.
157
+ for u in R:
158
+ for e in R[u].values():
159
+ e["flow"] = 0
160
+
161
+ # Use an arbitrary high value as infinite. It is computed
162
+ # when building the residual network.
163
+ INF = R.graph["inf"]
164
+
165
+ if cutoff is None:
166
+ cutoff = INF
167
+
168
+ R_succ = R.succ
169
+ R_pred = R.pred
170
+
171
+ def breath_first_search():
172
+ parents = {}
173
+ vertex_dist = {s: 0}
174
+ queue = deque([(s, 0)])
175
+ # Record all the potential edges of shortest augmenting paths
176
+ while queue:
177
+ if t in parents:
178
+ break
179
+ u, dist = queue.popleft()
180
+ for v, attr in R_succ[u].items():
181
+ if attr["capacity"] - attr["flow"] > 0:
182
+ if v in parents:
183
+ if vertex_dist[v] == dist + 1:
184
+ parents[v].append(u)
185
+ else:
186
+ parents[v] = deque([u])
187
+ vertex_dist[v] = dist + 1
188
+ queue.append((v, dist + 1))
189
+ return parents
190
+
191
+ def depth_first_search(parents):
192
+ # DFS to find all the shortest augmenting paths
193
+ """Build a path using DFS starting from the sink"""
194
+ total_flow = 0
195
+ u = t
196
+ # path also functions as a stack
197
+ path = [u]
198
+ # The loop ends with no augmenting path left in the layered graph
199
+ while True:
200
+ if len(parents[u]) > 0:
201
+ v = parents[u][0]
202
+ path.append(v)
203
+ else:
204
+ path.pop()
205
+ if len(path) == 0:
206
+ break
207
+ v = path[-1]
208
+ parents[v].popleft()
209
+ # Augment the flow along the path found
210
+ if v == s:
211
+ flow = INF
212
+ for u, v in pairwise(path):
213
+ flow = min(flow, R_pred[u][v]["capacity"] - R_pred[u][v]["flow"])
214
+ for u, v in pairwise(reversed(path)):
215
+ R_pred[v][u]["flow"] += flow
216
+ R_pred[u][v]["flow"] -= flow
217
+ # Find the proper node to continue the search
218
+ if R_pred[v][u]["capacity"] - R_pred[v][u]["flow"] == 0:
219
+ parents[v].popleft()
220
+ while path[-1] != v:
221
+ path.pop()
222
+ total_flow += flow
223
+ v = path[-1]
224
+ u = v
225
+ return total_flow
226
+
227
+ flow_value = 0
228
+ while flow_value < cutoff:
229
+ parents = breath_first_search()
230
+ if t not in parents:
231
+ break
232
+ this_flow = depth_first_search(parents)
233
+ if this_flow * 2 > INF:
234
+ raise nx.NetworkXUnbounded("Infinite capacity path, flow unbounded above.")
235
+ flow_value += this_flow
236
+
237
+ R.graph["flow_value"] = flow_value
238
+ return R
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/gomory_hu.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gomory-Hu tree of undirected Graphs.
3
+ """
4
+
5
+ import networkx as nx
6
+ from networkx.utils import not_implemented_for
7
+
8
+ from .edmondskarp import edmonds_karp
9
+ from .utils import build_residual_network
10
+
11
+ default_flow_func = edmonds_karp
12
+
13
+ __all__ = ["gomory_hu_tree"]
14
+
15
+
16
+ @not_implemented_for("directed")
17
+ @nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True)
18
+ def gomory_hu_tree(G, capacity="capacity", flow_func=None):
19
+ r"""Returns the Gomory-Hu tree of an undirected graph G.
20
+
21
+ A Gomory-Hu tree of an undirected graph with capacities is a
22
+ weighted tree that represents the minimum s-t cuts for all s-t
23
+ pairs in the graph.
24
+
25
+ It only requires `n-1` minimum cut computations instead of the
26
+ obvious `n(n-1)/2`. The tree represents all s-t cuts as the
27
+ minimum cut value among any pair of nodes is the minimum edge
28
+ weight in the shortest path between the two nodes in the
29
+ Gomory-Hu tree.
30
+
31
+ The Gomory-Hu tree also has the property that removing the
32
+ edge with the minimum weight in the shortest path between
33
+ any two nodes leaves two connected components that form
34
+ a partition of the nodes in G that defines the minimum s-t
35
+ cut.
36
+
37
+ See Examples section below for details.
38
+
39
+ Parameters
40
+ ----------
41
+ G : NetworkX graph
42
+ Undirected graph
43
+
44
+ capacity : string
45
+ Edges of the graph G are expected to have an attribute capacity
46
+ that indicates how much flow the edge can support. If this
47
+ attribute is not present, the edge is considered to have
48
+ infinite capacity. Default value: 'capacity'.
49
+
50
+ flow_func : function
51
+ Function to perform the underlying flow computations. Default value
52
+ :func:`edmonds_karp`. This function performs better in sparse graphs
53
+ with right tailed degree distributions.
54
+ :func:`shortest_augmenting_path` will perform better in denser
55
+ graphs.
56
+
57
+ Returns
58
+ -------
59
+ Tree : NetworkX graph
60
+ A NetworkX graph representing the Gomory-Hu tree of the input graph.
61
+
62
+ Raises
63
+ ------
64
+ NetworkXNotImplemented
65
+ Raised if the input graph is directed.
66
+
67
+ NetworkXError
68
+ Raised if the input graph is an empty Graph.
69
+
70
+ Examples
71
+ --------
72
+ >>> G = nx.karate_club_graph()
73
+ >>> nx.set_edge_attributes(G, 1, "capacity")
74
+ >>> T = nx.gomory_hu_tree(G)
75
+ >>> # The value of the minimum cut between any pair
76
+ ... # of nodes in G is the minimum edge weight in the
77
+ ... # shortest path between the two nodes in the
78
+ ... # Gomory-Hu tree.
79
+ ... def minimum_edge_weight_in_shortest_path(T, u, v):
80
+ ... path = nx.shortest_path(T, u, v, weight="weight")
81
+ ... return min((T[u][v]["weight"], (u, v)) for (u, v) in zip(path, path[1:]))
82
+ >>> u, v = 0, 33
83
+ >>> cut_value, edge = minimum_edge_weight_in_shortest_path(T, u, v)
84
+ >>> cut_value
85
+ 10
86
+ >>> nx.minimum_cut_value(G, u, v)
87
+ 10
88
+ >>> # The Gomory-Hu tree also has the property that removing the
89
+ ... # edge with the minimum weight in the shortest path between
90
+ ... # any two nodes leaves two connected components that form
91
+ ... # a partition of the nodes in G that defines the minimum s-t
92
+ ... # cut.
93
+ ... cut_value, edge = minimum_edge_weight_in_shortest_path(T, u, v)
94
+ >>> T.remove_edge(*edge)
95
+ >>> U, V = list(nx.connected_components(T))
96
+ >>> # Thus U and V form a partition that defines a minimum cut
97
+ ... # between u and v in G. You can compute the edge cut set,
98
+ ... # that is, the set of edges that if removed from G will
99
+ ... # disconnect u from v in G, with this information:
100
+ ... cutset = set()
101
+ >>> for x, nbrs in ((n, G[n]) for n in U):
102
+ ... cutset.update((x, y) for y in nbrs if y in V)
103
+ >>> # Because we have set the capacities of all edges to 1
104
+ ... # the cutset contains ten edges
105
+ ... len(cutset)
106
+ 10
107
+ >>> # You can use any maximum flow algorithm for the underlying
108
+ ... # flow computations using the argument flow_func
109
+ ... from networkx.algorithms import flow
110
+ >>> T = nx.gomory_hu_tree(G, flow_func=flow.boykov_kolmogorov)
111
+ >>> cut_value, edge = minimum_edge_weight_in_shortest_path(T, u, v)
112
+ >>> cut_value
113
+ 10
114
+ >>> nx.minimum_cut_value(G, u, v, flow_func=flow.boykov_kolmogorov)
115
+ 10
116
+
117
+ Notes
118
+ -----
119
+ This implementation is based on Gusfield approach [1]_ to compute
120
+ Gomory-Hu trees, which does not require node contractions and has
121
+ the same computational complexity than the original method.
122
+
123
+ See also
124
+ --------
125
+ :func:`minimum_cut`
126
+ :func:`maximum_flow`
127
+
128
+ References
129
+ ----------
130
+ .. [1] Gusfield D: Very simple methods for all pairs network flow analysis.
131
+ SIAM J Comput 19(1):143-155, 1990.
132
+
133
+ """
134
+ if flow_func is None:
135
+ flow_func = default_flow_func
136
+
137
+ if len(G) == 0: # empty graph
138
+ msg = "Empty Graph does not have a Gomory-Hu tree representation"
139
+ raise nx.NetworkXError(msg)
140
+
141
+ # Start the tree as a star graph with an arbitrary node at the center
142
+ tree = {}
143
+ labels = {}
144
+ iter_nodes = iter(G)
145
+ root = next(iter_nodes)
146
+ for n in iter_nodes:
147
+ tree[n] = root
148
+
149
+ # Reuse residual network
150
+ R = build_residual_network(G, capacity)
151
+
152
+ # For all the leaves in the star graph tree (that is n-1 nodes).
153
+ for source in tree:
154
+ # Find neighbor in the tree
155
+ target = tree[source]
156
+ # compute minimum cut
157
+ cut_value, partition = nx.minimum_cut(
158
+ G, source, target, capacity=capacity, flow_func=flow_func, residual=R
159
+ )
160
+ labels[(source, target)] = cut_value
161
+ # Update the tree
162
+ # Source will always be in partition[0] and target in partition[1]
163
+ for node in partition[0]:
164
+ if node != source and node in tree and tree[node] == target:
165
+ tree[node] = source
166
+ labels[node, source] = labels.get((node, target), cut_value)
167
+ #
168
+ if target != root and tree[target] in partition[0]:
169
+ labels[source, tree[target]] = labels[target, tree[target]]
170
+ labels[target, source] = cut_value
171
+ tree[source] = tree[target]
172
+ tree[target] = source
173
+
174
+ # Build the tree
175
+ T = nx.Graph()
176
+ T.add_nodes_from(G)
177
+ T.add_weighted_edges_from(((u, v, labels[u, v]) for u, v in tree.items()))
178
+ return T
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/maxflow.py ADDED
@@ -0,0 +1,607 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Maximum flow (and minimum cut) algorithms on capacitated graphs.
3
+ """
4
+
5
+ import networkx as nx
6
+
7
+ from .boykovkolmogorov import boykov_kolmogorov
8
+ from .dinitz_alg import dinitz
9
+ from .edmondskarp import edmonds_karp
10
+ from .preflowpush import preflow_push
11
+ from .shortestaugmentingpath import shortest_augmenting_path
12
+ from .utils import build_flow_dict
13
+
14
+ # Define the default flow function for computing maximum flow.
15
+ default_flow_func = preflow_push
16
+
17
+ __all__ = ["maximum_flow", "maximum_flow_value", "minimum_cut", "minimum_cut_value"]
18
+
19
+
20
+ @nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")})
21
+ def maximum_flow(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs):
22
+ """Find a maximum single-commodity flow.
23
+
24
+ Parameters
25
+ ----------
26
+ flowG : NetworkX graph
27
+ Edges of the graph are expected to have an attribute called
28
+ 'capacity'. If this attribute is not present, the edge is
29
+ considered to have infinite capacity.
30
+
31
+ _s : node
32
+ Source node for the flow.
33
+
34
+ _t : node
35
+ Sink node for the flow.
36
+
37
+ capacity : string
38
+ Edges of the graph G are expected to have an attribute capacity
39
+ that indicates how much flow the edge can support. If this
40
+ attribute is not present, the edge is considered to have
41
+ infinite capacity. Default value: 'capacity'.
42
+
43
+ flow_func : function
44
+ A function for computing the maximum flow among a pair of nodes
45
+ in a capacitated graph. The function has to accept at least three
46
+ parameters: a Graph or Digraph, a source node, and a target node.
47
+ And return a residual network that follows NetworkX conventions
48
+ (see Notes). If flow_func is None, the default maximum
49
+ flow function (:meth:`preflow_push`) is used. See below for
50
+ alternative algorithms. The choice of the default function may change
51
+ from version to version and should not be relied on. Default value:
52
+ None.
53
+
54
+ kwargs : Any other keyword parameter is passed to the function that
55
+ computes the maximum flow.
56
+
57
+ Returns
58
+ -------
59
+ flow_value : integer, float
60
+ Value of the maximum flow, i.e., net outflow from the source.
61
+
62
+ flow_dict : dict
63
+ A dictionary containing the value of the flow that went through
64
+ each edge.
65
+
66
+ Raises
67
+ ------
68
+ NetworkXError
69
+ The algorithm does not support MultiGraph and MultiDiGraph. If
70
+ the input graph is an instance of one of these two classes, a
71
+ NetworkXError is raised.
72
+
73
+ NetworkXUnbounded
74
+ If the graph has a path of infinite capacity, the value of a
75
+ feasible flow on the graph is unbounded above and the function
76
+ raises a NetworkXUnbounded.
77
+
78
+ See also
79
+ --------
80
+ :meth:`maximum_flow_value`
81
+ :meth:`minimum_cut`
82
+ :meth:`minimum_cut_value`
83
+ :meth:`edmonds_karp`
84
+ :meth:`preflow_push`
85
+ :meth:`shortest_augmenting_path`
86
+
87
+ Notes
88
+ -----
89
+ The function used in the flow_func parameter has to return a residual
90
+ network that follows NetworkX conventions:
91
+
92
+ The residual network :samp:`R` from an input graph :samp:`G` has the
93
+ same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
94
+ of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
95
+ self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
96
+ in :samp:`G`.
97
+
98
+ For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
99
+ is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
100
+ in :samp:`G` or zero otherwise. If the capacity is infinite,
101
+ :samp:`R[u][v]['capacity']` will have a high arbitrary finite value
102
+ that does not affect the solution of the problem. This value is stored in
103
+ :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
104
+ :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
105
+ satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
106
+
107
+ The flow value, defined as the total flow into :samp:`t`, the sink, is
108
+ stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using
109
+ only edges :samp:`(u, v)` such that
110
+ :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
111
+ :samp:`s`-:samp:`t` cut.
112
+
113
+ Specific algorithms may store extra data in :samp:`R`.
114
+
115
+ The function should supports an optional boolean parameter value_only. When
116
+ True, it can optionally terminate the algorithm as soon as the maximum flow
117
+ value and the minimum cut can be determined.
118
+
119
+ Examples
120
+ --------
121
+ >>> G = nx.DiGraph()
122
+ >>> G.add_edge("x", "a", capacity=3.0)
123
+ >>> G.add_edge("x", "b", capacity=1.0)
124
+ >>> G.add_edge("a", "c", capacity=3.0)
125
+ >>> G.add_edge("b", "c", capacity=5.0)
126
+ >>> G.add_edge("b", "d", capacity=4.0)
127
+ >>> G.add_edge("d", "e", capacity=2.0)
128
+ >>> G.add_edge("c", "y", capacity=2.0)
129
+ >>> G.add_edge("e", "y", capacity=3.0)
130
+
131
+ maximum_flow returns both the value of the maximum flow and a
132
+ dictionary with all flows.
133
+
134
+ >>> flow_value, flow_dict = nx.maximum_flow(G, "x", "y")
135
+ >>> flow_value
136
+ 3.0
137
+ >>> print(flow_dict["x"]["b"])
138
+ 1.0
139
+
140
+ You can also use alternative algorithms for computing the
141
+ maximum flow by using the flow_func parameter.
142
+
143
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
144
+ >>> flow_value == nx.maximum_flow(G, "x", "y", flow_func=shortest_augmenting_path)[
145
+ ... 0
146
+ ... ]
147
+ True
148
+
149
+ """
150
+ if flow_func is None:
151
+ if kwargs:
152
+ raise nx.NetworkXError(
153
+ "You have to explicitly set a flow_func if"
154
+ " you need to pass parameters via kwargs."
155
+ )
156
+ flow_func = default_flow_func
157
+
158
+ if not callable(flow_func):
159
+ raise nx.NetworkXError("flow_func has to be callable.")
160
+
161
+ R = flow_func(flowG, _s, _t, capacity=capacity, value_only=False, **kwargs)
162
+ flow_dict = build_flow_dict(flowG, R)
163
+
164
+ return (R.graph["flow_value"], flow_dict)
165
+
166
+
167
+ @nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")})
168
+ def maximum_flow_value(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs):
169
+ """Find the value of maximum single-commodity flow.
170
+
171
+ Parameters
172
+ ----------
173
+ flowG : NetworkX graph
174
+ Edges of the graph are expected to have an attribute called
175
+ 'capacity'. If this attribute is not present, the edge is
176
+ considered to have infinite capacity.
177
+
178
+ _s : node
179
+ Source node for the flow.
180
+
181
+ _t : node
182
+ Sink node for the flow.
183
+
184
+ capacity : string
185
+ Edges of the graph G are expected to have an attribute capacity
186
+ that indicates how much flow the edge can support. If this
187
+ attribute is not present, the edge is considered to have
188
+ infinite capacity. Default value: 'capacity'.
189
+
190
+ flow_func : function
191
+ A function for computing the maximum flow among a pair of nodes
192
+ in a capacitated graph. The function has to accept at least three
193
+ parameters: a Graph or Digraph, a source node, and a target node.
194
+ And return a residual network that follows NetworkX conventions
195
+ (see Notes). If flow_func is None, the default maximum
196
+ flow function (:meth:`preflow_push`) is used. See below for
197
+ alternative algorithms. The choice of the default function may change
198
+ from version to version and should not be relied on. Default value:
199
+ None.
200
+
201
+ kwargs : Any other keyword parameter is passed to the function that
202
+ computes the maximum flow.
203
+
204
+ Returns
205
+ -------
206
+ flow_value : integer, float
207
+ Value of the maximum flow, i.e., net outflow from the source.
208
+
209
+ Raises
210
+ ------
211
+ NetworkXError
212
+ The algorithm does not support MultiGraph and MultiDiGraph. If
213
+ the input graph is an instance of one of these two classes, a
214
+ NetworkXError is raised.
215
+
216
+ NetworkXUnbounded
217
+ If the graph has a path of infinite capacity, the value of a
218
+ feasible flow on the graph is unbounded above and the function
219
+ raises a NetworkXUnbounded.
220
+
221
+ See also
222
+ --------
223
+ :meth:`maximum_flow`
224
+ :meth:`minimum_cut`
225
+ :meth:`minimum_cut_value`
226
+ :meth:`edmonds_karp`
227
+ :meth:`preflow_push`
228
+ :meth:`shortest_augmenting_path`
229
+
230
+ Notes
231
+ -----
232
+ The function used in the flow_func parameter has to return a residual
233
+ network that follows NetworkX conventions:
234
+
235
+ The residual network :samp:`R` from an input graph :samp:`G` has the
236
+ same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
237
+ of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
238
+ self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
239
+ in :samp:`G`.
240
+
241
+ For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
242
+ is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
243
+ in :samp:`G` or zero otherwise. If the capacity is infinite,
244
+ :samp:`R[u][v]['capacity']` will have a high arbitrary finite value
245
+ that does not affect the solution of the problem. This value is stored in
246
+ :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
247
+ :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
248
+ satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
249
+
250
+ The flow value, defined as the total flow into :samp:`t`, the sink, is
251
+ stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using
252
+ only edges :samp:`(u, v)` such that
253
+ :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
254
+ :samp:`s`-:samp:`t` cut.
255
+
256
+ Specific algorithms may store extra data in :samp:`R`.
257
+
258
+ The function should supports an optional boolean parameter value_only. When
259
+ True, it can optionally terminate the algorithm as soon as the maximum flow
260
+ value and the minimum cut can be determined.
261
+
262
+ Examples
263
+ --------
264
+ >>> G = nx.DiGraph()
265
+ >>> G.add_edge("x", "a", capacity=3.0)
266
+ >>> G.add_edge("x", "b", capacity=1.0)
267
+ >>> G.add_edge("a", "c", capacity=3.0)
268
+ >>> G.add_edge("b", "c", capacity=5.0)
269
+ >>> G.add_edge("b", "d", capacity=4.0)
270
+ >>> G.add_edge("d", "e", capacity=2.0)
271
+ >>> G.add_edge("c", "y", capacity=2.0)
272
+ >>> G.add_edge("e", "y", capacity=3.0)
273
+
274
+ maximum_flow_value computes only the value of the
275
+ maximum flow:
276
+
277
+ >>> flow_value = nx.maximum_flow_value(G, "x", "y")
278
+ >>> flow_value
279
+ 3.0
280
+
281
+ You can also use alternative algorithms for computing the
282
+ maximum flow by using the flow_func parameter.
283
+
284
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
285
+ >>> flow_value == nx.maximum_flow_value(
286
+ ... G, "x", "y", flow_func=shortest_augmenting_path
287
+ ... )
288
+ True
289
+
290
+ """
291
+ if flow_func is None:
292
+ if kwargs:
293
+ raise nx.NetworkXError(
294
+ "You have to explicitly set a flow_func if"
295
+ " you need to pass parameters via kwargs."
296
+ )
297
+ flow_func = default_flow_func
298
+
299
+ if not callable(flow_func):
300
+ raise nx.NetworkXError("flow_func has to be callable.")
301
+
302
+ R = flow_func(flowG, _s, _t, capacity=capacity, value_only=True, **kwargs)
303
+
304
+ return R.graph["flow_value"]
305
+
306
+
307
+ @nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")})
308
+ def minimum_cut(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs):
309
+ """Compute the value and the node partition of a minimum (s, t)-cut.
310
+
311
+ Use the max-flow min-cut theorem, i.e., the capacity of a minimum
312
+ capacity cut is equal to the flow value of a maximum flow.
313
+
314
+ Parameters
315
+ ----------
316
+ flowG : NetworkX graph
317
+ Edges of the graph are expected to have an attribute called
318
+ 'capacity'. If this attribute is not present, the edge is
319
+ considered to have infinite capacity.
320
+
321
+ _s : node
322
+ Source node for the flow.
323
+
324
+ _t : node
325
+ Sink node for the flow.
326
+
327
+ capacity : string
328
+ Edges of the graph G are expected to have an attribute capacity
329
+ that indicates how much flow the edge can support. If this
330
+ attribute is not present, the edge is considered to have
331
+ infinite capacity. Default value: 'capacity'.
332
+
333
+ flow_func : function
334
+ A function for computing the maximum flow among a pair of nodes
335
+ in a capacitated graph. The function has to accept at least three
336
+ parameters: a Graph or Digraph, a source node, and a target node.
337
+ And return a residual network that follows NetworkX conventions
338
+ (see Notes). If flow_func is None, the default maximum
339
+ flow function (:meth:`preflow_push`) is used. See below for
340
+ alternative algorithms. The choice of the default function may change
341
+ from version to version and should not be relied on. Default value:
342
+ None.
343
+
344
+ kwargs : Any other keyword parameter is passed to the function that
345
+ computes the maximum flow.
346
+
347
+ Returns
348
+ -------
349
+ cut_value : integer, float
350
+ Value of the minimum cut.
351
+
352
+ partition : pair of node sets
353
+ A partitioning of the nodes that defines a minimum cut.
354
+
355
+ Raises
356
+ ------
357
+ NetworkXUnbounded
358
+ If the graph has a path of infinite capacity, all cuts have
359
+ infinite capacity and the function raises a NetworkXError.
360
+
361
+ See also
362
+ --------
363
+ :meth:`maximum_flow`
364
+ :meth:`maximum_flow_value`
365
+ :meth:`minimum_cut_value`
366
+ :meth:`edmonds_karp`
367
+ :meth:`preflow_push`
368
+ :meth:`shortest_augmenting_path`
369
+
370
+ Notes
371
+ -----
372
+ The function used in the flow_func parameter has to return a residual
373
+ network that follows NetworkX conventions:
374
+
375
+ The residual network :samp:`R` from an input graph :samp:`G` has the
376
+ same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
377
+ of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
378
+ self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
379
+ in :samp:`G`.
380
+
381
+ For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
382
+ is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
383
+ in :samp:`G` or zero otherwise. If the capacity is infinite,
384
+ :samp:`R[u][v]['capacity']` will have a high arbitrary finite value
385
+ that does not affect the solution of the problem. This value is stored in
386
+ :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
387
+ :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
388
+ satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
389
+
390
+ The flow value, defined as the total flow into :samp:`t`, the sink, is
391
+ stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using
392
+ only edges :samp:`(u, v)` such that
393
+ :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
394
+ :samp:`s`-:samp:`t` cut.
395
+
396
+ Specific algorithms may store extra data in :samp:`R`.
397
+
398
+ The function should supports an optional boolean parameter value_only. When
399
+ True, it can optionally terminate the algorithm as soon as the maximum flow
400
+ value and the minimum cut can be determined.
401
+
402
+ Examples
403
+ --------
404
+ >>> G = nx.DiGraph()
405
+ >>> G.add_edge("x", "a", capacity=3.0)
406
+ >>> G.add_edge("x", "b", capacity=1.0)
407
+ >>> G.add_edge("a", "c", capacity=3.0)
408
+ >>> G.add_edge("b", "c", capacity=5.0)
409
+ >>> G.add_edge("b", "d", capacity=4.0)
410
+ >>> G.add_edge("d", "e", capacity=2.0)
411
+ >>> G.add_edge("c", "y", capacity=2.0)
412
+ >>> G.add_edge("e", "y", capacity=3.0)
413
+
414
+ minimum_cut computes both the value of the
415
+ minimum cut and the node partition:
416
+
417
+ >>> cut_value, partition = nx.minimum_cut(G, "x", "y")
418
+ >>> reachable, non_reachable = partition
419
+
420
+ 'partition' here is a tuple with the two sets of nodes that define
421
+ the minimum cut. You can compute the cut set of edges that induce
422
+ the minimum cut as follows:
423
+
424
+ >>> cutset = set()
425
+ >>> for u, nbrs in ((n, G[n]) for n in reachable):
426
+ ... cutset.update((u, v) for v in nbrs if v in non_reachable)
427
+ >>> print(sorted(cutset))
428
+ [('c', 'y'), ('x', 'b')]
429
+ >>> cut_value == sum(G.edges[u, v]["capacity"] for (u, v) in cutset)
430
+ True
431
+
432
+ You can also use alternative algorithms for computing the
433
+ minimum cut by using the flow_func parameter.
434
+
435
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
436
+ >>> cut_value == nx.minimum_cut(G, "x", "y", flow_func=shortest_augmenting_path)[0]
437
+ True
438
+
439
+ """
440
+ if flow_func is None:
441
+ if kwargs:
442
+ raise nx.NetworkXError(
443
+ "You have to explicitly set a flow_func if"
444
+ " you need to pass parameters via kwargs."
445
+ )
446
+ flow_func = default_flow_func
447
+
448
+ if not callable(flow_func):
449
+ raise nx.NetworkXError("flow_func has to be callable.")
450
+
451
+ if kwargs.get("cutoff") is not None and flow_func is preflow_push:
452
+ raise nx.NetworkXError("cutoff should not be specified.")
453
+
454
+ R = flow_func(flowG, _s, _t, capacity=capacity, value_only=True, **kwargs)
455
+ # Remove saturated edges from the residual network
456
+ cutset = [(u, v, d) for u, v, d in R.edges(data=True) if d["flow"] == d["capacity"]]
457
+ R.remove_edges_from(cutset)
458
+
459
+ # Then, reachable and non reachable nodes from source in the
460
+ # residual network form the node partition that defines
461
+ # the minimum cut.
462
+ non_reachable = set(dict(nx.shortest_path_length(R, target=_t)))
463
+ partition = (set(flowG) - non_reachable, non_reachable)
464
+ # Finally add again cutset edges to the residual network to make
465
+ # sure that it is reusable.
466
+ R.add_edges_from(cutset)
467
+ return (R.graph["flow_value"], partition)
468
+
469
+
470
+ @nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")})
471
+ def minimum_cut_value(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs):
472
+ """Compute the value of a minimum (s, t)-cut.
473
+
474
+ Use the max-flow min-cut theorem, i.e., the capacity of a minimum
475
+ capacity cut is equal to the flow value of a maximum flow.
476
+
477
+ Parameters
478
+ ----------
479
+ flowG : NetworkX graph
480
+ Edges of the graph are expected to have an attribute called
481
+ 'capacity'. If this attribute is not present, the edge is
482
+ considered to have infinite capacity.
483
+
484
+ _s : node
485
+ Source node for the flow.
486
+
487
+ _t : node
488
+ Sink node for the flow.
489
+
490
+ capacity : string
491
+ Edges of the graph G are expected to have an attribute capacity
492
+ that indicates how much flow the edge can support. If this
493
+ attribute is not present, the edge is considered to have
494
+ infinite capacity. Default value: 'capacity'.
495
+
496
+ flow_func : function
497
+ A function for computing the maximum flow among a pair of nodes
498
+ in a capacitated graph. The function has to accept at least three
499
+ parameters: a Graph or Digraph, a source node, and a target node.
500
+ And return a residual network that follows NetworkX conventions
501
+ (see Notes). If flow_func is None, the default maximum
502
+ flow function (:meth:`preflow_push`) is used. See below for
503
+ alternative algorithms. The choice of the default function may change
504
+ from version to version and should not be relied on. Default value:
505
+ None.
506
+
507
+ kwargs : Any other keyword parameter is passed to the function that
508
+ computes the maximum flow.
509
+
510
+ Returns
511
+ -------
512
+ cut_value : integer, float
513
+ Value of the minimum cut.
514
+
515
+ Raises
516
+ ------
517
+ NetworkXUnbounded
518
+ If the graph has a path of infinite capacity, all cuts have
519
+ infinite capacity and the function raises a NetworkXError.
520
+
521
+ See also
522
+ --------
523
+ :meth:`maximum_flow`
524
+ :meth:`maximum_flow_value`
525
+ :meth:`minimum_cut`
526
+ :meth:`edmonds_karp`
527
+ :meth:`preflow_push`
528
+ :meth:`shortest_augmenting_path`
529
+
530
+ Notes
531
+ -----
532
+ The function used in the flow_func parameter has to return a residual
533
+ network that follows NetworkX conventions:
534
+
535
+ The residual network :samp:`R` from an input graph :samp:`G` has the
536
+ same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
537
+ of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
538
+ self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
539
+ in :samp:`G`.
540
+
541
+ For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
542
+ is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
543
+ in :samp:`G` or zero otherwise. If the capacity is infinite,
544
+ :samp:`R[u][v]['capacity']` will have a high arbitrary finite value
545
+ that does not affect the solution of the problem. This value is stored in
546
+ :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
547
+ :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
548
+ satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
549
+
550
+ The flow value, defined as the total flow into :samp:`t`, the sink, is
551
+ stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using
552
+ only edges :samp:`(u, v)` such that
553
+ :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
554
+ :samp:`s`-:samp:`t` cut.
555
+
556
+ Specific algorithms may store extra data in :samp:`R`.
557
+
558
+ The function should supports an optional boolean parameter value_only. When
559
+ True, it can optionally terminate the algorithm as soon as the maximum flow
560
+ value and the minimum cut can be determined.
561
+
562
+ Examples
563
+ --------
564
+ >>> G = nx.DiGraph()
565
+ >>> G.add_edge("x", "a", capacity=3.0)
566
+ >>> G.add_edge("x", "b", capacity=1.0)
567
+ >>> G.add_edge("a", "c", capacity=3.0)
568
+ >>> G.add_edge("b", "c", capacity=5.0)
569
+ >>> G.add_edge("b", "d", capacity=4.0)
570
+ >>> G.add_edge("d", "e", capacity=2.0)
571
+ >>> G.add_edge("c", "y", capacity=2.0)
572
+ >>> G.add_edge("e", "y", capacity=3.0)
573
+
574
+ minimum_cut_value computes only the value of the
575
+ minimum cut:
576
+
577
+ >>> cut_value = nx.minimum_cut_value(G, "x", "y")
578
+ >>> cut_value
579
+ 3.0
580
+
581
+ You can also use alternative algorithms for computing the
582
+ minimum cut by using the flow_func parameter.
583
+
584
+ >>> from networkx.algorithms.flow import shortest_augmenting_path
585
+ >>> cut_value == nx.minimum_cut_value(
586
+ ... G, "x", "y", flow_func=shortest_augmenting_path
587
+ ... )
588
+ True
589
+
590
+ """
591
+ if flow_func is None:
592
+ if kwargs:
593
+ raise nx.NetworkXError(
594
+ "You have to explicitly set a flow_func if"
595
+ " you need to pass parameters via kwargs."
596
+ )
597
+ flow_func = default_flow_func
598
+
599
+ if not callable(flow_func):
600
+ raise nx.NetworkXError("flow_func has to be callable.")
601
+
602
+ if kwargs.get("cutoff") is not None and flow_func is preflow_push:
603
+ raise nx.NetworkXError("cutoff should not be specified.")
604
+
605
+ R = flow_func(flowG, _s, _t, capacity=capacity, value_only=True, **kwargs)
606
+
607
+ return R.graph["flow_value"]
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/networksimplex.py ADDED
@@ -0,0 +1,666 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Minimum cost flow algorithms on directed connected graphs.
3
+ """
4
+
5
+ __all__ = ["network_simplex"]
6
+
7
+ from itertools import chain, islice, repeat
8
+ from math import ceil, sqrt
9
+
10
+ import networkx as nx
11
+ from networkx.utils import not_implemented_for
12
+
13
+
14
+ class _DataEssentialsAndFunctions:
15
+ def __init__(
16
+ self, G, multigraph, demand="demand", capacity="capacity", weight="weight"
17
+ ):
18
+ # Number all nodes and edges and hereafter reference them using ONLY their numbers
19
+ self.node_list = list(G) # nodes
20
+ self.node_indices = {u: i for i, u in enumerate(self.node_list)} # node indices
21
+ self.node_demands = [
22
+ G.nodes[u].get(demand, 0) for u in self.node_list
23
+ ] # node demands
24
+
25
+ self.edge_sources = [] # edge sources
26
+ self.edge_targets = [] # edge targets
27
+ if multigraph:
28
+ self.edge_keys = [] # edge keys
29
+ self.edge_indices = {} # edge indices
30
+ self.edge_capacities = [] # edge capacities
31
+ self.edge_weights = [] # edge weights
32
+
33
+ if not multigraph:
34
+ edges = G.edges(data=True)
35
+ else:
36
+ edges = G.edges(data=True, keys=True)
37
+
38
+ inf = float("inf")
39
+ edges = (e for e in edges if e[0] != e[1] and e[-1].get(capacity, inf) != 0)
40
+ for i, e in enumerate(edges):
41
+ self.edge_sources.append(self.node_indices[e[0]])
42
+ self.edge_targets.append(self.node_indices[e[1]])
43
+ if multigraph:
44
+ self.edge_keys.append(e[2])
45
+ self.edge_indices[e[:-1]] = i
46
+ self.edge_capacities.append(e[-1].get(capacity, inf))
47
+ self.edge_weights.append(e[-1].get(weight, 0))
48
+
49
+ # spanning tree specific data to be initialized
50
+
51
+ self.edge_count = None # number of edges
52
+ self.edge_flow = None # edge flows
53
+ self.node_potentials = None # node potentials
54
+ self.parent = None # parent nodes
55
+ self.parent_edge = None # edges to parents
56
+ self.subtree_size = None # subtree sizes
57
+ self.next_node_dft = None # next nodes in depth-first thread
58
+ self.prev_node_dft = None # previous nodes in depth-first thread
59
+ self.last_descendent_dft = None # last descendants in depth-first thread
60
+ self._spanning_tree_initialized = (
61
+ False # False until initialize_spanning_tree() is called
62
+ )
63
+
64
+ def initialize_spanning_tree(self, n, faux_inf):
65
+ self.edge_count = len(self.edge_indices) # number of edges
66
+ self.edge_flow = list(
67
+ chain(repeat(0, self.edge_count), (abs(d) for d in self.node_demands))
68
+ ) # edge flows
69
+ self.node_potentials = [
70
+ faux_inf if d <= 0 else -faux_inf for d in self.node_demands
71
+ ] # node potentials
72
+ self.parent = list(chain(repeat(-1, n), [None])) # parent nodes
73
+ self.parent_edge = list(
74
+ range(self.edge_count, self.edge_count + n)
75
+ ) # edges to parents
76
+ self.subtree_size = list(chain(repeat(1, n), [n + 1])) # subtree sizes
77
+ self.next_node_dft = list(
78
+ chain(range(1, n), [-1, 0])
79
+ ) # next nodes in depth-first thread
80
+ self.prev_node_dft = list(range(-1, n)) # previous nodes in depth-first thread
81
+ self.last_descendent_dft = list(
82
+ chain(range(n), [n - 1])
83
+ ) # last descendants in depth-first thread
84
+ self._spanning_tree_initialized = True # True only if all the assignments pass
85
+
86
+ def find_apex(self, p, q):
87
+ """
88
+ Find the lowest common ancestor of nodes p and q in the spanning tree.
89
+ """
90
+ size_p = self.subtree_size[p]
91
+ size_q = self.subtree_size[q]
92
+ while True:
93
+ while size_p < size_q:
94
+ p = self.parent[p]
95
+ size_p = self.subtree_size[p]
96
+ while size_p > size_q:
97
+ q = self.parent[q]
98
+ size_q = self.subtree_size[q]
99
+ if size_p == size_q:
100
+ if p != q:
101
+ p = self.parent[p]
102
+ size_p = self.subtree_size[p]
103
+ q = self.parent[q]
104
+ size_q = self.subtree_size[q]
105
+ else:
106
+ return p
107
+
108
+ def trace_path(self, p, w):
109
+ """
110
+ Returns the nodes and edges on the path from node p to its ancestor w.
111
+ """
112
+ Wn = [p]
113
+ We = []
114
+ while p != w:
115
+ We.append(self.parent_edge[p])
116
+ p = self.parent[p]
117
+ Wn.append(p)
118
+ return Wn, We
119
+
120
+ def find_cycle(self, i, p, q):
121
+ """
122
+ Returns the nodes and edges on the cycle containing edge i == (p, q)
123
+ when the latter is added to the spanning tree.
124
+
125
+ The cycle is oriented in the direction from p to q.
126
+ """
127
+ w = self.find_apex(p, q)
128
+ Wn, We = self.trace_path(p, w)
129
+ Wn.reverse()
130
+ We.reverse()
131
+ if We != [i]:
132
+ We.append(i)
133
+ WnR, WeR = self.trace_path(q, w)
134
+ del WnR[-1]
135
+ Wn += WnR
136
+ We += WeR
137
+ return Wn, We
138
+
139
+ def augment_flow(self, Wn, We, f):
140
+ """
141
+ Augment f units of flow along a cycle represented by Wn and We.
142
+ """
143
+ for i, p in zip(We, Wn):
144
+ if self.edge_sources[i] == p:
145
+ self.edge_flow[i] += f
146
+ else:
147
+ self.edge_flow[i] -= f
148
+
149
+ def trace_subtree(self, p):
150
+ """
151
+ Yield the nodes in the subtree rooted at a node p.
152
+ """
153
+ yield p
154
+ l = self.last_descendent_dft[p]
155
+ while p != l:
156
+ p = self.next_node_dft[p]
157
+ yield p
158
+
159
+ def remove_edge(self, s, t):
160
+ """
161
+ Remove an edge (s, t) where parent[t] == s from the spanning tree.
162
+ """
163
+ size_t = self.subtree_size[t]
164
+ prev_t = self.prev_node_dft[t]
165
+ last_t = self.last_descendent_dft[t]
166
+ next_last_t = self.next_node_dft[last_t]
167
+ # Remove (s, t).
168
+ self.parent[t] = None
169
+ self.parent_edge[t] = None
170
+ # Remove the subtree rooted at t from the depth-first thread.
171
+ self.next_node_dft[prev_t] = next_last_t
172
+ self.prev_node_dft[next_last_t] = prev_t
173
+ self.next_node_dft[last_t] = t
174
+ self.prev_node_dft[t] = last_t
175
+ # Update the subtree sizes and last descendants of the (old) ancestors
176
+ # of t.
177
+ while s is not None:
178
+ self.subtree_size[s] -= size_t
179
+ if self.last_descendent_dft[s] == last_t:
180
+ self.last_descendent_dft[s] = prev_t
181
+ s = self.parent[s]
182
+
183
+ def make_root(self, q):
184
+ """
185
+ Make a node q the root of its containing subtree.
186
+ """
187
+ ancestors = []
188
+ while q is not None:
189
+ ancestors.append(q)
190
+ q = self.parent[q]
191
+ ancestors.reverse()
192
+ for p, q in zip(ancestors, islice(ancestors, 1, None)):
193
+ size_p = self.subtree_size[p]
194
+ last_p = self.last_descendent_dft[p]
195
+ prev_q = self.prev_node_dft[q]
196
+ last_q = self.last_descendent_dft[q]
197
+ next_last_q = self.next_node_dft[last_q]
198
+ # Make p a child of q.
199
+ self.parent[p] = q
200
+ self.parent[q] = None
201
+ self.parent_edge[p] = self.parent_edge[q]
202
+ self.parent_edge[q] = None
203
+ self.subtree_size[p] = size_p - self.subtree_size[q]
204
+ self.subtree_size[q] = size_p
205
+ # Remove the subtree rooted at q from the depth-first thread.
206
+ self.next_node_dft[prev_q] = next_last_q
207
+ self.prev_node_dft[next_last_q] = prev_q
208
+ self.next_node_dft[last_q] = q
209
+ self.prev_node_dft[q] = last_q
210
+ if last_p == last_q:
211
+ self.last_descendent_dft[p] = prev_q
212
+ last_p = prev_q
213
+ # Add the remaining parts of the subtree rooted at p as a subtree
214
+ # of q in the depth-first thread.
215
+ self.prev_node_dft[p] = last_q
216
+ self.next_node_dft[last_q] = p
217
+ self.next_node_dft[last_p] = q
218
+ self.prev_node_dft[q] = last_p
219
+ self.last_descendent_dft[q] = last_p
220
+
221
+ def add_edge(self, i, p, q):
222
+ """
223
+ Add an edge (p, q) to the spanning tree where q is the root of a subtree.
224
+ """
225
+ last_p = self.last_descendent_dft[p]
226
+ next_last_p = self.next_node_dft[last_p]
227
+ size_q = self.subtree_size[q]
228
+ last_q = self.last_descendent_dft[q]
229
+ # Make q a child of p.
230
+ self.parent[q] = p
231
+ self.parent_edge[q] = i
232
+ # Insert the subtree rooted at q into the depth-first thread.
233
+ self.next_node_dft[last_p] = q
234
+ self.prev_node_dft[q] = last_p
235
+ self.prev_node_dft[next_last_p] = last_q
236
+ self.next_node_dft[last_q] = next_last_p
237
+ # Update the subtree sizes and last descendants of the (new) ancestors
238
+ # of q.
239
+ while p is not None:
240
+ self.subtree_size[p] += size_q
241
+ if self.last_descendent_dft[p] == last_p:
242
+ self.last_descendent_dft[p] = last_q
243
+ p = self.parent[p]
244
+
245
+ def update_potentials(self, i, p, q):
246
+ """
247
+ Update the potentials of the nodes in the subtree rooted at a node
248
+ q connected to its parent p by an edge i.
249
+ """
250
+ if q == self.edge_targets[i]:
251
+ d = self.node_potentials[p] - self.edge_weights[i] - self.node_potentials[q]
252
+ else:
253
+ d = self.node_potentials[p] + self.edge_weights[i] - self.node_potentials[q]
254
+ for q in self.trace_subtree(q):
255
+ self.node_potentials[q] += d
256
+
257
+ def reduced_cost(self, i):
258
+ """Returns the reduced cost of an edge i."""
259
+ c = (
260
+ self.edge_weights[i]
261
+ - self.node_potentials[self.edge_sources[i]]
262
+ + self.node_potentials[self.edge_targets[i]]
263
+ )
264
+ return c if self.edge_flow[i] == 0 else -c
265
+
266
+ def find_entering_edges(self):
267
+ """Yield entering edges until none can be found."""
268
+ if self.edge_count == 0:
269
+ return
270
+
271
+ # Entering edges are found by combining Dantzig's rule and Bland's
272
+ # rule. The edges are cyclically grouped into blocks of size B. Within
273
+ # each block, Dantzig's rule is applied to find an entering edge. The
274
+ # blocks to search is determined following Bland's rule.
275
+ B = int(ceil(sqrt(self.edge_count))) # pivot block size
276
+ M = (self.edge_count + B - 1) // B # number of blocks needed to cover all edges
277
+ m = 0 # number of consecutive blocks without eligible
278
+ # entering edges
279
+ f = 0 # first edge in block
280
+ while m < M:
281
+ # Determine the next block of edges.
282
+ l = f + B
283
+ if l <= self.edge_count:
284
+ edges = range(f, l)
285
+ else:
286
+ l -= self.edge_count
287
+ edges = chain(range(f, self.edge_count), range(l))
288
+ f = l
289
+ # Find the first edge with the lowest reduced cost.
290
+ i = min(edges, key=self.reduced_cost)
291
+ c = self.reduced_cost(i)
292
+ if c >= 0:
293
+ # No entering edge found in the current block.
294
+ m += 1
295
+ else:
296
+ # Entering edge found.
297
+ if self.edge_flow[i] == 0:
298
+ p = self.edge_sources[i]
299
+ q = self.edge_targets[i]
300
+ else:
301
+ p = self.edge_targets[i]
302
+ q = self.edge_sources[i]
303
+ yield i, p, q
304
+ m = 0
305
+ # All edges have nonnegative reduced costs. The current flow is
306
+ # optimal.
307
+
308
+ def residual_capacity(self, i, p):
309
+ """Returns the residual capacity of an edge i in the direction away
310
+ from its endpoint p.
311
+ """
312
+ return (
313
+ self.edge_capacities[i] - self.edge_flow[i]
314
+ if self.edge_sources[i] == p
315
+ else self.edge_flow[i]
316
+ )
317
+
318
+ def find_leaving_edge(self, Wn, We):
319
+ """Returns the leaving edge in a cycle represented by Wn and We."""
320
+ j, s = min(
321
+ zip(reversed(We), reversed(Wn)),
322
+ key=lambda i_p: self.residual_capacity(*i_p),
323
+ )
324
+ t = self.edge_targets[j] if self.edge_sources[j] == s else self.edge_sources[j]
325
+ return j, s, t
326
+
327
+
328
+ @not_implemented_for("undirected")
329
+ @nx._dispatchable(
330
+ node_attrs="demand", edge_attrs={"capacity": float("inf"), "weight": 0}
331
+ )
332
+ def network_simplex(G, demand="demand", capacity="capacity", weight="weight"):
333
+ r"""Find a minimum cost flow satisfying all demands in digraph G.
334
+
335
+ This is a primal network simplex algorithm that uses the leaving
336
+ arc rule to prevent cycling.
337
+
338
+ G is a digraph with edge costs and capacities and in which nodes
339
+ have demand, i.e., they want to send or receive some amount of
340
+ flow. A negative demand means that the node wants to send flow, a
341
+ positive demand means that the node want to receive flow. A flow on
342
+ the digraph G satisfies all demand if the net flow into each node
343
+ is equal to the demand of that node.
344
+
345
+ Parameters
346
+ ----------
347
+ G : NetworkX graph
348
+ DiGraph on which a minimum cost flow satisfying all demands is
349
+ to be found.
350
+
351
+ demand : string
352
+ Nodes of the graph G are expected to have an attribute demand
353
+ that indicates how much flow a node wants to send (negative
354
+ demand) or receive (positive demand). Note that the sum of the
355
+ demands should be 0 otherwise the problem in not feasible. If
356
+ this attribute is not present, a node is considered to have 0
357
+ demand. Default value: 'demand'.
358
+
359
+ capacity : string
360
+ Edges of the graph G are expected to have an attribute capacity
361
+ that indicates how much flow the edge can support. If this
362
+ attribute is not present, the edge is considered to have
363
+ infinite capacity. Default value: 'capacity'.
364
+
365
+ weight : string
366
+ Edges of the graph G are expected to have an attribute weight
367
+ that indicates the cost incurred by sending one unit of flow on
368
+ that edge. If not present, the weight is considered to be 0.
369
+ Default value: 'weight'.
370
+
371
+ Returns
372
+ -------
373
+ flowCost : integer, float
374
+ Cost of a minimum cost flow satisfying all demands.
375
+
376
+ flowDict : dictionary
377
+ Dictionary of dictionaries keyed by nodes such that
378
+ flowDict[u][v] is the flow edge (u, v).
379
+
380
+ Raises
381
+ ------
382
+ NetworkXError
383
+ This exception is raised if the input graph is not directed or
384
+ not connected.
385
+
386
+ NetworkXUnfeasible
387
+ This exception is raised in the following situations:
388
+
389
+ * The sum of the demands is not zero. Then, there is no
390
+ flow satisfying all demands.
391
+ * There is no flow satisfying all demand.
392
+
393
+ NetworkXUnbounded
394
+ This exception is raised if the digraph G has a cycle of
395
+ negative cost and infinite capacity. Then, the cost of a flow
396
+ satisfying all demands is unbounded below.
397
+
398
+ Notes
399
+ -----
400
+ This algorithm is not guaranteed to work if edge weights or demands
401
+ are floating point numbers (overflows and roundoff errors can
402
+ cause problems). As a workaround you can use integer numbers by
403
+ multiplying the relevant edge attributes by a convenient
404
+ constant factor (eg 100).
405
+
406
+ See also
407
+ --------
408
+ cost_of_flow, max_flow_min_cost, min_cost_flow, min_cost_flow_cost
409
+
410
+ Examples
411
+ --------
412
+ A simple example of a min cost flow problem.
413
+
414
+ >>> G = nx.DiGraph()
415
+ >>> G.add_node("a", demand=-5)
416
+ >>> G.add_node("d", demand=5)
417
+ >>> G.add_edge("a", "b", weight=3, capacity=4)
418
+ >>> G.add_edge("a", "c", weight=6, capacity=10)
419
+ >>> G.add_edge("b", "d", weight=1, capacity=9)
420
+ >>> G.add_edge("c", "d", weight=2, capacity=5)
421
+ >>> flowCost, flowDict = nx.network_simplex(G)
422
+ >>> flowCost
423
+ 24
424
+ >>> flowDict
425
+ {'a': {'b': 4, 'c': 1}, 'd': {}, 'b': {'d': 4}, 'c': {'d': 1}}
426
+
427
+ The mincost flow algorithm can also be used to solve shortest path
428
+ problems. To find the shortest path between two nodes u and v,
429
+ give all edges an infinite capacity, give node u a demand of -1 and
430
+ node v a demand a 1. Then run the network simplex. The value of a
431
+ min cost flow will be the distance between u and v and edges
432
+ carrying positive flow will indicate the path.
433
+
434
+ >>> G = nx.DiGraph()
435
+ >>> G.add_weighted_edges_from(
436
+ ... [
437
+ ... ("s", "u", 10),
438
+ ... ("s", "x", 5),
439
+ ... ("u", "v", 1),
440
+ ... ("u", "x", 2),
441
+ ... ("v", "y", 1),
442
+ ... ("x", "u", 3),
443
+ ... ("x", "v", 5),
444
+ ... ("x", "y", 2),
445
+ ... ("y", "s", 7),
446
+ ... ("y", "v", 6),
447
+ ... ]
448
+ ... )
449
+ >>> G.add_node("s", demand=-1)
450
+ >>> G.add_node("v", demand=1)
451
+ >>> flowCost, flowDict = nx.network_simplex(G)
452
+ >>> flowCost == nx.shortest_path_length(G, "s", "v", weight="weight")
453
+ True
454
+ >>> sorted([(u, v) for u in flowDict for v in flowDict[u] if flowDict[u][v] > 0])
455
+ [('s', 'x'), ('u', 'v'), ('x', 'u')]
456
+ >>> nx.shortest_path(G, "s", "v", weight="weight")
457
+ ['s', 'x', 'u', 'v']
458
+
459
+ It is possible to change the name of the attributes used for the
460
+ algorithm.
461
+
462
+ >>> G = nx.DiGraph()
463
+ >>> G.add_node("p", spam=-4)
464
+ >>> G.add_node("q", spam=2)
465
+ >>> G.add_node("a", spam=-2)
466
+ >>> G.add_node("d", spam=-1)
467
+ >>> G.add_node("t", spam=2)
468
+ >>> G.add_node("w", spam=3)
469
+ >>> G.add_edge("p", "q", cost=7, vacancies=5)
470
+ >>> G.add_edge("p", "a", cost=1, vacancies=4)
471
+ >>> G.add_edge("q", "d", cost=2, vacancies=3)
472
+ >>> G.add_edge("t", "q", cost=1, vacancies=2)
473
+ >>> G.add_edge("a", "t", cost=2, vacancies=4)
474
+ >>> G.add_edge("d", "w", cost=3, vacancies=4)
475
+ >>> G.add_edge("t", "w", cost=4, vacancies=1)
476
+ >>> flowCost, flowDict = nx.network_simplex(
477
+ ... G, demand="spam", capacity="vacancies", weight="cost"
478
+ ... )
479
+ >>> flowCost
480
+ 37
481
+ >>> flowDict
482
+ {'p': {'q': 2, 'a': 2}, 'q': {'d': 1}, 'a': {'t': 4}, 'd': {'w': 2}, 't': {'q': 1, 'w': 1}, 'w': {}}
483
+
484
+ References
485
+ ----------
486
+ .. [1] Z. Kiraly, P. Kovacs.
487
+ Efficient implementation of minimum-cost flow algorithms.
488
+ Acta Universitatis Sapientiae, Informatica 4(1):67--118. 2012.
489
+ .. [2] R. Barr, F. Glover, D. Klingman.
490
+ Enhancement of spanning tree labeling procedures for network
491
+ optimization.
492
+ INFOR 17(1):16--34. 1979.
493
+ """
494
+ ###########################################################################
495
+ # Problem essentials extraction and sanity check
496
+ ###########################################################################
497
+
498
+ if len(G) == 0:
499
+ raise nx.NetworkXError("graph has no nodes")
500
+
501
+ multigraph = G.is_multigraph()
502
+
503
+ # extracting data essential to problem
504
+ DEAF = _DataEssentialsAndFunctions(
505
+ G, multigraph, demand=demand, capacity=capacity, weight=weight
506
+ )
507
+
508
+ ###########################################################################
509
+ # Quick Error Detection
510
+ ###########################################################################
511
+
512
+ inf = float("inf")
513
+ for u, d in zip(DEAF.node_list, DEAF.node_demands):
514
+ if abs(d) == inf:
515
+ raise nx.NetworkXError(f"node {u!r} has infinite demand")
516
+ for e, w in zip(DEAF.edge_indices, DEAF.edge_weights):
517
+ if abs(w) == inf:
518
+ raise nx.NetworkXError(f"edge {e!r} has infinite weight")
519
+ if not multigraph:
520
+ edges = nx.selfloop_edges(G, data=True)
521
+ else:
522
+ edges = nx.selfloop_edges(G, data=True, keys=True)
523
+ for e in edges:
524
+ if abs(e[-1].get(weight, 0)) == inf:
525
+ raise nx.NetworkXError(f"edge {e[:-1]!r} has infinite weight")
526
+
527
+ ###########################################################################
528
+ # Quick Infeasibility Detection
529
+ ###########################################################################
530
+
531
+ if sum(DEAF.node_demands) != 0:
532
+ raise nx.NetworkXUnfeasible("total node demand is not zero")
533
+ for e, c in zip(DEAF.edge_indices, DEAF.edge_capacities):
534
+ if c < 0:
535
+ raise nx.NetworkXUnfeasible(f"edge {e!r} has negative capacity")
536
+ if not multigraph:
537
+ edges = nx.selfloop_edges(G, data=True)
538
+ else:
539
+ edges = nx.selfloop_edges(G, data=True, keys=True)
540
+ for e in edges:
541
+ if e[-1].get(capacity, inf) < 0:
542
+ raise nx.NetworkXUnfeasible(f"edge {e[:-1]!r} has negative capacity")
543
+
544
+ ###########################################################################
545
+ # Initialization
546
+ ###########################################################################
547
+
548
+ # Add a dummy node -1 and connect all existing nodes to it with infinite-
549
+ # capacity dummy edges. Node -1 will serve as the root of the
550
+ # spanning tree of the network simplex method. The new edges will used to
551
+ # trivially satisfy the node demands and create an initial strongly
552
+ # feasible spanning tree.
553
+ for i, d in enumerate(DEAF.node_demands):
554
+ # Must be greater-than here. Zero-demand nodes must have
555
+ # edges pointing towards the root to ensure strong feasibility.
556
+ if d > 0:
557
+ DEAF.edge_sources.append(-1)
558
+ DEAF.edge_targets.append(i)
559
+ else:
560
+ DEAF.edge_sources.append(i)
561
+ DEAF.edge_targets.append(-1)
562
+ faux_inf = (
563
+ 3
564
+ * max(
565
+ chain(
566
+ [
567
+ sum(c for c in DEAF.edge_capacities if c < inf),
568
+ sum(abs(w) for w in DEAF.edge_weights),
569
+ ],
570
+ (abs(d) for d in DEAF.node_demands),
571
+ )
572
+ )
573
+ or 1
574
+ )
575
+
576
+ n = len(DEAF.node_list) # number of nodes
577
+ DEAF.edge_weights.extend(repeat(faux_inf, n))
578
+ DEAF.edge_capacities.extend(repeat(faux_inf, n))
579
+
580
+ # Construct the initial spanning tree.
581
+ DEAF.initialize_spanning_tree(n, faux_inf)
582
+
583
+ ###########################################################################
584
+ # Pivot loop
585
+ ###########################################################################
586
+
587
+ for i, p, q in DEAF.find_entering_edges():
588
+ Wn, We = DEAF.find_cycle(i, p, q)
589
+ j, s, t = DEAF.find_leaving_edge(Wn, We)
590
+ DEAF.augment_flow(Wn, We, DEAF.residual_capacity(j, s))
591
+ # Do nothing more if the entering edge is the same as the leaving edge.
592
+ if i != j:
593
+ if DEAF.parent[t] != s:
594
+ # Ensure that s is the parent of t.
595
+ s, t = t, s
596
+ if We.index(i) > We.index(j):
597
+ # Ensure that q is in the subtree rooted at t.
598
+ p, q = q, p
599
+ DEAF.remove_edge(s, t)
600
+ DEAF.make_root(q)
601
+ DEAF.add_edge(i, p, q)
602
+ DEAF.update_potentials(i, p, q)
603
+
604
+ ###########################################################################
605
+ # Infeasibility and unboundedness detection
606
+ ###########################################################################
607
+
608
+ if any(DEAF.edge_flow[i] != 0 for i in range(-n, 0)):
609
+ raise nx.NetworkXUnfeasible("no flow satisfies all node demands")
610
+
611
+ if any(DEAF.edge_flow[i] * 2 >= faux_inf for i in range(DEAF.edge_count)) or any(
612
+ e[-1].get(capacity, inf) == inf and e[-1].get(weight, 0) < 0
613
+ for e in nx.selfloop_edges(G, data=True)
614
+ ):
615
+ raise nx.NetworkXUnbounded("negative cycle with infinite capacity found")
616
+
617
+ ###########################################################################
618
+ # Flow cost calculation and flow dict construction
619
+ ###########################################################################
620
+
621
+ del DEAF.edge_flow[DEAF.edge_count :]
622
+ flow_cost = sum(w * x for w, x in zip(DEAF.edge_weights, DEAF.edge_flow))
623
+ flow_dict = {n: {} for n in DEAF.node_list}
624
+
625
+ def add_entry(e):
626
+ """Add a flow dict entry."""
627
+ d = flow_dict[e[0]]
628
+ for k in e[1:-2]:
629
+ try:
630
+ d = d[k]
631
+ except KeyError:
632
+ t = {}
633
+ d[k] = t
634
+ d = t
635
+ d[e[-2]] = e[-1]
636
+
637
+ DEAF.edge_sources = (
638
+ DEAF.node_list[s] for s in DEAF.edge_sources
639
+ ) # Use original nodes.
640
+ DEAF.edge_targets = (
641
+ DEAF.node_list[t] for t in DEAF.edge_targets
642
+ ) # Use original nodes.
643
+ if not multigraph:
644
+ for e in zip(DEAF.edge_sources, DEAF.edge_targets, DEAF.edge_flow):
645
+ add_entry(e)
646
+ edges = G.edges(data=True)
647
+ else:
648
+ for e in zip(
649
+ DEAF.edge_sources, DEAF.edge_targets, DEAF.edge_keys, DEAF.edge_flow
650
+ ):
651
+ add_entry(e)
652
+ edges = G.edges(data=True, keys=True)
653
+ for e in edges:
654
+ if e[0] != e[1]:
655
+ if e[-1].get(capacity, inf) == 0:
656
+ add_entry(e[:-1] + (0,))
657
+ else:
658
+ w = e[-1].get(weight, 0)
659
+ if w >= 0:
660
+ add_entry(e[:-1] + (0,))
661
+ else:
662
+ c = e[-1][capacity]
663
+ flow_cost += w * c
664
+ add_entry(e[:-1] + (c,))
665
+
666
+ return flow_cost, flow_dict
.venv/lib/python3.11/site-packages/networkx/algorithms/flow/preflowpush.py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Highest-label preflow-push algorithm for maximum flow problems.
3
+ """
4
+
5
+ from collections import deque
6
+ from itertools import islice
7
+
8
+ import networkx as nx
9
+
10
+ from ...utils import arbitrary_element
11
+ from .utils import (
12
+ CurrentEdge,
13
+ GlobalRelabelThreshold,
14
+ Level,
15
+ build_residual_network,
16
+ detect_unboundedness,
17
+ )
18
+
19
+ __all__ = ["preflow_push"]
20
+
21
+
22
+ def preflow_push_impl(G, s, t, capacity, residual, global_relabel_freq, value_only):
23
+ """Implementation of the highest-label preflow-push algorithm."""
24
+ if s not in G:
25
+ raise nx.NetworkXError(f"node {str(s)} not in graph")
26
+ if t not in G:
27
+ raise nx.NetworkXError(f"node {str(t)} not in graph")
28
+ if s == t:
29
+ raise nx.NetworkXError("source and sink are the same node")
30
+
31
+ if global_relabel_freq is None:
32
+ global_relabel_freq = 0
33
+ if global_relabel_freq < 0:
34
+ raise nx.NetworkXError("global_relabel_freq must be nonnegative.")
35
+
36
+ if residual is None:
37
+ R = build_residual_network(G, capacity)
38
+ else:
39
+ R = residual
40
+
41
+ detect_unboundedness(R, s, t)
42
+
43
+ R_nodes = R.nodes
44
+ R_pred = R.pred
45
+ R_succ = R.succ
46
+
47
+ # Initialize/reset the residual network.
48
+ for u in R:
49
+ R_nodes[u]["excess"] = 0
50
+ for e in R_succ[u].values():
51
+ e["flow"] = 0
52
+
53
+ def reverse_bfs(src):
54
+ """Perform a reverse breadth-first search from src in the residual
55
+ network.
56
+ """
57
+ heights = {src: 0}
58
+ q = deque([(src, 0)])
59
+ while q:
60
+ u, height = q.popleft()
61
+ height += 1
62
+ for v, attr in R_pred[u].items():
63
+ if v not in heights and attr["flow"] < attr["capacity"]:
64
+ heights[v] = height
65
+ q.append((v, height))
66
+ return heights
67
+
68
+ # Initialize heights of the nodes.
69
+ heights = reverse_bfs(t)
70
+
71
+ if s not in heights:
72
+ # t is not reachable from s in the residual network. The maximum flow
73
+ # must be zero.
74
+ R.graph["flow_value"] = 0
75
+ return R
76
+
77
+ n = len(R)
78
+ # max_height represents the height of the highest level below level n with
79
+ # at least one active node.
80
+ max_height = max(heights[u] for u in heights if u != s)
81
+ heights[s] = n
82
+
83
+ grt = GlobalRelabelThreshold(n, R.size(), global_relabel_freq)
84
+
85
+ # Initialize heights and 'current edge' data structures of the nodes.
86
+ for u in R:
87
+ R_nodes[u]["height"] = heights[u] if u in heights else n + 1
88
+ R_nodes[u]["curr_edge"] = CurrentEdge(R_succ[u])
89
+
90
+ def push(u, v, flow):
91
+ """Push flow units of flow from u to v."""
92
+ R_succ[u][v]["flow"] += flow
93
+ R_succ[v][u]["flow"] -= flow
94
+ R_nodes[u]["excess"] -= flow
95
+ R_nodes[v]["excess"] += flow
96
+
97
+ # The maximum flow must be nonzero now. Initialize the preflow by
98
+ # saturating all edges emanating from s.
99
+ for u, attr in R_succ[s].items():
100
+ flow = attr["capacity"]
101
+ if flow > 0:
102
+ push(s, u, flow)
103
+
104
+ # Partition nodes into levels.
105
+ levels = [Level() for i in range(2 * n)]
106
+ for u in R:
107
+ if u != s and u != t:
108
+ level = levels[R_nodes[u]["height"]]
109
+ if R_nodes[u]["excess"] > 0:
110
+ level.active.add(u)
111
+ else:
112
+ level.inactive.add(u)
113
+
114
+ def activate(v):
115
+ """Move a node from the inactive set to the active set of its level."""
116
+ if v != s and v != t:
117
+ level = levels[R_nodes[v]["height"]]
118
+ if v in level.inactive:
119
+ level.inactive.remove(v)
120
+ level.active.add(v)
121
+
122
+ def relabel(u):
123
+ """Relabel a node to create an admissible edge."""
124
+ grt.add_work(len(R_succ[u]))
125
+ return (
126
+ min(
127
+ R_nodes[v]["height"]
128
+ for v, attr in R_succ[u].items()
129
+ if attr["flow"] < attr["capacity"]
130
+ )
131
+ + 1
132
+ )
133
+
134
+ def discharge(u, is_phase1):
135
+ """Discharge a node until it becomes inactive or, during phase 1 (see
136
+ below), its height reaches at least n. The node is known to have the
137
+ largest height among active nodes.
138
+ """
139
+ height = R_nodes[u]["height"]
140
+ curr_edge = R_nodes[u]["curr_edge"]
141
+ # next_height represents the next height to examine after discharging
142
+ # the current node. During phase 1, it is capped to below n.
143
+ next_height = height
144
+ levels[height].active.remove(u)
145
+ while True:
146
+ v, attr = curr_edge.get()
147
+ if height == R_nodes[v]["height"] + 1 and attr["flow"] < attr["capacity"]:
148
+ flow = min(R_nodes[u]["excess"], attr["capacity"] - attr["flow"])
149
+ push(u, v, flow)
150
+ activate(v)
151
+ if R_nodes[u]["excess"] == 0:
152
+ # The node has become inactive.
153
+ levels[height].inactive.add(u)
154
+ break
155
+ try:
156
+ curr_edge.move_to_next()
157
+ except StopIteration:
158
+ # We have run off the end of the adjacency list, and there can
159
+ # be no more admissible edges. Relabel the node to create one.
160
+ height = relabel(u)
161
+ if is_phase1 and height >= n - 1:
162
+ # Although the node is still active, with a height at least
163
+ # n - 1, it is now known to be on the s side of the minimum
164
+ # s-t cut. Stop processing it until phase 2.
165
+ levels[height].active.add(u)
166
+ break
167
+ # The first relabel operation after global relabeling may not
168
+ # increase the height of the node since the 'current edge' data
169
+ # structure is not rewound. Use height instead of (height - 1)
170
+ # in case other active nodes at the same level are missed.
171
+ next_height = height
172
+ R_nodes[u]["height"] = height
173
+ return next_height
174
+
175
+ def gap_heuristic(height):
176
+ """Apply the gap heuristic."""
177
+ # Move all nodes at levels (height + 1) to max_height to level n + 1.
178
+ for level in islice(levels, height + 1, max_height + 1):
179
+ for u in level.active:
180
+ R_nodes[u]["height"] = n + 1
181
+ for u in level.inactive:
182
+ R_nodes[u]["height"] = n + 1
183
+ levels[n + 1].active.update(level.active)
184
+ level.active.clear()
185
+ levels[n + 1].inactive.update(level.inactive)
186
+ level.inactive.clear()
187
+
188
+ def global_relabel(from_sink):
189
+ """Apply the global relabeling heuristic."""
190
+ src = t if from_sink else s
191
+ heights = reverse_bfs(src)
192
+ if not from_sink:
193
+ # s must be reachable from t. Remove t explicitly.
194
+ del heights[t]
195
+ max_height = max(heights.values())
196
+ if from_sink:
197
+ # Also mark nodes from which t is unreachable for relabeling. This
198
+ # serves the same purpose as the gap heuristic.
199
+ for u in R:
200
+ if u not in heights and R_nodes[u]["height"] < n:
201
+ heights[u] = n + 1
202
+ else:
203
+ # Shift the computed heights because the height of s is n.
204
+ for u in heights:
205
+ heights[u] += n
206
+ max_height += n
207
+ del heights[src]
208
+ for u, new_height in heights.items():
209
+ old_height = R_nodes[u]["height"]
210
+ if new_height != old_height:
211
+ if u in levels[old_height].active:
212
+ levels[old_height].active.remove(u)
213
+ levels[new_height].active.add(u)
214
+ else:
215
+ levels[old_height].inactive.remove(u)
216
+ levels[new_height].inactive.add(u)
217
+ R_nodes[u]["height"] = new_height
218
+ return max_height
219
+
220
+ # Phase 1: Find the maximum preflow by pushing as much flow as possible to
221
+ # t.
222
+
223
+ height = max_height
224
+ while height > 0:
225
+ # Discharge active nodes in the current level.
226
+ while True:
227
+ level = levels[height]
228
+ if not level.active:
229
+ # All active nodes in the current level have been discharged.
230
+ # Move to the next lower level.
231
+ height -= 1
232
+ break
233
+ # Record the old height and level for the gap heuristic.
234
+ old_height = height
235
+ old_level = level
236
+ u = arbitrary_element(level.active)
237
+ height = discharge(u, True)
238
+ if grt.is_reached():
239
+ # Global relabeling heuristic: Recompute the exact heights of
240
+ # all nodes.
241
+ height = global_relabel(True)
242
+ max_height = height
243
+ grt.clear_work()
244
+ elif not old_level.active and not old_level.inactive:
245
+ # Gap heuristic: If the level at old_height is empty (a 'gap'),
246
+ # a minimum cut has been identified. All nodes with heights
247
+ # above old_height can have their heights set to n + 1 and not
248
+ # be further processed before a maximum preflow is found.
249
+ gap_heuristic(old_height)
250
+ height = old_height - 1
251
+ max_height = height
252
+ else:
253
+ # Update the height of the highest level with at least one
254
+ # active node.
255
+ max_height = max(max_height, height)
256
+
257
+ # A maximum preflow has been found. The excess at t is the maximum flow
258
+ # value.
259
+ if value_only:
260
+ R.graph["flow_value"] = R_nodes[t]["excess"]
261
+ return R
262
+
263
+ # Phase 2: Convert the maximum preflow into a maximum flow by returning the
264
+ # excess to s.
265
+
266
+ # Relabel all nodes so that they have accurate heights.
267
+ height = global_relabel(False)
268
+ grt.clear_work()
269
+
270
+ # Continue to discharge the active nodes.
271
+ while height > n:
272
+ # Discharge active nodes in the current level.
273
+ while True:
274
+ level = levels[height]
275
+ if not level.active:
276
+ # All active nodes in the current level have been discharged.
277
+ # Move to the next lower level.
278
+ height -= 1
279
+ break
280
+ u = arbitrary_element(level.active)
281
+ height = discharge(u, False)
282
+ if grt.is_reached():
283
+ # Global relabeling heuristic.
284
+ height = global_relabel(False)
285
+ grt.clear_work()
286
+
287
+ R.graph["flow_value"] = R_nodes[t]["excess"]
288
+ return R
289
+
290
+
291
+ @nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True)
292
+ def preflow_push(
293
+ G, s, t, capacity="capacity", residual=None, global_relabel_freq=1, value_only=False
294
+ ):
295
+ r"""Find a maximum single-commodity flow using the highest-label
296
+ preflow-push algorithm.
297
+
298
+ This function returns the residual network resulting after computing
299
+ the maximum flow. See below for details about the conventions
300
+ NetworkX uses for defining residual networks.
301
+
302
+ This algorithm has a running time of $O(n^2 \sqrt{m})$ for $n$ nodes and
303
+ $m$ edges.
304
+
305
+
306
+ Parameters
307
+ ----------
308
+ G : NetworkX graph
309
+ Edges of the graph are expected to have an attribute called
310
+ 'capacity'. If this attribute is not present, the edge is
311
+ considered to have infinite capacity.
312
+
313
+ s : node
314
+ Source node for the flow.
315
+
316
+ t : node
317
+ Sink node for the flow.
318
+
319
+ capacity : string
320
+ Edges of the graph G are expected to have an attribute capacity
321
+ that indicates how much flow the edge can support. If this
322
+ attribute is not present, the edge is considered to have
323
+ infinite capacity. Default value: 'capacity'.
324
+
325
+ residual : NetworkX graph
326
+ Residual network on which the algorithm is to be executed. If None, a
327
+ new residual network is created. Default value: None.
328
+
329
+ global_relabel_freq : integer, float
330
+ Relative frequency of applying the global relabeling heuristic to speed
331
+ up the algorithm. If it is None, the heuristic is disabled. Default
332
+ value: 1.
333
+
334
+ value_only : bool
335
+ If False, compute a maximum flow; otherwise, compute a maximum preflow
336
+ which is enough for computing the maximum flow value. Default value:
337
+ False.
338
+
339
+ Returns
340
+ -------
341
+ R : NetworkX DiGraph
342
+ Residual network after computing the maximum flow.
343
+
344
+ Raises
345
+ ------
346
+ NetworkXError
347
+ The algorithm does not support MultiGraph and MultiDiGraph. If
348
+ the input graph is an instance of one of these two classes, a
349
+ NetworkXError is raised.
350
+
351
+ NetworkXUnbounded
352
+ If the graph has a path of infinite capacity, the value of a
353
+ feasible flow on the graph is unbounded above and the function
354
+ raises a NetworkXUnbounded.
355
+
356
+ See also
357
+ --------
358
+ :meth:`maximum_flow`
359
+ :meth:`minimum_cut`
360
+ :meth:`edmonds_karp`
361
+ :meth:`shortest_augmenting_path`
362
+
363
+ Notes
364
+ -----
365
+ The residual network :samp:`R` from an input graph :samp:`G` has the
366
+ same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
367
+ of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
368
+ self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
369
+ in :samp:`G`. For each node :samp:`u` in :samp:`R`,
370
+ :samp:`R.nodes[u]['excess']` represents the difference between flow into
371
+ :samp:`u` and flow out of :samp:`u`.
372
+
373
+ For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
374
+ is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
375
+ in :samp:`G` or zero otherwise. If the capacity is infinite,
376
+ :samp:`R[u][v]['capacity']` will have a high arbitrary finite value
377
+ that does not affect the solution of the problem. This value is stored in
378
+ :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
379
+ :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
380
+ satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
381
+
382
+ The flow value, defined as the total flow into :samp:`t`, the sink, is
383
+ stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using
384
+ only edges :samp:`(u, v)` such that
385
+ :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
386
+ :samp:`s`-:samp:`t` cut.
387
+
388
+ Examples
389
+ --------
390
+ >>> from networkx.algorithms.flow import preflow_push
391
+
392
+ The functions that implement flow algorithms and output a residual
393
+ network, such as this one, are not imported to the base NetworkX
394
+ namespace, so you have to explicitly import them from the flow package.
395
+
396
+ >>> G = nx.DiGraph()
397
+ >>> G.add_edge("x", "a", capacity=3.0)
398
+ >>> G.add_edge("x", "b", capacity=1.0)
399
+ >>> G.add_edge("a", "c", capacity=3.0)
400
+ >>> G.add_edge("b", "c", capacity=5.0)
401
+ >>> G.add_edge("b", "d", capacity=4.0)
402
+ >>> G.add_edge("d", "e", capacity=2.0)
403
+ >>> G.add_edge("c", "y", capacity=2.0)
404
+ >>> G.add_edge("e", "y", capacity=3.0)
405
+ >>> R = preflow_push(G, "x", "y")
406
+ >>> flow_value = nx.maximum_flow_value(G, "x", "y")
407
+ >>> flow_value == R.graph["flow_value"]
408
+ True
409
+ >>> # preflow_push also stores the maximum flow value
410
+ >>> # in the excess attribute of the sink node t
411
+ >>> flow_value == R.nodes["y"]["excess"]
412
+ True
413
+ >>> # For some problems, you might only want to compute a
414
+ >>> # maximum preflow.
415
+ >>> R = preflow_push(G, "x", "y", value_only=True)
416
+ >>> flow_value == R.graph["flow_value"]
417
+ True
418
+ >>> flow_value == R.nodes["y"]["excess"]
419
+ True
420
+
421
+ """
422
+ R = preflow_push_impl(G, s, t, capacity, residual, global_relabel_freq, value_only)
423
+ R.graph["algorithm"] = "preflow_push"
424
+ nx._clear_cache(R)
425
+ return R
.venv/lib/python3.11/site-packages/networkx/algorithms/graph_hashing.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Functions for hashing graphs to strings.
3
+ Isomorphic graphs should be assigned identical hashes.
4
+ For now, only Weisfeiler-Lehman hashing is implemented.
5
+ """
6
+
7
+ from collections import Counter, defaultdict
8
+ from hashlib import blake2b
9
+
10
+ import networkx as nx
11
+
12
+ __all__ = ["weisfeiler_lehman_graph_hash", "weisfeiler_lehman_subgraph_hashes"]
13
+
14
+
15
+ def _hash_label(label, digest_size):
16
+ return blake2b(label.encode("ascii"), digest_size=digest_size).hexdigest()
17
+
18
+
19
+ def _init_node_labels(G, edge_attr, node_attr):
20
+ if node_attr:
21
+ return {u: str(dd[node_attr]) for u, dd in G.nodes(data=True)}
22
+ elif edge_attr:
23
+ return {u: "" for u in G}
24
+ else:
25
+ return {u: str(deg) for u, deg in G.degree()}
26
+
27
+
28
+ def _neighborhood_aggregate(G, node, node_labels, edge_attr=None):
29
+ """
30
+ Compute new labels for given node by aggregating
31
+ the labels of each node's neighbors.
32
+ """
33
+ label_list = []
34
+ for nbr in G.neighbors(node):
35
+ prefix = "" if edge_attr is None else str(G[node][nbr][edge_attr])
36
+ label_list.append(prefix + node_labels[nbr])
37
+ return node_labels[node] + "".join(sorted(label_list))
38
+
39
+
40
+ @nx.utils.not_implemented_for("multigraph")
41
+ @nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr")
42
+ def weisfeiler_lehman_graph_hash(
43
+ G, edge_attr=None, node_attr=None, iterations=3, digest_size=16
44
+ ):
45
+ """Return Weisfeiler Lehman (WL) graph hash.
46
+
47
+ The function iteratively aggregates and hashes neighborhoods of each node.
48
+ After each node's neighbors are hashed to obtain updated node labels,
49
+ a hashed histogram of resulting labels is returned as the final hash.
50
+
51
+ Hashes are identical for isomorphic graphs and strong guarantees that
52
+ non-isomorphic graphs will get different hashes. See [1]_ for details.
53
+
54
+ If no node or edge attributes are provided, the degree of each node
55
+ is used as its initial label.
56
+ Otherwise, node and/or edge labels are used to compute the hash.
57
+
58
+ Parameters
59
+ ----------
60
+ G : graph
61
+ The graph to be hashed.
62
+ Can have node and/or edge attributes. Can also have no attributes.
63
+ edge_attr : string, optional (default=None)
64
+ The key in edge attribute dictionary to be used for hashing.
65
+ If None, edge labels are ignored.
66
+ node_attr: string, optional (default=None)
67
+ The key in node attribute dictionary to be used for hashing.
68
+ If None, and no edge_attr given, use the degrees of the nodes as labels.
69
+ iterations: int, optional (default=3)
70
+ Number of neighbor aggregations to perform.
71
+ Should be larger for larger graphs.
72
+ digest_size: int, optional (default=16)
73
+ Size (in bits) of blake2b hash digest to use for hashing node labels.
74
+
75
+ Returns
76
+ -------
77
+ h : string
78
+ Hexadecimal string corresponding to hash of the input graph.
79
+
80
+ Examples
81
+ --------
82
+ Two graphs with edge attributes that are isomorphic, except for
83
+ differences in the edge labels.
84
+
85
+ >>> G1 = nx.Graph()
86
+ >>> G1.add_edges_from(
87
+ ... [
88
+ ... (1, 2, {"label": "A"}),
89
+ ... (2, 3, {"label": "A"}),
90
+ ... (3, 1, {"label": "A"}),
91
+ ... (1, 4, {"label": "B"}),
92
+ ... ]
93
+ ... )
94
+ >>> G2 = nx.Graph()
95
+ >>> G2.add_edges_from(
96
+ ... [
97
+ ... (5, 6, {"label": "B"}),
98
+ ... (6, 7, {"label": "A"}),
99
+ ... (7, 5, {"label": "A"}),
100
+ ... (7, 8, {"label": "A"}),
101
+ ... ]
102
+ ... )
103
+
104
+ Omitting the `edge_attr` option, results in identical hashes.
105
+
106
+ >>> nx.weisfeiler_lehman_graph_hash(G1)
107
+ '7bc4dde9a09d0b94c5097b219891d81a'
108
+ >>> nx.weisfeiler_lehman_graph_hash(G2)
109
+ '7bc4dde9a09d0b94c5097b219891d81a'
110
+
111
+ With edge labels, the graphs are no longer assigned
112
+ the same hash digest.
113
+
114
+ >>> nx.weisfeiler_lehman_graph_hash(G1, edge_attr="label")
115
+ 'c653d85538bcf041d88c011f4f905f10'
116
+ >>> nx.weisfeiler_lehman_graph_hash(G2, edge_attr="label")
117
+ '3dcd84af1ca855d0eff3c978d88e7ec7'
118
+
119
+ Notes
120
+ -----
121
+ To return the WL hashes of each subgraph of a graph, use
122
+ `weisfeiler_lehman_subgraph_hashes`
123
+
124
+ Similarity between hashes does not imply similarity between graphs.
125
+
126
+ References
127
+ ----------
128
+ .. [1] Shervashidze, Nino, Pascal Schweitzer, Erik Jan Van Leeuwen,
129
+ Kurt Mehlhorn, and Karsten M. Borgwardt. Weisfeiler Lehman
130
+ Graph Kernels. Journal of Machine Learning Research. 2011.
131
+ http://www.jmlr.org/papers/volume12/shervashidze11a/shervashidze11a.pdf
132
+
133
+ See also
134
+ --------
135
+ weisfeiler_lehman_subgraph_hashes
136
+ """
137
+
138
+ def weisfeiler_lehman_step(G, labels, edge_attr=None):
139
+ """
140
+ Apply neighborhood aggregation to each node
141
+ in the graph.
142
+ Computes a dictionary with labels for each node.
143
+ """
144
+ new_labels = {}
145
+ for node in G.nodes():
146
+ label = _neighborhood_aggregate(G, node, labels, edge_attr=edge_attr)
147
+ new_labels[node] = _hash_label(label, digest_size)
148
+ return new_labels
149
+
150
+ # set initial node labels
151
+ node_labels = _init_node_labels(G, edge_attr, node_attr)
152
+
153
+ subgraph_hash_counts = []
154
+ for _ in range(iterations):
155
+ node_labels = weisfeiler_lehman_step(G, node_labels, edge_attr=edge_attr)
156
+ counter = Counter(node_labels.values())
157
+ # sort the counter, extend total counts
158
+ subgraph_hash_counts.extend(sorted(counter.items(), key=lambda x: x[0]))
159
+
160
+ # hash the final counter
161
+ return _hash_label(str(tuple(subgraph_hash_counts)), digest_size)
162
+
163
+
164
+ @nx.utils.not_implemented_for("multigraph")
165
+ @nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr")
166
+ def weisfeiler_lehman_subgraph_hashes(
167
+ G,
168
+ edge_attr=None,
169
+ node_attr=None,
170
+ iterations=3,
171
+ digest_size=16,
172
+ include_initial_labels=False,
173
+ ):
174
+ """
175
+ Return a dictionary of subgraph hashes by node.
176
+
177
+ Dictionary keys are nodes in `G`, and values are a list of hashes.
178
+ Each hash corresponds to a subgraph rooted at a given node u in `G`.
179
+ Lists of subgraph hashes are sorted in increasing order of depth from
180
+ their root node, with the hash at index i corresponding to a subgraph
181
+ of nodes at most i edges distance from u. Thus, each list will contain
182
+ `iterations` elements - a hash for a subgraph at each depth. If
183
+ `include_initial_labels` is set to `True`, each list will additionally
184
+ have contain a hash of the initial node label (or equivalently a
185
+ subgraph of depth 0) prepended, totalling ``iterations + 1`` elements.
186
+
187
+ The function iteratively aggregates and hashes neighborhoods of each node.
188
+ This is achieved for each step by replacing for each node its label from
189
+ the previous iteration with its hashed 1-hop neighborhood aggregate.
190
+ The new node label is then appended to a list of node labels for each
191
+ node.
192
+
193
+ To aggregate neighborhoods for a node $u$ at each step, all labels of
194
+ nodes adjacent to $u$ are concatenated. If the `edge_attr` parameter is set,
195
+ labels for each neighboring node are prefixed with the value of this attribute
196
+ along the connecting edge from this neighbor to node $u$. The resulting string
197
+ is then hashed to compress this information into a fixed digest size.
198
+
199
+ Thus, at the $i$-th iteration, nodes within $i$ hops influence any given
200
+ hashed node label. We can therefore say that at depth $i$ for node $u$
201
+ we have a hash for a subgraph induced by the $i$-hop neighborhood of $u$.
202
+
203
+ The output can be used to create general Weisfeiler-Lehman graph kernels,
204
+ or generate features for graphs or nodes - for example to generate 'words' in
205
+ a graph as seen in the 'graph2vec' algorithm.
206
+ See [1]_ & [2]_ respectively for details.
207
+
208
+ Hashes are identical for isomorphic subgraphs and there exist strong
209
+ guarantees that non-isomorphic graphs will get different hashes.
210
+ See [1]_ for details.
211
+
212
+ If no node or edge attributes are provided, the degree of each node
213
+ is used as its initial label.
214
+ Otherwise, node and/or edge labels are used to compute the hash.
215
+
216
+ Parameters
217
+ ----------
218
+ G : graph
219
+ The graph to be hashed.
220
+ Can have node and/or edge attributes. Can also have no attributes.
221
+ edge_attr : string, optional (default=None)
222
+ The key in edge attribute dictionary to be used for hashing.
223
+ If None, edge labels are ignored.
224
+ node_attr : string, optional (default=None)
225
+ The key in node attribute dictionary to be used for hashing.
226
+ If None, and no edge_attr given, use the degrees of the nodes as labels.
227
+ If None, and edge_attr is given, each node starts with an identical label.
228
+ iterations : int, optional (default=3)
229
+ Number of neighbor aggregations to perform.
230
+ Should be larger for larger graphs.
231
+ digest_size : int, optional (default=16)
232
+ Size (in bits) of blake2b hash digest to use for hashing node labels.
233
+ The default size is 16 bits.
234
+ include_initial_labels : bool, optional (default=False)
235
+ If True, include the hashed initial node label as the first subgraph
236
+ hash for each node.
237
+
238
+ Returns
239
+ -------
240
+ node_subgraph_hashes : dict
241
+ A dictionary with each key given by a node in G, and each value given
242
+ by the subgraph hashes in order of depth from the key node.
243
+
244
+ Examples
245
+ --------
246
+ Finding similar nodes in different graphs:
247
+
248
+ >>> G1 = nx.Graph()
249
+ >>> G1.add_edges_from([(1, 2), (2, 3), (2, 4), (3, 5), (4, 6), (5, 7), (6, 7)])
250
+ >>> G2 = nx.Graph()
251
+ >>> G2.add_edges_from([(1, 3), (2, 3), (1, 6), (1, 5), (4, 6)])
252
+ >>> g1_hashes = nx.weisfeiler_lehman_subgraph_hashes(
253
+ ... G1, iterations=3, digest_size=8
254
+ ... )
255
+ >>> g2_hashes = nx.weisfeiler_lehman_subgraph_hashes(
256
+ ... G2, iterations=3, digest_size=8
257
+ ... )
258
+
259
+ Even though G1 and G2 are not isomorphic (they have different numbers of edges),
260
+ the hash sequence of depth 3 for node 1 in G1 and node 5 in G2 are similar:
261
+
262
+ >>> g1_hashes[1]
263
+ ['a93b64973cfc8897', 'db1b43ae35a1878f', '57872a7d2059c1c0']
264
+ >>> g2_hashes[5]
265
+ ['a93b64973cfc8897', 'db1b43ae35a1878f', '1716d2a4012fa4bc']
266
+
267
+ The first 2 WL subgraph hashes match. From this we can conclude that it's very
268
+ likely the neighborhood of 2 hops around these nodes are isomorphic.
269
+
270
+ However the 3-hop neighborhoods of ``G1`` and ``G2`` are not isomorphic since the
271
+ 3rd hashes in the lists above are not equal.
272
+
273
+ These nodes may be candidates to be classified together since their local topology
274
+ is similar.
275
+
276
+ Notes
277
+ -----
278
+ To hash the full graph when subgraph hashes are not needed, use
279
+ `weisfeiler_lehman_graph_hash` for efficiency.
280
+
281
+ Similarity between hashes does not imply similarity between graphs.
282
+
283
+ References
284
+ ----------
285
+ .. [1] Shervashidze, Nino, Pascal Schweitzer, Erik Jan Van Leeuwen,
286
+ Kurt Mehlhorn, and Karsten M. Borgwardt. Weisfeiler Lehman
287
+ Graph Kernels. Journal of Machine Learning Research. 2011.
288
+ http://www.jmlr.org/papers/volume12/shervashidze11a/shervashidze11a.pdf
289
+ .. [2] Annamalai Narayanan, Mahinthan Chandramohan, Rajasekar Venkatesan,
290
+ Lihui Chen, Yang Liu and Shantanu Jaiswa. graph2vec: Learning
291
+ Distributed Representations of Graphs. arXiv. 2017
292
+ https://arxiv.org/pdf/1707.05005.pdf
293
+
294
+ See also
295
+ --------
296
+ weisfeiler_lehman_graph_hash
297
+ """
298
+
299
+ def weisfeiler_lehman_step(G, labels, node_subgraph_hashes, edge_attr=None):
300
+ """
301
+ Apply neighborhood aggregation to each node
302
+ in the graph.
303
+ Computes a dictionary with labels for each node.
304
+ Appends the new hashed label to the dictionary of subgraph hashes
305
+ originating from and indexed by each node in G
306
+ """
307
+ new_labels = {}
308
+ for node in G.nodes():
309
+ label = _neighborhood_aggregate(G, node, labels, edge_attr=edge_attr)
310
+ hashed_label = _hash_label(label, digest_size)
311
+ new_labels[node] = hashed_label
312
+ node_subgraph_hashes[node].append(hashed_label)
313
+ return new_labels
314
+
315
+ node_labels = _init_node_labels(G, edge_attr, node_attr)
316
+ if include_initial_labels:
317
+ node_subgraph_hashes = {
318
+ k: [_hash_label(v, digest_size)] for k, v in node_labels.items()
319
+ }
320
+ else:
321
+ node_subgraph_hashes = defaultdict(list)
322
+
323
+ for _ in range(iterations):
324
+ node_labels = weisfeiler_lehman_step(
325
+ G, node_labels, node_subgraph_hashes, edge_attr
326
+ )
327
+
328
+ return dict(node_subgraph_hashes)
.venv/lib/python3.11/site-packages/networkx/algorithms/graphical.py ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test sequences for graphiness."""
2
+
3
+ import heapq
4
+
5
+ import networkx as nx
6
+
7
+ __all__ = [
8
+ "is_graphical",
9
+ "is_multigraphical",
10
+ "is_pseudographical",
11
+ "is_digraphical",
12
+ "is_valid_degree_sequence_erdos_gallai",
13
+ "is_valid_degree_sequence_havel_hakimi",
14
+ ]
15
+
16
+
17
+ @nx._dispatchable(graphs=None)
18
+ def is_graphical(sequence, method="eg"):
19
+ """Returns True if sequence is a valid degree sequence.
20
+
21
+ A degree sequence is valid if some graph can realize it.
22
+
23
+ Parameters
24
+ ----------
25
+ sequence : list or iterable container
26
+ A sequence of integer node degrees
27
+
28
+ method : "eg" | "hh" (default: 'eg')
29
+ The method used to validate the degree sequence.
30
+ "eg" corresponds to the Erdős-Gallai algorithm
31
+ [EG1960]_, [choudum1986]_, and
32
+ "hh" to the Havel-Hakimi algorithm
33
+ [havel1955]_, [hakimi1962]_, [CL1996]_.
34
+
35
+ Returns
36
+ -------
37
+ valid : bool
38
+ True if the sequence is a valid degree sequence and False if not.
39
+
40
+ Examples
41
+ --------
42
+ >>> G = nx.path_graph(4)
43
+ >>> sequence = (d for n, d in G.degree())
44
+ >>> nx.is_graphical(sequence)
45
+ True
46
+
47
+ To test a non-graphical sequence:
48
+ >>> sequence_list = [d for n, d in G.degree()]
49
+ >>> sequence_list[-1] += 1
50
+ >>> nx.is_graphical(sequence_list)
51
+ False
52
+
53
+ References
54
+ ----------
55
+ .. [EG1960] Erdős and Gallai, Mat. Lapok 11 264, 1960.
56
+ .. [choudum1986] S.A. Choudum. "A simple proof of the Erdős-Gallai theorem on
57
+ graph sequences." Bulletin of the Australian Mathematical Society, 33,
58
+ pp 67-70, 1986. https://doi.org/10.1017/S0004972700002872
59
+ .. [havel1955] Havel, V. "A Remark on the Existence of Finite Graphs"
60
+ Casopis Pest. Mat. 80, 477-480, 1955.
61
+ .. [hakimi1962] Hakimi, S. "On the Realizability of a Set of Integers as
62
+ Degrees of the Vertices of a Graph." SIAM J. Appl. Math. 10, 496-506, 1962.
63
+ .. [CL1996] G. Chartrand and L. Lesniak, "Graphs and Digraphs",
64
+ Chapman and Hall/CRC, 1996.
65
+ """
66
+ if method == "eg":
67
+ valid = is_valid_degree_sequence_erdos_gallai(list(sequence))
68
+ elif method == "hh":
69
+ valid = is_valid_degree_sequence_havel_hakimi(list(sequence))
70
+ else:
71
+ msg = "`method` must be 'eg' or 'hh'"
72
+ raise nx.NetworkXException(msg)
73
+ return valid
74
+
75
+
76
+ def _basic_graphical_tests(deg_sequence):
77
+ # Sort and perform some simple tests on the sequence
78
+ deg_sequence = nx.utils.make_list_of_ints(deg_sequence)
79
+ p = len(deg_sequence)
80
+ num_degs = [0] * p
81
+ dmax, dmin, dsum, n = 0, p, 0, 0
82
+ for d in deg_sequence:
83
+ # Reject if degree is negative or larger than the sequence length
84
+ if d < 0 or d >= p:
85
+ raise nx.NetworkXUnfeasible
86
+ # Process only the non-zero integers
87
+ elif d > 0:
88
+ dmax, dmin, dsum, n = max(dmax, d), min(dmin, d), dsum + d, n + 1
89
+ num_degs[d] += 1
90
+ # Reject sequence if it has odd sum or is oversaturated
91
+ if dsum % 2 or dsum > n * (n - 1):
92
+ raise nx.NetworkXUnfeasible
93
+ return dmax, dmin, dsum, n, num_degs
94
+
95
+
96
+ @nx._dispatchable(graphs=None)
97
+ def is_valid_degree_sequence_havel_hakimi(deg_sequence):
98
+ r"""Returns True if deg_sequence can be realized by a simple graph.
99
+
100
+ The validation proceeds using the Havel-Hakimi theorem
101
+ [havel1955]_, [hakimi1962]_, [CL1996]_.
102
+ Worst-case run time is $O(s)$ where $s$ is the sum of the sequence.
103
+
104
+ Parameters
105
+ ----------
106
+ deg_sequence : list
107
+ A list of integers where each element specifies the degree of a node
108
+ in a graph.
109
+
110
+ Returns
111
+ -------
112
+ valid : bool
113
+ True if deg_sequence is graphical and False if not.
114
+
115
+ Examples
116
+ --------
117
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
118
+ >>> sequence = (d for _, d in G.degree())
119
+ >>> nx.is_valid_degree_sequence_havel_hakimi(sequence)
120
+ True
121
+
122
+ To test a non-valid sequence:
123
+ >>> sequence_list = [d for _, d in G.degree()]
124
+ >>> sequence_list[-1] += 1
125
+ >>> nx.is_valid_degree_sequence_havel_hakimi(sequence_list)
126
+ False
127
+
128
+ Notes
129
+ -----
130
+ The ZZ condition says that for the sequence d if
131
+
132
+ .. math::
133
+ |d| >= \frac{(\max(d) + \min(d) + 1)^2}{4*\min(d)}
134
+
135
+ then d is graphical. This was shown in Theorem 6 in [1]_.
136
+
137
+ References
138
+ ----------
139
+ .. [1] I.E. Zverovich and V.E. Zverovich. "Contributions to the theory
140
+ of graphic sequences", Discrete Mathematics, 105, pp. 292-303 (1992).
141
+ .. [havel1955] Havel, V. "A Remark on the Existence of Finite Graphs"
142
+ Casopis Pest. Mat. 80, 477-480, 1955.
143
+ .. [hakimi1962] Hakimi, S. "On the Realizability of a Set of Integers as
144
+ Degrees of the Vertices of a Graph." SIAM J. Appl. Math. 10, 496-506, 1962.
145
+ .. [CL1996] G. Chartrand and L. Lesniak, "Graphs and Digraphs",
146
+ Chapman and Hall/CRC, 1996.
147
+ """
148
+ try:
149
+ dmax, dmin, dsum, n, num_degs = _basic_graphical_tests(deg_sequence)
150
+ except nx.NetworkXUnfeasible:
151
+ return False
152
+ # Accept if sequence has no non-zero degrees or passes the ZZ condition
153
+ if n == 0 or 4 * dmin * n >= (dmax + dmin + 1) * (dmax + dmin + 1):
154
+ return True
155
+
156
+ modstubs = [0] * (dmax + 1)
157
+ # Successively reduce degree sequence by removing the maximum degree
158
+ while n > 0:
159
+ # Retrieve the maximum degree in the sequence
160
+ while num_degs[dmax] == 0:
161
+ dmax -= 1
162
+ # If there are not enough stubs to connect to, then the sequence is
163
+ # not graphical
164
+ if dmax > n - 1:
165
+ return False
166
+
167
+ # Remove largest stub in list
168
+ num_degs[dmax], n = num_degs[dmax] - 1, n - 1
169
+ # Reduce the next dmax largest stubs
170
+ mslen = 0
171
+ k = dmax
172
+ for i in range(dmax):
173
+ while num_degs[k] == 0:
174
+ k -= 1
175
+ num_degs[k], n = num_degs[k] - 1, n - 1
176
+ if k > 1:
177
+ modstubs[mslen] = k - 1
178
+ mslen += 1
179
+ # Add back to the list any non-zero stubs that were removed
180
+ for i in range(mslen):
181
+ stub = modstubs[i]
182
+ num_degs[stub], n = num_degs[stub] + 1, n + 1
183
+ return True
184
+
185
+
186
+ @nx._dispatchable(graphs=None)
187
+ def is_valid_degree_sequence_erdos_gallai(deg_sequence):
188
+ r"""Returns True if deg_sequence can be realized by a simple graph.
189
+
190
+ The validation is done using the Erdős-Gallai theorem [EG1960]_.
191
+
192
+ Parameters
193
+ ----------
194
+ deg_sequence : list
195
+ A list of integers
196
+
197
+ Returns
198
+ -------
199
+ valid : bool
200
+ True if deg_sequence is graphical and False if not.
201
+
202
+ Examples
203
+ --------
204
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
205
+ >>> sequence = (d for _, d in G.degree())
206
+ >>> nx.is_valid_degree_sequence_erdos_gallai(sequence)
207
+ True
208
+
209
+ To test a non-valid sequence:
210
+ >>> sequence_list = [d for _, d in G.degree()]
211
+ >>> sequence_list[-1] += 1
212
+ >>> nx.is_valid_degree_sequence_erdos_gallai(sequence_list)
213
+ False
214
+
215
+ Notes
216
+ -----
217
+
218
+ This implementation uses an equivalent form of the Erdős-Gallai criterion.
219
+ Worst-case run time is $O(n)$ where $n$ is the length of the sequence.
220
+
221
+ Specifically, a sequence d is graphical if and only if the
222
+ sum of the sequence is even and for all strong indices k in the sequence,
223
+
224
+ .. math::
225
+
226
+ \sum_{i=1}^{k} d_i \leq k(k-1) + \sum_{j=k+1}^{n} \min(d_i,k)
227
+ = k(n-1) - ( k \sum_{j=0}^{k-1} n_j - \sum_{j=0}^{k-1} j n_j )
228
+
229
+ A strong index k is any index where d_k >= k and the value n_j is the
230
+ number of occurrences of j in d. The maximal strong index is called the
231
+ Durfee index.
232
+
233
+ This particular rearrangement comes from the proof of Theorem 3 in [2]_.
234
+
235
+ The ZZ condition says that for the sequence d if
236
+
237
+ .. math::
238
+ |d| >= \frac{(\max(d) + \min(d) + 1)^2}{4*\min(d)}
239
+
240
+ then d is graphical. This was shown in Theorem 6 in [2]_.
241
+
242
+ References
243
+ ----------
244
+ .. [1] A. Tripathi and S. Vijay. "A note on a theorem of Erdős & Gallai",
245
+ Discrete Mathematics, 265, pp. 417-420 (2003).
246
+ .. [2] I.E. Zverovich and V.E. Zverovich. "Contributions to the theory
247
+ of graphic sequences", Discrete Mathematics, 105, pp. 292-303 (1992).
248
+ .. [EG1960] Erdős and Gallai, Mat. Lapok 11 264, 1960.
249
+ """
250
+ try:
251
+ dmax, dmin, dsum, n, num_degs = _basic_graphical_tests(deg_sequence)
252
+ except nx.NetworkXUnfeasible:
253
+ return False
254
+ # Accept if sequence has no non-zero degrees or passes the ZZ condition
255
+ if n == 0 or 4 * dmin * n >= (dmax + dmin + 1) * (dmax + dmin + 1):
256
+ return True
257
+
258
+ # Perform the EG checks using the reformulation of Zverovich and Zverovich
259
+ k, sum_deg, sum_nj, sum_jnj = 0, 0, 0, 0
260
+ for dk in range(dmax, dmin - 1, -1):
261
+ if dk < k + 1: # Check if already past Durfee index
262
+ return True
263
+ if num_degs[dk] > 0:
264
+ run_size = num_degs[dk] # Process a run of identical-valued degrees
265
+ if dk < k + run_size: # Check if end of run is past Durfee index
266
+ run_size = dk - k # Adjust back to Durfee index
267
+ sum_deg += run_size * dk
268
+ for v in range(run_size):
269
+ sum_nj += num_degs[k + v]
270
+ sum_jnj += (k + v) * num_degs[k + v]
271
+ k += run_size
272
+ if sum_deg > k * (n - 1) - k * sum_nj + sum_jnj:
273
+ return False
274
+ return True
275
+
276
+
277
+ @nx._dispatchable(graphs=None)
278
+ def is_multigraphical(sequence):
279
+ """Returns True if some multigraph can realize the sequence.
280
+
281
+ Parameters
282
+ ----------
283
+ sequence : list
284
+ A list of integers
285
+
286
+ Returns
287
+ -------
288
+ valid : bool
289
+ True if deg_sequence is a multigraphic degree sequence and False if not.
290
+
291
+ Examples
292
+ --------
293
+ >>> G = nx.MultiGraph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
294
+ >>> sequence = (d for _, d in G.degree())
295
+ >>> nx.is_multigraphical(sequence)
296
+ True
297
+
298
+ To test a non-multigraphical sequence:
299
+ >>> sequence_list = [d for _, d in G.degree()]
300
+ >>> sequence_list[-1] += 1
301
+ >>> nx.is_multigraphical(sequence_list)
302
+ False
303
+
304
+ Notes
305
+ -----
306
+ The worst-case run time is $O(n)$ where $n$ is the length of the sequence.
307
+
308
+ References
309
+ ----------
310
+ .. [1] S. L. Hakimi. "On the realizability of a set of integers as
311
+ degrees of the vertices of a linear graph", J. SIAM, 10, pp. 496-506
312
+ (1962).
313
+ """
314
+ try:
315
+ deg_sequence = nx.utils.make_list_of_ints(sequence)
316
+ except nx.NetworkXError:
317
+ return False
318
+ dsum, dmax = 0, 0
319
+ for d in deg_sequence:
320
+ if d < 0:
321
+ return False
322
+ dsum, dmax = dsum + d, max(dmax, d)
323
+ if dsum % 2 or dsum < 2 * dmax:
324
+ return False
325
+ return True
326
+
327
+
328
+ @nx._dispatchable(graphs=None)
329
+ def is_pseudographical(sequence):
330
+ """Returns True if some pseudograph can realize the sequence.
331
+
332
+ Every nonnegative integer sequence with an even sum is pseudographical
333
+ (see [1]_).
334
+
335
+ Parameters
336
+ ----------
337
+ sequence : list or iterable container
338
+ A sequence of integer node degrees
339
+
340
+ Returns
341
+ -------
342
+ valid : bool
343
+ True if the sequence is a pseudographic degree sequence and False if not.
344
+
345
+ Examples
346
+ --------
347
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
348
+ >>> sequence = (d for _, d in G.degree())
349
+ >>> nx.is_pseudographical(sequence)
350
+ True
351
+
352
+ To test a non-pseudographical sequence:
353
+ >>> sequence_list = [d for _, d in G.degree()]
354
+ >>> sequence_list[-1] += 1
355
+ >>> nx.is_pseudographical(sequence_list)
356
+ False
357
+
358
+ Notes
359
+ -----
360
+ The worst-case run time is $O(n)$ where n is the length of the sequence.
361
+
362
+ References
363
+ ----------
364
+ .. [1] F. Boesch and F. Harary. "Line removal algorithms for graphs
365
+ and their degree lists", IEEE Trans. Circuits and Systems, CAS-23(12),
366
+ pp. 778-782 (1976).
367
+ """
368
+ try:
369
+ deg_sequence = nx.utils.make_list_of_ints(sequence)
370
+ except nx.NetworkXError:
371
+ return False
372
+ return sum(deg_sequence) % 2 == 0 and min(deg_sequence) >= 0
373
+
374
+
375
+ @nx._dispatchable(graphs=None)
376
+ def is_digraphical(in_sequence, out_sequence):
377
+ r"""Returns True if some directed graph can realize the in- and out-degree
378
+ sequences.
379
+
380
+ Parameters
381
+ ----------
382
+ in_sequence : list or iterable container
383
+ A sequence of integer node in-degrees
384
+
385
+ out_sequence : list or iterable container
386
+ A sequence of integer node out-degrees
387
+
388
+ Returns
389
+ -------
390
+ valid : bool
391
+ True if in and out-sequences are digraphic False if not.
392
+
393
+ Examples
394
+ --------
395
+ >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
396
+ >>> in_seq = (d for n, d in G.in_degree())
397
+ >>> out_seq = (d for n, d in G.out_degree())
398
+ >>> nx.is_digraphical(in_seq, out_seq)
399
+ True
400
+
401
+ To test a non-digraphical scenario:
402
+ >>> in_seq_list = [d for n, d in G.in_degree()]
403
+ >>> in_seq_list[-1] += 1
404
+ >>> nx.is_digraphical(in_seq_list, out_seq)
405
+ False
406
+
407
+ Notes
408
+ -----
409
+ This algorithm is from Kleitman and Wang [1]_.
410
+ The worst case runtime is $O(s \times \log n)$ where $s$ and $n$ are the
411
+ sum and length of the sequences respectively.
412
+
413
+ References
414
+ ----------
415
+ .. [1] D.J. Kleitman and D.L. Wang
416
+ Algorithms for Constructing Graphs and Digraphs with Given Valences
417
+ and Factors, Discrete Mathematics, 6(1), pp. 79-88 (1973)
418
+ """
419
+ try:
420
+ in_deg_sequence = nx.utils.make_list_of_ints(in_sequence)
421
+ out_deg_sequence = nx.utils.make_list_of_ints(out_sequence)
422
+ except nx.NetworkXError:
423
+ return False
424
+ # Process the sequences and form two heaps to store degree pairs with
425
+ # either zero or non-zero out degrees
426
+ sumin, sumout, nin, nout = 0, 0, len(in_deg_sequence), len(out_deg_sequence)
427
+ maxn = max(nin, nout)
428
+ maxin = 0
429
+ if maxn == 0:
430
+ return True
431
+ stubheap, zeroheap = [], []
432
+ for n in range(maxn):
433
+ in_deg, out_deg = 0, 0
434
+ if n < nout:
435
+ out_deg = out_deg_sequence[n]
436
+ if n < nin:
437
+ in_deg = in_deg_sequence[n]
438
+ if in_deg < 0 or out_deg < 0:
439
+ return False
440
+ sumin, sumout, maxin = sumin + in_deg, sumout + out_deg, max(maxin, in_deg)
441
+ if in_deg > 0:
442
+ stubheap.append((-1 * out_deg, -1 * in_deg))
443
+ elif out_deg > 0:
444
+ zeroheap.append(-1 * out_deg)
445
+ if sumin != sumout:
446
+ return False
447
+ heapq.heapify(stubheap)
448
+ heapq.heapify(zeroheap)
449
+
450
+ modstubs = [(0, 0)] * (maxin + 1)
451
+ # Successively reduce degree sequence by removing the maximum out degree
452
+ while stubheap:
453
+ # Take the first value in the sequence with non-zero in degree
454
+ (freeout, freein) = heapq.heappop(stubheap)
455
+ freein *= -1
456
+ if freein > len(stubheap) + len(zeroheap):
457
+ return False
458
+
459
+ # Attach out stubs to the nodes with the most in stubs
460
+ mslen = 0
461
+ for i in range(freein):
462
+ if zeroheap and (not stubheap or stubheap[0][0] > zeroheap[0]):
463
+ stubout = heapq.heappop(zeroheap)
464
+ stubin = 0
465
+ else:
466
+ (stubout, stubin) = heapq.heappop(stubheap)
467
+ if stubout == 0:
468
+ return False
469
+ # Check if target is now totally connected
470
+ if stubout + 1 < 0 or stubin < 0:
471
+ modstubs[mslen] = (stubout + 1, stubin)
472
+ mslen += 1
473
+
474
+ # Add back the nodes to the heap that still have available stubs
475
+ for i in range(mslen):
476
+ stub = modstubs[i]
477
+ if stub[1] < 0:
478
+ heapq.heappush(stubheap, stub)
479
+ else:
480
+ heapq.heappush(zeroheap, stub[0])
481
+ if freeout < 0:
482
+ heapq.heappush(zeroheap, freeout)
483
+ return True
.venv/lib/python3.11/site-packages/networkx/algorithms/hierarchy.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Flow Hierarchy.
3
+ """
4
+
5
+ import networkx as nx
6
+
7
+ __all__ = ["flow_hierarchy"]
8
+
9
+
10
+ @nx._dispatchable(edge_attrs="weight")
11
+ def flow_hierarchy(G, weight=None):
12
+ """Returns the flow hierarchy of a directed network.
13
+
14
+ Flow hierarchy is defined as the fraction of edges not participating
15
+ in cycles in a directed graph [1]_.
16
+
17
+ Parameters
18
+ ----------
19
+ G : DiGraph or MultiDiGraph
20
+ A directed graph
21
+
22
+ weight : string, optional (default=None)
23
+ Attribute to use for edge weights. If None the weight defaults to 1.
24
+
25
+ Returns
26
+ -------
27
+ h : float
28
+ Flow hierarchy value
29
+
30
+ Raises
31
+ ------
32
+ NetworkXError
33
+ If `G` is not a directed graph or if `G` has no edges.
34
+
35
+ Notes
36
+ -----
37
+ The algorithm described in [1]_ computes the flow hierarchy through
38
+ exponentiation of the adjacency matrix. This function implements an
39
+ alternative approach that finds strongly connected components.
40
+ An edge is in a cycle if and only if it is in a strongly connected
41
+ component, which can be found in $O(m)$ time using Tarjan's algorithm.
42
+
43
+ References
44
+ ----------
45
+ .. [1] Luo, J.; Magee, C.L. (2011),
46
+ Detecting evolving patterns of self-organizing networks by flow
47
+ hierarchy measurement, Complexity, Volume 16 Issue 6 53-61.
48
+ DOI: 10.1002/cplx.20368
49
+ http://web.mit.edu/~cmagee/www/documents/28-DetectingEvolvingPatterns_FlowHierarchy.pdf
50
+ """
51
+ # corner case: G has no edges
52
+ if nx.is_empty(G):
53
+ raise nx.NetworkXError("flow_hierarchy not applicable to empty graphs")
54
+ if not G.is_directed():
55
+ raise nx.NetworkXError("G must be a digraph in flow_hierarchy")
56
+ scc = nx.strongly_connected_components(G)
57
+ return 1 - sum(G.subgraph(c).size(weight) for c in scc) / G.size(weight)
.venv/lib/python3.11/site-packages/networkx/algorithms/hybrid.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Provides functions for finding and testing for locally `(k, l)`-connected
3
+ graphs.
4
+
5
+ """
6
+
7
+ import copy
8
+
9
+ import networkx as nx
10
+
11
+ __all__ = ["kl_connected_subgraph", "is_kl_connected"]
12
+
13
+
14
+ @nx._dispatchable(returns_graph=True)
15
+ def kl_connected_subgraph(G, k, l, low_memory=False, same_as_graph=False):
16
+ """Returns the maximum locally `(k, l)`-connected subgraph of `G`.
17
+
18
+ A graph is locally `(k, l)`-connected if for each edge `(u, v)` in the
19
+ graph there are at least `l` edge-disjoint paths of length at most `k`
20
+ joining `u` to `v`.
21
+
22
+ Parameters
23
+ ----------
24
+ G : NetworkX graph
25
+ The graph in which to find a maximum locally `(k, l)`-connected
26
+ subgraph.
27
+
28
+ k : integer
29
+ The maximum length of paths to consider. A higher number means a looser
30
+ connectivity requirement.
31
+
32
+ l : integer
33
+ The number of edge-disjoint paths. A higher number means a stricter
34
+ connectivity requirement.
35
+
36
+ low_memory : bool
37
+ If this is True, this function uses an algorithm that uses slightly
38
+ more time but less memory.
39
+
40
+ same_as_graph : bool
41
+ If True then return a tuple of the form `(H, is_same)`,
42
+ where `H` is the maximum locally `(k, l)`-connected subgraph and
43
+ `is_same` is a Boolean representing whether `G` is locally `(k,
44
+ l)`-connected (and hence, whether `H` is simply a copy of the input
45
+ graph `G`).
46
+
47
+ Returns
48
+ -------
49
+ NetworkX graph or two-tuple
50
+ If `same_as_graph` is True, then this function returns a
51
+ two-tuple as described above. Otherwise, it returns only the maximum
52
+ locally `(k, l)`-connected subgraph.
53
+
54
+ See also
55
+ --------
56
+ is_kl_connected
57
+
58
+ References
59
+ ----------
60
+ .. [1] Chung, Fan and Linyuan Lu. "The Small World Phenomenon in Hybrid
61
+ Power Law Graphs." *Complex Networks*. Springer Berlin Heidelberg,
62
+ 2004. 89--104.
63
+
64
+ """
65
+ H = copy.deepcopy(G) # subgraph we construct by removing from G
66
+
67
+ graphOK = True
68
+ deleted_some = True # hack to start off the while loop
69
+ while deleted_some:
70
+ deleted_some = False
71
+ # We use `for edge in list(H.edges()):` instead of
72
+ # `for edge in H.edges():` because we edit the graph `H` in
73
+ # the loop. Hence using an iterator will result in
74
+ # `RuntimeError: dictionary changed size during iteration`
75
+ for edge in list(H.edges()):
76
+ (u, v) = edge
77
+ # Get copy of graph needed for this search
78
+ if low_memory:
79
+ verts = {u, v}
80
+ for i in range(k):
81
+ for w in verts.copy():
82
+ verts.update(G[w])
83
+ G2 = G.subgraph(verts).copy()
84
+ else:
85
+ G2 = copy.deepcopy(G)
86
+ ###
87
+ path = [u, v]
88
+ cnt = 0
89
+ accept = 0
90
+ while path:
91
+ cnt += 1 # Found a path
92
+ if cnt >= l:
93
+ accept = 1
94
+ break
95
+ # record edges along this graph
96
+ prev = u
97
+ for w in path:
98
+ if prev != w:
99
+ G2.remove_edge(prev, w)
100
+ prev = w
101
+ # path = shortest_path(G2, u, v, k) # ??? should "Cutoff" be k+1?
102
+ try:
103
+ path = nx.shortest_path(G2, u, v) # ??? should "Cutoff" be k+1?
104
+ except nx.NetworkXNoPath:
105
+ path = False
106
+ # No Other Paths
107
+ if accept == 0:
108
+ H.remove_edge(u, v)
109
+ deleted_some = True
110
+ if graphOK:
111
+ graphOK = False
112
+ # We looked through all edges and removed none of them.
113
+ # So, H is the maximal (k,l)-connected subgraph of G
114
+ if same_as_graph:
115
+ return (H, graphOK)
116
+ return H
117
+
118
+
119
+ @nx._dispatchable
120
+ def is_kl_connected(G, k, l, low_memory=False):
121
+ """Returns True if and only if `G` is locally `(k, l)`-connected.
122
+
123
+ A graph is locally `(k, l)`-connected if for each edge `(u, v)` in the
124
+ graph there are at least `l` edge-disjoint paths of length at most `k`
125
+ joining `u` to `v`.
126
+
127
+ Parameters
128
+ ----------
129
+ G : NetworkX graph
130
+ The graph to test for local `(k, l)`-connectedness.
131
+
132
+ k : integer
133
+ The maximum length of paths to consider. A higher number means a looser
134
+ connectivity requirement.
135
+
136
+ l : integer
137
+ The number of edge-disjoint paths. A higher number means a stricter
138
+ connectivity requirement.
139
+
140
+ low_memory : bool
141
+ If this is True, this function uses an algorithm that uses slightly
142
+ more time but less memory.
143
+
144
+ Returns
145
+ -------
146
+ bool
147
+ Whether the graph is locally `(k, l)`-connected subgraph.
148
+
149
+ See also
150
+ --------
151
+ kl_connected_subgraph
152
+
153
+ References
154
+ ----------
155
+ .. [1] Chung, Fan and Linyuan Lu. "The Small World Phenomenon in Hybrid
156
+ Power Law Graphs." *Complex Networks*. Springer Berlin Heidelberg,
157
+ 2004. 89--104.
158
+
159
+ """
160
+ graphOK = True
161
+ for edge in G.edges():
162
+ (u, v) = edge
163
+ # Get copy of graph needed for this search
164
+ if low_memory:
165
+ verts = {u, v}
166
+ for i in range(k):
167
+ [verts.update(G.neighbors(w)) for w in verts.copy()]
168
+ G2 = G.subgraph(verts)
169
+ else:
170
+ G2 = copy.deepcopy(G)
171
+ ###
172
+ path = [u, v]
173
+ cnt = 0
174
+ accept = 0
175
+ while path:
176
+ cnt += 1 # Found a path
177
+ if cnt >= l:
178
+ accept = 1
179
+ break
180
+ # record edges along this graph
181
+ prev = u
182
+ for w in path:
183
+ if w != prev:
184
+ G2.remove_edge(prev, w)
185
+ prev = w
186
+ # path = shortest_path(G2, u, v, k) # ??? should "Cutoff" be k+1?
187
+ try:
188
+ path = nx.shortest_path(G2, u, v) # ??? should "Cutoff" be k+1?
189
+ except nx.NetworkXNoPath:
190
+ path = False
191
+ # No Other Paths
192
+ if accept == 0:
193
+ graphOK = False
194
+ break
195
+ # return status
196
+ return graphOK
.venv/lib/python3.11/site-packages/networkx/algorithms/isolate.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Functions for identifying isolate (degree zero) nodes.
3
+ """
4
+
5
+ import networkx as nx
6
+
7
+ __all__ = ["is_isolate", "isolates", "number_of_isolates"]
8
+
9
+
10
+ @nx._dispatchable
11
+ def is_isolate(G, n):
12
+ """Determines whether a node is an isolate.
13
+
14
+ An *isolate* is a node with no neighbors (that is, with degree
15
+ zero). For directed graphs, this means no in-neighbors and no
16
+ out-neighbors.
17
+
18
+ Parameters
19
+ ----------
20
+ G : NetworkX graph
21
+
22
+ n : node
23
+ A node in `G`.
24
+
25
+ Returns
26
+ -------
27
+ is_isolate : bool
28
+ True if and only if `n` has no neighbors.
29
+
30
+ Examples
31
+ --------
32
+ >>> G = nx.Graph()
33
+ >>> G.add_edge(1, 2)
34
+ >>> G.add_node(3)
35
+ >>> nx.is_isolate(G, 2)
36
+ False
37
+ >>> nx.is_isolate(G, 3)
38
+ True
39
+ """
40
+ return G.degree(n) == 0
41
+
42
+
43
+ @nx._dispatchable
44
+ def isolates(G):
45
+ """Iterator over isolates in the graph.
46
+
47
+ An *isolate* is a node with no neighbors (that is, with degree
48
+ zero). For directed graphs, this means no in-neighbors and no
49
+ out-neighbors.
50
+
51
+ Parameters
52
+ ----------
53
+ G : NetworkX graph
54
+
55
+ Returns
56
+ -------
57
+ iterator
58
+ An iterator over the isolates of `G`.
59
+
60
+ Examples
61
+ --------
62
+ To get a list of all isolates of a graph, use the :class:`list`
63
+ constructor::
64
+
65
+ >>> G = nx.Graph()
66
+ >>> G.add_edge(1, 2)
67
+ >>> G.add_node(3)
68
+ >>> list(nx.isolates(G))
69
+ [3]
70
+
71
+ To remove all isolates in the graph, first create a list of the
72
+ isolates, then use :meth:`Graph.remove_nodes_from`::
73
+
74
+ >>> G.remove_nodes_from(list(nx.isolates(G)))
75
+ >>> list(G)
76
+ [1, 2]
77
+
78
+ For digraphs, isolates have zero in-degree and zero out_degre::
79
+
80
+ >>> G = nx.DiGraph([(0, 1), (1, 2)])
81
+ >>> G.add_node(3)
82
+ >>> list(nx.isolates(G))
83
+ [3]
84
+
85
+ """
86
+ return (n for n, d in G.degree() if d == 0)
87
+
88
+
89
+ @nx._dispatchable
90
+ def number_of_isolates(G):
91
+ """Returns the number of isolates in the graph.
92
+
93
+ An *isolate* is a node with no neighbors (that is, with degree
94
+ zero). For directed graphs, this means no in-neighbors and no
95
+ out-neighbors.
96
+
97
+ Parameters
98
+ ----------
99
+ G : NetworkX graph
100
+
101
+ Returns
102
+ -------
103
+ int
104
+ The number of degree zero nodes in the graph `G`.
105
+
106
+ """
107
+ return sum(1 for v in isolates(G))
.venv/lib/python3.11/site-packages/networkx/algorithms/link_prediction.py ADDED
@@ -0,0 +1,687 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Link prediction algorithms.
3
+ """
4
+
5
+ from math import log
6
+
7
+ import networkx as nx
8
+ from networkx.utils import not_implemented_for
9
+
10
+ __all__ = [
11
+ "resource_allocation_index",
12
+ "jaccard_coefficient",
13
+ "adamic_adar_index",
14
+ "preferential_attachment",
15
+ "cn_soundarajan_hopcroft",
16
+ "ra_index_soundarajan_hopcroft",
17
+ "within_inter_cluster",
18
+ "common_neighbor_centrality",
19
+ ]
20
+
21
+
22
+ def _apply_prediction(G, func, ebunch=None):
23
+ """Applies the given function to each edge in the specified iterable
24
+ of edges.
25
+
26
+ `G` is an instance of :class:`networkx.Graph`.
27
+
28
+ `func` is a function on two inputs, each of which is a node in the
29
+ graph. The function can return anything, but it should return a
30
+ value representing a prediction of the likelihood of a "link"
31
+ joining the two nodes.
32
+
33
+ `ebunch` is an iterable of pairs of nodes. If not specified, all
34
+ non-edges in the graph `G` will be used.
35
+
36
+ """
37
+ if ebunch is None:
38
+ ebunch = nx.non_edges(G)
39
+ else:
40
+ for u, v in ebunch:
41
+ if u not in G:
42
+ raise nx.NodeNotFound(f"Node {u} not in G.")
43
+ if v not in G:
44
+ raise nx.NodeNotFound(f"Node {v} not in G.")
45
+ return ((u, v, func(u, v)) for u, v in ebunch)
46
+
47
+
48
+ @not_implemented_for("directed")
49
+ @not_implemented_for("multigraph")
50
+ @nx._dispatchable
51
+ def resource_allocation_index(G, ebunch=None):
52
+ r"""Compute the resource allocation index of all node pairs in ebunch.
53
+
54
+ Resource allocation index of `u` and `v` is defined as
55
+
56
+ .. math::
57
+
58
+ \sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{1}{|\Gamma(w)|}
59
+
60
+ where $\Gamma(u)$ denotes the set of neighbors of $u$.
61
+
62
+ Parameters
63
+ ----------
64
+ G : graph
65
+ A NetworkX undirected graph.
66
+
67
+ ebunch : iterable of node pairs, optional (default = None)
68
+ Resource allocation index will be computed for each pair of
69
+ nodes given in the iterable. The pairs must be given as
70
+ 2-tuples (u, v) where u and v are nodes in the graph. If ebunch
71
+ is None then all nonexistent edges in the graph will be used.
72
+ Default value: None.
73
+
74
+ Returns
75
+ -------
76
+ piter : iterator
77
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
78
+ pair of nodes and p is their resource allocation index.
79
+
80
+ Raises
81
+ ------
82
+ NetworkXNotImplemented
83
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
84
+
85
+ NodeNotFound
86
+ If `ebunch` has a node that is not in `G`.
87
+
88
+ Examples
89
+ --------
90
+ >>> G = nx.complete_graph(5)
91
+ >>> preds = nx.resource_allocation_index(G, [(0, 1), (2, 3)])
92
+ >>> for u, v, p in preds:
93
+ ... print(f"({u}, {v}) -> {p:.8f}")
94
+ (0, 1) -> 0.75000000
95
+ (2, 3) -> 0.75000000
96
+
97
+ References
98
+ ----------
99
+ .. [1] T. Zhou, L. Lu, Y.-C. Zhang.
100
+ Predicting missing links via local information.
101
+ Eur. Phys. J. B 71 (2009) 623.
102
+ https://arxiv.org/pdf/0901.0553.pdf
103
+ """
104
+
105
+ def predict(u, v):
106
+ return sum(1 / G.degree(w) for w in nx.common_neighbors(G, u, v))
107
+
108
+ return _apply_prediction(G, predict, ebunch)
109
+
110
+
111
+ @not_implemented_for("directed")
112
+ @not_implemented_for("multigraph")
113
+ @nx._dispatchable
114
+ def jaccard_coefficient(G, ebunch=None):
115
+ r"""Compute the Jaccard coefficient of all node pairs in ebunch.
116
+
117
+ Jaccard coefficient of nodes `u` and `v` is defined as
118
+
119
+ .. math::
120
+
121
+ \frac{|\Gamma(u) \cap \Gamma(v)|}{|\Gamma(u) \cup \Gamma(v)|}
122
+
123
+ where $\Gamma(u)$ denotes the set of neighbors of $u$.
124
+
125
+ Parameters
126
+ ----------
127
+ G : graph
128
+ A NetworkX undirected graph.
129
+
130
+ ebunch : iterable of node pairs, optional (default = None)
131
+ Jaccard coefficient will be computed for each pair of nodes
132
+ given in the iterable. The pairs must be given as 2-tuples
133
+ (u, v) where u and v are nodes in the graph. If ebunch is None
134
+ then all nonexistent edges in the graph will be used.
135
+ Default value: None.
136
+
137
+ Returns
138
+ -------
139
+ piter : iterator
140
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
141
+ pair of nodes and p is their Jaccard coefficient.
142
+
143
+ Raises
144
+ ------
145
+ NetworkXNotImplemented
146
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
147
+
148
+ NodeNotFound
149
+ If `ebunch` has a node that is not in `G`.
150
+
151
+ Examples
152
+ --------
153
+ >>> G = nx.complete_graph(5)
154
+ >>> preds = nx.jaccard_coefficient(G, [(0, 1), (2, 3)])
155
+ >>> for u, v, p in preds:
156
+ ... print(f"({u}, {v}) -> {p:.8f}")
157
+ (0, 1) -> 0.60000000
158
+ (2, 3) -> 0.60000000
159
+
160
+ References
161
+ ----------
162
+ .. [1] D. Liben-Nowell, J. Kleinberg.
163
+ The Link Prediction Problem for Social Networks (2004).
164
+ http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
165
+ """
166
+
167
+ def predict(u, v):
168
+ union_size = len(set(G[u]) | set(G[v]))
169
+ if union_size == 0:
170
+ return 0
171
+ return len(nx.common_neighbors(G, u, v)) / union_size
172
+
173
+ return _apply_prediction(G, predict, ebunch)
174
+
175
+
176
+ @not_implemented_for("directed")
177
+ @not_implemented_for("multigraph")
178
+ @nx._dispatchable
179
+ def adamic_adar_index(G, ebunch=None):
180
+ r"""Compute the Adamic-Adar index of all node pairs in ebunch.
181
+
182
+ Adamic-Adar index of `u` and `v` is defined as
183
+
184
+ .. math::
185
+
186
+ \sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{1}{\log |\Gamma(w)|}
187
+
188
+ where $\Gamma(u)$ denotes the set of neighbors of $u$.
189
+ This index leads to zero-division for nodes only connected via self-loops.
190
+ It is intended to be used when no self-loops are present.
191
+
192
+ Parameters
193
+ ----------
194
+ G : graph
195
+ NetworkX undirected graph.
196
+
197
+ ebunch : iterable of node pairs, optional (default = None)
198
+ Adamic-Adar index will be computed for each pair of nodes given
199
+ in the iterable. The pairs must be given as 2-tuples (u, v)
200
+ where u and v are nodes in the graph. If ebunch is None then all
201
+ nonexistent edges in the graph will be used.
202
+ Default value: None.
203
+
204
+ Returns
205
+ -------
206
+ piter : iterator
207
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
208
+ pair of nodes and p is their Adamic-Adar index.
209
+
210
+ Raises
211
+ ------
212
+ NetworkXNotImplemented
213
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
214
+
215
+ NodeNotFound
216
+ If `ebunch` has a node that is not in `G`.
217
+
218
+ Examples
219
+ --------
220
+ >>> G = nx.complete_graph(5)
221
+ >>> preds = nx.adamic_adar_index(G, [(0, 1), (2, 3)])
222
+ >>> for u, v, p in preds:
223
+ ... print(f"({u}, {v}) -> {p:.8f}")
224
+ (0, 1) -> 2.16404256
225
+ (2, 3) -> 2.16404256
226
+
227
+ References
228
+ ----------
229
+ .. [1] D. Liben-Nowell, J. Kleinberg.
230
+ The Link Prediction Problem for Social Networks (2004).
231
+ http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
232
+ """
233
+
234
+ def predict(u, v):
235
+ return sum(1 / log(G.degree(w)) for w in nx.common_neighbors(G, u, v))
236
+
237
+ return _apply_prediction(G, predict, ebunch)
238
+
239
+
240
+ @not_implemented_for("directed")
241
+ @not_implemented_for("multigraph")
242
+ @nx._dispatchable
243
+ def common_neighbor_centrality(G, ebunch=None, alpha=0.8):
244
+ r"""Return the CCPA score for each pair of nodes.
245
+
246
+ Compute the Common Neighbor and Centrality based Parameterized Algorithm(CCPA)
247
+ score of all node pairs in ebunch.
248
+
249
+ CCPA score of `u` and `v` is defined as
250
+
251
+ .. math::
252
+
253
+ \alpha \cdot (|\Gamma (u){\cap }^{}\Gamma (v)|)+(1-\alpha )\cdot \frac{N}{{d}_{uv}}
254
+
255
+ where $\Gamma(u)$ denotes the set of neighbors of $u$, $\Gamma(v)$ denotes the
256
+ set of neighbors of $v$, $\alpha$ is parameter varies between [0,1], $N$ denotes
257
+ total number of nodes in the Graph and ${d}_{uv}$ denotes shortest distance
258
+ between $u$ and $v$.
259
+
260
+ This algorithm is based on two vital properties of nodes, namely the number
261
+ of common neighbors and their centrality. Common neighbor refers to the common
262
+ nodes between two nodes. Centrality refers to the prestige that a node enjoys
263
+ in a network.
264
+
265
+ .. seealso::
266
+
267
+ :func:`common_neighbors`
268
+
269
+ Parameters
270
+ ----------
271
+ G : graph
272
+ NetworkX undirected graph.
273
+
274
+ ebunch : iterable of node pairs, optional (default = None)
275
+ Preferential attachment score will be computed for each pair of
276
+ nodes given in the iterable. The pairs must be given as
277
+ 2-tuples (u, v) where u and v are nodes in the graph. If ebunch
278
+ is None then all nonexistent edges in the graph will be used.
279
+ Default value: None.
280
+
281
+ alpha : Parameter defined for participation of Common Neighbor
282
+ and Centrality Algorithm share. Values for alpha should
283
+ normally be between 0 and 1. Default value set to 0.8
284
+ because author found better performance at 0.8 for all the
285
+ dataset.
286
+ Default value: 0.8
287
+
288
+
289
+ Returns
290
+ -------
291
+ piter : iterator
292
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
293
+ pair of nodes and p is their Common Neighbor and Centrality based
294
+ Parameterized Algorithm(CCPA) score.
295
+
296
+ Raises
297
+ ------
298
+ NetworkXNotImplemented
299
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
300
+
301
+ NetworkXAlgorithmError
302
+ If self loops exist in `ebunch` or in `G` (if `ebunch` is `None`).
303
+
304
+ NodeNotFound
305
+ If `ebunch` has a node that is not in `G`.
306
+
307
+ Examples
308
+ --------
309
+ >>> G = nx.complete_graph(5)
310
+ >>> preds = nx.common_neighbor_centrality(G, [(0, 1), (2, 3)])
311
+ >>> for u, v, p in preds:
312
+ ... print(f"({u}, {v}) -> {p}")
313
+ (0, 1) -> 3.4000000000000004
314
+ (2, 3) -> 3.4000000000000004
315
+
316
+ References
317
+ ----------
318
+ .. [1] Ahmad, I., Akhtar, M.U., Noor, S. et al.
319
+ Missing Link Prediction using Common Neighbor and Centrality based Parameterized Algorithm.
320
+ Sci Rep 10, 364 (2020).
321
+ https://doi.org/10.1038/s41598-019-57304-y
322
+ """
323
+
324
+ # When alpha == 1, the CCPA score simplifies to the number of common neighbors.
325
+ if alpha == 1:
326
+
327
+ def predict(u, v):
328
+ if u == v:
329
+ raise nx.NetworkXAlgorithmError("Self loops are not supported")
330
+
331
+ return len(nx.common_neighbors(G, u, v))
332
+
333
+ else:
334
+ spl = dict(nx.shortest_path_length(G))
335
+ inf = float("inf")
336
+
337
+ def predict(u, v):
338
+ if u == v:
339
+ raise nx.NetworkXAlgorithmError("Self loops are not supported")
340
+ path_len = spl[u].get(v, inf)
341
+
342
+ n_nbrs = len(nx.common_neighbors(G, u, v))
343
+ return alpha * n_nbrs + (1 - alpha) * len(G) / path_len
344
+
345
+ return _apply_prediction(G, predict, ebunch)
346
+
347
+
348
+ @not_implemented_for("directed")
349
+ @not_implemented_for("multigraph")
350
+ @nx._dispatchable
351
+ def preferential_attachment(G, ebunch=None):
352
+ r"""Compute the preferential attachment score of all node pairs in ebunch.
353
+
354
+ Preferential attachment score of `u` and `v` is defined as
355
+
356
+ .. math::
357
+
358
+ |\Gamma(u)| |\Gamma(v)|
359
+
360
+ where $\Gamma(u)$ denotes the set of neighbors of $u$.
361
+
362
+ Parameters
363
+ ----------
364
+ G : graph
365
+ NetworkX undirected graph.
366
+
367
+ ebunch : iterable of node pairs, optional (default = None)
368
+ Preferential attachment score will be computed for each pair of
369
+ nodes given in the iterable. The pairs must be given as
370
+ 2-tuples (u, v) where u and v are nodes in the graph. If ebunch
371
+ is None then all nonexistent edges in the graph will be used.
372
+ Default value: None.
373
+
374
+ Returns
375
+ -------
376
+ piter : iterator
377
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
378
+ pair of nodes and p is their preferential attachment score.
379
+
380
+ Raises
381
+ ------
382
+ NetworkXNotImplemented
383
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
384
+
385
+ NodeNotFound
386
+ If `ebunch` has a node that is not in `G`.
387
+
388
+ Examples
389
+ --------
390
+ >>> G = nx.complete_graph(5)
391
+ >>> preds = nx.preferential_attachment(G, [(0, 1), (2, 3)])
392
+ >>> for u, v, p in preds:
393
+ ... print(f"({u}, {v}) -> {p}")
394
+ (0, 1) -> 16
395
+ (2, 3) -> 16
396
+
397
+ References
398
+ ----------
399
+ .. [1] D. Liben-Nowell, J. Kleinberg.
400
+ The Link Prediction Problem for Social Networks (2004).
401
+ http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
402
+ """
403
+
404
+ def predict(u, v):
405
+ return G.degree(u) * G.degree(v)
406
+
407
+ return _apply_prediction(G, predict, ebunch)
408
+
409
+
410
+ @not_implemented_for("directed")
411
+ @not_implemented_for("multigraph")
412
+ @nx._dispatchable(node_attrs="community")
413
+ def cn_soundarajan_hopcroft(G, ebunch=None, community="community"):
414
+ r"""Count the number of common neighbors of all node pairs in ebunch
415
+ using community information.
416
+
417
+ For two nodes $u$ and $v$, this function computes the number of
418
+ common neighbors and bonus one for each common neighbor belonging to
419
+ the same community as $u$ and $v$. Mathematically,
420
+
421
+ .. math::
422
+
423
+ |\Gamma(u) \cap \Gamma(v)| + \sum_{w \in \Gamma(u) \cap \Gamma(v)} f(w)
424
+
425
+ where $f(w)$ equals 1 if $w$ belongs to the same community as $u$
426
+ and $v$ or 0 otherwise and $\Gamma(u)$ denotes the set of
427
+ neighbors of $u$.
428
+
429
+ Parameters
430
+ ----------
431
+ G : graph
432
+ A NetworkX undirected graph.
433
+
434
+ ebunch : iterable of node pairs, optional (default = None)
435
+ The score will be computed for each pair of nodes given in the
436
+ iterable. The pairs must be given as 2-tuples (u, v) where u
437
+ and v are nodes in the graph. If ebunch is None then all
438
+ nonexistent edges in the graph will be used.
439
+ Default value: None.
440
+
441
+ community : string, optional (default = 'community')
442
+ Nodes attribute name containing the community information.
443
+ G[u][community] identifies which community u belongs to. Each
444
+ node belongs to at most one community. Default value: 'community'.
445
+
446
+ Returns
447
+ -------
448
+ piter : iterator
449
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
450
+ pair of nodes and p is their score.
451
+
452
+ Raises
453
+ ------
454
+ NetworkXNotImplemented
455
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
456
+
457
+ NetworkXAlgorithmError
458
+ If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
459
+
460
+ NodeNotFound
461
+ If `ebunch` has a node that is not in `G`.
462
+
463
+ Examples
464
+ --------
465
+ >>> G = nx.path_graph(3)
466
+ >>> G.nodes[0]["community"] = 0
467
+ >>> G.nodes[1]["community"] = 0
468
+ >>> G.nodes[2]["community"] = 0
469
+ >>> preds = nx.cn_soundarajan_hopcroft(G, [(0, 2)])
470
+ >>> for u, v, p in preds:
471
+ ... print(f"({u}, {v}) -> {p}")
472
+ (0, 2) -> 2
473
+
474
+ References
475
+ ----------
476
+ .. [1] Sucheta Soundarajan and John Hopcroft.
477
+ Using community information to improve the precision of link
478
+ prediction methods.
479
+ In Proceedings of the 21st international conference companion on
480
+ World Wide Web (WWW '12 Companion). ACM, New York, NY, USA, 607-608.
481
+ http://doi.acm.org/10.1145/2187980.2188150
482
+ """
483
+
484
+ def predict(u, v):
485
+ Cu = _community(G, u, community)
486
+ Cv = _community(G, v, community)
487
+ cnbors = nx.common_neighbors(G, u, v)
488
+ neighbors = (
489
+ sum(_community(G, w, community) == Cu for w in cnbors) if Cu == Cv else 0
490
+ )
491
+ return len(cnbors) + neighbors
492
+
493
+ return _apply_prediction(G, predict, ebunch)
494
+
495
+
496
+ @not_implemented_for("directed")
497
+ @not_implemented_for("multigraph")
498
+ @nx._dispatchable(node_attrs="community")
499
+ def ra_index_soundarajan_hopcroft(G, ebunch=None, community="community"):
500
+ r"""Compute the resource allocation index of all node pairs in
501
+ ebunch using community information.
502
+
503
+ For two nodes $u$ and $v$, this function computes the resource
504
+ allocation index considering only common neighbors belonging to the
505
+ same community as $u$ and $v$. Mathematically,
506
+
507
+ .. math::
508
+
509
+ \sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{f(w)}{|\Gamma(w)|}
510
+
511
+ where $f(w)$ equals 1 if $w$ belongs to the same community as $u$
512
+ and $v$ or 0 otherwise and $\Gamma(u)$ denotes the set of
513
+ neighbors of $u$.
514
+
515
+ Parameters
516
+ ----------
517
+ G : graph
518
+ A NetworkX undirected graph.
519
+
520
+ ebunch : iterable of node pairs, optional (default = None)
521
+ The score will be computed for each pair of nodes given in the
522
+ iterable. The pairs must be given as 2-tuples (u, v) where u
523
+ and v are nodes in the graph. If ebunch is None then all
524
+ nonexistent edges in the graph will be used.
525
+ Default value: None.
526
+
527
+ community : string, optional (default = 'community')
528
+ Nodes attribute name containing the community information.
529
+ G[u][community] identifies which community u belongs to. Each
530
+ node belongs to at most one community. Default value: 'community'.
531
+
532
+ Returns
533
+ -------
534
+ piter : iterator
535
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
536
+ pair of nodes and p is their score.
537
+
538
+ Raises
539
+ ------
540
+ NetworkXNotImplemented
541
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
542
+
543
+ NetworkXAlgorithmError
544
+ If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
545
+
546
+ NodeNotFound
547
+ If `ebunch` has a node that is not in `G`.
548
+
549
+ Examples
550
+ --------
551
+ >>> G = nx.Graph()
552
+ >>> G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
553
+ >>> G.nodes[0]["community"] = 0
554
+ >>> G.nodes[1]["community"] = 0
555
+ >>> G.nodes[2]["community"] = 1
556
+ >>> G.nodes[3]["community"] = 0
557
+ >>> preds = nx.ra_index_soundarajan_hopcroft(G, [(0, 3)])
558
+ >>> for u, v, p in preds:
559
+ ... print(f"({u}, {v}) -> {p:.8f}")
560
+ (0, 3) -> 0.50000000
561
+
562
+ References
563
+ ----------
564
+ .. [1] Sucheta Soundarajan and John Hopcroft.
565
+ Using community information to improve the precision of link
566
+ prediction methods.
567
+ In Proceedings of the 21st international conference companion on
568
+ World Wide Web (WWW '12 Companion). ACM, New York, NY, USA, 607-608.
569
+ http://doi.acm.org/10.1145/2187980.2188150
570
+ """
571
+
572
+ def predict(u, v):
573
+ Cu = _community(G, u, community)
574
+ Cv = _community(G, v, community)
575
+ if Cu != Cv:
576
+ return 0
577
+ cnbors = nx.common_neighbors(G, u, v)
578
+ return sum(1 / G.degree(w) for w in cnbors if _community(G, w, community) == Cu)
579
+
580
+ return _apply_prediction(G, predict, ebunch)
581
+
582
+
583
+ @not_implemented_for("directed")
584
+ @not_implemented_for("multigraph")
585
+ @nx._dispatchable(node_attrs="community")
586
+ def within_inter_cluster(G, ebunch=None, delta=0.001, community="community"):
587
+ """Compute the ratio of within- and inter-cluster common neighbors
588
+ of all node pairs in ebunch.
589
+
590
+ For two nodes `u` and `v`, if a common neighbor `w` belongs to the
591
+ same community as them, `w` is considered as within-cluster common
592
+ neighbor of `u` and `v`. Otherwise, it is considered as
593
+ inter-cluster common neighbor of `u` and `v`. The ratio between the
594
+ size of the set of within- and inter-cluster common neighbors is
595
+ defined as the WIC measure. [1]_
596
+
597
+ Parameters
598
+ ----------
599
+ G : graph
600
+ A NetworkX undirected graph.
601
+
602
+ ebunch : iterable of node pairs, optional (default = None)
603
+ The WIC measure will be computed for each pair of nodes given in
604
+ the iterable. The pairs must be given as 2-tuples (u, v) where
605
+ u and v are nodes in the graph. If ebunch is None then all
606
+ nonexistent edges in the graph will be used.
607
+ Default value: None.
608
+
609
+ delta : float, optional (default = 0.001)
610
+ Value to prevent division by zero in case there is no
611
+ inter-cluster common neighbor between two nodes. See [1]_ for
612
+ details. Default value: 0.001.
613
+
614
+ community : string, optional (default = 'community')
615
+ Nodes attribute name containing the community information.
616
+ G[u][community] identifies which community u belongs to. Each
617
+ node belongs to at most one community. Default value: 'community'.
618
+
619
+ Returns
620
+ -------
621
+ piter : iterator
622
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
623
+ pair of nodes and p is their WIC measure.
624
+
625
+ Raises
626
+ ------
627
+ NetworkXNotImplemented
628
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
629
+
630
+ NetworkXAlgorithmError
631
+ - If `delta` is less than or equal to zero.
632
+ - If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
633
+
634
+ NodeNotFound
635
+ If `ebunch` has a node that is not in `G`.
636
+
637
+ Examples
638
+ --------
639
+ >>> G = nx.Graph()
640
+ >>> G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)])
641
+ >>> G.nodes[0]["community"] = 0
642
+ >>> G.nodes[1]["community"] = 1
643
+ >>> G.nodes[2]["community"] = 0
644
+ >>> G.nodes[3]["community"] = 0
645
+ >>> G.nodes[4]["community"] = 0
646
+ >>> preds = nx.within_inter_cluster(G, [(0, 4)])
647
+ >>> for u, v, p in preds:
648
+ ... print(f"({u}, {v}) -> {p:.8f}")
649
+ (0, 4) -> 1.99800200
650
+ >>> preds = nx.within_inter_cluster(G, [(0, 4)], delta=0.5)
651
+ >>> for u, v, p in preds:
652
+ ... print(f"({u}, {v}) -> {p:.8f}")
653
+ (0, 4) -> 1.33333333
654
+
655
+ References
656
+ ----------
657
+ .. [1] Jorge Carlos Valverde-Rebaza and Alneu de Andrade Lopes.
658
+ Link prediction in complex networks based on cluster information.
659
+ In Proceedings of the 21st Brazilian conference on Advances in
660
+ Artificial Intelligence (SBIA'12)
661
+ https://doi.org/10.1007/978-3-642-34459-6_10
662
+ """
663
+ if delta <= 0:
664
+ raise nx.NetworkXAlgorithmError("Delta must be greater than zero")
665
+
666
+ def predict(u, v):
667
+ Cu = _community(G, u, community)
668
+ Cv = _community(G, v, community)
669
+ if Cu != Cv:
670
+ return 0
671
+ cnbors = nx.common_neighbors(G, u, v)
672
+ within = {w for w in cnbors if _community(G, w, community) == Cu}
673
+ inter = cnbors - within
674
+ return len(within) / (len(inter) + delta)
675
+
676
+ return _apply_prediction(G, predict, ebunch)
677
+
678
+
679
+ def _community(G, u, community):
680
+ """Get the community of the given node."""
681
+ node_u = G.nodes[u]
682
+ try:
683
+ return node_u[community]
684
+ except KeyError as err:
685
+ raise nx.NetworkXAlgorithmError(
686
+ f"No community information available for Node {u}"
687
+ ) from err
.venv/lib/python3.11/site-packages/networkx/algorithms/lowest_common_ancestors.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Algorithms for finding the lowest common ancestor of trees and DAGs."""
2
+
3
+ from collections import defaultdict
4
+ from collections.abc import Mapping, Set
5
+ from itertools import combinations_with_replacement
6
+
7
+ import networkx as nx
8
+ from networkx.utils import UnionFind, arbitrary_element, not_implemented_for
9
+
10
+ __all__ = [
11
+ "all_pairs_lowest_common_ancestor",
12
+ "tree_all_pairs_lowest_common_ancestor",
13
+ "lowest_common_ancestor",
14
+ ]
15
+
16
+
17
+ @not_implemented_for("undirected")
18
+ @nx._dispatchable
19
+ def all_pairs_lowest_common_ancestor(G, pairs=None):
20
+ """Return the lowest common ancestor of all pairs or the provided pairs
21
+
22
+ Parameters
23
+ ----------
24
+ G : NetworkX directed graph
25
+
26
+ pairs : iterable of pairs of nodes, optional (default: all pairs)
27
+ The pairs of nodes of interest.
28
+ If None, will find the LCA of all pairs of nodes.
29
+
30
+ Yields
31
+ ------
32
+ ((node1, node2), lca) : 2-tuple
33
+ Where lca is least common ancestor of node1 and node2.
34
+ Note that for the default case, the order of the node pair is not considered,
35
+ e.g. you will not get both ``(a, b)`` and ``(b, a)``
36
+
37
+ Raises
38
+ ------
39
+ NetworkXPointlessConcept
40
+ If `G` is null.
41
+ NetworkXError
42
+ If `G` is not a DAG.
43
+
44
+ Examples
45
+ --------
46
+ The default behavior is to yield the lowest common ancestor for all
47
+ possible combinations of nodes in `G`, including self-pairings:
48
+
49
+ >>> G = nx.DiGraph([(0, 1), (0, 3), (1, 2)])
50
+ >>> dict(nx.all_pairs_lowest_common_ancestor(G))
51
+ {(0, 0): 0, (0, 1): 0, (0, 3): 0, (0, 2): 0, (1, 1): 1, (1, 3): 0, (1, 2): 1, (3, 3): 3, (3, 2): 0, (2, 2): 2}
52
+
53
+ The pairs argument can be used to limit the output to only the
54
+ specified node pairings:
55
+
56
+ >>> dict(nx.all_pairs_lowest_common_ancestor(G, pairs=[(1, 2), (2, 3)]))
57
+ {(1, 2): 1, (2, 3): 0}
58
+
59
+ Notes
60
+ -----
61
+ Only defined on non-null directed acyclic graphs.
62
+
63
+ See Also
64
+ --------
65
+ lowest_common_ancestor
66
+ """
67
+ if not nx.is_directed_acyclic_graph(G):
68
+ raise nx.NetworkXError("LCA only defined on directed acyclic graphs.")
69
+ if len(G) == 0:
70
+ raise nx.NetworkXPointlessConcept("LCA meaningless on null graphs.")
71
+
72
+ if pairs is None:
73
+ pairs = combinations_with_replacement(G, 2)
74
+ else:
75
+ # Convert iterator to iterable, if necessary. Trim duplicates.
76
+ pairs = dict.fromkeys(pairs)
77
+ # Verify that each of the nodes in the provided pairs is in G
78
+ nodeset = set(G)
79
+ for pair in pairs:
80
+ if set(pair) - nodeset:
81
+ raise nx.NodeNotFound(
82
+ f"Node(s) {set(pair) - nodeset} from pair {pair} not in G."
83
+ )
84
+
85
+ # Once input validation is done, construct the generator
86
+ def generate_lca_from_pairs(G, pairs):
87
+ ancestor_cache = {}
88
+
89
+ for v, w in pairs:
90
+ if v not in ancestor_cache:
91
+ ancestor_cache[v] = nx.ancestors(G, v)
92
+ ancestor_cache[v].add(v)
93
+ if w not in ancestor_cache:
94
+ ancestor_cache[w] = nx.ancestors(G, w)
95
+ ancestor_cache[w].add(w)
96
+
97
+ common_ancestors = ancestor_cache[v] & ancestor_cache[w]
98
+
99
+ if common_ancestors:
100
+ common_ancestor = next(iter(common_ancestors))
101
+ while True:
102
+ successor = None
103
+ for lower_ancestor in G.successors(common_ancestor):
104
+ if lower_ancestor in common_ancestors:
105
+ successor = lower_ancestor
106
+ break
107
+ if successor is None:
108
+ break
109
+ common_ancestor = successor
110
+ yield ((v, w), common_ancestor)
111
+
112
+ return generate_lca_from_pairs(G, pairs)
113
+
114
+
115
+ @not_implemented_for("undirected")
116
+ @nx._dispatchable
117
+ def lowest_common_ancestor(G, node1, node2, default=None):
118
+ """Compute the lowest common ancestor of the given pair of nodes.
119
+
120
+ Parameters
121
+ ----------
122
+ G : NetworkX directed graph
123
+
124
+ node1, node2 : nodes in the graph.
125
+
126
+ default : object
127
+ Returned if no common ancestor between `node1` and `node2`
128
+
129
+ Returns
130
+ -------
131
+ The lowest common ancestor of node1 and node2,
132
+ or default if they have no common ancestors.
133
+
134
+ Examples
135
+ --------
136
+ >>> G = nx.DiGraph()
137
+ >>> nx.add_path(G, (0, 1, 2, 3))
138
+ >>> nx.add_path(G, (0, 4, 3))
139
+ >>> nx.lowest_common_ancestor(G, 2, 4)
140
+ 0
141
+
142
+ See Also
143
+ --------
144
+ all_pairs_lowest_common_ancestor"""
145
+
146
+ ans = list(all_pairs_lowest_common_ancestor(G, pairs=[(node1, node2)]))
147
+ if ans:
148
+ assert len(ans) == 1
149
+ return ans[0][1]
150
+ return default
151
+
152
+
153
+ @not_implemented_for("undirected")
154
+ @nx._dispatchable
155
+ def tree_all_pairs_lowest_common_ancestor(G, root=None, pairs=None):
156
+ r"""Yield the lowest common ancestor for sets of pairs in a tree.
157
+
158
+ Parameters
159
+ ----------
160
+ G : NetworkX directed graph (must be a tree)
161
+
162
+ root : node, optional (default: None)
163
+ The root of the subtree to operate on.
164
+ If None, assume the entire graph has exactly one source and use that.
165
+
166
+ pairs : iterable or iterator of pairs of nodes, optional (default: None)
167
+ The pairs of interest. If None, Defaults to all pairs of nodes
168
+ under `root` that have a lowest common ancestor.
169
+
170
+ Returns
171
+ -------
172
+ lcas : generator of tuples `((u, v), lca)` where `u` and `v` are nodes
173
+ in `pairs` and `lca` is their lowest common ancestor.
174
+
175
+ Examples
176
+ --------
177
+ >>> import pprint
178
+ >>> G = nx.DiGraph([(1, 3), (2, 4), (1, 2)])
179
+ >>> pprint.pprint(dict(nx.tree_all_pairs_lowest_common_ancestor(G)))
180
+ {(1, 1): 1,
181
+ (2, 1): 1,
182
+ (2, 2): 2,
183
+ (3, 1): 1,
184
+ (3, 2): 1,
185
+ (3, 3): 3,
186
+ (3, 4): 1,
187
+ (4, 1): 1,
188
+ (4, 2): 2,
189
+ (4, 4): 4}
190
+
191
+ We can also use `pairs` argument to specify the pairs of nodes for which we
192
+ want to compute lowest common ancestors. Here is an example:
193
+
194
+ >>> dict(nx.tree_all_pairs_lowest_common_ancestor(G, pairs=[(1, 4), (2, 3)]))
195
+ {(2, 3): 1, (1, 4): 1}
196
+
197
+ Notes
198
+ -----
199
+ Only defined on non-null trees represented with directed edges from
200
+ parents to children. Uses Tarjan's off-line lowest-common-ancestors
201
+ algorithm. Runs in time $O(4 \times (V + E + P))$ time, where 4 is the largest
202
+ value of the inverse Ackermann function likely to ever come up in actual
203
+ use, and $P$ is the number of pairs requested (or $V^2$ if all are needed).
204
+
205
+ Tarjan, R. E. (1979), "Applications of path compression on balanced trees",
206
+ Journal of the ACM 26 (4): 690-715, doi:10.1145/322154.322161.
207
+
208
+ See Also
209
+ --------
210
+ all_pairs_lowest_common_ancestor: similar routine for general DAGs
211
+ lowest_common_ancestor: just a single pair for general DAGs
212
+ """
213
+ if len(G) == 0:
214
+ raise nx.NetworkXPointlessConcept("LCA meaningless on null graphs.")
215
+
216
+ # Index pairs of interest for efficient lookup from either side.
217
+ if pairs is not None:
218
+ pair_dict = defaultdict(set)
219
+ # See note on all_pairs_lowest_common_ancestor.
220
+ if not isinstance(pairs, Mapping | Set):
221
+ pairs = set(pairs)
222
+ for u, v in pairs:
223
+ for n in (u, v):
224
+ if n not in G:
225
+ msg = f"The node {str(n)} is not in the digraph."
226
+ raise nx.NodeNotFound(msg)
227
+ pair_dict[u].add(v)
228
+ pair_dict[v].add(u)
229
+
230
+ # If root is not specified, find the exactly one node with in degree 0 and
231
+ # use it. Raise an error if none are found, or more than one is. Also check
232
+ # for any nodes with in degree larger than 1, which would imply G is not a
233
+ # tree.
234
+ if root is None:
235
+ for n, deg in G.in_degree:
236
+ if deg == 0:
237
+ if root is not None:
238
+ msg = "No root specified and tree has multiple sources."
239
+ raise nx.NetworkXError(msg)
240
+ root = n
241
+ # checking deg>1 is not sufficient for MultiDiGraphs
242
+ elif deg > 1 and len(G.pred[n]) > 1:
243
+ msg = "Tree LCA only defined on trees; use DAG routine."
244
+ raise nx.NetworkXError(msg)
245
+ if root is None:
246
+ raise nx.NetworkXError("Graph contains a cycle.")
247
+
248
+ # Iterative implementation of Tarjan's offline lca algorithm
249
+ # as described in CLRS on page 521 (2nd edition)/page 584 (3rd edition)
250
+ uf = UnionFind()
251
+ ancestors = {}
252
+ for node in G:
253
+ ancestors[node] = uf[node]
254
+
255
+ colors = defaultdict(bool)
256
+ for node in nx.dfs_postorder_nodes(G, root):
257
+ colors[node] = True
258
+ for v in pair_dict[node] if pairs is not None else G:
259
+ if colors[v]:
260
+ # If the user requested both directions of a pair, give it.
261
+ # Otherwise, just give one.
262
+ if pairs is not None and (node, v) in pairs:
263
+ yield (node, v), ancestors[uf[v]]
264
+ if pairs is None or (v, node) in pairs:
265
+ yield (v, node), ancestors[uf[v]]
266
+ if node != root:
267
+ parent = arbitrary_element(G.pred[node])
268
+ uf.union(parent, node)
269
+ ancestors[uf[parent]] = parent
.venv/lib/python3.11/site-packages/networkx/algorithms/matching.py ADDED
@@ -0,0 +1,1152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions for computing and verifying matchings in a graph."""
2
+
3
+ from collections import Counter
4
+ from itertools import combinations, repeat
5
+
6
+ import networkx as nx
7
+ from networkx.utils import not_implemented_for
8
+
9
+ __all__ = [
10
+ "is_matching",
11
+ "is_maximal_matching",
12
+ "is_perfect_matching",
13
+ "max_weight_matching",
14
+ "min_weight_matching",
15
+ "maximal_matching",
16
+ ]
17
+
18
+
19
+ @not_implemented_for("multigraph")
20
+ @not_implemented_for("directed")
21
+ @nx._dispatchable
22
+ def maximal_matching(G):
23
+ r"""Find a maximal matching in the graph.
24
+
25
+ A matching is a subset of edges in which no node occurs more than once.
26
+ A maximal matching cannot add more edges and still be a matching.
27
+
28
+ Parameters
29
+ ----------
30
+ G : NetworkX graph
31
+ Undirected graph
32
+
33
+ Returns
34
+ -------
35
+ matching : set
36
+ A maximal matching of the graph.
37
+
38
+ Examples
39
+ --------
40
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)])
41
+ >>> sorted(nx.maximal_matching(G))
42
+ [(1, 2), (3, 5)]
43
+
44
+ Notes
45
+ -----
46
+ The algorithm greedily selects a maximal matching M of the graph G
47
+ (i.e. no superset of M exists). It runs in $O(|E|)$ time.
48
+ """
49
+ matching = set()
50
+ nodes = set()
51
+ for edge in G.edges():
52
+ # If the edge isn't covered, add it to the matching
53
+ # then remove neighborhood of u and v from consideration.
54
+ u, v = edge
55
+ if u not in nodes and v not in nodes and u != v:
56
+ matching.add(edge)
57
+ nodes.update(edge)
58
+ return matching
59
+
60
+
61
+ def matching_dict_to_set(matching):
62
+ """Converts matching dict format to matching set format
63
+
64
+ Converts a dictionary representing a matching (as returned by
65
+ :func:`max_weight_matching`) to a set representing a matching (as
66
+ returned by :func:`maximal_matching`).
67
+
68
+ In the definition of maximal matching adopted by NetworkX,
69
+ self-loops are not allowed, so the provided dictionary is expected
70
+ to never have any mapping from a key to itself. However, the
71
+ dictionary is expected to have mirrored key/value pairs, for
72
+ example, key ``u`` with value ``v`` and key ``v`` with value ``u``.
73
+
74
+ """
75
+ edges = set()
76
+ for edge in matching.items():
77
+ u, v = edge
78
+ if (v, u) in edges or edge in edges:
79
+ continue
80
+ if u == v:
81
+ raise nx.NetworkXError(f"Selfloops cannot appear in matchings {edge}")
82
+ edges.add(edge)
83
+ return edges
84
+
85
+
86
+ @nx._dispatchable
87
+ def is_matching(G, matching):
88
+ """Return True if ``matching`` is a valid matching of ``G``
89
+
90
+ A *matching* in a graph is a set of edges in which no two distinct
91
+ edges share a common endpoint. Each node is incident to at most one
92
+ edge in the matching. The edges are said to be independent.
93
+
94
+ Parameters
95
+ ----------
96
+ G : NetworkX graph
97
+
98
+ matching : dict or set
99
+ A dictionary or set representing a matching. If a dictionary, it
100
+ must have ``matching[u] == v`` and ``matching[v] == u`` for each
101
+ edge ``(u, v)`` in the matching. If a set, it must have elements
102
+ of the form ``(u, v)``, where ``(u, v)`` is an edge in the
103
+ matching.
104
+
105
+ Returns
106
+ -------
107
+ bool
108
+ Whether the given set or dictionary represents a valid matching
109
+ in the graph.
110
+
111
+ Raises
112
+ ------
113
+ NetworkXError
114
+ If the proposed matching has an edge to a node not in G.
115
+ Or if the matching is not a collection of 2-tuple edges.
116
+
117
+ Examples
118
+ --------
119
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)])
120
+ >>> nx.is_maximal_matching(G, {1: 3, 2: 4}) # using dict to represent matching
121
+ True
122
+
123
+ >>> nx.is_matching(G, {(1, 3), (2, 4)}) # using set to represent matching
124
+ True
125
+
126
+ """
127
+ if isinstance(matching, dict):
128
+ matching = matching_dict_to_set(matching)
129
+
130
+ nodes = set()
131
+ for edge in matching:
132
+ if len(edge) != 2:
133
+ raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
134
+ u, v = edge
135
+ if u not in G or v not in G:
136
+ raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
137
+ if u == v:
138
+ return False
139
+ if not G.has_edge(u, v):
140
+ return False
141
+ if u in nodes or v in nodes:
142
+ return False
143
+ nodes.update(edge)
144
+ return True
145
+
146
+
147
+ @nx._dispatchable
148
+ def is_maximal_matching(G, matching):
149
+ """Return True if ``matching`` is a maximal matching of ``G``
150
+
151
+ A *maximal matching* in a graph is a matching in which adding any
152
+ edge would cause the set to no longer be a valid matching.
153
+
154
+ Parameters
155
+ ----------
156
+ G : NetworkX graph
157
+
158
+ matching : dict or set
159
+ A dictionary or set representing a matching. If a dictionary, it
160
+ must have ``matching[u] == v`` and ``matching[v] == u`` for each
161
+ edge ``(u, v)`` in the matching. If a set, it must have elements
162
+ of the form ``(u, v)``, where ``(u, v)`` is an edge in the
163
+ matching.
164
+
165
+ Returns
166
+ -------
167
+ bool
168
+ Whether the given set or dictionary represents a valid maximal
169
+ matching in the graph.
170
+
171
+ Examples
172
+ --------
173
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (3, 5)])
174
+ >>> nx.is_maximal_matching(G, {(1, 2), (3, 4)})
175
+ True
176
+
177
+ """
178
+ if isinstance(matching, dict):
179
+ matching = matching_dict_to_set(matching)
180
+ # If the given set is not a matching, then it is not a maximal matching.
181
+ edges = set()
182
+ nodes = set()
183
+ for edge in matching:
184
+ if len(edge) != 2:
185
+ raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
186
+ u, v = edge
187
+ if u not in G or v not in G:
188
+ raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
189
+ if u == v:
190
+ return False
191
+ if not G.has_edge(u, v):
192
+ return False
193
+ if u in nodes or v in nodes:
194
+ return False
195
+ nodes.update(edge)
196
+ edges.add(edge)
197
+ edges.add((v, u))
198
+ # A matching is maximal if adding any new edge from G to it
199
+ # causes the resulting set to match some node twice.
200
+ # Be careful to check for adding selfloops
201
+ for u, v in G.edges:
202
+ if (u, v) not in edges:
203
+ # could add edge (u, v) to edges and have a bigger matching
204
+ if u not in nodes and v not in nodes and u != v:
205
+ return False
206
+ return True
207
+
208
+
209
+ @nx._dispatchable
210
+ def is_perfect_matching(G, matching):
211
+ """Return True if ``matching`` is a perfect matching for ``G``
212
+
213
+ A *perfect matching* in a graph is a matching in which exactly one edge
214
+ is incident upon each vertex.
215
+
216
+ Parameters
217
+ ----------
218
+ G : NetworkX graph
219
+
220
+ matching : dict or set
221
+ A dictionary or set representing a matching. If a dictionary, it
222
+ must have ``matching[u] == v`` and ``matching[v] == u`` for each
223
+ edge ``(u, v)`` in the matching. If a set, it must have elements
224
+ of the form ``(u, v)``, where ``(u, v)`` is an edge in the
225
+ matching.
226
+
227
+ Returns
228
+ -------
229
+ bool
230
+ Whether the given set or dictionary represents a valid perfect
231
+ matching in the graph.
232
+
233
+ Examples
234
+ --------
235
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5), (4, 6)])
236
+ >>> my_match = {1: 2, 3: 5, 4: 6}
237
+ >>> nx.is_perfect_matching(G, my_match)
238
+ True
239
+
240
+ """
241
+ if isinstance(matching, dict):
242
+ matching = matching_dict_to_set(matching)
243
+
244
+ nodes = set()
245
+ for edge in matching:
246
+ if len(edge) != 2:
247
+ raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
248
+ u, v = edge
249
+ if u not in G or v not in G:
250
+ raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
251
+ if u == v:
252
+ return False
253
+ if not G.has_edge(u, v):
254
+ return False
255
+ if u in nodes or v in nodes:
256
+ return False
257
+ nodes.update(edge)
258
+ return len(nodes) == len(G)
259
+
260
+
261
+ @not_implemented_for("multigraph")
262
+ @not_implemented_for("directed")
263
+ @nx._dispatchable(edge_attrs="weight")
264
+ def min_weight_matching(G, weight="weight"):
265
+ """Computing a minimum-weight maximal matching of G.
266
+
267
+ Use the maximum-weight algorithm with edge weights subtracted
268
+ from the maximum weight of all edges.
269
+
270
+ A matching is a subset of edges in which no node occurs more than once.
271
+ The weight of a matching is the sum of the weights of its edges.
272
+ A maximal matching cannot add more edges and still be a matching.
273
+ The cardinality of a matching is the number of matched edges.
274
+
275
+ This method replaces the edge weights with 1 plus the maximum edge weight
276
+ minus the original edge weight.
277
+
278
+ new_weight = (max_weight + 1) - edge_weight
279
+
280
+ then runs :func:`max_weight_matching` with the new weights.
281
+ The max weight matching with these new weights corresponds
282
+ to the min weight matching using the original weights.
283
+ Adding 1 to the max edge weight keeps all edge weights positive
284
+ and as integers if they started as integers.
285
+
286
+ You might worry that adding 1 to each weight would make the algorithm
287
+ favor matchings with more edges. But we use the parameter
288
+ `maxcardinality=True` in `max_weight_matching` to ensure that the
289
+ number of edges in the competing matchings are the same and thus
290
+ the optimum does not change due to changes in the number of edges.
291
+
292
+ Read the documentation of `max_weight_matching` for more information.
293
+
294
+ Parameters
295
+ ----------
296
+ G : NetworkX graph
297
+ Undirected graph
298
+
299
+ weight: string, optional (default='weight')
300
+ Edge data key corresponding to the edge weight.
301
+ If key not found, uses 1 as weight.
302
+
303
+ Returns
304
+ -------
305
+ matching : set
306
+ A minimal weight matching of the graph.
307
+
308
+ See Also
309
+ --------
310
+ max_weight_matching
311
+ """
312
+ if len(G.edges) == 0:
313
+ return max_weight_matching(G, maxcardinality=True, weight=weight)
314
+ G_edges = G.edges(data=weight, default=1)
315
+ max_weight = 1 + max(w for _, _, w in G_edges)
316
+ InvG = nx.Graph()
317
+ edges = ((u, v, max_weight - w) for u, v, w in G_edges)
318
+ InvG.add_weighted_edges_from(edges, weight=weight)
319
+ return max_weight_matching(InvG, maxcardinality=True, weight=weight)
320
+
321
+
322
+ @not_implemented_for("multigraph")
323
+ @not_implemented_for("directed")
324
+ @nx._dispatchable(edge_attrs="weight")
325
+ def max_weight_matching(G, maxcardinality=False, weight="weight"):
326
+ """Compute a maximum-weighted matching of G.
327
+
328
+ A matching is a subset of edges in which no node occurs more than once.
329
+ The weight of a matching is the sum of the weights of its edges.
330
+ A maximal matching cannot add more edges and still be a matching.
331
+ The cardinality of a matching is the number of matched edges.
332
+
333
+ Parameters
334
+ ----------
335
+ G : NetworkX graph
336
+ Undirected graph
337
+
338
+ maxcardinality: bool, optional (default=False)
339
+ If maxcardinality is True, compute the maximum-cardinality matching
340
+ with maximum weight among all maximum-cardinality matchings.
341
+
342
+ weight: string, optional (default='weight')
343
+ Edge data key corresponding to the edge weight.
344
+ If key not found, uses 1 as weight.
345
+
346
+
347
+ Returns
348
+ -------
349
+ matching : set
350
+ A maximal matching of the graph.
351
+
352
+ Examples
353
+ --------
354
+ >>> G = nx.Graph()
355
+ >>> edges = [(1, 2, 6), (1, 3, 2), (2, 3, 1), (2, 4, 7), (3, 5, 9), (4, 5, 3)]
356
+ >>> G.add_weighted_edges_from(edges)
357
+ >>> sorted(nx.max_weight_matching(G))
358
+ [(2, 4), (5, 3)]
359
+
360
+ Notes
361
+ -----
362
+ If G has edges with weight attributes the edge data are used as
363
+ weight values else the weights are assumed to be 1.
364
+
365
+ This function takes time O(number_of_nodes ** 3).
366
+
367
+ If all edge weights are integers, the algorithm uses only integer
368
+ computations. If floating point weights are used, the algorithm
369
+ could return a slightly suboptimal matching due to numeric
370
+ precision errors.
371
+
372
+ This method is based on the "blossom" method for finding augmenting
373
+ paths and the "primal-dual" method for finding a matching of maximum
374
+ weight, both methods invented by Jack Edmonds [1]_.
375
+
376
+ Bipartite graphs can also be matched using the functions present in
377
+ :mod:`networkx.algorithms.bipartite.matching`.
378
+
379
+ References
380
+ ----------
381
+ .. [1] "Efficient Algorithms for Finding Maximum Matching in Graphs",
382
+ Zvi Galil, ACM Computing Surveys, 1986.
383
+ """
384
+ #
385
+ # The algorithm is taken from "Efficient Algorithms for Finding Maximum
386
+ # Matching in Graphs" by Zvi Galil, ACM Computing Surveys, 1986.
387
+ # It is based on the "blossom" method for finding augmenting paths and
388
+ # the "primal-dual" method for finding a matching of maximum weight, both
389
+ # methods invented by Jack Edmonds.
390
+ #
391
+ # A C program for maximum weight matching by Ed Rothberg was used
392
+ # extensively to validate this new code.
393
+ #
394
+ # Many terms used in the code comments are explained in the paper
395
+ # by Galil. You will probably need the paper to make sense of this code.
396
+ #
397
+
398
+ class NoNode:
399
+ """Dummy value which is different from any node."""
400
+
401
+ class Blossom:
402
+ """Representation of a non-trivial blossom or sub-blossom."""
403
+
404
+ __slots__ = ["childs", "edges", "mybestedges"]
405
+
406
+ # b.childs is an ordered list of b's sub-blossoms, starting with
407
+ # the base and going round the blossom.
408
+
409
+ # b.edges is the list of b's connecting edges, such that
410
+ # b.edges[i] = (v, w) where v is a vertex in b.childs[i]
411
+ # and w is a vertex in b.childs[wrap(i+1)].
412
+
413
+ # If b is a top-level S-blossom,
414
+ # b.mybestedges is a list of least-slack edges to neighboring
415
+ # S-blossoms, or None if no such list has been computed yet.
416
+ # This is used for efficient computation of delta3.
417
+
418
+ # Generate the blossom's leaf vertices.
419
+ def leaves(self):
420
+ stack = [*self.childs]
421
+ while stack:
422
+ t = stack.pop()
423
+ if isinstance(t, Blossom):
424
+ stack.extend(t.childs)
425
+ else:
426
+ yield t
427
+
428
+ # Get a list of vertices.
429
+ gnodes = list(G)
430
+ if not gnodes:
431
+ return set() # don't bother with empty graphs
432
+
433
+ # Find the maximum edge weight.
434
+ maxweight = 0
435
+ allinteger = True
436
+ for i, j, d in G.edges(data=True):
437
+ wt = d.get(weight, 1)
438
+ if i != j and wt > maxweight:
439
+ maxweight = wt
440
+ allinteger = allinteger and (str(type(wt)).split("'")[1] in ("int", "long"))
441
+
442
+ # If v is a matched vertex, mate[v] is its partner vertex.
443
+ # If v is a single vertex, v does not occur as a key in mate.
444
+ # Initially all vertices are single; updated during augmentation.
445
+ mate = {}
446
+
447
+ # If b is a top-level blossom,
448
+ # label.get(b) is None if b is unlabeled (free),
449
+ # 1 if b is an S-blossom,
450
+ # 2 if b is a T-blossom.
451
+ # The label of a vertex is found by looking at the label of its top-level
452
+ # containing blossom.
453
+ # If v is a vertex inside a T-blossom, label[v] is 2 iff v is reachable
454
+ # from an S-vertex outside the blossom.
455
+ # Labels are assigned during a stage and reset after each augmentation.
456
+ label = {}
457
+
458
+ # If b is a labeled top-level blossom,
459
+ # labeledge[b] = (v, w) is the edge through which b obtained its label
460
+ # such that w is a vertex in b, or None if b's base vertex is single.
461
+ # If w is a vertex inside a T-blossom and label[w] == 2,
462
+ # labeledge[w] = (v, w) is an edge through which w is reachable from
463
+ # outside the blossom.
464
+ labeledge = {}
465
+
466
+ # If v is a vertex, inblossom[v] is the top-level blossom to which v
467
+ # belongs.
468
+ # If v is a top-level vertex, inblossom[v] == v since v is itself
469
+ # a (trivial) top-level blossom.
470
+ # Initially all vertices are top-level trivial blossoms.
471
+ inblossom = dict(zip(gnodes, gnodes))
472
+
473
+ # If b is a sub-blossom,
474
+ # blossomparent[b] is its immediate parent (sub-)blossom.
475
+ # If b is a top-level blossom, blossomparent[b] is None.
476
+ blossomparent = dict(zip(gnodes, repeat(None)))
477
+
478
+ # If b is a (sub-)blossom,
479
+ # blossombase[b] is its base VERTEX (i.e. recursive sub-blossom).
480
+ blossombase = dict(zip(gnodes, gnodes))
481
+
482
+ # If w is a free vertex (or an unreached vertex inside a T-blossom),
483
+ # bestedge[w] = (v, w) is the least-slack edge from an S-vertex,
484
+ # or None if there is no such edge.
485
+ # If b is a (possibly trivial) top-level S-blossom,
486
+ # bestedge[b] = (v, w) is the least-slack edge to a different S-blossom
487
+ # (v inside b), or None if there is no such edge.
488
+ # This is used for efficient computation of delta2 and delta3.
489
+ bestedge = {}
490
+
491
+ # If v is a vertex,
492
+ # dualvar[v] = 2 * u(v) where u(v) is the v's variable in the dual
493
+ # optimization problem (if all edge weights are integers, multiplication
494
+ # by two ensures that all values remain integers throughout the algorithm).
495
+ # Initially, u(v) = maxweight / 2.
496
+ dualvar = dict(zip(gnodes, repeat(maxweight)))
497
+
498
+ # If b is a non-trivial blossom,
499
+ # blossomdual[b] = z(b) where z(b) is b's variable in the dual
500
+ # optimization problem.
501
+ blossomdual = {}
502
+
503
+ # If (v, w) in allowedge or (w, v) in allowedg, then the edge
504
+ # (v, w) is known to have zero slack in the optimization problem;
505
+ # otherwise the edge may or may not have zero slack.
506
+ allowedge = {}
507
+
508
+ # Queue of newly discovered S-vertices.
509
+ queue = []
510
+
511
+ # Return 2 * slack of edge (v, w) (does not work inside blossoms).
512
+ def slack(v, w):
513
+ return dualvar[v] + dualvar[w] - 2 * G[v][w].get(weight, 1)
514
+
515
+ # Assign label t to the top-level blossom containing vertex w,
516
+ # coming through an edge from vertex v.
517
+ def assignLabel(w, t, v):
518
+ b = inblossom[w]
519
+ assert label.get(w) is None and label.get(b) is None
520
+ label[w] = label[b] = t
521
+ if v is not None:
522
+ labeledge[w] = labeledge[b] = (v, w)
523
+ else:
524
+ labeledge[w] = labeledge[b] = None
525
+ bestedge[w] = bestedge[b] = None
526
+ if t == 1:
527
+ # b became an S-vertex/blossom; add it(s vertices) to the queue.
528
+ if isinstance(b, Blossom):
529
+ queue.extend(b.leaves())
530
+ else:
531
+ queue.append(b)
532
+ elif t == 2:
533
+ # b became a T-vertex/blossom; assign label S to its mate.
534
+ # (If b is a non-trivial blossom, its base is the only vertex
535
+ # with an external mate.)
536
+ base = blossombase[b]
537
+ assignLabel(mate[base], 1, base)
538
+
539
+ # Trace back from vertices v and w to discover either a new blossom
540
+ # or an augmenting path. Return the base vertex of the new blossom,
541
+ # or NoNode if an augmenting path was found.
542
+ def scanBlossom(v, w):
543
+ # Trace back from v and w, placing breadcrumbs as we go.
544
+ path = []
545
+ base = NoNode
546
+ while v is not NoNode:
547
+ # Look for a breadcrumb in v's blossom or put a new breadcrumb.
548
+ b = inblossom[v]
549
+ if label[b] & 4:
550
+ base = blossombase[b]
551
+ break
552
+ assert label[b] == 1
553
+ path.append(b)
554
+ label[b] = 5
555
+ # Trace one step back.
556
+ if labeledge[b] is None:
557
+ # The base of blossom b is single; stop tracing this path.
558
+ assert blossombase[b] not in mate
559
+ v = NoNode
560
+ else:
561
+ assert labeledge[b][0] == mate[blossombase[b]]
562
+ v = labeledge[b][0]
563
+ b = inblossom[v]
564
+ assert label[b] == 2
565
+ # b is a T-blossom; trace one more step back.
566
+ v = labeledge[b][0]
567
+ # Swap v and w so that we alternate between both paths.
568
+ if w is not NoNode:
569
+ v, w = w, v
570
+ # Remove breadcrumbs.
571
+ for b in path:
572
+ label[b] = 1
573
+ # Return base vertex, if we found one.
574
+ return base
575
+
576
+ # Construct a new blossom with given base, through S-vertices v and w.
577
+ # Label the new blossom as S; set its dual variable to zero;
578
+ # relabel its T-vertices to S and add them to the queue.
579
+ def addBlossom(base, v, w):
580
+ bb = inblossom[base]
581
+ bv = inblossom[v]
582
+ bw = inblossom[w]
583
+ # Create blossom.
584
+ b = Blossom()
585
+ blossombase[b] = base
586
+ blossomparent[b] = None
587
+ blossomparent[bb] = b
588
+ # Make list of sub-blossoms and their interconnecting edge endpoints.
589
+ b.childs = path = []
590
+ b.edges = edgs = [(v, w)]
591
+ # Trace back from v to base.
592
+ while bv != bb:
593
+ # Add bv to the new blossom.
594
+ blossomparent[bv] = b
595
+ path.append(bv)
596
+ edgs.append(labeledge[bv])
597
+ assert label[bv] == 2 or (
598
+ label[bv] == 1 and labeledge[bv][0] == mate[blossombase[bv]]
599
+ )
600
+ # Trace one step back.
601
+ v = labeledge[bv][0]
602
+ bv = inblossom[v]
603
+ # Add base sub-blossom; reverse lists.
604
+ path.append(bb)
605
+ path.reverse()
606
+ edgs.reverse()
607
+ # Trace back from w to base.
608
+ while bw != bb:
609
+ # Add bw to the new blossom.
610
+ blossomparent[bw] = b
611
+ path.append(bw)
612
+ edgs.append((labeledge[bw][1], labeledge[bw][0]))
613
+ assert label[bw] == 2 or (
614
+ label[bw] == 1 and labeledge[bw][0] == mate[blossombase[bw]]
615
+ )
616
+ # Trace one step back.
617
+ w = labeledge[bw][0]
618
+ bw = inblossom[w]
619
+ # Set label to S.
620
+ assert label[bb] == 1
621
+ label[b] = 1
622
+ labeledge[b] = labeledge[bb]
623
+ # Set dual variable to zero.
624
+ blossomdual[b] = 0
625
+ # Relabel vertices.
626
+ for v in b.leaves():
627
+ if label[inblossom[v]] == 2:
628
+ # This T-vertex now turns into an S-vertex because it becomes
629
+ # part of an S-blossom; add it to the queue.
630
+ queue.append(v)
631
+ inblossom[v] = b
632
+ # Compute b.mybestedges.
633
+ bestedgeto = {}
634
+ for bv in path:
635
+ if isinstance(bv, Blossom):
636
+ if bv.mybestedges is not None:
637
+ # Walk this subblossom's least-slack edges.
638
+ nblist = bv.mybestedges
639
+ # The sub-blossom won't need this data again.
640
+ bv.mybestedges = None
641
+ else:
642
+ # This subblossom does not have a list of least-slack
643
+ # edges; get the information from the vertices.
644
+ nblist = [
645
+ (v, w) for v in bv.leaves() for w in G.neighbors(v) if v != w
646
+ ]
647
+ else:
648
+ nblist = [(bv, w) for w in G.neighbors(bv) if bv != w]
649
+ for k in nblist:
650
+ (i, j) = k
651
+ if inblossom[j] == b:
652
+ i, j = j, i
653
+ bj = inblossom[j]
654
+ if (
655
+ bj != b
656
+ and label.get(bj) == 1
657
+ and ((bj not in bestedgeto) or slack(i, j) < slack(*bestedgeto[bj]))
658
+ ):
659
+ bestedgeto[bj] = k
660
+ # Forget about least-slack edge of the subblossom.
661
+ bestedge[bv] = None
662
+ b.mybestedges = list(bestedgeto.values())
663
+ # Select bestedge[b].
664
+ mybestedge = None
665
+ bestedge[b] = None
666
+ for k in b.mybestedges:
667
+ kslack = slack(*k)
668
+ if mybestedge is None or kslack < mybestslack:
669
+ mybestedge = k
670
+ mybestslack = kslack
671
+ bestedge[b] = mybestedge
672
+
673
+ # Expand the given top-level blossom.
674
+ def expandBlossom(b, endstage):
675
+ # This is an obnoxiously complicated recursive function for the sake of
676
+ # a stack-transformation. So, we hack around the complexity by using
677
+ # a trampoline pattern. By yielding the arguments to each recursive
678
+ # call, we keep the actual callstack flat.
679
+
680
+ def _recurse(b, endstage):
681
+ # Convert sub-blossoms into top-level blossoms.
682
+ for s in b.childs:
683
+ blossomparent[s] = None
684
+ if isinstance(s, Blossom):
685
+ if endstage and blossomdual[s] == 0:
686
+ # Recursively expand this sub-blossom.
687
+ yield s
688
+ else:
689
+ for v in s.leaves():
690
+ inblossom[v] = s
691
+ else:
692
+ inblossom[s] = s
693
+ # If we expand a T-blossom during a stage, its sub-blossoms must be
694
+ # relabeled.
695
+ if (not endstage) and label.get(b) == 2:
696
+ # Start at the sub-blossom through which the expanding
697
+ # blossom obtained its label, and relabel sub-blossoms untili
698
+ # we reach the base.
699
+ # Figure out through which sub-blossom the expanding blossom
700
+ # obtained its label initially.
701
+ entrychild = inblossom[labeledge[b][1]]
702
+ # Decide in which direction we will go round the blossom.
703
+ j = b.childs.index(entrychild)
704
+ if j & 1:
705
+ # Start index is odd; go forward and wrap.
706
+ j -= len(b.childs)
707
+ jstep = 1
708
+ else:
709
+ # Start index is even; go backward.
710
+ jstep = -1
711
+ # Move along the blossom until we get to the base.
712
+ v, w = labeledge[b]
713
+ while j != 0:
714
+ # Relabel the T-sub-blossom.
715
+ if jstep == 1:
716
+ p, q = b.edges[j]
717
+ else:
718
+ q, p = b.edges[j - 1]
719
+ label[w] = None
720
+ label[q] = None
721
+ assignLabel(w, 2, v)
722
+ # Step to the next S-sub-blossom and note its forward edge.
723
+ allowedge[(p, q)] = allowedge[(q, p)] = True
724
+ j += jstep
725
+ if jstep == 1:
726
+ v, w = b.edges[j]
727
+ else:
728
+ w, v = b.edges[j - 1]
729
+ # Step to the next T-sub-blossom.
730
+ allowedge[(v, w)] = allowedge[(w, v)] = True
731
+ j += jstep
732
+ # Relabel the base T-sub-blossom WITHOUT stepping through to
733
+ # its mate (so don't call assignLabel).
734
+ bw = b.childs[j]
735
+ label[w] = label[bw] = 2
736
+ labeledge[w] = labeledge[bw] = (v, w)
737
+ bestedge[bw] = None
738
+ # Continue along the blossom until we get back to entrychild.
739
+ j += jstep
740
+ while b.childs[j] != entrychild:
741
+ # Examine the vertices of the sub-blossom to see whether
742
+ # it is reachable from a neighboring S-vertex outside the
743
+ # expanding blossom.
744
+ bv = b.childs[j]
745
+ if label.get(bv) == 1:
746
+ # This sub-blossom just got label S through one of its
747
+ # neighbors; leave it be.
748
+ j += jstep
749
+ continue
750
+ if isinstance(bv, Blossom):
751
+ for v in bv.leaves():
752
+ if label.get(v):
753
+ break
754
+ else:
755
+ v = bv
756
+ # If the sub-blossom contains a reachable vertex, assign
757
+ # label T to the sub-blossom.
758
+ if label.get(v):
759
+ assert label[v] == 2
760
+ assert inblossom[v] == bv
761
+ label[v] = None
762
+ label[mate[blossombase[bv]]] = None
763
+ assignLabel(v, 2, labeledge[v][0])
764
+ j += jstep
765
+ # Remove the expanded blossom entirely.
766
+ label.pop(b, None)
767
+ labeledge.pop(b, None)
768
+ bestedge.pop(b, None)
769
+ del blossomparent[b]
770
+ del blossombase[b]
771
+ del blossomdual[b]
772
+
773
+ # Now, we apply the trampoline pattern. We simulate a recursive
774
+ # callstack by maintaining a stack of generators, each yielding a
775
+ # sequence of function arguments. We grow the stack by appending a call
776
+ # to _recurse on each argument tuple, and shrink the stack whenever a
777
+ # generator is exhausted.
778
+ stack = [_recurse(b, endstage)]
779
+ while stack:
780
+ top = stack[-1]
781
+ for s in top:
782
+ stack.append(_recurse(s, endstage))
783
+ break
784
+ else:
785
+ stack.pop()
786
+
787
+ # Swap matched/unmatched edges over an alternating path through blossom b
788
+ # between vertex v and the base vertex. Keep blossom bookkeeping
789
+ # consistent.
790
+ def augmentBlossom(b, v):
791
+ # This is an obnoxiously complicated recursive function for the sake of
792
+ # a stack-transformation. So, we hack around the complexity by using
793
+ # a trampoline pattern. By yielding the arguments to each recursive
794
+ # call, we keep the actual callstack flat.
795
+
796
+ def _recurse(b, v):
797
+ # Bubble up through the blossom tree from vertex v to an immediate
798
+ # sub-blossom of b.
799
+ t = v
800
+ while blossomparent[t] != b:
801
+ t = blossomparent[t]
802
+ # Recursively deal with the first sub-blossom.
803
+ if isinstance(t, Blossom):
804
+ yield (t, v)
805
+ # Decide in which direction we will go round the blossom.
806
+ i = j = b.childs.index(t)
807
+ if i & 1:
808
+ # Start index is odd; go forward and wrap.
809
+ j -= len(b.childs)
810
+ jstep = 1
811
+ else:
812
+ # Start index is even; go backward.
813
+ jstep = -1
814
+ # Move along the blossom until we get to the base.
815
+ while j != 0:
816
+ # Step to the next sub-blossom and augment it recursively.
817
+ j += jstep
818
+ t = b.childs[j]
819
+ if jstep == 1:
820
+ w, x = b.edges[j]
821
+ else:
822
+ x, w = b.edges[j - 1]
823
+ if isinstance(t, Blossom):
824
+ yield (t, w)
825
+ # Step to the next sub-blossom and augment it recursively.
826
+ j += jstep
827
+ t = b.childs[j]
828
+ if isinstance(t, Blossom):
829
+ yield (t, x)
830
+ # Match the edge connecting those sub-blossoms.
831
+ mate[w] = x
832
+ mate[x] = w
833
+ # Rotate the list of sub-blossoms to put the new base at the front.
834
+ b.childs = b.childs[i:] + b.childs[:i]
835
+ b.edges = b.edges[i:] + b.edges[:i]
836
+ blossombase[b] = blossombase[b.childs[0]]
837
+ assert blossombase[b] == v
838
+
839
+ # Now, we apply the trampoline pattern. We simulate a recursive
840
+ # callstack by maintaining a stack of generators, each yielding a
841
+ # sequence of function arguments. We grow the stack by appending a call
842
+ # to _recurse on each argument tuple, and shrink the stack whenever a
843
+ # generator is exhausted.
844
+ stack = [_recurse(b, v)]
845
+ while stack:
846
+ top = stack[-1]
847
+ for args in top:
848
+ stack.append(_recurse(*args))
849
+ break
850
+ else:
851
+ stack.pop()
852
+
853
+ # Swap matched/unmatched edges over an alternating path between two
854
+ # single vertices. The augmenting path runs through S-vertices v and w.
855
+ def augmentMatching(v, w):
856
+ for s, j in ((v, w), (w, v)):
857
+ # Match vertex s to vertex j. Then trace back from s
858
+ # until we find a single vertex, swapping matched and unmatched
859
+ # edges as we go.
860
+ while 1:
861
+ bs = inblossom[s]
862
+ assert label[bs] == 1
863
+ assert (labeledge[bs] is None and blossombase[bs] not in mate) or (
864
+ labeledge[bs][0] == mate[blossombase[bs]]
865
+ )
866
+ # Augment through the S-blossom from s to base.
867
+ if isinstance(bs, Blossom):
868
+ augmentBlossom(bs, s)
869
+ # Update mate[s]
870
+ mate[s] = j
871
+ # Trace one step back.
872
+ if labeledge[bs] is None:
873
+ # Reached single vertex; stop.
874
+ break
875
+ t = labeledge[bs][0]
876
+ bt = inblossom[t]
877
+ assert label[bt] == 2
878
+ # Trace one more step back.
879
+ s, j = labeledge[bt]
880
+ # Augment through the T-blossom from j to base.
881
+ assert blossombase[bt] == t
882
+ if isinstance(bt, Blossom):
883
+ augmentBlossom(bt, j)
884
+ # Update mate[j]
885
+ mate[j] = s
886
+
887
+ # Verify that the optimum solution has been reached.
888
+ def verifyOptimum():
889
+ if maxcardinality:
890
+ # Vertices may have negative dual;
891
+ # find a constant non-negative number to add to all vertex duals.
892
+ vdualoffset = max(0, -min(dualvar.values()))
893
+ else:
894
+ vdualoffset = 0
895
+ # 0. all dual variables are non-negative
896
+ assert min(dualvar.values()) + vdualoffset >= 0
897
+ assert len(blossomdual) == 0 or min(blossomdual.values()) >= 0
898
+ # 0. all edges have non-negative slack and
899
+ # 1. all matched edges have zero slack;
900
+ for i, j, d in G.edges(data=True):
901
+ wt = d.get(weight, 1)
902
+ if i == j:
903
+ continue # ignore self-loops
904
+ s = dualvar[i] + dualvar[j] - 2 * wt
905
+ iblossoms = [i]
906
+ jblossoms = [j]
907
+ while blossomparent[iblossoms[-1]] is not None:
908
+ iblossoms.append(blossomparent[iblossoms[-1]])
909
+ while blossomparent[jblossoms[-1]] is not None:
910
+ jblossoms.append(blossomparent[jblossoms[-1]])
911
+ iblossoms.reverse()
912
+ jblossoms.reverse()
913
+ for bi, bj in zip(iblossoms, jblossoms):
914
+ if bi != bj:
915
+ break
916
+ s += 2 * blossomdual[bi]
917
+ assert s >= 0
918
+ if mate.get(i) == j or mate.get(j) == i:
919
+ assert mate[i] == j and mate[j] == i
920
+ assert s == 0
921
+ # 2. all single vertices have zero dual value;
922
+ for v in gnodes:
923
+ assert (v in mate) or dualvar[v] + vdualoffset == 0
924
+ # 3. all blossoms with positive dual value are full.
925
+ for b in blossomdual:
926
+ if blossomdual[b] > 0:
927
+ assert len(b.edges) % 2 == 1
928
+ for i, j in b.edges[1::2]:
929
+ assert mate[i] == j and mate[j] == i
930
+ # Ok.
931
+
932
+ # Main loop: continue until no further improvement is possible.
933
+ while 1:
934
+ # Each iteration of this loop is a "stage".
935
+ # A stage finds an augmenting path and uses that to improve
936
+ # the matching.
937
+
938
+ # Remove labels from top-level blossoms/vertices.
939
+ label.clear()
940
+ labeledge.clear()
941
+
942
+ # Forget all about least-slack edges.
943
+ bestedge.clear()
944
+ for b in blossomdual:
945
+ b.mybestedges = None
946
+
947
+ # Loss of labeling means that we can not be sure that currently
948
+ # allowable edges remain allowable throughout this stage.
949
+ allowedge.clear()
950
+
951
+ # Make queue empty.
952
+ queue[:] = []
953
+
954
+ # Label single blossoms/vertices with S and put them in the queue.
955
+ for v in gnodes:
956
+ if (v not in mate) and label.get(inblossom[v]) is None:
957
+ assignLabel(v, 1, None)
958
+
959
+ # Loop until we succeed in augmenting the matching.
960
+ augmented = 0
961
+ while 1:
962
+ # Each iteration of this loop is a "substage".
963
+ # A substage tries to find an augmenting path;
964
+ # if found, the path is used to improve the matching and
965
+ # the stage ends. If there is no augmenting path, the
966
+ # primal-dual method is used to pump some slack out of
967
+ # the dual variables.
968
+
969
+ # Continue labeling until all vertices which are reachable
970
+ # through an alternating path have got a label.
971
+ while queue and not augmented:
972
+ # Take an S vertex from the queue.
973
+ v = queue.pop()
974
+ assert label[inblossom[v]] == 1
975
+
976
+ # Scan its neighbors:
977
+ for w in G.neighbors(v):
978
+ if w == v:
979
+ continue # ignore self-loops
980
+ # w is a neighbor to v
981
+ bv = inblossom[v]
982
+ bw = inblossom[w]
983
+ if bv == bw:
984
+ # this edge is internal to a blossom; ignore it
985
+ continue
986
+ if (v, w) not in allowedge:
987
+ kslack = slack(v, w)
988
+ if kslack <= 0:
989
+ # edge k has zero slack => it is allowable
990
+ allowedge[(v, w)] = allowedge[(w, v)] = True
991
+ if (v, w) in allowedge:
992
+ if label.get(bw) is None:
993
+ # (C1) w is a free vertex;
994
+ # label w with T and label its mate with S (R12).
995
+ assignLabel(w, 2, v)
996
+ elif label.get(bw) == 1:
997
+ # (C2) w is an S-vertex (not in the same blossom);
998
+ # follow back-links to discover either an
999
+ # augmenting path or a new blossom.
1000
+ base = scanBlossom(v, w)
1001
+ if base is not NoNode:
1002
+ # Found a new blossom; add it to the blossom
1003
+ # bookkeeping and turn it into an S-blossom.
1004
+ addBlossom(base, v, w)
1005
+ else:
1006
+ # Found an augmenting path; augment the
1007
+ # matching and end this stage.
1008
+ augmentMatching(v, w)
1009
+ augmented = 1
1010
+ break
1011
+ elif label.get(w) is None:
1012
+ # w is inside a T-blossom, but w itself has not
1013
+ # yet been reached from outside the blossom;
1014
+ # mark it as reached (we need this to relabel
1015
+ # during T-blossom expansion).
1016
+ assert label[bw] == 2
1017
+ label[w] = 2
1018
+ labeledge[w] = (v, w)
1019
+ elif label.get(bw) == 1:
1020
+ # keep track of the least-slack non-allowable edge to
1021
+ # a different S-blossom.
1022
+ if bestedge.get(bv) is None or kslack < slack(*bestedge[bv]):
1023
+ bestedge[bv] = (v, w)
1024
+ elif label.get(w) is None:
1025
+ # w is a free vertex (or an unreached vertex inside
1026
+ # a T-blossom) but we can not reach it yet;
1027
+ # keep track of the least-slack edge that reaches w.
1028
+ if bestedge.get(w) is None or kslack < slack(*bestedge[w]):
1029
+ bestedge[w] = (v, w)
1030
+
1031
+ if augmented:
1032
+ break
1033
+
1034
+ # There is no augmenting path under these constraints;
1035
+ # compute delta and reduce slack in the optimization problem.
1036
+ # (Note that our vertex dual variables, edge slacks and delta's
1037
+ # are pre-multiplied by two.)
1038
+ deltatype = -1
1039
+ delta = deltaedge = deltablossom = None
1040
+
1041
+ # Compute delta1: the minimum value of any vertex dual.
1042
+ if not maxcardinality:
1043
+ deltatype = 1
1044
+ delta = min(dualvar.values())
1045
+
1046
+ # Compute delta2: the minimum slack on any edge between
1047
+ # an S-vertex and a free vertex.
1048
+ for v in G.nodes():
1049
+ if label.get(inblossom[v]) is None and bestedge.get(v) is not None:
1050
+ d = slack(*bestedge[v])
1051
+ if deltatype == -1 or d < delta:
1052
+ delta = d
1053
+ deltatype = 2
1054
+ deltaedge = bestedge[v]
1055
+
1056
+ # Compute delta3: half the minimum slack on any edge between
1057
+ # a pair of S-blossoms.
1058
+ for b in blossomparent:
1059
+ if (
1060
+ blossomparent[b] is None
1061
+ and label.get(b) == 1
1062
+ and bestedge.get(b) is not None
1063
+ ):
1064
+ kslack = slack(*bestedge[b])
1065
+ if allinteger:
1066
+ assert (kslack % 2) == 0
1067
+ d = kslack // 2
1068
+ else:
1069
+ d = kslack / 2.0
1070
+ if deltatype == -1 or d < delta:
1071
+ delta = d
1072
+ deltatype = 3
1073
+ deltaedge = bestedge[b]
1074
+
1075
+ # Compute delta4: minimum z variable of any T-blossom.
1076
+ for b in blossomdual:
1077
+ if (
1078
+ blossomparent[b] is None
1079
+ and label.get(b) == 2
1080
+ and (deltatype == -1 or blossomdual[b] < delta)
1081
+ ):
1082
+ delta = blossomdual[b]
1083
+ deltatype = 4
1084
+ deltablossom = b
1085
+
1086
+ if deltatype == -1:
1087
+ # No further improvement possible; max-cardinality optimum
1088
+ # reached. Do a final delta update to make the optimum
1089
+ # verifiable.
1090
+ assert maxcardinality
1091
+ deltatype = 1
1092
+ delta = max(0, min(dualvar.values()))
1093
+
1094
+ # Update dual variables according to delta.
1095
+ for v in gnodes:
1096
+ if label.get(inblossom[v]) == 1:
1097
+ # S-vertex: 2*u = 2*u - 2*delta
1098
+ dualvar[v] -= delta
1099
+ elif label.get(inblossom[v]) == 2:
1100
+ # T-vertex: 2*u = 2*u + 2*delta
1101
+ dualvar[v] += delta
1102
+ for b in blossomdual:
1103
+ if blossomparent[b] is None:
1104
+ if label.get(b) == 1:
1105
+ # top-level S-blossom: z = z + 2*delta
1106
+ blossomdual[b] += delta
1107
+ elif label.get(b) == 2:
1108
+ # top-level T-blossom: z = z - 2*delta
1109
+ blossomdual[b] -= delta
1110
+
1111
+ # Take action at the point where minimum delta occurred.
1112
+ if deltatype == 1:
1113
+ # No further improvement possible; optimum reached.
1114
+ break
1115
+ elif deltatype == 2:
1116
+ # Use the least-slack edge to continue the search.
1117
+ (v, w) = deltaedge
1118
+ assert label[inblossom[v]] == 1
1119
+ allowedge[(v, w)] = allowedge[(w, v)] = True
1120
+ queue.append(v)
1121
+ elif deltatype == 3:
1122
+ # Use the least-slack edge to continue the search.
1123
+ (v, w) = deltaedge
1124
+ allowedge[(v, w)] = allowedge[(w, v)] = True
1125
+ assert label[inblossom[v]] == 1
1126
+ queue.append(v)
1127
+ elif deltatype == 4:
1128
+ # Expand the least-z blossom.
1129
+ expandBlossom(deltablossom, False)
1130
+
1131
+ # End of a this substage.
1132
+
1133
+ # Paranoia check that the matching is symmetric.
1134
+ for v in mate:
1135
+ assert mate[mate[v]] == v
1136
+
1137
+ # Stop when no more augmenting path can be found.
1138
+ if not augmented:
1139
+ break
1140
+
1141
+ # End of a stage; expand all S-blossoms which have zero dual.
1142
+ for b in list(blossomdual.keys()):
1143
+ if b not in blossomdual:
1144
+ continue # already expanded
1145
+ if blossomparent[b] is None and label.get(b) == 1 and blossomdual[b] == 0:
1146
+ expandBlossom(b, True)
1147
+
1148
+ # Verify that we reached the optimum solution (only for integer weights).
1149
+ if allinteger:
1150
+ verifyOptimum()
1151
+
1152
+ return matching_dict_to_set(mate)
.venv/lib/python3.11/site-packages/networkx/algorithms/moral.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r"""Function for computing the moral graph of a directed graph."""
2
+
3
+ import itertools
4
+
5
+ import networkx as nx
6
+ from networkx.utils import not_implemented_for
7
+
8
+ __all__ = ["moral_graph"]
9
+
10
+
11
+ @not_implemented_for("undirected")
12
+ @nx._dispatchable(returns_graph=True)
13
+ def moral_graph(G):
14
+ r"""Return the Moral Graph
15
+
16
+ Returns the moralized graph of a given directed graph.
17
+
18
+ Parameters
19
+ ----------
20
+ G : NetworkX graph
21
+ Directed graph
22
+
23
+ Returns
24
+ -------
25
+ H : NetworkX graph
26
+ The undirected moralized graph of G
27
+
28
+ Raises
29
+ ------
30
+ NetworkXNotImplemented
31
+ If `G` is undirected.
32
+
33
+ Examples
34
+ --------
35
+ >>> G = nx.DiGraph([(1, 2), (2, 3), (2, 5), (3, 4), (4, 3)])
36
+ >>> G_moral = nx.moral_graph(G)
37
+ >>> G_moral.edges()
38
+ EdgeView([(1, 2), (2, 3), (2, 5), (2, 4), (3, 4)])
39
+
40
+ Notes
41
+ -----
42
+ A moral graph is an undirected graph H = (V, E) generated from a
43
+ directed Graph, where if a node has more than one parent node, edges
44
+ between these parent nodes are inserted and all directed edges become
45
+ undirected.
46
+
47
+ https://en.wikipedia.org/wiki/Moral_graph
48
+
49
+ References
50
+ ----------
51
+ .. [1] Wray L. Buntine. 1995. Chain graphs for learning.
52
+ In Proceedings of the Eleventh conference on Uncertainty
53
+ in artificial intelligence (UAI'95)
54
+ """
55
+ H = G.to_undirected()
56
+ for preds in G.pred.values():
57
+ predecessors_combinations = itertools.combinations(preds, r=2)
58
+ H.add_edges_from(predecessors_combinations)
59
+ return H
.venv/lib/python3.11/site-packages/networkx/algorithms/non_randomness.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r"""Computation of graph non-randomness"""
2
+
3
+ import math
4
+
5
+ import networkx as nx
6
+ from networkx.utils import not_implemented_for
7
+
8
+ __all__ = ["non_randomness"]
9
+
10
+
11
+ @not_implemented_for("directed")
12
+ @not_implemented_for("multigraph")
13
+ @nx._dispatchable(edge_attrs="weight")
14
+ def non_randomness(G, k=None, weight="weight"):
15
+ """Compute the non-randomness of graph G.
16
+
17
+ The first returned value nr is the sum of non-randomness values of all
18
+ edges within the graph (where the non-randomness of an edge tends to be
19
+ small when the two nodes linked by that edge are from two different
20
+ communities).
21
+
22
+ The second computed value nr_rd is a relative measure that indicates
23
+ to what extent graph G is different from random graphs in terms
24
+ of probability. When it is close to 0, the graph tends to be more
25
+ likely generated by an Erdos Renyi model.
26
+
27
+ Parameters
28
+ ----------
29
+ G : NetworkX graph
30
+ Graph must be symmetric, connected, and without self-loops.
31
+
32
+ k : int
33
+ The number of communities in G.
34
+ If k is not set, the function will use a default community
35
+ detection algorithm to set it.
36
+
37
+ weight : string or None, optional (default=None)
38
+ The name of an edge attribute that holds the numerical value used
39
+ as a weight. If None, then each edge has weight 1, i.e., the graph is
40
+ binary.
41
+
42
+ Returns
43
+ -------
44
+ non-randomness : (float, float) tuple
45
+ Non-randomness, Relative non-randomness w.r.t.
46
+ Erdos Renyi random graphs.
47
+
48
+ Raises
49
+ ------
50
+ NetworkXException
51
+ if the input graph is not connected.
52
+ NetworkXError
53
+ if the input graph contains self-loops or if graph has no edges.
54
+
55
+ Examples
56
+ --------
57
+ >>> G = nx.karate_club_graph()
58
+ >>> nr, nr_rd = nx.non_randomness(G, 2)
59
+ >>> nr, nr_rd = nx.non_randomness(G, 2, "weight")
60
+
61
+ Notes
62
+ -----
63
+ This computes Eq. (4.4) and (4.5) in Ref. [1]_.
64
+
65
+ If a weight field is passed, this algorithm will use the eigenvalues
66
+ of the weighted adjacency matrix to compute Eq. (4.4) and (4.5).
67
+
68
+ References
69
+ ----------
70
+ .. [1] Xiaowei Ying and Xintao Wu,
71
+ On Randomness Measures for Social Networks,
72
+ SIAM International Conference on Data Mining. 2009
73
+ """
74
+ import numpy as np
75
+
76
+ # corner case: graph has no edges
77
+ if nx.is_empty(G):
78
+ raise nx.NetworkXError("non_randomness not applicable to empty graphs")
79
+ if not nx.is_connected(G):
80
+ raise nx.NetworkXException("Non connected graph.")
81
+ if len(list(nx.selfloop_edges(G))) > 0:
82
+ raise nx.NetworkXError("Graph must not contain self-loops")
83
+
84
+ if k is None:
85
+ k = len(tuple(nx.community.label_propagation_communities(G)))
86
+
87
+ # eq. 4.4
88
+ eigenvalues = np.linalg.eigvals(nx.to_numpy_array(G, weight=weight))
89
+ nr = float(np.real(np.sum(eigenvalues[:k])))
90
+
91
+ n = G.number_of_nodes()
92
+ m = G.number_of_edges()
93
+ p = (2 * k * m) / (n * (n - k))
94
+
95
+ # eq. 4.5
96
+ nr_rd = (nr - ((n - 2 * k) * p + k)) / math.sqrt(2 * k * p * (1 - p))
97
+
98
+ return nr, nr_rd
.venv/lib/python3.11/site-packages/networkx/algorithms/planar_drawing.py ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import defaultdict
2
+
3
+ import networkx as nx
4
+
5
+ __all__ = ["combinatorial_embedding_to_pos"]
6
+
7
+
8
+ def combinatorial_embedding_to_pos(embedding, fully_triangulate=False):
9
+ """Assigns every node a (x, y) position based on the given embedding
10
+
11
+ The algorithm iteratively inserts nodes of the input graph in a certain
12
+ order and rearranges previously inserted nodes so that the planar drawing
13
+ stays valid. This is done efficiently by only maintaining relative
14
+ positions during the node placements and calculating the absolute positions
15
+ at the end. For more information see [1]_.
16
+
17
+ Parameters
18
+ ----------
19
+ embedding : nx.PlanarEmbedding
20
+ This defines the order of the edges
21
+
22
+ fully_triangulate : bool
23
+ If set to True the algorithm adds edges to a copy of the input
24
+ embedding and makes it chordal.
25
+
26
+ Returns
27
+ -------
28
+ pos : dict
29
+ Maps each node to a tuple that defines the (x, y) position
30
+
31
+ References
32
+ ----------
33
+ .. [1] M. Chrobak and T.H. Payne:
34
+ A Linear-time Algorithm for Drawing a Planar Graph on a Grid 1989
35
+ http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.6677
36
+
37
+ """
38
+ if len(embedding.nodes()) < 4:
39
+ # Position the node in any triangle
40
+ default_positions = [(0, 0), (2, 0), (1, 1)]
41
+ pos = {}
42
+ for i, v in enumerate(embedding.nodes()):
43
+ pos[v] = default_positions[i]
44
+ return pos
45
+
46
+ embedding, outer_face = triangulate_embedding(embedding, fully_triangulate)
47
+
48
+ # The following dicts map a node to another node
49
+ # If a node is not in the key set it means that the node is not yet in G_k
50
+ # If a node maps to None then the corresponding subtree does not exist
51
+ left_t_child = {}
52
+ right_t_child = {}
53
+
54
+ # The following dicts map a node to an integer
55
+ delta_x = {}
56
+ y_coordinate = {}
57
+
58
+ node_list = get_canonical_ordering(embedding, outer_face)
59
+
60
+ # 1. Phase: Compute relative positions
61
+
62
+ # Initialization
63
+ v1, v2, v3 = node_list[0][0], node_list[1][0], node_list[2][0]
64
+
65
+ delta_x[v1] = 0
66
+ y_coordinate[v1] = 0
67
+ right_t_child[v1] = v3
68
+ left_t_child[v1] = None
69
+
70
+ delta_x[v2] = 1
71
+ y_coordinate[v2] = 0
72
+ right_t_child[v2] = None
73
+ left_t_child[v2] = None
74
+
75
+ delta_x[v3] = 1
76
+ y_coordinate[v3] = 1
77
+ right_t_child[v3] = v2
78
+ left_t_child[v3] = None
79
+
80
+ for k in range(3, len(node_list)):
81
+ vk, contour_nbrs = node_list[k]
82
+ wp = contour_nbrs[0]
83
+ wp1 = contour_nbrs[1]
84
+ wq = contour_nbrs[-1]
85
+ wq1 = contour_nbrs[-2]
86
+ adds_mult_tri = len(contour_nbrs) > 2
87
+
88
+ # Stretch gaps:
89
+ delta_x[wp1] += 1
90
+ delta_x[wq] += 1
91
+
92
+ delta_x_wp_wq = sum(delta_x[x] for x in contour_nbrs[1:])
93
+
94
+ # Adjust offsets
95
+ delta_x[vk] = (-y_coordinate[wp] + delta_x_wp_wq + y_coordinate[wq]) // 2
96
+ y_coordinate[vk] = (y_coordinate[wp] + delta_x_wp_wq + y_coordinate[wq]) // 2
97
+ delta_x[wq] = delta_x_wp_wq - delta_x[vk]
98
+ if adds_mult_tri:
99
+ delta_x[wp1] -= delta_x[vk]
100
+
101
+ # Install v_k:
102
+ right_t_child[wp] = vk
103
+ right_t_child[vk] = wq
104
+ if adds_mult_tri:
105
+ left_t_child[vk] = wp1
106
+ right_t_child[wq1] = None
107
+ else:
108
+ left_t_child[vk] = None
109
+
110
+ # 2. Phase: Set absolute positions
111
+ pos = {}
112
+ pos[v1] = (0, y_coordinate[v1])
113
+ remaining_nodes = [v1]
114
+ while remaining_nodes:
115
+ parent_node = remaining_nodes.pop()
116
+
117
+ # Calculate position for left child
118
+ set_position(
119
+ parent_node, left_t_child, remaining_nodes, delta_x, y_coordinate, pos
120
+ )
121
+ # Calculate position for right child
122
+ set_position(
123
+ parent_node, right_t_child, remaining_nodes, delta_x, y_coordinate, pos
124
+ )
125
+ return pos
126
+
127
+
128
+ def set_position(parent, tree, remaining_nodes, delta_x, y_coordinate, pos):
129
+ """Helper method to calculate the absolute position of nodes."""
130
+ child = tree[parent]
131
+ parent_node_x = pos[parent][0]
132
+ if child is not None:
133
+ # Calculate pos of child
134
+ child_x = parent_node_x + delta_x[child]
135
+ pos[child] = (child_x, y_coordinate[child])
136
+ # Remember to calculate pos of its children
137
+ remaining_nodes.append(child)
138
+
139
+
140
+ def get_canonical_ordering(embedding, outer_face):
141
+ """Returns a canonical ordering of the nodes
142
+
143
+ The canonical ordering of nodes (v1, ..., vn) must fulfill the following
144
+ conditions:
145
+ (See Lemma 1 in [2]_)
146
+
147
+ - For the subgraph G_k of the input graph induced by v1, ..., vk it holds:
148
+ - 2-connected
149
+ - internally triangulated
150
+ - the edge (v1, v2) is part of the outer face
151
+ - For a node v(k+1) the following holds:
152
+ - The node v(k+1) is part of the outer face of G_k
153
+ - It has at least two neighbors in G_k
154
+ - All neighbors of v(k+1) in G_k lie consecutively on the outer face of
155
+ G_k (excluding the edge (v1, v2)).
156
+
157
+ The algorithm used here starts with G_n (containing all nodes). It first
158
+ selects the nodes v1 and v2. And then tries to find the order of the other
159
+ nodes by checking which node can be removed in order to fulfill the
160
+ conditions mentioned above. This is done by calculating the number of
161
+ chords of nodes on the outer face. For more information see [1]_.
162
+
163
+ Parameters
164
+ ----------
165
+ embedding : nx.PlanarEmbedding
166
+ The embedding must be triangulated
167
+ outer_face : list
168
+ The nodes on the outer face of the graph
169
+
170
+ Returns
171
+ -------
172
+ ordering : list
173
+ A list of tuples `(vk, wp_wq)`. Here `vk` is the node at this position
174
+ in the canonical ordering. The element `wp_wq` is a list of nodes that
175
+ make up the outer face of G_k.
176
+
177
+ References
178
+ ----------
179
+ .. [1] Steven Chaplick.
180
+ Canonical Orders of Planar Graphs and (some of) Their Applications 2015
181
+ https://wuecampus2.uni-wuerzburg.de/moodle/pluginfile.php/545727/mod_resource/content/0/vg-ss15-vl03-canonical-orders-druckversion.pdf
182
+ .. [2] M. Chrobak and T.H. Payne:
183
+ A Linear-time Algorithm for Drawing a Planar Graph on a Grid 1989
184
+ http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.6677
185
+
186
+ """
187
+ v1 = outer_face[0]
188
+ v2 = outer_face[1]
189
+ chords = defaultdict(int) # Maps nodes to the number of their chords
190
+ marked_nodes = set()
191
+ ready_to_pick = set(outer_face)
192
+
193
+ # Initialize outer_face_ccw_nbr (do not include v1 -> v2)
194
+ outer_face_ccw_nbr = {}
195
+ prev_nbr = v2
196
+ for idx in range(2, len(outer_face)):
197
+ outer_face_ccw_nbr[prev_nbr] = outer_face[idx]
198
+ prev_nbr = outer_face[idx]
199
+ outer_face_ccw_nbr[prev_nbr] = v1
200
+
201
+ # Initialize outer_face_cw_nbr (do not include v2 -> v1)
202
+ outer_face_cw_nbr = {}
203
+ prev_nbr = v1
204
+ for idx in range(len(outer_face) - 1, 0, -1):
205
+ outer_face_cw_nbr[prev_nbr] = outer_face[idx]
206
+ prev_nbr = outer_face[idx]
207
+
208
+ def is_outer_face_nbr(x, y):
209
+ if x not in outer_face_ccw_nbr:
210
+ return outer_face_cw_nbr[x] == y
211
+ if x not in outer_face_cw_nbr:
212
+ return outer_face_ccw_nbr[x] == y
213
+ return outer_face_ccw_nbr[x] == y or outer_face_cw_nbr[x] == y
214
+
215
+ def is_on_outer_face(x):
216
+ return x not in marked_nodes and (x in outer_face_ccw_nbr or x == v1)
217
+
218
+ # Initialize number of chords
219
+ for v in outer_face:
220
+ for nbr in embedding.neighbors_cw_order(v):
221
+ if is_on_outer_face(nbr) and not is_outer_face_nbr(v, nbr):
222
+ chords[v] += 1
223
+ ready_to_pick.discard(v)
224
+
225
+ # Initialize canonical_ordering
226
+ canonical_ordering = [None] * len(embedding.nodes())
227
+ canonical_ordering[0] = (v1, [])
228
+ canonical_ordering[1] = (v2, [])
229
+ ready_to_pick.discard(v1)
230
+ ready_to_pick.discard(v2)
231
+
232
+ for k in range(len(embedding.nodes()) - 1, 1, -1):
233
+ # 1. Pick v from ready_to_pick
234
+ v = ready_to_pick.pop()
235
+ marked_nodes.add(v)
236
+
237
+ # v has exactly two neighbors on the outer face (wp and wq)
238
+ wp = None
239
+ wq = None
240
+ # Iterate over neighbors of v to find wp and wq
241
+ nbr_iterator = iter(embedding.neighbors_cw_order(v))
242
+ while True:
243
+ nbr = next(nbr_iterator)
244
+ if nbr in marked_nodes:
245
+ # Only consider nodes that are not yet removed
246
+ continue
247
+ if is_on_outer_face(nbr):
248
+ # nbr is either wp or wq
249
+ if nbr == v1:
250
+ wp = v1
251
+ elif nbr == v2:
252
+ wq = v2
253
+ else:
254
+ if outer_face_cw_nbr[nbr] == v:
255
+ # nbr is wp
256
+ wp = nbr
257
+ else:
258
+ # nbr is wq
259
+ wq = nbr
260
+ if wp is not None and wq is not None:
261
+ # We don't need to iterate any further
262
+ break
263
+
264
+ # Obtain new nodes on outer face (neighbors of v from wp to wq)
265
+ wp_wq = [wp]
266
+ nbr = wp
267
+ while nbr != wq:
268
+ # Get next neighbor (clockwise on the outer face)
269
+ next_nbr = embedding[v][nbr]["ccw"]
270
+ wp_wq.append(next_nbr)
271
+ # Update outer face
272
+ outer_face_cw_nbr[nbr] = next_nbr
273
+ outer_face_ccw_nbr[next_nbr] = nbr
274
+ # Move to next neighbor of v
275
+ nbr = next_nbr
276
+
277
+ if len(wp_wq) == 2:
278
+ # There was a chord between wp and wq, decrease number of chords
279
+ chords[wp] -= 1
280
+ if chords[wp] == 0:
281
+ ready_to_pick.add(wp)
282
+ chords[wq] -= 1
283
+ if chords[wq] == 0:
284
+ ready_to_pick.add(wq)
285
+ else:
286
+ # Update all chords involving w_(p+1) to w_(q-1)
287
+ new_face_nodes = set(wp_wq[1:-1])
288
+ for w in new_face_nodes:
289
+ # If we do not find a chord for w later we can pick it next
290
+ ready_to_pick.add(w)
291
+ for nbr in embedding.neighbors_cw_order(w):
292
+ if is_on_outer_face(nbr) and not is_outer_face_nbr(w, nbr):
293
+ # There is a chord involving w
294
+ chords[w] += 1
295
+ ready_to_pick.discard(w)
296
+ if nbr not in new_face_nodes:
297
+ # Also increase chord for the neighbor
298
+ # We only iterator over new_face_nodes
299
+ chords[nbr] += 1
300
+ ready_to_pick.discard(nbr)
301
+ # Set the canonical ordering node and the list of contour neighbors
302
+ canonical_ordering[k] = (v, wp_wq)
303
+
304
+ return canonical_ordering
305
+
306
+
307
+ def triangulate_face(embedding, v1, v2):
308
+ """Triangulates the face given by half edge (v, w)
309
+
310
+ Parameters
311
+ ----------
312
+ embedding : nx.PlanarEmbedding
313
+ v1 : node
314
+ The half-edge (v1, v2) belongs to the face that gets triangulated
315
+ v2 : node
316
+ """
317
+ _, v3 = embedding.next_face_half_edge(v1, v2)
318
+ _, v4 = embedding.next_face_half_edge(v2, v3)
319
+ if v1 in (v2, v3):
320
+ # The component has less than 3 nodes
321
+ return
322
+ while v1 != v4:
323
+ # Add edge if not already present on other side
324
+ if embedding.has_edge(v1, v3):
325
+ # Cannot triangulate at this position
326
+ v1, v2, v3 = v2, v3, v4
327
+ else:
328
+ # Add edge for triangulation
329
+ embedding.add_half_edge(v1, v3, ccw=v2)
330
+ embedding.add_half_edge(v3, v1, cw=v2)
331
+ v1, v2, v3 = v1, v3, v4
332
+ # Get next node
333
+ _, v4 = embedding.next_face_half_edge(v2, v3)
334
+
335
+
336
+ def triangulate_embedding(embedding, fully_triangulate=True):
337
+ """Triangulates the embedding.
338
+
339
+ Traverses faces of the embedding and adds edges to a copy of the
340
+ embedding to triangulate it.
341
+ The method also ensures that the resulting graph is 2-connected by adding
342
+ edges if the same vertex is contained twice on a path around a face.
343
+
344
+ Parameters
345
+ ----------
346
+ embedding : nx.PlanarEmbedding
347
+ The input graph must contain at least 3 nodes.
348
+
349
+ fully_triangulate : bool
350
+ If set to False the face with the most nodes is chooses as outer face.
351
+ This outer face does not get triangulated.
352
+
353
+ Returns
354
+ -------
355
+ (embedding, outer_face) : (nx.PlanarEmbedding, list) tuple
356
+ The element `embedding` is a new embedding containing all edges from
357
+ the input embedding and the additional edges to triangulate the graph.
358
+ The element `outer_face` is a list of nodes that lie on the outer face.
359
+ If the graph is fully triangulated these are three arbitrary connected
360
+ nodes.
361
+
362
+ """
363
+ if len(embedding.nodes) <= 1:
364
+ return embedding, list(embedding.nodes)
365
+ embedding = nx.PlanarEmbedding(embedding)
366
+
367
+ # Get a list with a node for each connected component
368
+ component_nodes = [next(iter(x)) for x in nx.connected_components(embedding)]
369
+
370
+ # 1. Make graph a single component (add edge between components)
371
+ for i in range(len(component_nodes) - 1):
372
+ v1 = component_nodes[i]
373
+ v2 = component_nodes[i + 1]
374
+ embedding.connect_components(v1, v2)
375
+
376
+ # 2. Calculate faces, ensure 2-connectedness and determine outer face
377
+ outer_face = [] # A face with the most number of nodes
378
+ face_list = []
379
+ edges_visited = set() # Used to keep track of already visited faces
380
+ for v in embedding.nodes():
381
+ for w in embedding.neighbors_cw_order(v):
382
+ new_face = make_bi_connected(embedding, v, w, edges_visited)
383
+ if new_face:
384
+ # Found a new face
385
+ face_list.append(new_face)
386
+ if len(new_face) > len(outer_face):
387
+ # The face is a candidate to be the outer face
388
+ outer_face = new_face
389
+
390
+ # 3. Triangulate (internal) faces
391
+ for face in face_list:
392
+ if face is not outer_face or fully_triangulate:
393
+ # Triangulate this face
394
+ triangulate_face(embedding, face[0], face[1])
395
+
396
+ if fully_triangulate:
397
+ v1 = outer_face[0]
398
+ v2 = outer_face[1]
399
+ v3 = embedding[v2][v1]["ccw"]
400
+ outer_face = [v1, v2, v3]
401
+
402
+ return embedding, outer_face
403
+
404
+
405
+ def make_bi_connected(embedding, starting_node, outgoing_node, edges_counted):
406
+ """Triangulate a face and make it 2-connected
407
+
408
+ This method also adds all edges on the face to `edges_counted`.
409
+
410
+ Parameters
411
+ ----------
412
+ embedding: nx.PlanarEmbedding
413
+ The embedding that defines the faces
414
+ starting_node : node
415
+ A node on the face
416
+ outgoing_node : node
417
+ A node such that the half edge (starting_node, outgoing_node) belongs
418
+ to the face
419
+ edges_counted: set
420
+ Set of all half-edges that belong to a face that have been visited
421
+
422
+ Returns
423
+ -------
424
+ face_nodes: list
425
+ A list of all nodes at the border of this face
426
+ """
427
+
428
+ # Check if the face has already been calculated
429
+ if (starting_node, outgoing_node) in edges_counted:
430
+ # This face was already counted
431
+ return []
432
+ edges_counted.add((starting_node, outgoing_node))
433
+
434
+ # Add all edges to edges_counted which have this face to their left
435
+ v1 = starting_node
436
+ v2 = outgoing_node
437
+ face_list = [starting_node] # List of nodes around the face
438
+ face_set = set(face_list) # Set for faster queries
439
+ _, v3 = embedding.next_face_half_edge(v1, v2)
440
+
441
+ # Move the nodes v1, v2, v3 around the face:
442
+ while v2 != starting_node or v3 != outgoing_node:
443
+ if v1 == v2:
444
+ raise nx.NetworkXException("Invalid half-edge")
445
+ # cycle is not completed yet
446
+ if v2 in face_set:
447
+ # v2 encountered twice: Add edge to ensure 2-connectedness
448
+ embedding.add_half_edge(v1, v3, ccw=v2)
449
+ embedding.add_half_edge(v3, v1, cw=v2)
450
+ edges_counted.add((v2, v3))
451
+ edges_counted.add((v3, v1))
452
+ v2 = v1
453
+ else:
454
+ face_set.add(v2)
455
+ face_list.append(v2)
456
+
457
+ # set next edge
458
+ v1 = v2
459
+ v2, v3 = embedding.next_face_half_edge(v2, v3)
460
+
461
+ # remember that this edge has been counted
462
+ edges_counted.add((v1, v2))
463
+
464
+ return face_list
.venv/lib/python3.11/site-packages/networkx/algorithms/polynomials.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Provides algorithms supporting the computation of graph polynomials.
2
+
3
+ Graph polynomials are polynomial-valued graph invariants that encode a wide
4
+ variety of structural information. Examples include the Tutte polynomial,
5
+ chromatic polynomial, characteristic polynomial, and matching polynomial. An
6
+ extensive treatment is provided in [1]_.
7
+
8
+ For a simple example, the `~sympy.matrices.matrices.MatrixDeterminant.charpoly`
9
+ method can be used to compute the characteristic polynomial from the adjacency
10
+ matrix of a graph. Consider the complete graph ``K_4``:
11
+
12
+ >>> import sympy
13
+ >>> x = sympy.Symbol("x")
14
+ >>> G = nx.complete_graph(4)
15
+ >>> A = nx.to_numpy_array(G, dtype=int)
16
+ >>> M = sympy.SparseMatrix(A)
17
+ >>> M.charpoly(x).as_expr()
18
+ x**4 - 6*x**2 - 8*x - 3
19
+
20
+
21
+ .. [1] Y. Shi, M. Dehmer, X. Li, I. Gutman,
22
+ "Graph Polynomials"
23
+ """
24
+
25
+ from collections import deque
26
+
27
+ import networkx as nx
28
+ from networkx.utils import not_implemented_for
29
+
30
+ __all__ = ["tutte_polynomial", "chromatic_polynomial"]
31
+
32
+
33
+ @not_implemented_for("directed")
34
+ @nx._dispatchable
35
+ def tutte_polynomial(G):
36
+ r"""Returns the Tutte polynomial of `G`
37
+
38
+ This function computes the Tutte polynomial via an iterative version of
39
+ the deletion-contraction algorithm.
40
+
41
+ The Tutte polynomial `T_G(x, y)` is a fundamental graph polynomial invariant in
42
+ two variables. It encodes a wide array of information related to the
43
+ edge-connectivity of a graph; "Many problems about graphs can be reduced to
44
+ problems of finding and evaluating the Tutte polynomial at certain values" [1]_.
45
+ In fact, every deletion-contraction-expressible feature of a graph is a
46
+ specialization of the Tutte polynomial [2]_ (see Notes for examples).
47
+
48
+ There are several equivalent definitions; here are three:
49
+
50
+ Def 1 (rank-nullity expansion): For `G` an undirected graph, `n(G)` the
51
+ number of vertices of `G`, `E` the edge set of `G`, `V` the vertex set of
52
+ `G`, and `c(A)` the number of connected components of the graph with vertex
53
+ set `V` and edge set `A` [3]_:
54
+
55
+ .. math::
56
+
57
+ T_G(x, y) = \sum_{A \in E} (x-1)^{c(A) - c(E)} (y-1)^{c(A) + |A| - n(G)}
58
+
59
+ Def 2 (spanning tree expansion): Let `G` be an undirected graph, `T` a spanning
60
+ tree of `G`, and `E` the edge set of `G`. Let `E` have an arbitrary strict
61
+ linear order `L`. Let `B_e` be the unique minimal nonempty edge cut of
62
+ $E \setminus T \cup {e}$. An edge `e` is internally active with respect to
63
+ `T` and `L` if `e` is the least edge in `B_e` according to the linear order
64
+ `L`. The internal activity of `T` (denoted `i(T)`) is the number of edges
65
+ in $E \setminus T$ that are internally active with respect to `T` and `L`.
66
+ Let `P_e` be the unique path in $T \cup {e}$ whose source and target vertex
67
+ are the same. An edge `e` is externally active with respect to `T` and `L`
68
+ if `e` is the least edge in `P_e` according to the linear order `L`. The
69
+ external activity of `T` (denoted `e(T)`) is the number of edges in
70
+ $E \setminus T$ that are externally active with respect to `T` and `L`.
71
+ Then [4]_ [5]_:
72
+
73
+ .. math::
74
+
75
+ T_G(x, y) = \sum_{T \text{ a spanning tree of } G} x^{i(T)} y^{e(T)}
76
+
77
+ Def 3 (deletion-contraction recurrence): For `G` an undirected graph, `G-e`
78
+ the graph obtained from `G` by deleting edge `e`, `G/e` the graph obtained
79
+ from `G` by contracting edge `e`, `k(G)` the number of cut-edges of `G`,
80
+ and `l(G)` the number of self-loops of `G`:
81
+
82
+ .. math::
83
+ T_G(x, y) = \begin{cases}
84
+ x^{k(G)} y^{l(G)}, & \text{if all edges are cut-edges or self-loops} \\
85
+ T_{G-e}(x, y) + T_{G/e}(x, y), & \text{otherwise, for an arbitrary edge $e$ not a cut-edge or loop}
86
+ \end{cases}
87
+
88
+ Parameters
89
+ ----------
90
+ G : NetworkX graph
91
+
92
+ Returns
93
+ -------
94
+ instance of `sympy.core.add.Add`
95
+ A Sympy expression representing the Tutte polynomial for `G`.
96
+
97
+ Examples
98
+ --------
99
+ >>> C = nx.cycle_graph(5)
100
+ >>> nx.tutte_polynomial(C)
101
+ x**4 + x**3 + x**2 + x + y
102
+
103
+ >>> D = nx.diamond_graph()
104
+ >>> nx.tutte_polynomial(D)
105
+ x**3 + 2*x**2 + 2*x*y + x + y**2 + y
106
+
107
+ Notes
108
+ -----
109
+ Some specializations of the Tutte polynomial:
110
+
111
+ - `T_G(1, 1)` counts the number of spanning trees of `G`
112
+ - `T_G(1, 2)` counts the number of connected spanning subgraphs of `G`
113
+ - `T_G(2, 1)` counts the number of spanning forests in `G`
114
+ - `T_G(0, 2)` counts the number of strong orientations of `G`
115
+ - `T_G(2, 0)` counts the number of acyclic orientations of `G`
116
+
117
+ Edge contraction is defined and deletion-contraction is introduced in [6]_.
118
+ Combinatorial meaning of the coefficients is introduced in [7]_.
119
+ Universality, properties, and applications are discussed in [8]_.
120
+
121
+ Practically, up-front computation of the Tutte polynomial may be useful when
122
+ users wish to repeatedly calculate edge-connectivity-related information
123
+ about one or more graphs.
124
+
125
+ References
126
+ ----------
127
+ .. [1] M. Brandt,
128
+ "The Tutte Polynomial."
129
+ Talking About Combinatorial Objects Seminar, 2015
130
+ https://math.berkeley.edu/~brandtm/talks/tutte.pdf
131
+ .. [2] A. Björklund, T. Husfeldt, P. Kaski, M. Koivisto,
132
+ "Computing the Tutte polynomial in vertex-exponential time"
133
+ 49th Annual IEEE Symposium on Foundations of Computer Science, 2008
134
+ https://ieeexplore.ieee.org/abstract/document/4691000
135
+ .. [3] Y. Shi, M. Dehmer, X. Li, I. Gutman,
136
+ "Graph Polynomials," p. 14
137
+ .. [4] Y. Shi, M. Dehmer, X. Li, I. Gutman,
138
+ "Graph Polynomials," p. 46
139
+ .. [5] A. Nešetril, J. Goodall,
140
+ "Graph invariants, homomorphisms, and the Tutte polynomial"
141
+ https://iuuk.mff.cuni.cz/~andrew/Tutte.pdf
142
+ .. [6] D. B. West,
143
+ "Introduction to Graph Theory," p. 84
144
+ .. [7] G. Coutinho,
145
+ "A brief introduction to the Tutte polynomial"
146
+ Structural Analysis of Complex Networks, 2011
147
+ https://homepages.dcc.ufmg.br/~gabriel/seminars/coutinho_tuttepolynomial_seminar.pdf
148
+ .. [8] J. A. Ellis-Monaghan, C. Merino,
149
+ "Graph polynomials and their applications I: The Tutte polynomial"
150
+ Structural Analysis of Complex Networks, 2011
151
+ https://arxiv.org/pdf/0803.3079.pdf
152
+ """
153
+ import sympy
154
+
155
+ x = sympy.Symbol("x")
156
+ y = sympy.Symbol("y")
157
+ stack = deque()
158
+ stack.append(nx.MultiGraph(G))
159
+
160
+ polynomial = 0
161
+ while stack:
162
+ G = stack.pop()
163
+ bridges = set(nx.bridges(G))
164
+
165
+ e = None
166
+ for i in G.edges:
167
+ if (i[0], i[1]) not in bridges and i[0] != i[1]:
168
+ e = i
169
+ break
170
+ if not e:
171
+ loops = list(nx.selfloop_edges(G, keys=True))
172
+ polynomial += x ** len(bridges) * y ** len(loops)
173
+ else:
174
+ # deletion-contraction
175
+ C = nx.contracted_edge(G, e, self_loops=True)
176
+ C.remove_edge(e[0], e[0])
177
+ G.remove_edge(*e)
178
+ stack.append(G)
179
+ stack.append(C)
180
+ return sympy.simplify(polynomial)
181
+
182
+
183
+ @not_implemented_for("directed")
184
+ @nx._dispatchable
185
+ def chromatic_polynomial(G):
186
+ r"""Returns the chromatic polynomial of `G`
187
+
188
+ This function computes the chromatic polynomial via an iterative version of
189
+ the deletion-contraction algorithm.
190
+
191
+ The chromatic polynomial `X_G(x)` is a fundamental graph polynomial
192
+ invariant in one variable. Evaluating `X_G(k)` for an natural number `k`
193
+ enumerates the proper k-colorings of `G`.
194
+
195
+ There are several equivalent definitions; here are three:
196
+
197
+ Def 1 (explicit formula):
198
+ For `G` an undirected graph, `c(G)` the number of connected components of
199
+ `G`, `E` the edge set of `G`, and `G(S)` the spanning subgraph of `G` with
200
+ edge set `S` [1]_:
201
+
202
+ .. math::
203
+
204
+ X_G(x) = \sum_{S \subseteq E} (-1)^{|S|} x^{c(G(S))}
205
+
206
+
207
+ Def 2 (interpolating polynomial):
208
+ For `G` an undirected graph, `n(G)` the number of vertices of `G`, `k_0 = 0`,
209
+ and `k_i` the number of distinct ways to color the vertices of `G` with `i`
210
+ unique colors (for `i` a natural number at most `n(G)`), `X_G(x)` is the
211
+ unique Lagrange interpolating polynomial of degree `n(G)` through the points
212
+ `(0, k_0), (1, k_1), \dots, (n(G), k_{n(G)})` [2]_.
213
+
214
+
215
+ Def 3 (chromatic recurrence):
216
+ For `G` an undirected graph, `G-e` the graph obtained from `G` by deleting
217
+ edge `e`, `G/e` the graph obtained from `G` by contracting edge `e`, `n(G)`
218
+ the number of vertices of `G`, and `e(G)` the number of edges of `G` [3]_:
219
+
220
+ .. math::
221
+ X_G(x) = \begin{cases}
222
+ x^{n(G)}, & \text{if $e(G)=0$} \\
223
+ X_{G-e}(x) - X_{G/e}(x), & \text{otherwise, for an arbitrary edge $e$}
224
+ \end{cases}
225
+
226
+ This formulation is also known as the Fundamental Reduction Theorem [4]_.
227
+
228
+
229
+ Parameters
230
+ ----------
231
+ G : NetworkX graph
232
+
233
+ Returns
234
+ -------
235
+ instance of `sympy.core.add.Add`
236
+ A Sympy expression representing the chromatic polynomial for `G`.
237
+
238
+ Examples
239
+ --------
240
+ >>> C = nx.cycle_graph(5)
241
+ >>> nx.chromatic_polynomial(C)
242
+ x**5 - 5*x**4 + 10*x**3 - 10*x**2 + 4*x
243
+
244
+ >>> G = nx.complete_graph(4)
245
+ >>> nx.chromatic_polynomial(G)
246
+ x**4 - 6*x**3 + 11*x**2 - 6*x
247
+
248
+ Notes
249
+ -----
250
+ Interpretation of the coefficients is discussed in [5]_. Several special
251
+ cases are listed in [2]_.
252
+
253
+ The chromatic polynomial is a specialization of the Tutte polynomial; in
254
+ particular, ``X_G(x) = T_G(x, 0)`` [6]_.
255
+
256
+ The chromatic polynomial may take negative arguments, though evaluations
257
+ may not have chromatic interpretations. For instance, ``X_G(-1)`` enumerates
258
+ the acyclic orientations of `G` [7]_.
259
+
260
+ References
261
+ ----------
262
+ .. [1] D. B. West,
263
+ "Introduction to Graph Theory," p. 222
264
+ .. [2] E. W. Weisstein
265
+ "Chromatic Polynomial"
266
+ MathWorld--A Wolfram Web Resource
267
+ https://mathworld.wolfram.com/ChromaticPolynomial.html
268
+ .. [3] D. B. West,
269
+ "Introduction to Graph Theory," p. 221
270
+ .. [4] J. Zhang, J. Goodall,
271
+ "An Introduction to Chromatic Polynomials"
272
+ https://math.mit.edu/~apost/courses/18.204_2018/Julie_Zhang_paper.pdf
273
+ .. [5] R. C. Read,
274
+ "An Introduction to Chromatic Polynomials"
275
+ Journal of Combinatorial Theory, 1968
276
+ https://math.berkeley.edu/~mrklug/ReadChromatic.pdf
277
+ .. [6] W. T. Tutte,
278
+ "Graph-polynomials"
279
+ Advances in Applied Mathematics, 2004
280
+ https://www.sciencedirect.com/science/article/pii/S0196885803000411
281
+ .. [7] R. P. Stanley,
282
+ "Acyclic orientations of graphs"
283
+ Discrete Mathematics, 2006
284
+ https://math.mit.edu/~rstan/pubs/pubfiles/18.pdf
285
+ """
286
+ import sympy
287
+
288
+ x = sympy.Symbol("x")
289
+ stack = deque()
290
+ stack.append(nx.MultiGraph(G, contraction_idx=0))
291
+
292
+ polynomial = 0
293
+ while stack:
294
+ G = stack.pop()
295
+ edges = list(G.edges)
296
+ if not edges:
297
+ polynomial += (-1) ** G.graph["contraction_idx"] * x ** len(G)
298
+ else:
299
+ e = edges[0]
300
+ C = nx.contracted_edge(G, e, self_loops=True)
301
+ C.graph["contraction_idx"] = G.graph["contraction_idx"] + 1
302
+ C.remove_edge(e[0], e[0])
303
+ G.remove_edge(*e)
304
+ stack.append(G)
305
+ stack.append(C)
306
+ return polynomial
.venv/lib/python3.11/site-packages/networkx/algorithms/reciprocity.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Algorithms to calculate reciprocity in a directed graph."""
2
+
3
+ import networkx as nx
4
+ from networkx import NetworkXError
5
+
6
+ from ..utils import not_implemented_for
7
+
8
+ __all__ = ["reciprocity", "overall_reciprocity"]
9
+
10
+
11
+ @not_implemented_for("undirected", "multigraph")
12
+ @nx._dispatchable
13
+ def reciprocity(G, nodes=None):
14
+ r"""Compute the reciprocity in a directed graph.
15
+
16
+ The reciprocity of a directed graph is defined as the ratio
17
+ of the number of edges pointing in both directions to the total
18
+ number of edges in the graph.
19
+ Formally, $r = |{(u,v) \in G|(v,u) \in G}| / |{(u,v) \in G}|$.
20
+
21
+ The reciprocity of a single node u is defined similarly,
22
+ it is the ratio of the number of edges in both directions to
23
+ the total number of edges attached to node u.
24
+
25
+ Parameters
26
+ ----------
27
+ G : graph
28
+ A networkx directed graph
29
+ nodes : container of nodes, optional (default=whole graph)
30
+ Compute reciprocity for nodes in this container.
31
+
32
+ Returns
33
+ -------
34
+ out : dictionary
35
+ Reciprocity keyed by node label.
36
+
37
+ Notes
38
+ -----
39
+ The reciprocity is not defined for isolated nodes.
40
+ In such cases this function will return None.
41
+
42
+ """
43
+ # If `nodes` is not specified, calculate the reciprocity of the graph.
44
+ if nodes is None:
45
+ return overall_reciprocity(G)
46
+
47
+ # If `nodes` represents a single node in the graph, return only its
48
+ # reciprocity.
49
+ if nodes in G:
50
+ reciprocity = next(_reciprocity_iter(G, nodes))[1]
51
+ if reciprocity is None:
52
+ raise NetworkXError("Not defined for isolated nodes.")
53
+ else:
54
+ return reciprocity
55
+
56
+ # Otherwise, `nodes` represents an iterable of nodes, so return a
57
+ # dictionary mapping node to its reciprocity.
58
+ return dict(_reciprocity_iter(G, nodes))
59
+
60
+
61
+ def _reciprocity_iter(G, nodes):
62
+ """Return an iterator of (node, reciprocity)."""
63
+ n = G.nbunch_iter(nodes)
64
+ for node in n:
65
+ pred = set(G.predecessors(node))
66
+ succ = set(G.successors(node))
67
+ overlap = pred & succ
68
+ n_total = len(pred) + len(succ)
69
+
70
+ # Reciprocity is not defined for isolated nodes.
71
+ # Return None.
72
+ if n_total == 0:
73
+ yield (node, None)
74
+ else:
75
+ reciprocity = 2 * len(overlap) / n_total
76
+ yield (node, reciprocity)
77
+
78
+
79
+ @not_implemented_for("undirected", "multigraph")
80
+ @nx._dispatchable
81
+ def overall_reciprocity(G):
82
+ """Compute the reciprocity for the whole graph.
83
+
84
+ See the doc of reciprocity for the definition.
85
+
86
+ Parameters
87
+ ----------
88
+ G : graph
89
+ A networkx graph
90
+
91
+ """
92
+ n_all_edge = G.number_of_edges()
93
+ n_overlap_edge = (n_all_edge - G.to_undirected().number_of_edges()) * 2
94
+
95
+ if n_all_edge == 0:
96
+ raise NetworkXError("Not defined for empty graphs")
97
+
98
+ return n_overlap_edge / n_all_edge
.venv/lib/python3.11/site-packages/networkx/algorithms/regular.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions for computing and verifying regular graphs."""
2
+
3
+ import networkx as nx
4
+ from networkx.utils import not_implemented_for
5
+
6
+ __all__ = ["is_regular", "is_k_regular", "k_factor"]
7
+
8
+
9
+ @nx._dispatchable
10
+ def is_regular(G):
11
+ """Determines whether the graph ``G`` is a regular graph.
12
+
13
+ A regular graph is a graph where each vertex has the same degree. A
14
+ regular digraph is a graph where the indegree and outdegree of each
15
+ vertex are equal.
16
+
17
+ Parameters
18
+ ----------
19
+ G : NetworkX graph
20
+
21
+ Returns
22
+ -------
23
+ bool
24
+ Whether the given graph or digraph is regular.
25
+
26
+ Examples
27
+ --------
28
+ >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 1)])
29
+ >>> nx.is_regular(G)
30
+ True
31
+
32
+ """
33
+ if len(G) == 0:
34
+ raise nx.NetworkXPointlessConcept("Graph has no nodes.")
35
+ n1 = nx.utils.arbitrary_element(G)
36
+ if not G.is_directed():
37
+ d1 = G.degree(n1)
38
+ return all(d1 == d for _, d in G.degree)
39
+ else:
40
+ d_in = G.in_degree(n1)
41
+ in_regular = all(d_in == d for _, d in G.in_degree)
42
+ d_out = G.out_degree(n1)
43
+ out_regular = all(d_out == d for _, d in G.out_degree)
44
+ return in_regular and out_regular
45
+
46
+
47
+ @not_implemented_for("directed")
48
+ @nx._dispatchable
49
+ def is_k_regular(G, k):
50
+ """Determines whether the graph ``G`` is a k-regular graph.
51
+
52
+ A k-regular graph is a graph where each vertex has degree k.
53
+
54
+ Parameters
55
+ ----------
56
+ G : NetworkX graph
57
+
58
+ Returns
59
+ -------
60
+ bool
61
+ Whether the given graph is k-regular.
62
+
63
+ Examples
64
+ --------
65
+ >>> G = nx.Graph([(1, 2), (2, 3), (3, 4), (4, 1)])
66
+ >>> nx.is_k_regular(G, k=3)
67
+ False
68
+
69
+ """
70
+ return all(d == k for n, d in G.degree)
71
+
72
+
73
+ @not_implemented_for("directed")
74
+ @not_implemented_for("multigraph")
75
+ @nx._dispatchable(preserve_edge_attrs=True, returns_graph=True)
76
+ def k_factor(G, k, matching_weight="weight"):
77
+ """Compute a k-factor of G
78
+
79
+ A k-factor of a graph is a spanning k-regular subgraph.
80
+ A spanning k-regular subgraph of G is a subgraph that contains
81
+ each vertex of G and a subset of the edges of G such that each
82
+ vertex has degree k.
83
+
84
+ Parameters
85
+ ----------
86
+ G : NetworkX graph
87
+ Undirected graph
88
+
89
+ matching_weight: string, optional (default='weight')
90
+ Edge data key corresponding to the edge weight.
91
+ Used for finding the max-weighted perfect matching.
92
+ If key not found, uses 1 as weight.
93
+
94
+ Returns
95
+ -------
96
+ G2 : NetworkX graph
97
+ A k-factor of G
98
+
99
+ Examples
100
+ --------
101
+ >>> G = nx.Graph([(1, 2), (2, 3), (3, 4), (4, 1)])
102
+ >>> G2 = nx.k_factor(G, k=1)
103
+ >>> G2.edges()
104
+ EdgeView([(1, 2), (3, 4)])
105
+
106
+ References
107
+ ----------
108
+ .. [1] "An algorithm for computing simple k-factors.",
109
+ Meijer, Henk, Yurai Núñez-Rodríguez, and David Rappaport,
110
+ Information processing letters, 2009.
111
+ """
112
+
113
+ from networkx.algorithms.matching import is_perfect_matching, max_weight_matching
114
+
115
+ class LargeKGadget:
116
+ def __init__(self, k, degree, node, g):
117
+ self.original = node
118
+ self.g = g
119
+ self.k = k
120
+ self.degree = degree
121
+
122
+ self.outer_vertices = [(node, x) for x in range(degree)]
123
+ self.core_vertices = [(node, x + degree) for x in range(degree - k)]
124
+
125
+ def replace_node(self):
126
+ adj_view = self.g[self.original]
127
+ neighbors = list(adj_view.keys())
128
+ edge_attrs = list(adj_view.values())
129
+ for outer, neighbor, edge_attrs in zip(
130
+ self.outer_vertices, neighbors, edge_attrs
131
+ ):
132
+ self.g.add_edge(outer, neighbor, **edge_attrs)
133
+ for core in self.core_vertices:
134
+ for outer in self.outer_vertices:
135
+ self.g.add_edge(core, outer)
136
+ self.g.remove_node(self.original)
137
+
138
+ def restore_node(self):
139
+ self.g.add_node(self.original)
140
+ for outer in self.outer_vertices:
141
+ adj_view = self.g[outer]
142
+ for neighbor, edge_attrs in list(adj_view.items()):
143
+ if neighbor not in self.core_vertices:
144
+ self.g.add_edge(self.original, neighbor, **edge_attrs)
145
+ break
146
+ g.remove_nodes_from(self.outer_vertices)
147
+ g.remove_nodes_from(self.core_vertices)
148
+
149
+ class SmallKGadget:
150
+ def __init__(self, k, degree, node, g):
151
+ self.original = node
152
+ self.k = k
153
+ self.degree = degree
154
+ self.g = g
155
+
156
+ self.outer_vertices = [(node, x) for x in range(degree)]
157
+ self.inner_vertices = [(node, x + degree) for x in range(degree)]
158
+ self.core_vertices = [(node, x + 2 * degree) for x in range(k)]
159
+
160
+ def replace_node(self):
161
+ adj_view = self.g[self.original]
162
+ for outer, inner, (neighbor, edge_attrs) in zip(
163
+ self.outer_vertices, self.inner_vertices, list(adj_view.items())
164
+ ):
165
+ self.g.add_edge(outer, inner)
166
+ self.g.add_edge(outer, neighbor, **edge_attrs)
167
+ for core in self.core_vertices:
168
+ for inner in self.inner_vertices:
169
+ self.g.add_edge(core, inner)
170
+ self.g.remove_node(self.original)
171
+
172
+ def restore_node(self):
173
+ self.g.add_node(self.original)
174
+ for outer in self.outer_vertices:
175
+ adj_view = self.g[outer]
176
+ for neighbor, edge_attrs in adj_view.items():
177
+ if neighbor not in self.core_vertices:
178
+ self.g.add_edge(self.original, neighbor, **edge_attrs)
179
+ break
180
+ self.g.remove_nodes_from(self.outer_vertices)
181
+ self.g.remove_nodes_from(self.inner_vertices)
182
+ self.g.remove_nodes_from(self.core_vertices)
183
+
184
+ # Step 1
185
+ if any(d < k for _, d in G.degree):
186
+ raise nx.NetworkXUnfeasible("Graph contains a vertex with degree less than k")
187
+ g = G.copy()
188
+
189
+ # Step 2
190
+ gadgets = []
191
+ for node, degree in list(g.degree):
192
+ if k < degree / 2.0:
193
+ gadget = SmallKGadget(k, degree, node, g)
194
+ else:
195
+ gadget = LargeKGadget(k, degree, node, g)
196
+ gadget.replace_node()
197
+ gadgets.append(gadget)
198
+
199
+ # Step 3
200
+ matching = max_weight_matching(g, maxcardinality=True, weight=matching_weight)
201
+
202
+ # Step 4
203
+ if not is_perfect_matching(g, matching):
204
+ raise nx.NetworkXUnfeasible(
205
+ "Cannot find k-factor because no perfect matching exists"
206
+ )
207
+
208
+ for edge in g.edges():
209
+ if edge not in matching and (edge[1], edge[0]) not in matching:
210
+ g.remove_edge(edge[0], edge[1])
211
+
212
+ for gadget in gadgets:
213
+ gadget.restore_node()
214
+
215
+ return g
.venv/lib/python3.11/site-packages/networkx/algorithms/richclub.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions for computing rich-club coefficients."""
2
+
3
+ from itertools import accumulate
4
+
5
+ import networkx as nx
6
+ from networkx.utils import not_implemented_for
7
+
8
+ __all__ = ["rich_club_coefficient"]
9
+
10
+
11
+ @not_implemented_for("directed")
12
+ @not_implemented_for("multigraph")
13
+ @nx._dispatchable
14
+ def rich_club_coefficient(G, normalized=True, Q=100, seed=None):
15
+ r"""Returns the rich-club coefficient of the graph `G`.
16
+
17
+ For each degree *k*, the *rich-club coefficient* is the ratio of the
18
+ number of actual to the number of potential edges for nodes with
19
+ degree greater than *k*:
20
+
21
+ .. math::
22
+
23
+ \phi(k) = \frac{2 E_k}{N_k (N_k - 1)}
24
+
25
+ where `N_k` is the number of nodes with degree larger than *k*, and
26
+ `E_k` is the number of edges among those nodes.
27
+
28
+ Parameters
29
+ ----------
30
+ G : NetworkX graph
31
+ Undirected graph with neither parallel edges nor self-loops.
32
+ normalized : bool (optional)
33
+ Normalize using randomized network as in [1]_
34
+ Q : float (optional, default=100)
35
+ If `normalized` is True, perform `Q * m` double-edge
36
+ swaps, where `m` is the number of edges in `G`, to use as a
37
+ null-model for normalization.
38
+ seed : integer, random_state, or None (default)
39
+ Indicator of random number generation state.
40
+ See :ref:`Randomness<randomness>`.
41
+
42
+ Returns
43
+ -------
44
+ rc : dictionary
45
+ A dictionary, keyed by degree, with rich-club coefficient values.
46
+
47
+ Raises
48
+ ------
49
+ NetworkXError
50
+ If `G` has fewer than four nodes and ``normalized=True``.
51
+ A randomly sampled graph for normalization cannot be generated in this case.
52
+
53
+ Examples
54
+ --------
55
+ >>> G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)])
56
+ >>> rc = nx.rich_club_coefficient(G, normalized=False, seed=42)
57
+ >>> rc[0]
58
+ 0.4
59
+
60
+ Notes
61
+ -----
62
+ The rich club definition and algorithm are found in [1]_. This
63
+ algorithm ignores any edge weights and is not defined for directed
64
+ graphs or graphs with parallel edges or self loops.
65
+
66
+ Normalization is done by computing the rich club coefficient for a randomly
67
+ sampled graph with the same degree distribution as `G` by
68
+ repeatedly swapping the endpoints of existing edges. For graphs with fewer than 4
69
+ nodes, it is not possible to generate a random graph with a prescribed
70
+ degree distribution, as the degree distribution fully determines the graph
71
+ (hence making the coefficients trivially normalized to 1).
72
+ This function raises an exception in this case.
73
+
74
+ Estimates for appropriate values of `Q` are found in [2]_.
75
+
76
+ References
77
+ ----------
78
+ .. [1] Julian J. McAuley, Luciano da Fontoura Costa,
79
+ and Tibério S. Caetano,
80
+ "The rich-club phenomenon across complex network hierarchies",
81
+ Applied Physics Letters Vol 91 Issue 8, August 2007.
82
+ https://arxiv.org/abs/physics/0701290
83
+ .. [2] R. Milo, N. Kashtan, S. Itzkovitz, M. E. J. Newman, U. Alon,
84
+ "Uniform generation of random graphs with arbitrary degree
85
+ sequences", 2006. https://arxiv.org/abs/cond-mat/0312028
86
+ """
87
+ if nx.number_of_selfloops(G) > 0:
88
+ raise Exception(
89
+ "rich_club_coefficient is not implemented for graphs with self loops."
90
+ )
91
+ rc = _compute_rc(G)
92
+ if normalized:
93
+ # make R a copy of G, randomize with Q*|E| double edge swaps
94
+ # and use rich_club coefficient of R to normalize
95
+ R = G.copy()
96
+ E = R.number_of_edges()
97
+ nx.double_edge_swap(R, Q * E, max_tries=Q * E * 10, seed=seed)
98
+ rcran = _compute_rc(R)
99
+ rc = {k: v / rcran[k] for k, v in rc.items()}
100
+ return rc
101
+
102
+
103
+ def _compute_rc(G):
104
+ """Returns the rich-club coefficient for each degree in the graph
105
+ `G`.
106
+
107
+ `G` is an undirected graph without multiedges.
108
+
109
+ Returns a dictionary mapping degree to rich-club coefficient for
110
+ that degree.
111
+
112
+ """
113
+ deghist = nx.degree_histogram(G)
114
+ total = sum(deghist)
115
+ # Compute the number of nodes with degree greater than `k`, for each
116
+ # degree `k` (omitting the last entry, which is zero).
117
+ nks = (total - cs for cs in accumulate(deghist) if total - cs > 1)
118
+ # Create a sorted list of pairs of edge endpoint degrees.
119
+ #
120
+ # The list is sorted in reverse order so that we can pop from the
121
+ # right side of the list later, instead of popping from the left
122
+ # side of the list, which would have a linear time cost.
123
+ edge_degrees = sorted((sorted(map(G.degree, e)) for e in G.edges()), reverse=True)
124
+ ek = G.number_of_edges()
125
+ if ek == 0:
126
+ return {}
127
+
128
+ k1, k2 = edge_degrees.pop()
129
+ rc = {}
130
+ for d, nk in enumerate(nks):
131
+ while k1 <= d:
132
+ if len(edge_degrees) == 0:
133
+ ek = 0
134
+ break
135
+ k1, k2 = edge_degrees.pop()
136
+ ek -= 1
137
+ rc[d] = 2 * ek / (nk * (nk - 1))
138
+ return rc
.venv/lib/python3.11/site-packages/networkx/algorithms/similarity.py ADDED
@@ -0,0 +1,1780 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions measuring similarity using graph edit distance.
2
+
3
+ The graph edit distance is the number of edge/node changes needed
4
+ to make two graphs isomorphic.
5
+
6
+ The default algorithm/implementation is sub-optimal for some graphs.
7
+ The problem of finding the exact Graph Edit Distance (GED) is NP-hard
8
+ so it is often slow. If the simple interface `graph_edit_distance`
9
+ takes too long for your graph, try `optimize_graph_edit_distance`
10
+ and/or `optimize_edit_paths`.
11
+
12
+ At the same time, I encourage capable people to investigate
13
+ alternative GED algorithms, in order to improve the choices available.
14
+ """
15
+
16
+ import math
17
+ import time
18
+ import warnings
19
+ from dataclasses import dataclass
20
+ from itertools import product
21
+
22
+ import networkx as nx
23
+ from networkx.utils import np_random_state
24
+
25
+ __all__ = [
26
+ "graph_edit_distance",
27
+ "optimal_edit_paths",
28
+ "optimize_graph_edit_distance",
29
+ "optimize_edit_paths",
30
+ "simrank_similarity",
31
+ "panther_similarity",
32
+ "generate_random_paths",
33
+ ]
34
+
35
+
36
+ def debug_print(*args, **kwargs):
37
+ print(*args, **kwargs)
38
+
39
+
40
+ @nx._dispatchable(
41
+ graphs={"G1": 0, "G2": 1}, preserve_edge_attrs=True, preserve_node_attrs=True
42
+ )
43
+ def graph_edit_distance(
44
+ G1,
45
+ G2,
46
+ node_match=None,
47
+ edge_match=None,
48
+ node_subst_cost=None,
49
+ node_del_cost=None,
50
+ node_ins_cost=None,
51
+ edge_subst_cost=None,
52
+ edge_del_cost=None,
53
+ edge_ins_cost=None,
54
+ roots=None,
55
+ upper_bound=None,
56
+ timeout=None,
57
+ ):
58
+ """Returns GED (graph edit distance) between graphs G1 and G2.
59
+
60
+ Graph edit distance is a graph similarity measure analogous to
61
+ Levenshtein distance for strings. It is defined as minimum cost
62
+ of edit path (sequence of node and edge edit operations)
63
+ transforming graph G1 to graph isomorphic to G2.
64
+
65
+ Parameters
66
+ ----------
67
+ G1, G2: graphs
68
+ The two graphs G1 and G2 must be of the same type.
69
+
70
+ node_match : callable
71
+ A function that returns True if node n1 in G1 and n2 in G2
72
+ should be considered equal during matching.
73
+
74
+ The function will be called like
75
+
76
+ node_match(G1.nodes[n1], G2.nodes[n2]).
77
+
78
+ That is, the function will receive the node attribute
79
+ dictionaries for n1 and n2 as inputs.
80
+
81
+ Ignored if node_subst_cost is specified. If neither
82
+ node_match nor node_subst_cost are specified then node
83
+ attributes are not considered.
84
+
85
+ edge_match : callable
86
+ A function that returns True if the edge attribute dictionaries
87
+ for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
88
+ be considered equal during matching.
89
+
90
+ The function will be called like
91
+
92
+ edge_match(G1[u1][v1], G2[u2][v2]).
93
+
94
+ That is, the function will receive the edge attribute
95
+ dictionaries of the edges under consideration.
96
+
97
+ Ignored if edge_subst_cost is specified. If neither
98
+ edge_match nor edge_subst_cost are specified then edge
99
+ attributes are not considered.
100
+
101
+ node_subst_cost, node_del_cost, node_ins_cost : callable
102
+ Functions that return the costs of node substitution, node
103
+ deletion, and node insertion, respectively.
104
+
105
+ The functions will be called like
106
+
107
+ node_subst_cost(G1.nodes[n1], G2.nodes[n2]),
108
+ node_del_cost(G1.nodes[n1]),
109
+ node_ins_cost(G2.nodes[n2]).
110
+
111
+ That is, the functions will receive the node attribute
112
+ dictionaries as inputs. The functions are expected to return
113
+ positive numeric values.
114
+
115
+ Function node_subst_cost overrides node_match if specified.
116
+ If neither node_match nor node_subst_cost are specified then
117
+ default node substitution cost of 0 is used (node attributes
118
+ are not considered during matching).
119
+
120
+ If node_del_cost is not specified then default node deletion
121
+ cost of 1 is used. If node_ins_cost is not specified then
122
+ default node insertion cost of 1 is used.
123
+
124
+ edge_subst_cost, edge_del_cost, edge_ins_cost : callable
125
+ Functions that return the costs of edge substitution, edge
126
+ deletion, and edge insertion, respectively.
127
+
128
+ The functions will be called like
129
+
130
+ edge_subst_cost(G1[u1][v1], G2[u2][v2]),
131
+ edge_del_cost(G1[u1][v1]),
132
+ edge_ins_cost(G2[u2][v2]).
133
+
134
+ That is, the functions will receive the edge attribute
135
+ dictionaries as inputs. The functions are expected to return
136
+ positive numeric values.
137
+
138
+ Function edge_subst_cost overrides edge_match if specified.
139
+ If neither edge_match nor edge_subst_cost are specified then
140
+ default edge substitution cost of 0 is used (edge attributes
141
+ are not considered during matching).
142
+
143
+ If edge_del_cost is not specified then default edge deletion
144
+ cost of 1 is used. If edge_ins_cost is not specified then
145
+ default edge insertion cost of 1 is used.
146
+
147
+ roots : 2-tuple
148
+ Tuple where first element is a node in G1 and the second
149
+ is a node in G2.
150
+ These nodes are forced to be matched in the comparison to
151
+ allow comparison between rooted graphs.
152
+
153
+ upper_bound : numeric
154
+ Maximum edit distance to consider. Return None if no edit
155
+ distance under or equal to upper_bound exists.
156
+
157
+ timeout : numeric
158
+ Maximum number of seconds to execute.
159
+ After timeout is met, the current best GED is returned.
160
+
161
+ Examples
162
+ --------
163
+ >>> G1 = nx.cycle_graph(6)
164
+ >>> G2 = nx.wheel_graph(7)
165
+ >>> nx.graph_edit_distance(G1, G2)
166
+ 7.0
167
+
168
+ >>> G1 = nx.star_graph(5)
169
+ >>> G2 = nx.star_graph(5)
170
+ >>> nx.graph_edit_distance(G1, G2, roots=(0, 0))
171
+ 0.0
172
+ >>> nx.graph_edit_distance(G1, G2, roots=(1, 0))
173
+ 8.0
174
+
175
+ See Also
176
+ --------
177
+ optimal_edit_paths, optimize_graph_edit_distance,
178
+
179
+ is_isomorphic: test for graph edit distance of 0
180
+
181
+ References
182
+ ----------
183
+ .. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick
184
+ Martineau. An Exact Graph Edit Distance Algorithm for Solving
185
+ Pattern Recognition Problems. 4th International Conference on
186
+ Pattern Recognition Applications and Methods 2015, Jan 2015,
187
+ Lisbon, Portugal. 2015,
188
+ <10.5220/0005209202710278>. <hal-01168816>
189
+ https://hal.archives-ouvertes.fr/hal-01168816
190
+
191
+ """
192
+ bestcost = None
193
+ for _, _, cost in optimize_edit_paths(
194
+ G1,
195
+ G2,
196
+ node_match,
197
+ edge_match,
198
+ node_subst_cost,
199
+ node_del_cost,
200
+ node_ins_cost,
201
+ edge_subst_cost,
202
+ edge_del_cost,
203
+ edge_ins_cost,
204
+ upper_bound,
205
+ True,
206
+ roots,
207
+ timeout,
208
+ ):
209
+ # assert bestcost is None or cost < bestcost
210
+ bestcost = cost
211
+ return bestcost
212
+
213
+
214
+ @nx._dispatchable(graphs={"G1": 0, "G2": 1})
215
+ def optimal_edit_paths(
216
+ G1,
217
+ G2,
218
+ node_match=None,
219
+ edge_match=None,
220
+ node_subst_cost=None,
221
+ node_del_cost=None,
222
+ node_ins_cost=None,
223
+ edge_subst_cost=None,
224
+ edge_del_cost=None,
225
+ edge_ins_cost=None,
226
+ upper_bound=None,
227
+ ):
228
+ """Returns all minimum-cost edit paths transforming G1 to G2.
229
+
230
+ Graph edit path is a sequence of node and edge edit operations
231
+ transforming graph G1 to graph isomorphic to G2. Edit operations
232
+ include substitutions, deletions, and insertions.
233
+
234
+ Parameters
235
+ ----------
236
+ G1, G2: graphs
237
+ The two graphs G1 and G2 must be of the same type.
238
+
239
+ node_match : callable
240
+ A function that returns True if node n1 in G1 and n2 in G2
241
+ should be considered equal during matching.
242
+
243
+ The function will be called like
244
+
245
+ node_match(G1.nodes[n1], G2.nodes[n2]).
246
+
247
+ That is, the function will receive the node attribute
248
+ dictionaries for n1 and n2 as inputs.
249
+
250
+ Ignored if node_subst_cost is specified. If neither
251
+ node_match nor node_subst_cost are specified then node
252
+ attributes are not considered.
253
+
254
+ edge_match : callable
255
+ A function that returns True if the edge attribute dictionaries
256
+ for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
257
+ be considered equal during matching.
258
+
259
+ The function will be called like
260
+
261
+ edge_match(G1[u1][v1], G2[u2][v2]).
262
+
263
+ That is, the function will receive the edge attribute
264
+ dictionaries of the edges under consideration.
265
+
266
+ Ignored if edge_subst_cost is specified. If neither
267
+ edge_match nor edge_subst_cost are specified then edge
268
+ attributes are not considered.
269
+
270
+ node_subst_cost, node_del_cost, node_ins_cost : callable
271
+ Functions that return the costs of node substitution, node
272
+ deletion, and node insertion, respectively.
273
+
274
+ The functions will be called like
275
+
276
+ node_subst_cost(G1.nodes[n1], G2.nodes[n2]),
277
+ node_del_cost(G1.nodes[n1]),
278
+ node_ins_cost(G2.nodes[n2]).
279
+
280
+ That is, the functions will receive the node attribute
281
+ dictionaries as inputs. The functions are expected to return
282
+ positive numeric values.
283
+
284
+ Function node_subst_cost overrides node_match if specified.
285
+ If neither node_match nor node_subst_cost are specified then
286
+ default node substitution cost of 0 is used (node attributes
287
+ are not considered during matching).
288
+
289
+ If node_del_cost is not specified then default node deletion
290
+ cost of 1 is used. If node_ins_cost is not specified then
291
+ default node insertion cost of 1 is used.
292
+
293
+ edge_subst_cost, edge_del_cost, edge_ins_cost : callable
294
+ Functions that return the costs of edge substitution, edge
295
+ deletion, and edge insertion, respectively.
296
+
297
+ The functions will be called like
298
+
299
+ edge_subst_cost(G1[u1][v1], G2[u2][v2]),
300
+ edge_del_cost(G1[u1][v1]),
301
+ edge_ins_cost(G2[u2][v2]).
302
+
303
+ That is, the functions will receive the edge attribute
304
+ dictionaries as inputs. The functions are expected to return
305
+ positive numeric values.
306
+
307
+ Function edge_subst_cost overrides edge_match if specified.
308
+ If neither edge_match nor edge_subst_cost are specified then
309
+ default edge substitution cost of 0 is used (edge attributes
310
+ are not considered during matching).
311
+
312
+ If edge_del_cost is not specified then default edge deletion
313
+ cost of 1 is used. If edge_ins_cost is not specified then
314
+ default edge insertion cost of 1 is used.
315
+
316
+ upper_bound : numeric
317
+ Maximum edit distance to consider.
318
+
319
+ Returns
320
+ -------
321
+ edit_paths : list of tuples (node_edit_path, edge_edit_path)
322
+ - node_edit_path : list of tuples ``(u, v)`` indicating node transformations
323
+ between `G1` and `G2`. ``u`` is `None` for insertion, ``v`` is `None`
324
+ for deletion.
325
+ - edge_edit_path : list of tuples ``((u1, v1), (u2, v2))`` indicating edge
326
+ transformations between `G1` and `G2`. ``(None, (u2,v2))`` for insertion
327
+ and ``((u1,v1), None)`` for deletion.
328
+
329
+ cost : numeric
330
+ Optimal edit path cost (graph edit distance). When the cost
331
+ is zero, it indicates that `G1` and `G2` are isomorphic.
332
+
333
+ Examples
334
+ --------
335
+ >>> G1 = nx.cycle_graph(4)
336
+ >>> G2 = nx.wheel_graph(5)
337
+ >>> paths, cost = nx.optimal_edit_paths(G1, G2)
338
+ >>> len(paths)
339
+ 40
340
+ >>> cost
341
+ 5.0
342
+
343
+ Notes
344
+ -----
345
+ To transform `G1` into a graph isomorphic to `G2`, apply the node
346
+ and edge edits in the returned ``edit_paths``.
347
+ In the case of isomorphic graphs, the cost is zero, and the paths
348
+ represent different isomorphic mappings (isomorphisms). That is, the
349
+ edits involve renaming nodes and edges to match the structure of `G2`.
350
+
351
+ See Also
352
+ --------
353
+ graph_edit_distance, optimize_edit_paths
354
+
355
+ References
356
+ ----------
357
+ .. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick
358
+ Martineau. An Exact Graph Edit Distance Algorithm for Solving
359
+ Pattern Recognition Problems. 4th International Conference on
360
+ Pattern Recognition Applications and Methods 2015, Jan 2015,
361
+ Lisbon, Portugal. 2015,
362
+ <10.5220/0005209202710278>. <hal-01168816>
363
+ https://hal.archives-ouvertes.fr/hal-01168816
364
+
365
+ """
366
+ paths = []
367
+ bestcost = None
368
+ for vertex_path, edge_path, cost in optimize_edit_paths(
369
+ G1,
370
+ G2,
371
+ node_match,
372
+ edge_match,
373
+ node_subst_cost,
374
+ node_del_cost,
375
+ node_ins_cost,
376
+ edge_subst_cost,
377
+ edge_del_cost,
378
+ edge_ins_cost,
379
+ upper_bound,
380
+ False,
381
+ ):
382
+ # assert bestcost is None or cost <= bestcost
383
+ if bestcost is not None and cost < bestcost:
384
+ paths = []
385
+ paths.append((vertex_path, edge_path))
386
+ bestcost = cost
387
+ return paths, bestcost
388
+
389
+
390
+ @nx._dispatchable(graphs={"G1": 0, "G2": 1})
391
+ def optimize_graph_edit_distance(
392
+ G1,
393
+ G2,
394
+ node_match=None,
395
+ edge_match=None,
396
+ node_subst_cost=None,
397
+ node_del_cost=None,
398
+ node_ins_cost=None,
399
+ edge_subst_cost=None,
400
+ edge_del_cost=None,
401
+ edge_ins_cost=None,
402
+ upper_bound=None,
403
+ ):
404
+ """Returns consecutive approximations of GED (graph edit distance)
405
+ between graphs G1 and G2.
406
+
407
+ Graph edit distance is a graph similarity measure analogous to
408
+ Levenshtein distance for strings. It is defined as minimum cost
409
+ of edit path (sequence of node and edge edit operations)
410
+ transforming graph G1 to graph isomorphic to G2.
411
+
412
+ Parameters
413
+ ----------
414
+ G1, G2: graphs
415
+ The two graphs G1 and G2 must be of the same type.
416
+
417
+ node_match : callable
418
+ A function that returns True if node n1 in G1 and n2 in G2
419
+ should be considered equal during matching.
420
+
421
+ The function will be called like
422
+
423
+ node_match(G1.nodes[n1], G2.nodes[n2]).
424
+
425
+ That is, the function will receive the node attribute
426
+ dictionaries for n1 and n2 as inputs.
427
+
428
+ Ignored if node_subst_cost is specified. If neither
429
+ node_match nor node_subst_cost are specified then node
430
+ attributes are not considered.
431
+
432
+ edge_match : callable
433
+ A function that returns True if the edge attribute dictionaries
434
+ for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
435
+ be considered equal during matching.
436
+
437
+ The function will be called like
438
+
439
+ edge_match(G1[u1][v1], G2[u2][v2]).
440
+
441
+ That is, the function will receive the edge attribute
442
+ dictionaries of the edges under consideration.
443
+
444
+ Ignored if edge_subst_cost is specified. If neither
445
+ edge_match nor edge_subst_cost are specified then edge
446
+ attributes are not considered.
447
+
448
+ node_subst_cost, node_del_cost, node_ins_cost : callable
449
+ Functions that return the costs of node substitution, node
450
+ deletion, and node insertion, respectively.
451
+
452
+ The functions will be called like
453
+
454
+ node_subst_cost(G1.nodes[n1], G2.nodes[n2]),
455
+ node_del_cost(G1.nodes[n1]),
456
+ node_ins_cost(G2.nodes[n2]).
457
+
458
+ That is, the functions will receive the node attribute
459
+ dictionaries as inputs. The functions are expected to return
460
+ positive numeric values.
461
+
462
+ Function node_subst_cost overrides node_match if specified.
463
+ If neither node_match nor node_subst_cost are specified then
464
+ default node substitution cost of 0 is used (node attributes
465
+ are not considered during matching).
466
+
467
+ If node_del_cost is not specified then default node deletion
468
+ cost of 1 is used. If node_ins_cost is not specified then
469
+ default node insertion cost of 1 is used.
470
+
471
+ edge_subst_cost, edge_del_cost, edge_ins_cost : callable
472
+ Functions that return the costs of edge substitution, edge
473
+ deletion, and edge insertion, respectively.
474
+
475
+ The functions will be called like
476
+
477
+ edge_subst_cost(G1[u1][v1], G2[u2][v2]),
478
+ edge_del_cost(G1[u1][v1]),
479
+ edge_ins_cost(G2[u2][v2]).
480
+
481
+ That is, the functions will receive the edge attribute
482
+ dictionaries as inputs. The functions are expected to return
483
+ positive numeric values.
484
+
485
+ Function edge_subst_cost overrides edge_match if specified.
486
+ If neither edge_match nor edge_subst_cost are specified then
487
+ default edge substitution cost of 0 is used (edge attributes
488
+ are not considered during matching).
489
+
490
+ If edge_del_cost is not specified then default edge deletion
491
+ cost of 1 is used. If edge_ins_cost is not specified then
492
+ default edge insertion cost of 1 is used.
493
+
494
+ upper_bound : numeric
495
+ Maximum edit distance to consider.
496
+
497
+ Returns
498
+ -------
499
+ Generator of consecutive approximations of graph edit distance.
500
+
501
+ Examples
502
+ --------
503
+ >>> G1 = nx.cycle_graph(6)
504
+ >>> G2 = nx.wheel_graph(7)
505
+ >>> for v in nx.optimize_graph_edit_distance(G1, G2):
506
+ ... minv = v
507
+ >>> minv
508
+ 7.0
509
+
510
+ See Also
511
+ --------
512
+ graph_edit_distance, optimize_edit_paths
513
+
514
+ References
515
+ ----------
516
+ .. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick
517
+ Martineau. An Exact Graph Edit Distance Algorithm for Solving
518
+ Pattern Recognition Problems. 4th International Conference on
519
+ Pattern Recognition Applications and Methods 2015, Jan 2015,
520
+ Lisbon, Portugal. 2015,
521
+ <10.5220/0005209202710278>. <hal-01168816>
522
+ https://hal.archives-ouvertes.fr/hal-01168816
523
+ """
524
+ for _, _, cost in optimize_edit_paths(
525
+ G1,
526
+ G2,
527
+ node_match,
528
+ edge_match,
529
+ node_subst_cost,
530
+ node_del_cost,
531
+ node_ins_cost,
532
+ edge_subst_cost,
533
+ edge_del_cost,
534
+ edge_ins_cost,
535
+ upper_bound,
536
+ True,
537
+ ):
538
+ yield cost
539
+
540
+
541
+ @nx._dispatchable(
542
+ graphs={"G1": 0, "G2": 1}, preserve_edge_attrs=True, preserve_node_attrs=True
543
+ )
544
+ def optimize_edit_paths(
545
+ G1,
546
+ G2,
547
+ node_match=None,
548
+ edge_match=None,
549
+ node_subst_cost=None,
550
+ node_del_cost=None,
551
+ node_ins_cost=None,
552
+ edge_subst_cost=None,
553
+ edge_del_cost=None,
554
+ edge_ins_cost=None,
555
+ upper_bound=None,
556
+ strictly_decreasing=True,
557
+ roots=None,
558
+ timeout=None,
559
+ ):
560
+ """GED (graph edit distance) calculation: advanced interface.
561
+
562
+ Graph edit path is a sequence of node and edge edit operations
563
+ transforming graph G1 to graph isomorphic to G2. Edit operations
564
+ include substitutions, deletions, and insertions.
565
+
566
+ Graph edit distance is defined as minimum cost of edit path.
567
+
568
+ Parameters
569
+ ----------
570
+ G1, G2: graphs
571
+ The two graphs G1 and G2 must be of the same type.
572
+
573
+ node_match : callable
574
+ A function that returns True if node n1 in G1 and n2 in G2
575
+ should be considered equal during matching.
576
+
577
+ The function will be called like
578
+
579
+ node_match(G1.nodes[n1], G2.nodes[n2]).
580
+
581
+ That is, the function will receive the node attribute
582
+ dictionaries for n1 and n2 as inputs.
583
+
584
+ Ignored if node_subst_cost is specified. If neither
585
+ node_match nor node_subst_cost are specified then node
586
+ attributes are not considered.
587
+
588
+ edge_match : callable
589
+ A function that returns True if the edge attribute dictionaries
590
+ for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
591
+ be considered equal during matching.
592
+
593
+ The function will be called like
594
+
595
+ edge_match(G1[u1][v1], G2[u2][v2]).
596
+
597
+ That is, the function will receive the edge attribute
598
+ dictionaries of the edges under consideration.
599
+
600
+ Ignored if edge_subst_cost is specified. If neither
601
+ edge_match nor edge_subst_cost are specified then edge
602
+ attributes are not considered.
603
+
604
+ node_subst_cost, node_del_cost, node_ins_cost : callable
605
+ Functions that return the costs of node substitution, node
606
+ deletion, and node insertion, respectively.
607
+
608
+ The functions will be called like
609
+
610
+ node_subst_cost(G1.nodes[n1], G2.nodes[n2]),
611
+ node_del_cost(G1.nodes[n1]),
612
+ node_ins_cost(G2.nodes[n2]).
613
+
614
+ That is, the functions will receive the node attribute
615
+ dictionaries as inputs. The functions are expected to return
616
+ positive numeric values.
617
+
618
+ Function node_subst_cost overrides node_match if specified.
619
+ If neither node_match nor node_subst_cost are specified then
620
+ default node substitution cost of 0 is used (node attributes
621
+ are not considered during matching).
622
+
623
+ If node_del_cost is not specified then default node deletion
624
+ cost of 1 is used. If node_ins_cost is not specified then
625
+ default node insertion cost of 1 is used.
626
+
627
+ edge_subst_cost, edge_del_cost, edge_ins_cost : callable
628
+ Functions that return the costs of edge substitution, edge
629
+ deletion, and edge insertion, respectively.
630
+
631
+ The functions will be called like
632
+
633
+ edge_subst_cost(G1[u1][v1], G2[u2][v2]),
634
+ edge_del_cost(G1[u1][v1]),
635
+ edge_ins_cost(G2[u2][v2]).
636
+
637
+ That is, the functions will receive the edge attribute
638
+ dictionaries as inputs. The functions are expected to return
639
+ positive numeric values.
640
+
641
+ Function edge_subst_cost overrides edge_match if specified.
642
+ If neither edge_match nor edge_subst_cost are specified then
643
+ default edge substitution cost of 0 is used (edge attributes
644
+ are not considered during matching).
645
+
646
+ If edge_del_cost is not specified then default edge deletion
647
+ cost of 1 is used. If edge_ins_cost is not specified then
648
+ default edge insertion cost of 1 is used.
649
+
650
+ upper_bound : numeric
651
+ Maximum edit distance to consider.
652
+
653
+ strictly_decreasing : bool
654
+ If True, return consecutive approximations of strictly
655
+ decreasing cost. Otherwise, return all edit paths of cost
656
+ less than or equal to the previous minimum cost.
657
+
658
+ roots : 2-tuple
659
+ Tuple where first element is a node in G1 and the second
660
+ is a node in G2.
661
+ These nodes are forced to be matched in the comparison to
662
+ allow comparison between rooted graphs.
663
+
664
+ timeout : numeric
665
+ Maximum number of seconds to execute.
666
+ After timeout is met, the current best GED is returned.
667
+
668
+ Returns
669
+ -------
670
+ Generator of tuples (node_edit_path, edge_edit_path, cost)
671
+ node_edit_path : list of tuples (u, v)
672
+ edge_edit_path : list of tuples ((u1, v1), (u2, v2))
673
+ cost : numeric
674
+
675
+ See Also
676
+ --------
677
+ graph_edit_distance, optimize_graph_edit_distance, optimal_edit_paths
678
+
679
+ References
680
+ ----------
681
+ .. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick
682
+ Martineau. An Exact Graph Edit Distance Algorithm for Solving
683
+ Pattern Recognition Problems. 4th International Conference on
684
+ Pattern Recognition Applications and Methods 2015, Jan 2015,
685
+ Lisbon, Portugal. 2015,
686
+ <10.5220/0005209202710278>. <hal-01168816>
687
+ https://hal.archives-ouvertes.fr/hal-01168816
688
+
689
+ """
690
+ # TODO: support DiGraph
691
+
692
+ import numpy as np
693
+ import scipy as sp
694
+
695
+ @dataclass
696
+ class CostMatrix:
697
+ C: ...
698
+ lsa_row_ind: ...
699
+ lsa_col_ind: ...
700
+ ls: ...
701
+
702
+ def make_CostMatrix(C, m, n):
703
+ # assert(C.shape == (m + n, m + n))
704
+ lsa_row_ind, lsa_col_ind = sp.optimize.linear_sum_assignment(C)
705
+
706
+ # Fixup dummy assignments:
707
+ # each substitution i<->j should have dummy assignment m+j<->n+i
708
+ # NOTE: fast reduce of Cv relies on it
709
+ # Create masks for substitution and dummy indices
710
+ is_subst = (lsa_row_ind < m) & (lsa_col_ind < n)
711
+ is_dummy = (lsa_row_ind >= m) & (lsa_col_ind >= n)
712
+
713
+ # Map dummy assignments to the correct indices
714
+ lsa_row_ind[is_dummy] = lsa_col_ind[is_subst] + m
715
+ lsa_col_ind[is_dummy] = lsa_row_ind[is_subst] + n
716
+
717
+ return CostMatrix(
718
+ C, lsa_row_ind, lsa_col_ind, C[lsa_row_ind, lsa_col_ind].sum()
719
+ )
720
+
721
+ def extract_C(C, i, j, m, n):
722
+ # assert(C.shape == (m + n, m + n))
723
+ row_ind = [k in i or k - m in j for k in range(m + n)]
724
+ col_ind = [k in j or k - n in i for k in range(m + n)]
725
+ return C[row_ind, :][:, col_ind]
726
+
727
+ def reduce_C(C, i, j, m, n):
728
+ # assert(C.shape == (m + n, m + n))
729
+ row_ind = [k not in i and k - m not in j for k in range(m + n)]
730
+ col_ind = [k not in j and k - n not in i for k in range(m + n)]
731
+ return C[row_ind, :][:, col_ind]
732
+
733
+ def reduce_ind(ind, i):
734
+ # assert set(ind) == set(range(len(ind)))
735
+ rind = ind[[k not in i for k in ind]]
736
+ for k in set(i):
737
+ rind[rind >= k] -= 1
738
+ return rind
739
+
740
+ def match_edges(u, v, pending_g, pending_h, Ce, matched_uv=None):
741
+ """
742
+ Parameters:
743
+ u, v: matched vertices, u=None or v=None for
744
+ deletion/insertion
745
+ pending_g, pending_h: lists of edges not yet mapped
746
+ Ce: CostMatrix of pending edge mappings
747
+ matched_uv: partial vertex edit path
748
+ list of tuples (u, v) of previously matched vertex
749
+ mappings u<->v, u=None or v=None for
750
+ deletion/insertion
751
+
752
+ Returns:
753
+ list of (i, j): indices of edge mappings g<->h
754
+ localCe: local CostMatrix of edge mappings
755
+ (basically submatrix of Ce at cross of rows i, cols j)
756
+ """
757
+ M = len(pending_g)
758
+ N = len(pending_h)
759
+ # assert Ce.C.shape == (M + N, M + N)
760
+
761
+ # only attempt to match edges after one node match has been made
762
+ # this will stop self-edges on the first node being automatically deleted
763
+ # even when a substitution is the better option
764
+ if matched_uv is None or len(matched_uv) == 0:
765
+ g_ind = []
766
+ h_ind = []
767
+ else:
768
+ g_ind = [
769
+ i
770
+ for i in range(M)
771
+ if pending_g[i][:2] == (u, u)
772
+ or any(
773
+ pending_g[i][:2] in ((p, u), (u, p), (p, p)) for p, q in matched_uv
774
+ )
775
+ ]
776
+ h_ind = [
777
+ j
778
+ for j in range(N)
779
+ if pending_h[j][:2] == (v, v)
780
+ or any(
781
+ pending_h[j][:2] in ((q, v), (v, q), (q, q)) for p, q in matched_uv
782
+ )
783
+ ]
784
+
785
+ m = len(g_ind)
786
+ n = len(h_ind)
787
+
788
+ if m or n:
789
+ C = extract_C(Ce.C, g_ind, h_ind, M, N)
790
+ # assert C.shape == (m + n, m + n)
791
+
792
+ # Forbid structurally invalid matches
793
+ # NOTE: inf remembered from Ce construction
794
+ for k, i in enumerate(g_ind):
795
+ g = pending_g[i][:2]
796
+ for l, j in enumerate(h_ind):
797
+ h = pending_h[j][:2]
798
+ if nx.is_directed(G1) or nx.is_directed(G2):
799
+ if any(
800
+ g == (p, u) and h == (q, v) or g == (u, p) and h == (v, q)
801
+ for p, q in matched_uv
802
+ ):
803
+ continue
804
+ else:
805
+ if any(
806
+ g in ((p, u), (u, p)) and h in ((q, v), (v, q))
807
+ for p, q in matched_uv
808
+ ):
809
+ continue
810
+ if g == (u, u) or any(g == (p, p) for p, q in matched_uv):
811
+ continue
812
+ if h == (v, v) or any(h == (q, q) for p, q in matched_uv):
813
+ continue
814
+ C[k, l] = inf
815
+
816
+ localCe = make_CostMatrix(C, m, n)
817
+ ij = [
818
+ (
819
+ g_ind[k] if k < m else M + h_ind[l],
820
+ h_ind[l] if l < n else N + g_ind[k],
821
+ )
822
+ for k, l in zip(localCe.lsa_row_ind, localCe.lsa_col_ind)
823
+ if k < m or l < n
824
+ ]
825
+
826
+ else:
827
+ ij = []
828
+ localCe = CostMatrix(np.empty((0, 0)), [], [], 0)
829
+
830
+ return ij, localCe
831
+
832
+ def reduce_Ce(Ce, ij, m, n):
833
+ if len(ij):
834
+ i, j = zip(*ij)
835
+ m_i = m - sum(1 for t in i if t < m)
836
+ n_j = n - sum(1 for t in j if t < n)
837
+ return make_CostMatrix(reduce_C(Ce.C, i, j, m, n), m_i, n_j)
838
+ return Ce
839
+
840
+ def get_edit_ops(
841
+ matched_uv, pending_u, pending_v, Cv, pending_g, pending_h, Ce, matched_cost
842
+ ):
843
+ """
844
+ Parameters:
845
+ matched_uv: partial vertex edit path
846
+ list of tuples (u, v) of vertex mappings u<->v,
847
+ u=None or v=None for deletion/insertion
848
+ pending_u, pending_v: lists of vertices not yet mapped
849
+ Cv: CostMatrix of pending vertex mappings
850
+ pending_g, pending_h: lists of edges not yet mapped
851
+ Ce: CostMatrix of pending edge mappings
852
+ matched_cost: cost of partial edit path
853
+
854
+ Returns:
855
+ sequence of
856
+ (i, j): indices of vertex mapping u<->v
857
+ Cv_ij: reduced CostMatrix of pending vertex mappings
858
+ (basically Cv with row i, col j removed)
859
+ list of (x, y): indices of edge mappings g<->h
860
+ Ce_xy: reduced CostMatrix of pending edge mappings
861
+ (basically Ce with rows x, cols y removed)
862
+ cost: total cost of edit operation
863
+ NOTE: most promising ops first
864
+ """
865
+ m = len(pending_u)
866
+ n = len(pending_v)
867
+ # assert Cv.C.shape == (m + n, m + n)
868
+
869
+ # 1) a vertex mapping from optimal linear sum assignment
870
+ i, j = min(
871
+ (k, l) for k, l in zip(Cv.lsa_row_ind, Cv.lsa_col_ind) if k < m or l < n
872
+ )
873
+ xy, localCe = match_edges(
874
+ pending_u[i] if i < m else None,
875
+ pending_v[j] if j < n else None,
876
+ pending_g,
877
+ pending_h,
878
+ Ce,
879
+ matched_uv,
880
+ )
881
+ Ce_xy = reduce_Ce(Ce, xy, len(pending_g), len(pending_h))
882
+ # assert Ce.ls <= localCe.ls + Ce_xy.ls
883
+ if prune(matched_cost + Cv.ls + localCe.ls + Ce_xy.ls):
884
+ pass
885
+ else:
886
+ # get reduced Cv efficiently
887
+ Cv_ij = CostMatrix(
888
+ reduce_C(Cv.C, (i,), (j,), m, n),
889
+ reduce_ind(Cv.lsa_row_ind, (i, m + j)),
890
+ reduce_ind(Cv.lsa_col_ind, (j, n + i)),
891
+ Cv.ls - Cv.C[i, j],
892
+ )
893
+ yield (i, j), Cv_ij, xy, Ce_xy, Cv.C[i, j] + localCe.ls
894
+
895
+ # 2) other candidates, sorted by lower-bound cost estimate
896
+ other = []
897
+ fixed_i, fixed_j = i, j
898
+ if m <= n:
899
+ candidates = (
900
+ (t, fixed_j)
901
+ for t in range(m + n)
902
+ if t != fixed_i and (t < m or t == m + fixed_j)
903
+ )
904
+ else:
905
+ candidates = (
906
+ (fixed_i, t)
907
+ for t in range(m + n)
908
+ if t != fixed_j and (t < n or t == n + fixed_i)
909
+ )
910
+ for i, j in candidates:
911
+ if prune(matched_cost + Cv.C[i, j] + Ce.ls):
912
+ continue
913
+ Cv_ij = make_CostMatrix(
914
+ reduce_C(Cv.C, (i,), (j,), m, n),
915
+ m - 1 if i < m else m,
916
+ n - 1 if j < n else n,
917
+ )
918
+ # assert Cv.ls <= Cv.C[i, j] + Cv_ij.ls
919
+ if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + Ce.ls):
920
+ continue
921
+ xy, localCe = match_edges(
922
+ pending_u[i] if i < m else None,
923
+ pending_v[j] if j < n else None,
924
+ pending_g,
925
+ pending_h,
926
+ Ce,
927
+ matched_uv,
928
+ )
929
+ if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + localCe.ls):
930
+ continue
931
+ Ce_xy = reduce_Ce(Ce, xy, len(pending_g), len(pending_h))
932
+ # assert Ce.ls <= localCe.ls + Ce_xy.ls
933
+ if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + localCe.ls + Ce_xy.ls):
934
+ continue
935
+ other.append(((i, j), Cv_ij, xy, Ce_xy, Cv.C[i, j] + localCe.ls))
936
+
937
+ yield from sorted(other, key=lambda t: t[4] + t[1].ls + t[3].ls)
938
+
939
+ def get_edit_paths(
940
+ matched_uv,
941
+ pending_u,
942
+ pending_v,
943
+ Cv,
944
+ matched_gh,
945
+ pending_g,
946
+ pending_h,
947
+ Ce,
948
+ matched_cost,
949
+ ):
950
+ """
951
+ Parameters:
952
+ matched_uv: partial vertex edit path
953
+ list of tuples (u, v) of vertex mappings u<->v,
954
+ u=None or v=None for deletion/insertion
955
+ pending_u, pending_v: lists of vertices not yet mapped
956
+ Cv: CostMatrix of pending vertex mappings
957
+ matched_gh: partial edge edit path
958
+ list of tuples (g, h) of edge mappings g<->h,
959
+ g=None or h=None for deletion/insertion
960
+ pending_g, pending_h: lists of edges not yet mapped
961
+ Ce: CostMatrix of pending edge mappings
962
+ matched_cost: cost of partial edit path
963
+
964
+ Returns:
965
+ sequence of (vertex_path, edge_path, cost)
966
+ vertex_path: complete vertex edit path
967
+ list of tuples (u, v) of vertex mappings u<->v,
968
+ u=None or v=None for deletion/insertion
969
+ edge_path: complete edge edit path
970
+ list of tuples (g, h) of edge mappings g<->h,
971
+ g=None or h=None for deletion/insertion
972
+ cost: total cost of edit path
973
+ NOTE: path costs are non-increasing
974
+ """
975
+ # debug_print('matched-uv:', matched_uv)
976
+ # debug_print('matched-gh:', matched_gh)
977
+ # debug_print('matched-cost:', matched_cost)
978
+ # debug_print('pending-u:', pending_u)
979
+ # debug_print('pending-v:', pending_v)
980
+ # debug_print(Cv.C)
981
+ # assert list(sorted(G1.nodes)) == list(sorted(list(u for u, v in matched_uv if u is not None) + pending_u))
982
+ # assert list(sorted(G2.nodes)) == list(sorted(list(v for u, v in matched_uv if v is not None) + pending_v))
983
+ # debug_print('pending-g:', pending_g)
984
+ # debug_print('pending-h:', pending_h)
985
+ # debug_print(Ce.C)
986
+ # assert list(sorted(G1.edges)) == list(sorted(list(g for g, h in matched_gh if g is not None) + pending_g))
987
+ # assert list(sorted(G2.edges)) == list(sorted(list(h for g, h in matched_gh if h is not None) + pending_h))
988
+ # debug_print()
989
+
990
+ if prune(matched_cost + Cv.ls + Ce.ls):
991
+ return
992
+
993
+ if not max(len(pending_u), len(pending_v)):
994
+ # assert not len(pending_g)
995
+ # assert not len(pending_h)
996
+ # path completed!
997
+ # assert matched_cost <= maxcost_value
998
+ nonlocal maxcost_value
999
+ maxcost_value = min(maxcost_value, matched_cost)
1000
+ yield matched_uv, matched_gh, matched_cost
1001
+
1002
+ else:
1003
+ edit_ops = get_edit_ops(
1004
+ matched_uv,
1005
+ pending_u,
1006
+ pending_v,
1007
+ Cv,
1008
+ pending_g,
1009
+ pending_h,
1010
+ Ce,
1011
+ matched_cost,
1012
+ )
1013
+ for ij, Cv_ij, xy, Ce_xy, edit_cost in edit_ops:
1014
+ i, j = ij
1015
+ # assert Cv.C[i, j] + sum(Ce.C[t] for t in xy) == edit_cost
1016
+ if prune(matched_cost + edit_cost + Cv_ij.ls + Ce_xy.ls):
1017
+ continue
1018
+
1019
+ # dive deeper
1020
+ u = pending_u.pop(i) if i < len(pending_u) else None
1021
+ v = pending_v.pop(j) if j < len(pending_v) else None
1022
+ matched_uv.append((u, v))
1023
+ for x, y in xy:
1024
+ len_g = len(pending_g)
1025
+ len_h = len(pending_h)
1026
+ matched_gh.append(
1027
+ (
1028
+ pending_g[x] if x < len_g else None,
1029
+ pending_h[y] if y < len_h else None,
1030
+ )
1031
+ )
1032
+ sortedx = sorted(x for x, y in xy)
1033
+ sortedy = sorted(y for x, y in xy)
1034
+ G = [
1035
+ (pending_g.pop(x) if x < len(pending_g) else None)
1036
+ for x in reversed(sortedx)
1037
+ ]
1038
+ H = [
1039
+ (pending_h.pop(y) if y < len(pending_h) else None)
1040
+ for y in reversed(sortedy)
1041
+ ]
1042
+
1043
+ yield from get_edit_paths(
1044
+ matched_uv,
1045
+ pending_u,
1046
+ pending_v,
1047
+ Cv_ij,
1048
+ matched_gh,
1049
+ pending_g,
1050
+ pending_h,
1051
+ Ce_xy,
1052
+ matched_cost + edit_cost,
1053
+ )
1054
+
1055
+ # backtrack
1056
+ if u is not None:
1057
+ pending_u.insert(i, u)
1058
+ if v is not None:
1059
+ pending_v.insert(j, v)
1060
+ matched_uv.pop()
1061
+ for x, g in zip(sortedx, reversed(G)):
1062
+ if g is not None:
1063
+ pending_g.insert(x, g)
1064
+ for y, h in zip(sortedy, reversed(H)):
1065
+ if h is not None:
1066
+ pending_h.insert(y, h)
1067
+ for _ in xy:
1068
+ matched_gh.pop()
1069
+
1070
+ # Initialization
1071
+
1072
+ pending_u = list(G1.nodes)
1073
+ pending_v = list(G2.nodes)
1074
+
1075
+ initial_cost = 0
1076
+ if roots:
1077
+ root_u, root_v = roots
1078
+ if root_u not in pending_u or root_v not in pending_v:
1079
+ raise nx.NodeNotFound("Root node not in graph.")
1080
+
1081
+ # remove roots from pending
1082
+ pending_u.remove(root_u)
1083
+ pending_v.remove(root_v)
1084
+
1085
+ # cost matrix of vertex mappings
1086
+ m = len(pending_u)
1087
+ n = len(pending_v)
1088
+ C = np.zeros((m + n, m + n))
1089
+ if node_subst_cost:
1090
+ C[0:m, 0:n] = np.array(
1091
+ [
1092
+ node_subst_cost(G1.nodes[u], G2.nodes[v])
1093
+ for u in pending_u
1094
+ for v in pending_v
1095
+ ]
1096
+ ).reshape(m, n)
1097
+ if roots:
1098
+ initial_cost = node_subst_cost(G1.nodes[root_u], G2.nodes[root_v])
1099
+ elif node_match:
1100
+ C[0:m, 0:n] = np.array(
1101
+ [
1102
+ 1 - int(node_match(G1.nodes[u], G2.nodes[v]))
1103
+ for u in pending_u
1104
+ for v in pending_v
1105
+ ]
1106
+ ).reshape(m, n)
1107
+ if roots:
1108
+ initial_cost = 1 - node_match(G1.nodes[root_u], G2.nodes[root_v])
1109
+ else:
1110
+ # all zeroes
1111
+ pass
1112
+ # assert not min(m, n) or C[0:m, 0:n].min() >= 0
1113
+ if node_del_cost:
1114
+ del_costs = [node_del_cost(G1.nodes[u]) for u in pending_u]
1115
+ else:
1116
+ del_costs = [1] * len(pending_u)
1117
+ # assert not m or min(del_costs) >= 0
1118
+ if node_ins_cost:
1119
+ ins_costs = [node_ins_cost(G2.nodes[v]) for v in pending_v]
1120
+ else:
1121
+ ins_costs = [1] * len(pending_v)
1122
+ # assert not n or min(ins_costs) >= 0
1123
+ inf = C[0:m, 0:n].sum() + sum(del_costs) + sum(ins_costs) + 1
1124
+ C[0:m, n : n + m] = np.array(
1125
+ [del_costs[i] if i == j else inf for i in range(m) for j in range(m)]
1126
+ ).reshape(m, m)
1127
+ C[m : m + n, 0:n] = np.array(
1128
+ [ins_costs[i] if i == j else inf for i in range(n) for j in range(n)]
1129
+ ).reshape(n, n)
1130
+ Cv = make_CostMatrix(C, m, n)
1131
+ # debug_print(f"Cv: {m} x {n}")
1132
+ # debug_print(Cv.C)
1133
+
1134
+ pending_g = list(G1.edges)
1135
+ pending_h = list(G2.edges)
1136
+
1137
+ # cost matrix of edge mappings
1138
+ m = len(pending_g)
1139
+ n = len(pending_h)
1140
+ C = np.zeros((m + n, m + n))
1141
+ if edge_subst_cost:
1142
+ C[0:m, 0:n] = np.array(
1143
+ [
1144
+ edge_subst_cost(G1.edges[g], G2.edges[h])
1145
+ for g in pending_g
1146
+ for h in pending_h
1147
+ ]
1148
+ ).reshape(m, n)
1149
+ elif edge_match:
1150
+ C[0:m, 0:n] = np.array(
1151
+ [
1152
+ 1 - int(edge_match(G1.edges[g], G2.edges[h]))
1153
+ for g in pending_g
1154
+ for h in pending_h
1155
+ ]
1156
+ ).reshape(m, n)
1157
+ else:
1158
+ # all zeroes
1159
+ pass
1160
+ # assert not min(m, n) or C[0:m, 0:n].min() >= 0
1161
+ if edge_del_cost:
1162
+ del_costs = [edge_del_cost(G1.edges[g]) for g in pending_g]
1163
+ else:
1164
+ del_costs = [1] * len(pending_g)
1165
+ # assert not m or min(del_costs) >= 0
1166
+ if edge_ins_cost:
1167
+ ins_costs = [edge_ins_cost(G2.edges[h]) for h in pending_h]
1168
+ else:
1169
+ ins_costs = [1] * len(pending_h)
1170
+ # assert not n or min(ins_costs) >= 0
1171
+ inf = C[0:m, 0:n].sum() + sum(del_costs) + sum(ins_costs) + 1
1172
+ C[0:m, n : n + m] = np.array(
1173
+ [del_costs[i] if i == j else inf for i in range(m) for j in range(m)]
1174
+ ).reshape(m, m)
1175
+ C[m : m + n, 0:n] = np.array(
1176
+ [ins_costs[i] if i == j else inf for i in range(n) for j in range(n)]
1177
+ ).reshape(n, n)
1178
+ Ce = make_CostMatrix(C, m, n)
1179
+ # debug_print(f'Ce: {m} x {n}')
1180
+ # debug_print(Ce.C)
1181
+ # debug_print()
1182
+
1183
+ maxcost_value = Cv.C.sum() + Ce.C.sum() + 1
1184
+
1185
+ if timeout is not None:
1186
+ if timeout <= 0:
1187
+ raise nx.NetworkXError("Timeout value must be greater than 0")
1188
+ start = time.perf_counter()
1189
+
1190
+ def prune(cost):
1191
+ if timeout is not None:
1192
+ if time.perf_counter() - start > timeout:
1193
+ return True
1194
+ if upper_bound is not None:
1195
+ if cost > upper_bound:
1196
+ return True
1197
+ if cost > maxcost_value:
1198
+ return True
1199
+ if strictly_decreasing and cost >= maxcost_value:
1200
+ return True
1201
+ return False
1202
+
1203
+ # Now go!
1204
+
1205
+ done_uv = [] if roots is None else [roots]
1206
+
1207
+ for vertex_path, edge_path, cost in get_edit_paths(
1208
+ done_uv, pending_u, pending_v, Cv, [], pending_g, pending_h, Ce, initial_cost
1209
+ ):
1210
+ # assert sorted(G1.nodes) == sorted(u for u, v in vertex_path if u is not None)
1211
+ # assert sorted(G2.nodes) == sorted(v for u, v in vertex_path if v is not None)
1212
+ # assert sorted(G1.edges) == sorted(g for g, h in edge_path if g is not None)
1213
+ # assert sorted(G2.edges) == sorted(h for g, h in edge_path if h is not None)
1214
+ # print(vertex_path, edge_path, cost, file = sys.stderr)
1215
+ # assert cost == maxcost_value
1216
+ yield list(vertex_path), list(edge_path), float(cost)
1217
+
1218
+
1219
+ @nx._dispatchable
1220
+ def simrank_similarity(
1221
+ G,
1222
+ source=None,
1223
+ target=None,
1224
+ importance_factor=0.9,
1225
+ max_iterations=1000,
1226
+ tolerance=1e-4,
1227
+ ):
1228
+ """Returns the SimRank similarity of nodes in the graph ``G``.
1229
+
1230
+ SimRank is a similarity metric that says "two objects are considered
1231
+ to be similar if they are referenced by similar objects." [1]_.
1232
+
1233
+ The pseudo-code definition from the paper is::
1234
+
1235
+ def simrank(G, u, v):
1236
+ in_neighbors_u = G.predecessors(u)
1237
+ in_neighbors_v = G.predecessors(v)
1238
+ scale = C / (len(in_neighbors_u) * len(in_neighbors_v))
1239
+ return scale * sum(
1240
+ simrank(G, w, x) for w, x in product(in_neighbors_u, in_neighbors_v)
1241
+ )
1242
+
1243
+ where ``G`` is the graph, ``u`` is the source, ``v`` is the target,
1244
+ and ``C`` is a float decay or importance factor between 0 and 1.
1245
+
1246
+ The SimRank algorithm for determining node similarity is defined in
1247
+ [2]_.
1248
+
1249
+ Parameters
1250
+ ----------
1251
+ G : NetworkX graph
1252
+ A NetworkX graph
1253
+
1254
+ source : node
1255
+ If this is specified, the returned dictionary maps each node
1256
+ ``v`` in the graph to the similarity between ``source`` and
1257
+ ``v``.
1258
+
1259
+ target : node
1260
+ If both ``source`` and ``target`` are specified, the similarity
1261
+ value between ``source`` and ``target`` is returned. If
1262
+ ``target`` is specified but ``source`` is not, this argument is
1263
+ ignored.
1264
+
1265
+ importance_factor : float
1266
+ The relative importance of indirect neighbors with respect to
1267
+ direct neighbors.
1268
+
1269
+ max_iterations : integer
1270
+ Maximum number of iterations.
1271
+
1272
+ tolerance : float
1273
+ Error tolerance used to check convergence. When an iteration of
1274
+ the algorithm finds that no similarity value changes more than
1275
+ this amount, the algorithm halts.
1276
+
1277
+ Returns
1278
+ -------
1279
+ similarity : dictionary or float
1280
+ If ``source`` and ``target`` are both ``None``, this returns a
1281
+ dictionary of dictionaries, where keys are node pairs and value
1282
+ are similarity of the pair of nodes.
1283
+
1284
+ If ``source`` is not ``None`` but ``target`` is, this returns a
1285
+ dictionary mapping node to the similarity of ``source`` and that
1286
+ node.
1287
+
1288
+ If neither ``source`` nor ``target`` is ``None``, this returns
1289
+ the similarity value for the given pair of nodes.
1290
+
1291
+ Raises
1292
+ ------
1293
+ ExceededMaxIterations
1294
+ If the algorithm does not converge within ``max_iterations``.
1295
+
1296
+ NodeNotFound
1297
+ If either ``source`` or ``target`` is not in `G`.
1298
+
1299
+ Examples
1300
+ --------
1301
+ >>> G = nx.cycle_graph(2)
1302
+ >>> nx.simrank_similarity(G)
1303
+ {0: {0: 1.0, 1: 0.0}, 1: {0: 0.0, 1: 1.0}}
1304
+ >>> nx.simrank_similarity(G, source=0)
1305
+ {0: 1.0, 1: 0.0}
1306
+ >>> nx.simrank_similarity(G, source=0, target=0)
1307
+ 1.0
1308
+
1309
+ The result of this function can be converted to a numpy array
1310
+ representing the SimRank matrix by using the node order of the
1311
+ graph to determine which row and column represent each node.
1312
+ Other ordering of nodes is also possible.
1313
+
1314
+ >>> import numpy as np
1315
+ >>> sim = nx.simrank_similarity(G)
1316
+ >>> np.array([[sim[u][v] for v in G] for u in G])
1317
+ array([[1., 0.],
1318
+ [0., 1.]])
1319
+ >>> sim_1d = nx.simrank_similarity(G, source=0)
1320
+ >>> np.array([sim[0][v] for v in G])
1321
+ array([1., 0.])
1322
+
1323
+ References
1324
+ ----------
1325
+ .. [1] https://en.wikipedia.org/wiki/SimRank
1326
+ .. [2] G. Jeh and J. Widom.
1327
+ "SimRank: a measure of structural-context similarity",
1328
+ In KDD'02: Proceedings of the Eighth ACM SIGKDD
1329
+ International Conference on Knowledge Discovery and Data Mining,
1330
+ pp. 538--543. ACM Press, 2002.
1331
+ """
1332
+ import numpy as np
1333
+
1334
+ nodelist = list(G)
1335
+ if source is not None:
1336
+ if source not in nodelist:
1337
+ raise nx.NodeNotFound(f"Source node {source} not in G")
1338
+ else:
1339
+ s_indx = nodelist.index(source)
1340
+ else:
1341
+ s_indx = None
1342
+
1343
+ if target is not None:
1344
+ if target not in nodelist:
1345
+ raise nx.NodeNotFound(f"Target node {target} not in G")
1346
+ else:
1347
+ t_indx = nodelist.index(target)
1348
+ else:
1349
+ t_indx = None
1350
+
1351
+ x = _simrank_similarity_numpy(
1352
+ G, s_indx, t_indx, importance_factor, max_iterations, tolerance
1353
+ )
1354
+
1355
+ if isinstance(x, np.ndarray):
1356
+ if x.ndim == 1:
1357
+ return dict(zip(G, x.tolist()))
1358
+ # else x.ndim == 2
1359
+ return {u: dict(zip(G, row)) for u, row in zip(G, x.tolist())}
1360
+ return float(x)
1361
+
1362
+
1363
+ def _simrank_similarity_python(
1364
+ G,
1365
+ source=None,
1366
+ target=None,
1367
+ importance_factor=0.9,
1368
+ max_iterations=1000,
1369
+ tolerance=1e-4,
1370
+ ):
1371
+ """Returns the SimRank similarity of nodes in the graph ``G``.
1372
+
1373
+ This pure Python version is provided for pedagogical purposes.
1374
+
1375
+ Examples
1376
+ --------
1377
+ >>> G = nx.cycle_graph(2)
1378
+ >>> nx.similarity._simrank_similarity_python(G)
1379
+ {0: {0: 1, 1: 0.0}, 1: {0: 0.0, 1: 1}}
1380
+ >>> nx.similarity._simrank_similarity_python(G, source=0)
1381
+ {0: 1, 1: 0.0}
1382
+ >>> nx.similarity._simrank_similarity_python(G, source=0, target=0)
1383
+ 1
1384
+ """
1385
+ # build up our similarity adjacency dictionary output
1386
+ newsim = {u: {v: 1 if u == v else 0 for v in G} for u in G}
1387
+
1388
+ # These functions compute the update to the similarity value of the nodes
1389
+ # `u` and `v` with respect to the previous similarity values.
1390
+ def avg_sim(s):
1391
+ return sum(newsim[w][x] for (w, x) in s) / len(s) if s else 0.0
1392
+
1393
+ Gadj = G.pred if G.is_directed() else G.adj
1394
+
1395
+ def sim(u, v):
1396
+ return importance_factor * avg_sim(list(product(Gadj[u], Gadj[v])))
1397
+
1398
+ for its in range(max_iterations):
1399
+ oldsim = newsim
1400
+ newsim = {u: {v: sim(u, v) if u != v else 1 for v in G} for u in G}
1401
+ is_close = all(
1402
+ all(
1403
+ abs(newsim[u][v] - old) <= tolerance * (1 + abs(old))
1404
+ for v, old in nbrs.items()
1405
+ )
1406
+ for u, nbrs in oldsim.items()
1407
+ )
1408
+ if is_close:
1409
+ break
1410
+
1411
+ if its + 1 == max_iterations:
1412
+ raise nx.ExceededMaxIterations(
1413
+ f"simrank did not converge after {max_iterations} iterations."
1414
+ )
1415
+
1416
+ if source is not None and target is not None:
1417
+ return newsim[source][target]
1418
+ if source is not None:
1419
+ return newsim[source]
1420
+ return newsim
1421
+
1422
+
1423
+ def _simrank_similarity_numpy(
1424
+ G,
1425
+ source=None,
1426
+ target=None,
1427
+ importance_factor=0.9,
1428
+ max_iterations=1000,
1429
+ tolerance=1e-4,
1430
+ ):
1431
+ """Calculate SimRank of nodes in ``G`` using matrices with ``numpy``.
1432
+
1433
+ The SimRank algorithm for determining node similarity is defined in
1434
+ [1]_.
1435
+
1436
+ Parameters
1437
+ ----------
1438
+ G : NetworkX graph
1439
+ A NetworkX graph
1440
+
1441
+ source : node
1442
+ If this is specified, the returned dictionary maps each node
1443
+ ``v`` in the graph to the similarity between ``source`` and
1444
+ ``v``.
1445
+
1446
+ target : node
1447
+ If both ``source`` and ``target`` are specified, the similarity
1448
+ value between ``source`` and ``target`` is returned. If
1449
+ ``target`` is specified but ``source`` is not, this argument is
1450
+ ignored.
1451
+
1452
+ importance_factor : float
1453
+ The relative importance of indirect neighbors with respect to
1454
+ direct neighbors.
1455
+
1456
+ max_iterations : integer
1457
+ Maximum number of iterations.
1458
+
1459
+ tolerance : float
1460
+ Error tolerance used to check convergence. When an iteration of
1461
+ the algorithm finds that no similarity value changes more than
1462
+ this amount, the algorithm halts.
1463
+
1464
+ Returns
1465
+ -------
1466
+ similarity : numpy array or float
1467
+ If ``source`` and ``target`` are both ``None``, this returns a
1468
+ 2D array containing SimRank scores of the nodes.
1469
+
1470
+ If ``source`` is not ``None`` but ``target`` is, this returns an
1471
+ 1D array containing SimRank scores of ``source`` and that
1472
+ node.
1473
+
1474
+ If neither ``source`` nor ``target`` is ``None``, this returns
1475
+ the similarity value for the given pair of nodes.
1476
+
1477
+ Examples
1478
+ --------
1479
+ >>> G = nx.cycle_graph(2)
1480
+ >>> nx.similarity._simrank_similarity_numpy(G)
1481
+ array([[1., 0.],
1482
+ [0., 1.]])
1483
+ >>> nx.similarity._simrank_similarity_numpy(G, source=0)
1484
+ array([1., 0.])
1485
+ >>> nx.similarity._simrank_similarity_numpy(G, source=0, target=0)
1486
+ 1.0
1487
+
1488
+ References
1489
+ ----------
1490
+ .. [1] G. Jeh and J. Widom.
1491
+ "SimRank: a measure of structural-context similarity",
1492
+ In KDD'02: Proceedings of the Eighth ACM SIGKDD
1493
+ International Conference on Knowledge Discovery and Data Mining,
1494
+ pp. 538--543. ACM Press, 2002.
1495
+ """
1496
+ # This algorithm follows roughly
1497
+ #
1498
+ # S = max{C * (A.T * S * A), I}
1499
+ #
1500
+ # where C is the importance factor, A is the column normalized
1501
+ # adjacency matrix, and I is the identity matrix.
1502
+ import numpy as np
1503
+
1504
+ adjacency_matrix = nx.to_numpy_array(G)
1505
+
1506
+ # column-normalize the ``adjacency_matrix``
1507
+ s = np.array(adjacency_matrix.sum(axis=0))
1508
+ s[s == 0] = 1
1509
+ adjacency_matrix /= s # adjacency_matrix.sum(axis=0)
1510
+
1511
+ newsim = np.eye(len(G), dtype=np.float64)
1512
+ for its in range(max_iterations):
1513
+ prevsim = newsim.copy()
1514
+ newsim = importance_factor * ((adjacency_matrix.T @ prevsim) @ adjacency_matrix)
1515
+ np.fill_diagonal(newsim, 1.0)
1516
+
1517
+ if np.allclose(prevsim, newsim, atol=tolerance):
1518
+ break
1519
+
1520
+ if its + 1 == max_iterations:
1521
+ raise nx.ExceededMaxIterations(
1522
+ f"simrank did not converge after {max_iterations} iterations."
1523
+ )
1524
+
1525
+ if source is not None and target is not None:
1526
+ return float(newsim[source, target])
1527
+ if source is not None:
1528
+ return newsim[source]
1529
+ return newsim
1530
+
1531
+
1532
+ @nx._dispatchable(edge_attrs="weight")
1533
+ def panther_similarity(
1534
+ G, source, k=5, path_length=5, c=0.5, delta=0.1, eps=None, weight="weight"
1535
+ ):
1536
+ r"""Returns the Panther similarity of nodes in the graph `G` to node ``v``.
1537
+
1538
+ Panther is a similarity metric that says "two objects are considered
1539
+ to be similar if they frequently appear on the same paths." [1]_.
1540
+
1541
+ Parameters
1542
+ ----------
1543
+ G : NetworkX graph
1544
+ A NetworkX graph
1545
+ source : node
1546
+ Source node for which to find the top `k` similar other nodes
1547
+ k : int (default = 5)
1548
+ The number of most similar nodes to return.
1549
+ path_length : int (default = 5)
1550
+ How long the randomly generated paths should be (``T`` in [1]_)
1551
+ c : float (default = 0.5)
1552
+ A universal positive constant used to scale the number
1553
+ of sample random paths to generate.
1554
+ delta : float (default = 0.1)
1555
+ The probability that the similarity $S$ is not an epsilon-approximation to (R, phi),
1556
+ where $R$ is the number of random paths and $\phi$ is the probability
1557
+ that an element sampled from a set $A \subseteq D$, where $D$ is the domain.
1558
+ eps : float or None (default = None)
1559
+ The error bound. Per [1]_, a good value is ``sqrt(1/|E|)``. Therefore,
1560
+ if no value is provided, the recommended computed value will be used.
1561
+ weight : string or None, optional (default="weight")
1562
+ The name of an edge attribute that holds the numerical value
1563
+ used as a weight. If None then each edge has weight 1.
1564
+
1565
+ Returns
1566
+ -------
1567
+ similarity : dictionary
1568
+ Dictionary of nodes to similarity scores (as floats). Note:
1569
+ the self-similarity (i.e., ``v``) will not be included in
1570
+ the returned dictionary. So, for ``k = 5``, a dictionary of
1571
+ top 4 nodes and their similarity scores will be returned.
1572
+
1573
+ Raises
1574
+ ------
1575
+ NetworkXUnfeasible
1576
+ If `source` is an isolated node.
1577
+
1578
+ NodeNotFound
1579
+ If `source` is not in `G`.
1580
+
1581
+ Notes
1582
+ -----
1583
+ The isolated nodes in `G` are ignored.
1584
+
1585
+ Examples
1586
+ --------
1587
+ >>> G = nx.star_graph(10)
1588
+ >>> sim = nx.panther_similarity(G, 0)
1589
+
1590
+ References
1591
+ ----------
1592
+ .. [1] Zhang, J., Tang, J., Ma, C., Tong, H., Jing, Y., & Li, J.
1593
+ Panther: Fast top-k similarity search on large networks.
1594
+ In Proceedings of the ACM SIGKDD International Conference
1595
+ on Knowledge Discovery and Data Mining (Vol. 2015-August, pp. 1445–1454).
1596
+ Association for Computing Machinery. https://doi.org/10.1145/2783258.2783267.
1597
+ """
1598
+ import numpy as np
1599
+
1600
+ if source not in G:
1601
+ raise nx.NodeNotFound(f"Source node {source} not in G")
1602
+
1603
+ isolates = set(nx.isolates(G))
1604
+
1605
+ if source in isolates:
1606
+ raise nx.NetworkXUnfeasible(
1607
+ f"Panther similarity is not defined for the isolated source node {source}."
1608
+ )
1609
+
1610
+ G = G.subgraph([node for node in G.nodes if node not in isolates]).copy()
1611
+
1612
+ num_nodes = G.number_of_nodes()
1613
+ if num_nodes < k:
1614
+ warnings.warn(
1615
+ f"Number of nodes is {num_nodes}, but requested k is {k}. "
1616
+ "Setting k to number of nodes."
1617
+ )
1618
+ k = num_nodes
1619
+ # According to [1], they empirically determined
1620
+ # a good value for ``eps`` to be sqrt( 1 / |E| )
1621
+ if eps is None:
1622
+ eps = np.sqrt(1.0 / G.number_of_edges())
1623
+
1624
+ inv_node_map = {name: index for index, name in enumerate(G.nodes)}
1625
+ node_map = np.array(G)
1626
+
1627
+ # Calculate the sample size ``R`` for how many paths
1628
+ # to randomly generate
1629
+ t_choose_2 = math.comb(path_length, 2)
1630
+ sample_size = int((c / eps**2) * (np.log2(t_choose_2) + 1 + np.log(1 / delta)))
1631
+ index_map = {}
1632
+ _ = list(
1633
+ generate_random_paths(
1634
+ G, sample_size, path_length=path_length, index_map=index_map, weight=weight
1635
+ )
1636
+ )
1637
+ S = np.zeros(num_nodes)
1638
+
1639
+ inv_sample_size = 1 / sample_size
1640
+
1641
+ source_paths = set(index_map[source])
1642
+
1643
+ # Calculate the path similarities
1644
+ # between ``source`` (v) and ``node`` (v_j)
1645
+ # using our inverted index mapping of
1646
+ # vertices to paths
1647
+ for node, paths in index_map.items():
1648
+ # Only consider paths where both
1649
+ # ``node`` and ``source`` are present
1650
+ common_paths = source_paths.intersection(paths)
1651
+ S[inv_node_map[node]] = len(common_paths) * inv_sample_size
1652
+
1653
+ # Retrieve top ``k`` similar
1654
+ # Note: the below performed anywhere from 4-10x faster
1655
+ # (depending on input sizes) vs the equivalent ``np.argsort(S)[::-1]``
1656
+ top_k_unsorted = np.argpartition(S, -k)[-k:]
1657
+ top_k_sorted = top_k_unsorted[np.argsort(S[top_k_unsorted])][::-1]
1658
+
1659
+ # Add back the similarity scores
1660
+ top_k_with_val = dict(
1661
+ zip(node_map[top_k_sorted].tolist(), S[top_k_sorted].tolist())
1662
+ )
1663
+
1664
+ # Remove the self-similarity
1665
+ top_k_with_val.pop(source, None)
1666
+ return top_k_with_val
1667
+
1668
+
1669
+ @np_random_state(5)
1670
+ @nx._dispatchable(edge_attrs="weight")
1671
+ def generate_random_paths(
1672
+ G, sample_size, path_length=5, index_map=None, weight="weight", seed=None
1673
+ ):
1674
+ """Randomly generate `sample_size` paths of length `path_length`.
1675
+
1676
+ Parameters
1677
+ ----------
1678
+ G : NetworkX graph
1679
+ A NetworkX graph
1680
+ sample_size : integer
1681
+ The number of paths to generate. This is ``R`` in [1]_.
1682
+ path_length : integer (default = 5)
1683
+ The maximum size of the path to randomly generate.
1684
+ This is ``T`` in [1]_. According to the paper, ``T >= 5`` is
1685
+ recommended.
1686
+ index_map : dictionary, optional
1687
+ If provided, this will be populated with the inverted
1688
+ index of nodes mapped to the set of generated random path
1689
+ indices within ``paths``.
1690
+ weight : string or None, optional (default="weight")
1691
+ The name of an edge attribute that holds the numerical value
1692
+ used as a weight. If None then each edge has weight 1.
1693
+ seed : integer, random_state, or None (default)
1694
+ Indicator of random number generation state.
1695
+ See :ref:`Randomness<randomness>`.
1696
+
1697
+ Returns
1698
+ -------
1699
+ paths : generator of lists
1700
+ Generator of `sample_size` paths each with length `path_length`.
1701
+
1702
+ Examples
1703
+ --------
1704
+ Note that the return value is the list of paths:
1705
+
1706
+ >>> G = nx.star_graph(3)
1707
+ >>> random_path = nx.generate_random_paths(G, 2)
1708
+
1709
+ By passing a dictionary into `index_map`, it will build an
1710
+ inverted index mapping of nodes to the paths in which that node is present:
1711
+
1712
+ >>> G = nx.star_graph(3)
1713
+ >>> index_map = {}
1714
+ >>> random_path = nx.generate_random_paths(G, 3, index_map=index_map)
1715
+ >>> paths_containing_node_0 = [
1716
+ ... random_path[path_idx] for path_idx in index_map.get(0, [])
1717
+ ... ]
1718
+
1719
+ References
1720
+ ----------
1721
+ .. [1] Zhang, J., Tang, J., Ma, C., Tong, H., Jing, Y., & Li, J.
1722
+ Panther: Fast top-k similarity search on large networks.
1723
+ In Proceedings of the ACM SIGKDD International Conference
1724
+ on Knowledge Discovery and Data Mining (Vol. 2015-August, pp. 1445–1454).
1725
+ Association for Computing Machinery. https://doi.org/10.1145/2783258.2783267.
1726
+ """
1727
+ import numpy as np
1728
+
1729
+ randint_fn = (
1730
+ seed.integers if isinstance(seed, np.random.Generator) else seed.randint
1731
+ )
1732
+
1733
+ # Calculate transition probabilities between
1734
+ # every pair of vertices according to Eq. (3)
1735
+ adj_mat = nx.to_numpy_array(G, weight=weight)
1736
+ inv_row_sums = np.reciprocal(adj_mat.sum(axis=1)).reshape(-1, 1)
1737
+ transition_probabilities = adj_mat * inv_row_sums
1738
+
1739
+ node_map = list(G)
1740
+ num_nodes = G.number_of_nodes()
1741
+
1742
+ for path_index in range(sample_size):
1743
+ # Sample current vertex v = v_i uniformly at random
1744
+ node_index = randint_fn(num_nodes)
1745
+ node = node_map[node_index]
1746
+
1747
+ # Add v into p_r and add p_r into the path set
1748
+ # of v, i.e., P_v
1749
+ path = [node]
1750
+
1751
+ # Build the inverted index (P_v) of vertices to paths
1752
+ if index_map is not None:
1753
+ if node in index_map:
1754
+ index_map[node].add(path_index)
1755
+ else:
1756
+ index_map[node] = {path_index}
1757
+
1758
+ starting_index = node_index
1759
+ for _ in range(path_length):
1760
+ # Randomly sample a neighbor (v_j) according
1761
+ # to transition probabilities from ``node`` (v) to its neighbors
1762
+ nbr_index = seed.choice(
1763
+ num_nodes, p=transition_probabilities[starting_index]
1764
+ )
1765
+
1766
+ # Set current vertex (v = v_j)
1767
+ starting_index = nbr_index
1768
+
1769
+ # Add v into p_r
1770
+ nbr_node = node_map[nbr_index]
1771
+ path.append(nbr_node)
1772
+
1773
+ # Add p_r into P_v
1774
+ if index_map is not None:
1775
+ if nbr_node in index_map:
1776
+ index_map[nbr_node].add(path_index)
1777
+ else:
1778
+ index_map[nbr_node] = {path_index}
1779
+
1780
+ yield path
.venv/lib/python3.11/site-packages/networkx/algorithms/simple_paths.py ADDED
@@ -0,0 +1,950 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from heapq import heappop, heappush
2
+ from itertools import count
3
+
4
+ import networkx as nx
5
+ from networkx.algorithms.shortest_paths.weighted import _weight_function
6
+ from networkx.utils import not_implemented_for, pairwise
7
+
8
+ __all__ = [
9
+ "all_simple_paths",
10
+ "is_simple_path",
11
+ "shortest_simple_paths",
12
+ "all_simple_edge_paths",
13
+ ]
14
+
15
+
16
+ @nx._dispatchable
17
+ def is_simple_path(G, nodes):
18
+ """Returns True if and only if `nodes` form a simple path in `G`.
19
+
20
+ A *simple path* in a graph is a nonempty sequence of nodes in which
21
+ no node appears more than once in the sequence, and each adjacent
22
+ pair of nodes in the sequence is adjacent in the graph.
23
+
24
+ Parameters
25
+ ----------
26
+ G : graph
27
+ A NetworkX graph.
28
+ nodes : list
29
+ A list of one or more nodes in the graph `G`.
30
+
31
+ Returns
32
+ -------
33
+ bool
34
+ Whether the given list of nodes represents a simple path in `G`.
35
+
36
+ Notes
37
+ -----
38
+ An empty list of nodes is not a path but a list of one node is a
39
+ path. Here's an explanation why.
40
+
41
+ This function operates on *node paths*. One could also consider
42
+ *edge paths*. There is a bijection between node paths and edge
43
+ paths.
44
+
45
+ The *length of a path* is the number of edges in the path, so a list
46
+ of nodes of length *n* corresponds to a path of length *n* - 1.
47
+ Thus the smallest edge path would be a list of zero edges, the empty
48
+ path. This corresponds to a list of one node.
49
+
50
+ To convert between a node path and an edge path, you can use code
51
+ like the following::
52
+
53
+ >>> from networkx.utils import pairwise
54
+ >>> nodes = [0, 1, 2, 3]
55
+ >>> edges = list(pairwise(nodes))
56
+ >>> edges
57
+ [(0, 1), (1, 2), (2, 3)]
58
+ >>> nodes = [edges[0][0]] + [v for u, v in edges]
59
+ >>> nodes
60
+ [0, 1, 2, 3]
61
+
62
+ Examples
63
+ --------
64
+ >>> G = nx.cycle_graph(4)
65
+ >>> nx.is_simple_path(G, [2, 3, 0])
66
+ True
67
+ >>> nx.is_simple_path(G, [0, 2])
68
+ False
69
+
70
+ """
71
+ # The empty list is not a valid path. Could also return
72
+ # NetworkXPointlessConcept here.
73
+ if len(nodes) == 0:
74
+ return False
75
+
76
+ # If the list is a single node, just check that the node is actually
77
+ # in the graph.
78
+ if len(nodes) == 1:
79
+ return nodes[0] in G
80
+
81
+ # check that all nodes in the list are in the graph, if at least one
82
+ # is not in the graph, then this is not a simple path
83
+ if not all(n in G for n in nodes):
84
+ return False
85
+
86
+ # If the list contains repeated nodes, then it's not a simple path
87
+ if len(set(nodes)) != len(nodes):
88
+ return False
89
+
90
+ # Test that each adjacent pair of nodes is adjacent.
91
+ return all(v in G[u] for u, v in pairwise(nodes))
92
+
93
+
94
+ @nx._dispatchable
95
+ def all_simple_paths(G, source, target, cutoff=None):
96
+ """Generate all simple paths in the graph G from source to target.
97
+
98
+ A simple path is a path with no repeated nodes.
99
+
100
+ Parameters
101
+ ----------
102
+ G : NetworkX graph
103
+
104
+ source : node
105
+ Starting node for path
106
+
107
+ target : nodes
108
+ Single node or iterable of nodes at which to end path
109
+
110
+ cutoff : integer, optional
111
+ Depth to stop the search. Only paths of length <= cutoff are returned.
112
+
113
+ Returns
114
+ -------
115
+ path_generator: generator
116
+ A generator that produces lists of simple paths. If there are no paths
117
+ between the source and target within the given cutoff the generator
118
+ produces no output. If it is possible to traverse the same sequence of
119
+ nodes in multiple ways, namely through parallel edges, then it will be
120
+ returned multiple times (once for each viable edge combination).
121
+
122
+ Examples
123
+ --------
124
+ This iterator generates lists of nodes::
125
+
126
+ >>> G = nx.complete_graph(4)
127
+ >>> for path in nx.all_simple_paths(G, source=0, target=3):
128
+ ... print(path)
129
+ ...
130
+ [0, 1, 2, 3]
131
+ [0, 1, 3]
132
+ [0, 2, 1, 3]
133
+ [0, 2, 3]
134
+ [0, 3]
135
+
136
+ You can generate only those paths that are shorter than a certain
137
+ length by using the `cutoff` keyword argument::
138
+
139
+ >>> paths = nx.all_simple_paths(G, source=0, target=3, cutoff=2)
140
+ >>> print(list(paths))
141
+ [[0, 1, 3], [0, 2, 3], [0, 3]]
142
+
143
+ To get each path as the corresponding list of edges, you can use the
144
+ :func:`networkx.utils.pairwise` helper function::
145
+
146
+ >>> paths = nx.all_simple_paths(G, source=0, target=3)
147
+ >>> for path in map(nx.utils.pairwise, paths):
148
+ ... print(list(path))
149
+ [(0, 1), (1, 2), (2, 3)]
150
+ [(0, 1), (1, 3)]
151
+ [(0, 2), (2, 1), (1, 3)]
152
+ [(0, 2), (2, 3)]
153
+ [(0, 3)]
154
+
155
+ Pass an iterable of nodes as target to generate all paths ending in any of several nodes::
156
+
157
+ >>> G = nx.complete_graph(4)
158
+ >>> for path in nx.all_simple_paths(G, source=0, target=[3, 2]):
159
+ ... print(path)
160
+ ...
161
+ [0, 1, 2]
162
+ [0, 1, 2, 3]
163
+ [0, 1, 3]
164
+ [0, 1, 3, 2]
165
+ [0, 2]
166
+ [0, 2, 1, 3]
167
+ [0, 2, 3]
168
+ [0, 3]
169
+ [0, 3, 1, 2]
170
+ [0, 3, 2]
171
+
172
+ The singleton path from ``source`` to itself is considered a simple path and is
173
+ included in the results:
174
+
175
+ >>> G = nx.empty_graph(5)
176
+ >>> list(nx.all_simple_paths(G, source=0, target=0))
177
+ [[0]]
178
+
179
+ >>> G = nx.path_graph(3)
180
+ >>> list(nx.all_simple_paths(G, source=0, target={0, 1, 2}))
181
+ [[0], [0, 1], [0, 1, 2]]
182
+
183
+ Iterate over each path from the root nodes to the leaf nodes in a
184
+ directed acyclic graph using a functional programming approach::
185
+
186
+ >>> from itertools import chain
187
+ >>> from itertools import product
188
+ >>> from itertools import starmap
189
+ >>> from functools import partial
190
+ >>>
191
+ >>> chaini = chain.from_iterable
192
+ >>>
193
+ >>> G = nx.DiGraph([(0, 1), (1, 2), (0, 3), (3, 2)])
194
+ >>> roots = (v for v, d in G.in_degree() if d == 0)
195
+ >>> leaves = (v for v, d in G.out_degree() if d == 0)
196
+ >>> all_paths = partial(nx.all_simple_paths, G)
197
+ >>> list(chaini(starmap(all_paths, product(roots, leaves))))
198
+ [[0, 1, 2], [0, 3, 2]]
199
+
200
+ The same list computed using an iterative approach::
201
+
202
+ >>> G = nx.DiGraph([(0, 1), (1, 2), (0, 3), (3, 2)])
203
+ >>> roots = (v for v, d in G.in_degree() if d == 0)
204
+ >>> leaves = (v for v, d in G.out_degree() if d == 0)
205
+ >>> all_paths = []
206
+ >>> for root in roots:
207
+ ... for leaf in leaves:
208
+ ... paths = nx.all_simple_paths(G, root, leaf)
209
+ ... all_paths.extend(paths)
210
+ >>> all_paths
211
+ [[0, 1, 2], [0, 3, 2]]
212
+
213
+ Iterate over each path from the root nodes to the leaf nodes in a
214
+ directed acyclic graph passing all leaves together to avoid unnecessary
215
+ compute::
216
+
217
+ >>> G = nx.DiGraph([(0, 1), (2, 1), (1, 3), (1, 4)])
218
+ >>> roots = (v for v, d in G.in_degree() if d == 0)
219
+ >>> leaves = [v for v, d in G.out_degree() if d == 0]
220
+ >>> all_paths = []
221
+ >>> for root in roots:
222
+ ... paths = nx.all_simple_paths(G, root, leaves)
223
+ ... all_paths.extend(paths)
224
+ >>> all_paths
225
+ [[0, 1, 3], [0, 1, 4], [2, 1, 3], [2, 1, 4]]
226
+
227
+ If parallel edges offer multiple ways to traverse a given sequence of
228
+ nodes, this sequence of nodes will be returned multiple times:
229
+
230
+ >>> G = nx.MultiDiGraph([(0, 1), (0, 1), (1, 2)])
231
+ >>> list(nx.all_simple_paths(G, 0, 2))
232
+ [[0, 1, 2], [0, 1, 2]]
233
+
234
+ Notes
235
+ -----
236
+ This algorithm uses a modified depth-first search to generate the
237
+ paths [1]_. A single path can be found in $O(V+E)$ time but the
238
+ number of simple paths in a graph can be very large, e.g. $O(n!)$ in
239
+ the complete graph of order $n$.
240
+
241
+ This function does not check that a path exists between `source` and
242
+ `target`. For large graphs, this may result in very long runtimes.
243
+ Consider using `has_path` to check that a path exists between `source` and
244
+ `target` before calling this function on large graphs.
245
+
246
+ References
247
+ ----------
248
+ .. [1] R. Sedgewick, "Algorithms in C, Part 5: Graph Algorithms",
249
+ Addison Wesley Professional, 3rd ed., 2001.
250
+
251
+ See Also
252
+ --------
253
+ all_shortest_paths, shortest_path, has_path
254
+
255
+ """
256
+ for edge_path in all_simple_edge_paths(G, source, target, cutoff):
257
+ yield [source] + [edge[1] for edge in edge_path]
258
+
259
+
260
+ @nx._dispatchable
261
+ def all_simple_edge_paths(G, source, target, cutoff=None):
262
+ """Generate lists of edges for all simple paths in G from source to target.
263
+
264
+ A simple path is a path with no repeated nodes.
265
+
266
+ Parameters
267
+ ----------
268
+ G : NetworkX graph
269
+
270
+ source : node
271
+ Starting node for path
272
+
273
+ target : nodes
274
+ Single node or iterable of nodes at which to end path
275
+
276
+ cutoff : integer, optional
277
+ Depth to stop the search. Only paths of length <= cutoff are returned.
278
+
279
+ Returns
280
+ -------
281
+ path_generator: generator
282
+ A generator that produces lists of simple paths. If there are no paths
283
+ between the source and target within the given cutoff the generator
284
+ produces no output.
285
+ For multigraphs, the list of edges have elements of the form `(u,v,k)`.
286
+ Where `k` corresponds to the edge key.
287
+
288
+ Examples
289
+ --------
290
+
291
+ Print the simple path edges of a Graph::
292
+
293
+ >>> g = nx.Graph([(1, 2), (2, 4), (1, 3), (3, 4)])
294
+ >>> for path in sorted(nx.all_simple_edge_paths(g, 1, 4)):
295
+ ... print(path)
296
+ [(1, 2), (2, 4)]
297
+ [(1, 3), (3, 4)]
298
+
299
+ Print the simple path edges of a MultiGraph. Returned edges come with
300
+ their associated keys::
301
+
302
+ >>> mg = nx.MultiGraph()
303
+ >>> mg.add_edge(1, 2, key="k0")
304
+ 'k0'
305
+ >>> mg.add_edge(1, 2, key="k1")
306
+ 'k1'
307
+ >>> mg.add_edge(2, 3, key="k0")
308
+ 'k0'
309
+ >>> for path in sorted(nx.all_simple_edge_paths(mg, 1, 3)):
310
+ ... print(path)
311
+ [(1, 2, 'k0'), (2, 3, 'k0')]
312
+ [(1, 2, 'k1'), (2, 3, 'k0')]
313
+
314
+ When ``source`` is one of the targets, the empty path starting and ending at
315
+ ``source`` without traversing any edge is considered a valid simple edge path
316
+ and is included in the results:
317
+
318
+ >>> G = nx.Graph()
319
+ >>> G.add_node(0)
320
+ >>> paths = list(nx.all_simple_edge_paths(G, 0, 0))
321
+ >>> for path in paths:
322
+ ... print(path)
323
+ []
324
+ >>> len(paths)
325
+ 1
326
+
327
+
328
+ Notes
329
+ -----
330
+ This algorithm uses a modified depth-first search to generate the
331
+ paths [1]_. A single path can be found in $O(V+E)$ time but the
332
+ number of simple paths in a graph can be very large, e.g. $O(n!)$ in
333
+ the complete graph of order $n$.
334
+
335
+ References
336
+ ----------
337
+ .. [1] R. Sedgewick, "Algorithms in C, Part 5: Graph Algorithms",
338
+ Addison Wesley Professional, 3rd ed., 2001.
339
+
340
+ See Also
341
+ --------
342
+ all_shortest_paths, shortest_path, all_simple_paths
343
+
344
+ """
345
+ if source not in G:
346
+ raise nx.NodeNotFound(f"source node {source} not in graph")
347
+
348
+ if target in G:
349
+ targets = {target}
350
+ else:
351
+ try:
352
+ targets = set(target)
353
+ except TypeError as err:
354
+ raise nx.NodeNotFound(f"target node {target} not in graph") from err
355
+
356
+ cutoff = cutoff if cutoff is not None else len(G) - 1
357
+
358
+ if cutoff >= 0 and targets:
359
+ yield from _all_simple_edge_paths(G, source, targets, cutoff)
360
+
361
+
362
+ def _all_simple_edge_paths(G, source, targets, cutoff):
363
+ # We simulate recursion with a stack, keeping the current path being explored
364
+ # and the outgoing edge iterators at each point in the stack.
365
+ # To avoid unnecessary checks, the loop is structured in a way such that a path
366
+ # is considered for yielding only after a new node/edge is added.
367
+ # We bootstrap the search by adding a dummy iterator to the stack that only yields
368
+ # a dummy edge to source (so that the trivial path has a chance of being included).
369
+
370
+ get_edges = (
371
+ (lambda node: G.edges(node, keys=True))
372
+ if G.is_multigraph()
373
+ else (lambda node: G.edges(node))
374
+ )
375
+
376
+ # The current_path is a dictionary that maps nodes in the path to the edge that was
377
+ # used to enter that node (instead of a list of edges) because we want both a fast
378
+ # membership test for nodes in the path and the preservation of insertion order.
379
+ current_path = {None: None}
380
+ stack = [iter([(None, source)])]
381
+
382
+ while stack:
383
+ # 1. Try to extend the current path.
384
+ next_edge = next((e for e in stack[-1] if e[1] not in current_path), None)
385
+ if next_edge is None:
386
+ # All edges of the last node in the current path have been explored.
387
+ stack.pop()
388
+ current_path.popitem()
389
+ continue
390
+ previous_node, next_node, *_ = next_edge
391
+
392
+ # 2. Check if we've reached a target.
393
+ if next_node in targets:
394
+ yield (list(current_path.values()) + [next_edge])[2:] # remove dummy edge
395
+
396
+ # 3. Only expand the search through the next node if it makes sense.
397
+ if len(current_path) - 1 < cutoff and (
398
+ targets - current_path.keys() - {next_node}
399
+ ):
400
+ current_path[next_node] = next_edge
401
+ stack.append(iter(get_edges(next_node)))
402
+
403
+
404
+ @not_implemented_for("multigraph")
405
+ @nx._dispatchable(edge_attrs="weight")
406
+ def shortest_simple_paths(G, source, target, weight=None):
407
+ """Generate all simple paths in the graph G from source to target,
408
+ starting from shortest ones.
409
+
410
+ A simple path is a path with no repeated nodes.
411
+
412
+ If a weighted shortest path search is to be used, no negative weights
413
+ are allowed.
414
+
415
+ Parameters
416
+ ----------
417
+ G : NetworkX graph
418
+
419
+ source : node
420
+ Starting node for path
421
+
422
+ target : node
423
+ Ending node for path
424
+
425
+ weight : string or function
426
+ If it is a string, it is the name of the edge attribute to be
427
+ used as a weight.
428
+
429
+ If it is a function, the weight of an edge is the value returned
430
+ by the function. The function must accept exactly three positional
431
+ arguments: the two endpoints of an edge and the dictionary of edge
432
+ attributes for that edge. The function must return a number or None.
433
+ The weight function can be used to hide edges by returning None.
434
+ So ``weight = lambda u, v, d: 1 if d['color']=="red" else None``
435
+ will find the shortest red path.
436
+
437
+ If None all edges are considered to have unit weight. Default
438
+ value None.
439
+
440
+ Returns
441
+ -------
442
+ path_generator: generator
443
+ A generator that produces lists of simple paths, in order from
444
+ shortest to longest.
445
+
446
+ Raises
447
+ ------
448
+ NetworkXNoPath
449
+ If no path exists between source and target.
450
+
451
+ NetworkXError
452
+ If source or target nodes are not in the input graph.
453
+
454
+ NetworkXNotImplemented
455
+ If the input graph is a Multi[Di]Graph.
456
+
457
+ Examples
458
+ --------
459
+
460
+ >>> G = nx.cycle_graph(7)
461
+ >>> paths = list(nx.shortest_simple_paths(G, 0, 3))
462
+ >>> print(paths)
463
+ [[0, 1, 2, 3], [0, 6, 5, 4, 3]]
464
+
465
+ You can use this function to efficiently compute the k shortest/best
466
+ paths between two nodes.
467
+
468
+ >>> from itertools import islice
469
+ >>> def k_shortest_paths(G, source, target, k, weight=None):
470
+ ... return list(
471
+ ... islice(nx.shortest_simple_paths(G, source, target, weight=weight), k)
472
+ ... )
473
+ >>> for path in k_shortest_paths(G, 0, 3, 2):
474
+ ... print(path)
475
+ [0, 1, 2, 3]
476
+ [0, 6, 5, 4, 3]
477
+
478
+ Notes
479
+ -----
480
+ This procedure is based on algorithm by Jin Y. Yen [1]_. Finding
481
+ the first $K$ paths requires $O(KN^3)$ operations.
482
+
483
+ See Also
484
+ --------
485
+ all_shortest_paths
486
+ shortest_path
487
+ all_simple_paths
488
+
489
+ References
490
+ ----------
491
+ .. [1] Jin Y. Yen, "Finding the K Shortest Loopless Paths in a
492
+ Network", Management Science, Vol. 17, No. 11, Theory Series
493
+ (Jul., 1971), pp. 712-716.
494
+
495
+ """
496
+ if source not in G:
497
+ raise nx.NodeNotFound(f"source node {source} not in graph")
498
+
499
+ if target not in G:
500
+ raise nx.NodeNotFound(f"target node {target} not in graph")
501
+
502
+ if weight is None:
503
+ length_func = len
504
+ shortest_path_func = _bidirectional_shortest_path
505
+ else:
506
+ wt = _weight_function(G, weight)
507
+
508
+ def length_func(path):
509
+ return sum(
510
+ wt(u, v, G.get_edge_data(u, v)) for (u, v) in zip(path, path[1:])
511
+ )
512
+
513
+ shortest_path_func = _bidirectional_dijkstra
514
+
515
+ listA = []
516
+ listB = PathBuffer()
517
+ prev_path = None
518
+ while True:
519
+ if not prev_path:
520
+ length, path = shortest_path_func(G, source, target, weight=weight)
521
+ listB.push(length, path)
522
+ else:
523
+ ignore_nodes = set()
524
+ ignore_edges = set()
525
+ for i in range(1, len(prev_path)):
526
+ root = prev_path[:i]
527
+ root_length = length_func(root)
528
+ for path in listA:
529
+ if path[:i] == root:
530
+ ignore_edges.add((path[i - 1], path[i]))
531
+ try:
532
+ length, spur = shortest_path_func(
533
+ G,
534
+ root[-1],
535
+ target,
536
+ ignore_nodes=ignore_nodes,
537
+ ignore_edges=ignore_edges,
538
+ weight=weight,
539
+ )
540
+ path = root[:-1] + spur
541
+ listB.push(root_length + length, path)
542
+ except nx.NetworkXNoPath:
543
+ pass
544
+ ignore_nodes.add(root[-1])
545
+
546
+ if listB:
547
+ path = listB.pop()
548
+ yield path
549
+ listA.append(path)
550
+ prev_path = path
551
+ else:
552
+ break
553
+
554
+
555
+ class PathBuffer:
556
+ def __init__(self):
557
+ self.paths = set()
558
+ self.sortedpaths = []
559
+ self.counter = count()
560
+
561
+ def __len__(self):
562
+ return len(self.sortedpaths)
563
+
564
+ def push(self, cost, path):
565
+ hashable_path = tuple(path)
566
+ if hashable_path not in self.paths:
567
+ heappush(self.sortedpaths, (cost, next(self.counter), path))
568
+ self.paths.add(hashable_path)
569
+
570
+ def pop(self):
571
+ (cost, num, path) = heappop(self.sortedpaths)
572
+ hashable_path = tuple(path)
573
+ self.paths.remove(hashable_path)
574
+ return path
575
+
576
+
577
+ def _bidirectional_shortest_path(
578
+ G, source, target, ignore_nodes=None, ignore_edges=None, weight=None
579
+ ):
580
+ """Returns the shortest path between source and target ignoring
581
+ nodes and edges in the containers ignore_nodes and ignore_edges.
582
+
583
+ This is a custom modification of the standard bidirectional shortest
584
+ path implementation at networkx.algorithms.unweighted
585
+
586
+ Parameters
587
+ ----------
588
+ G : NetworkX graph
589
+
590
+ source : node
591
+ starting node for path
592
+
593
+ target : node
594
+ ending node for path
595
+
596
+ ignore_nodes : container of nodes
597
+ nodes to ignore, optional
598
+
599
+ ignore_edges : container of edges
600
+ edges to ignore, optional
601
+
602
+ weight : None
603
+ This function accepts a weight argument for convenience of
604
+ shortest_simple_paths function. It will be ignored.
605
+
606
+ Returns
607
+ -------
608
+ path: list
609
+ List of nodes in a path from source to target.
610
+
611
+ Raises
612
+ ------
613
+ NetworkXNoPath
614
+ If no path exists between source and target.
615
+
616
+ See Also
617
+ --------
618
+ shortest_path
619
+
620
+ """
621
+ # call helper to do the real work
622
+ results = _bidirectional_pred_succ(G, source, target, ignore_nodes, ignore_edges)
623
+ pred, succ, w = results
624
+
625
+ # build path from pred+w+succ
626
+ path = []
627
+ # from w to target
628
+ while w is not None:
629
+ path.append(w)
630
+ w = succ[w]
631
+ # from source to w
632
+ w = pred[path[0]]
633
+ while w is not None:
634
+ path.insert(0, w)
635
+ w = pred[w]
636
+
637
+ return len(path), path
638
+
639
+
640
+ def _bidirectional_pred_succ(G, source, target, ignore_nodes=None, ignore_edges=None):
641
+ """Bidirectional shortest path helper.
642
+ Returns (pred,succ,w) where
643
+ pred is a dictionary of predecessors from w to the source, and
644
+ succ is a dictionary of successors from w to the target.
645
+ """
646
+ # does BFS from both source and target and meets in the middle
647
+ if ignore_nodes and (source in ignore_nodes or target in ignore_nodes):
648
+ raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
649
+ if target == source:
650
+ return ({target: None}, {source: None}, source)
651
+
652
+ # handle either directed or undirected
653
+ if G.is_directed():
654
+ Gpred = G.predecessors
655
+ Gsucc = G.successors
656
+ else:
657
+ Gpred = G.neighbors
658
+ Gsucc = G.neighbors
659
+
660
+ # support optional nodes filter
661
+ if ignore_nodes:
662
+
663
+ def filter_iter(nodes):
664
+ def iterate(v):
665
+ for w in nodes(v):
666
+ if w not in ignore_nodes:
667
+ yield w
668
+
669
+ return iterate
670
+
671
+ Gpred = filter_iter(Gpred)
672
+ Gsucc = filter_iter(Gsucc)
673
+
674
+ # support optional edges filter
675
+ if ignore_edges:
676
+ if G.is_directed():
677
+
678
+ def filter_pred_iter(pred_iter):
679
+ def iterate(v):
680
+ for w in pred_iter(v):
681
+ if (w, v) not in ignore_edges:
682
+ yield w
683
+
684
+ return iterate
685
+
686
+ def filter_succ_iter(succ_iter):
687
+ def iterate(v):
688
+ for w in succ_iter(v):
689
+ if (v, w) not in ignore_edges:
690
+ yield w
691
+
692
+ return iterate
693
+
694
+ Gpred = filter_pred_iter(Gpred)
695
+ Gsucc = filter_succ_iter(Gsucc)
696
+
697
+ else:
698
+
699
+ def filter_iter(nodes):
700
+ def iterate(v):
701
+ for w in nodes(v):
702
+ if (v, w) not in ignore_edges and (w, v) not in ignore_edges:
703
+ yield w
704
+
705
+ return iterate
706
+
707
+ Gpred = filter_iter(Gpred)
708
+ Gsucc = filter_iter(Gsucc)
709
+
710
+ # predecessor and successors in search
711
+ pred = {source: None}
712
+ succ = {target: None}
713
+
714
+ # initialize fringes, start with forward
715
+ forward_fringe = [source]
716
+ reverse_fringe = [target]
717
+
718
+ while forward_fringe and reverse_fringe:
719
+ if len(forward_fringe) <= len(reverse_fringe):
720
+ this_level = forward_fringe
721
+ forward_fringe = []
722
+ for v in this_level:
723
+ for w in Gsucc(v):
724
+ if w not in pred:
725
+ forward_fringe.append(w)
726
+ pred[w] = v
727
+ if w in succ:
728
+ # found path
729
+ return pred, succ, w
730
+ else:
731
+ this_level = reverse_fringe
732
+ reverse_fringe = []
733
+ for v in this_level:
734
+ for w in Gpred(v):
735
+ if w not in succ:
736
+ succ[w] = v
737
+ reverse_fringe.append(w)
738
+ if w in pred:
739
+ # found path
740
+ return pred, succ, w
741
+
742
+ raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
743
+
744
+
745
+ def _bidirectional_dijkstra(
746
+ G, source, target, weight="weight", ignore_nodes=None, ignore_edges=None
747
+ ):
748
+ """Dijkstra's algorithm for shortest paths using bidirectional search.
749
+
750
+ This function returns the shortest path between source and target
751
+ ignoring nodes and edges in the containers ignore_nodes and
752
+ ignore_edges.
753
+
754
+ This is a custom modification of the standard Dijkstra bidirectional
755
+ shortest path implementation at networkx.algorithms.weighted
756
+
757
+ Parameters
758
+ ----------
759
+ G : NetworkX graph
760
+
761
+ source : node
762
+ Starting node.
763
+
764
+ target : node
765
+ Ending node.
766
+
767
+ weight: string, function, optional (default='weight')
768
+ Edge data key or weight function corresponding to the edge weight
769
+ If this is a function, the weight of an edge is the value
770
+ returned by the function. The function must accept exactly three
771
+ positional arguments: the two endpoints of an edge and the
772
+ dictionary of edge attributes for that edge. The function must
773
+ return a number or None to indicate a hidden edge.
774
+
775
+ ignore_nodes : container of nodes
776
+ nodes to ignore, optional
777
+
778
+ ignore_edges : container of edges
779
+ edges to ignore, optional
780
+
781
+ Returns
782
+ -------
783
+ length : number
784
+ Shortest path length.
785
+
786
+ Returns a tuple of two dictionaries keyed by node.
787
+ The first dictionary stores distance from the source.
788
+ The second stores the path from the source to that node.
789
+
790
+ Raises
791
+ ------
792
+ NetworkXNoPath
793
+ If no path exists between source and target.
794
+
795
+ Notes
796
+ -----
797
+ Edge weight attributes must be numerical.
798
+ Distances are calculated as sums of weighted edges traversed.
799
+
800
+ The weight function can be used to hide edges by returning None.
801
+ So ``weight = lambda u, v, d: 1 if d['color']=="red" else None``
802
+ will find the shortest red path.
803
+
804
+ In practice bidirectional Dijkstra is much more than twice as fast as
805
+ ordinary Dijkstra.
806
+
807
+ Ordinary Dijkstra expands nodes in a sphere-like manner from the
808
+ source. The radius of this sphere will eventually be the length
809
+ of the shortest path. Bidirectional Dijkstra will expand nodes
810
+ from both the source and the target, making two spheres of half
811
+ this radius. Volume of the first sphere is pi*r*r while the
812
+ others are 2*pi*r/2*r/2, making up half the volume.
813
+
814
+ This algorithm is not guaranteed to work if edge weights
815
+ are negative or are floating point numbers
816
+ (overflows and roundoff errors can cause problems).
817
+
818
+ See Also
819
+ --------
820
+ shortest_path
821
+ shortest_path_length
822
+ """
823
+ if ignore_nodes and (source in ignore_nodes or target in ignore_nodes):
824
+ raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
825
+ if source == target:
826
+ if source not in G:
827
+ raise nx.NodeNotFound(f"Node {source} not in graph")
828
+ return (0, [source])
829
+
830
+ # handle either directed or undirected
831
+ if G.is_directed():
832
+ Gpred = G.predecessors
833
+ Gsucc = G.successors
834
+ else:
835
+ Gpred = G.neighbors
836
+ Gsucc = G.neighbors
837
+
838
+ # support optional nodes filter
839
+ if ignore_nodes:
840
+
841
+ def filter_iter(nodes):
842
+ def iterate(v):
843
+ for w in nodes(v):
844
+ if w not in ignore_nodes:
845
+ yield w
846
+
847
+ return iterate
848
+
849
+ Gpred = filter_iter(Gpred)
850
+ Gsucc = filter_iter(Gsucc)
851
+
852
+ # support optional edges filter
853
+ if ignore_edges:
854
+ if G.is_directed():
855
+
856
+ def filter_pred_iter(pred_iter):
857
+ def iterate(v):
858
+ for w in pred_iter(v):
859
+ if (w, v) not in ignore_edges:
860
+ yield w
861
+
862
+ return iterate
863
+
864
+ def filter_succ_iter(succ_iter):
865
+ def iterate(v):
866
+ for w in succ_iter(v):
867
+ if (v, w) not in ignore_edges:
868
+ yield w
869
+
870
+ return iterate
871
+
872
+ Gpred = filter_pred_iter(Gpred)
873
+ Gsucc = filter_succ_iter(Gsucc)
874
+
875
+ else:
876
+
877
+ def filter_iter(nodes):
878
+ def iterate(v):
879
+ for w in nodes(v):
880
+ if (v, w) not in ignore_edges and (w, v) not in ignore_edges:
881
+ yield w
882
+
883
+ return iterate
884
+
885
+ Gpred = filter_iter(Gpred)
886
+ Gsucc = filter_iter(Gsucc)
887
+
888
+ wt = _weight_function(G, weight)
889
+ push = heappush
890
+ pop = heappop
891
+ # Init: Forward Backward
892
+ dists = [{}, {}] # dictionary of final distances
893
+ paths = [{source: [source]}, {target: [target]}] # dictionary of paths
894
+ fringe = [[], []] # heap of (distance, node) tuples for
895
+ # extracting next node to expand
896
+ seen = [{source: 0}, {target: 0}] # dictionary of distances to
897
+ # nodes seen
898
+ c = count()
899
+ # initialize fringe heap
900
+ push(fringe[0], (0, next(c), source))
901
+ push(fringe[1], (0, next(c), target))
902
+ # neighs for extracting correct neighbor information
903
+ neighs = [Gsucc, Gpred]
904
+ # variables to hold shortest discovered path
905
+ # finaldist = 1e30000
906
+ finalpath = []
907
+ dir = 1
908
+ while fringe[0] and fringe[1]:
909
+ # choose direction
910
+ # dir == 0 is forward direction and dir == 1 is back
911
+ dir = 1 - dir
912
+ # extract closest to expand
913
+ (dist, _, v) = pop(fringe[dir])
914
+ if v in dists[dir]:
915
+ # Shortest path to v has already been found
916
+ continue
917
+ # update distance
918
+ dists[dir][v] = dist # equal to seen[dir][v]
919
+ if v in dists[1 - dir]:
920
+ # if we have scanned v in both directions we are done
921
+ # we have now discovered the shortest path
922
+ return (finaldist, finalpath)
923
+
924
+ for w in neighs[dir](v):
925
+ if dir == 0: # forward
926
+ minweight = wt(v, w, G.get_edge_data(v, w))
927
+ else: # back, must remember to change v,w->w,v
928
+ minweight = wt(w, v, G.get_edge_data(w, v))
929
+ if minweight is None:
930
+ continue
931
+ vwLength = dists[dir][v] + minweight
932
+
933
+ if w in dists[dir]:
934
+ if vwLength < dists[dir][w]:
935
+ raise ValueError("Contradictory paths found: negative weights?")
936
+ elif w not in seen[dir] or vwLength < seen[dir][w]:
937
+ # relaxing
938
+ seen[dir][w] = vwLength
939
+ push(fringe[dir], (vwLength, next(c), w))
940
+ paths[dir][w] = paths[dir][v] + [w]
941
+ if w in seen[0] and w in seen[1]:
942
+ # see if this path is better than the already
943
+ # discovered shortest path
944
+ totaldist = seen[0][w] + seen[1][w]
945
+ if finalpath == [] or finaldist > totaldist:
946
+ finaldist = totaldist
947
+ revpath = paths[1][w][:]
948
+ revpath.reverse()
949
+ finalpath = paths[0][w] + revpath[1:]
950
+ raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
.venv/lib/python3.11/site-packages/networkx/algorithms/smallworld.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions for estimating the small-world-ness of graphs.
2
+
3
+ A small world network is characterized by a small average shortest path length,
4
+ and a large clustering coefficient.
5
+
6
+ Small-worldness is commonly measured with the coefficient sigma or omega.
7
+
8
+ Both coefficients compare the average clustering coefficient and shortest path
9
+ length of a given graph against the same quantities for an equivalent random
10
+ or lattice graph.
11
+
12
+ For more information, see the Wikipedia article on small-world network [1]_.
13
+
14
+ .. [1] Small-world network:: https://en.wikipedia.org/wiki/Small-world_network
15
+
16
+ """
17
+
18
+ import networkx as nx
19
+ from networkx.utils import not_implemented_for, py_random_state
20
+
21
+ __all__ = ["random_reference", "lattice_reference", "sigma", "omega"]
22
+
23
+
24
+ @not_implemented_for("directed")
25
+ @not_implemented_for("multigraph")
26
+ @py_random_state(3)
27
+ @nx._dispatchable(returns_graph=True)
28
+ def random_reference(G, niter=1, connectivity=True, seed=None):
29
+ """Compute a random graph by swapping edges of a given graph.
30
+
31
+ Parameters
32
+ ----------
33
+ G : graph
34
+ An undirected graph with 4 or more nodes.
35
+
36
+ niter : integer (optional, default=1)
37
+ An edge is rewired approximately `niter` times.
38
+
39
+ connectivity : boolean (optional, default=True)
40
+ When True, ensure connectivity for the randomized graph.
41
+
42
+ seed : integer, random_state, or None (default)
43
+ Indicator of random number generation state.
44
+ See :ref:`Randomness<randomness>`.
45
+
46
+ Returns
47
+ -------
48
+ G : graph
49
+ The randomized graph.
50
+
51
+ Raises
52
+ ------
53
+ NetworkXError
54
+ If there are fewer than 4 nodes or 2 edges in `G`
55
+
56
+ Notes
57
+ -----
58
+ The implementation is adapted from the algorithm by Maslov and Sneppen
59
+ (2002) [1]_.
60
+
61
+ References
62
+ ----------
63
+ .. [1] Maslov, Sergei, and Kim Sneppen.
64
+ "Specificity and stability in topology of protein networks."
65
+ Science 296.5569 (2002): 910-913.
66
+ """
67
+ if len(G) < 4:
68
+ raise nx.NetworkXError("Graph has fewer than four nodes.")
69
+ if len(G.edges) < 2:
70
+ raise nx.NetworkXError("Graph has fewer that 2 edges")
71
+
72
+ from networkx.utils import cumulative_distribution, discrete_sequence
73
+
74
+ local_conn = nx.connectivity.local_edge_connectivity
75
+
76
+ G = G.copy()
77
+ keys, degrees = zip(*G.degree()) # keys, degree
78
+ cdf = cumulative_distribution(degrees) # cdf of degree
79
+ nnodes = len(G)
80
+ nedges = nx.number_of_edges(G)
81
+ niter = niter * nedges
82
+ ntries = int(nnodes * nedges / (nnodes * (nnodes - 1) / 2))
83
+ swapcount = 0
84
+
85
+ for i in range(niter):
86
+ n = 0
87
+ while n < ntries:
88
+ # pick two random edges without creating edge list
89
+ # choose source node indices from discrete distribution
90
+ (ai, ci) = discrete_sequence(2, cdistribution=cdf, seed=seed)
91
+ if ai == ci:
92
+ continue # same source, skip
93
+ a = keys[ai] # convert index to label
94
+ c = keys[ci]
95
+ # choose target uniformly from neighbors
96
+ b = seed.choice(list(G.neighbors(a)))
97
+ d = seed.choice(list(G.neighbors(c)))
98
+ if b in [a, c, d] or d in [a, b, c]:
99
+ continue # all vertices should be different
100
+
101
+ # don't create parallel edges
102
+ if (d not in G[a]) and (b not in G[c]):
103
+ G.add_edge(a, d)
104
+ G.add_edge(c, b)
105
+ G.remove_edge(a, b)
106
+ G.remove_edge(c, d)
107
+
108
+ # Check if the graph is still connected
109
+ if connectivity and local_conn(G, a, b) == 0:
110
+ # Not connected, revert the swap
111
+ G.remove_edge(a, d)
112
+ G.remove_edge(c, b)
113
+ G.add_edge(a, b)
114
+ G.add_edge(c, d)
115
+ else:
116
+ swapcount += 1
117
+ break
118
+ n += 1
119
+ return G
120
+
121
+
122
+ @not_implemented_for("directed")
123
+ @not_implemented_for("multigraph")
124
+ @py_random_state(4)
125
+ @nx._dispatchable(returns_graph=True)
126
+ def lattice_reference(G, niter=5, D=None, connectivity=True, seed=None):
127
+ """Latticize the given graph by swapping edges.
128
+
129
+ Parameters
130
+ ----------
131
+ G : graph
132
+ An undirected graph.
133
+
134
+ niter : integer (optional, default=1)
135
+ An edge is rewired approximately niter times.
136
+
137
+ D : numpy.array (optional, default=None)
138
+ Distance to the diagonal matrix.
139
+
140
+ connectivity : boolean (optional, default=True)
141
+ Ensure connectivity for the latticized graph when set to True.
142
+
143
+ seed : integer, random_state, or None (default)
144
+ Indicator of random number generation state.
145
+ See :ref:`Randomness<randomness>`.
146
+
147
+ Returns
148
+ -------
149
+ G : graph
150
+ The latticized graph.
151
+
152
+ Raises
153
+ ------
154
+ NetworkXError
155
+ If there are fewer than 4 nodes or 2 edges in `G`
156
+
157
+ Notes
158
+ -----
159
+ The implementation is adapted from the algorithm by Sporns et al. [1]_.
160
+ which is inspired from the original work by Maslov and Sneppen(2002) [2]_.
161
+
162
+ References
163
+ ----------
164
+ .. [1] Sporns, Olaf, and Jonathan D. Zwi.
165
+ "The small world of the cerebral cortex."
166
+ Neuroinformatics 2.2 (2004): 145-162.
167
+ .. [2] Maslov, Sergei, and Kim Sneppen.
168
+ "Specificity and stability in topology of protein networks."
169
+ Science 296.5569 (2002): 910-913.
170
+ """
171
+ import numpy as np
172
+
173
+ from networkx.utils import cumulative_distribution, discrete_sequence
174
+
175
+ local_conn = nx.connectivity.local_edge_connectivity
176
+
177
+ if len(G) < 4:
178
+ raise nx.NetworkXError("Graph has fewer than four nodes.")
179
+ if len(G.edges) < 2:
180
+ raise nx.NetworkXError("Graph has fewer that 2 edges")
181
+ # Instead of choosing uniformly at random from a generated edge list,
182
+ # this algorithm chooses nonuniformly from the set of nodes with
183
+ # probability weighted by degree.
184
+ G = G.copy()
185
+ keys, degrees = zip(*G.degree()) # keys, degree
186
+ cdf = cumulative_distribution(degrees) # cdf of degree
187
+
188
+ nnodes = len(G)
189
+ nedges = nx.number_of_edges(G)
190
+ if D is None:
191
+ D = np.zeros((nnodes, nnodes))
192
+ un = np.arange(1, nnodes)
193
+ um = np.arange(nnodes - 1, 0, -1)
194
+ u = np.append((0,), np.where(un < um, un, um))
195
+
196
+ for v in range(int(np.ceil(nnodes / 2))):
197
+ D[nnodes - v - 1, :] = np.append(u[v + 1 :], u[: v + 1])
198
+ D[v, :] = D[nnodes - v - 1, :][::-1]
199
+
200
+ niter = niter * nedges
201
+ # maximal number of rewiring attempts per 'niter'
202
+ max_attempts = int(nnodes * nedges / (nnodes * (nnodes - 1) / 2))
203
+
204
+ for _ in range(niter):
205
+ n = 0
206
+ while n < max_attempts:
207
+ # pick two random edges without creating edge list
208
+ # choose source node indices from discrete distribution
209
+ (ai, ci) = discrete_sequence(2, cdistribution=cdf, seed=seed)
210
+ if ai == ci:
211
+ continue # same source, skip
212
+ a = keys[ai] # convert index to label
213
+ c = keys[ci]
214
+ # choose target uniformly from neighbors
215
+ b = seed.choice(list(G.neighbors(a)))
216
+ d = seed.choice(list(G.neighbors(c)))
217
+ bi = keys.index(b)
218
+ di = keys.index(d)
219
+
220
+ if b in [a, c, d] or d in [a, b, c]:
221
+ continue # all vertices should be different
222
+
223
+ # don't create parallel edges
224
+ if (d not in G[a]) and (b not in G[c]):
225
+ if D[ai, bi] + D[ci, di] >= D[ai, ci] + D[bi, di]:
226
+ # only swap if we get closer to the diagonal
227
+ G.add_edge(a, d)
228
+ G.add_edge(c, b)
229
+ G.remove_edge(a, b)
230
+ G.remove_edge(c, d)
231
+
232
+ # Check if the graph is still connected
233
+ if connectivity and local_conn(G, a, b) == 0:
234
+ # Not connected, revert the swap
235
+ G.remove_edge(a, d)
236
+ G.remove_edge(c, b)
237
+ G.add_edge(a, b)
238
+ G.add_edge(c, d)
239
+ else:
240
+ break
241
+ n += 1
242
+
243
+ return G
244
+
245
+
246
+ @not_implemented_for("directed")
247
+ @not_implemented_for("multigraph")
248
+ @py_random_state(3)
249
+ @nx._dispatchable
250
+ def sigma(G, niter=100, nrand=10, seed=None):
251
+ """Returns the small-world coefficient (sigma) of the given graph.
252
+
253
+ The small-world coefficient is defined as:
254
+ sigma = C/Cr / L/Lr
255
+ where C and L are respectively the average clustering coefficient and
256
+ average shortest path length of G. Cr and Lr are respectively the average
257
+ clustering coefficient and average shortest path length of an equivalent
258
+ random graph.
259
+
260
+ A graph is commonly classified as small-world if sigma>1.
261
+
262
+ Parameters
263
+ ----------
264
+ G : NetworkX graph
265
+ An undirected graph.
266
+ niter : integer (optional, default=100)
267
+ Approximate number of rewiring per edge to compute the equivalent
268
+ random graph.
269
+ nrand : integer (optional, default=10)
270
+ Number of random graphs generated to compute the average clustering
271
+ coefficient (Cr) and average shortest path length (Lr).
272
+ seed : integer, random_state, or None (default)
273
+ Indicator of random number generation state.
274
+ See :ref:`Randomness<randomness>`.
275
+
276
+ Returns
277
+ -------
278
+ sigma : float
279
+ The small-world coefficient of G.
280
+
281
+ Notes
282
+ -----
283
+ The implementation is adapted from Humphries et al. [1]_ [2]_.
284
+
285
+ References
286
+ ----------
287
+ .. [1] The brainstem reticular formation is a small-world, not scale-free,
288
+ network M. D. Humphries, K. Gurney and T. J. Prescott,
289
+ Proc. Roy. Soc. B 2006 273, 503-511, doi:10.1098/rspb.2005.3354.
290
+ .. [2] Humphries and Gurney (2008).
291
+ "Network 'Small-World-Ness': A Quantitative Method for Determining
292
+ Canonical Network Equivalence".
293
+ PLoS One. 3 (4). PMID 18446219. doi:10.1371/journal.pone.0002051.
294
+ """
295
+ import numpy as np
296
+
297
+ # Compute the mean clustering coefficient and average shortest path length
298
+ # for an equivalent random graph
299
+ randMetrics = {"C": [], "L": []}
300
+ for i in range(nrand):
301
+ Gr = random_reference(G, niter=niter, seed=seed)
302
+ randMetrics["C"].append(nx.transitivity(Gr))
303
+ randMetrics["L"].append(nx.average_shortest_path_length(Gr))
304
+
305
+ C = nx.transitivity(G)
306
+ L = nx.average_shortest_path_length(G)
307
+ Cr = np.mean(randMetrics["C"])
308
+ Lr = np.mean(randMetrics["L"])
309
+
310
+ sigma = (C / Cr) / (L / Lr)
311
+
312
+ return float(sigma)
313
+
314
+
315
+ @not_implemented_for("directed")
316
+ @not_implemented_for("multigraph")
317
+ @py_random_state(3)
318
+ @nx._dispatchable
319
+ def omega(G, niter=5, nrand=10, seed=None):
320
+ """Returns the small-world coefficient (omega) of a graph
321
+
322
+ The small-world coefficient of a graph G is:
323
+
324
+ omega = Lr/L - C/Cl
325
+
326
+ where C and L are respectively the average clustering coefficient and
327
+ average shortest path length of G. Lr is the average shortest path length
328
+ of an equivalent random graph and Cl is the average clustering coefficient
329
+ of an equivalent lattice graph.
330
+
331
+ The small-world coefficient (omega) measures how much G is like a lattice
332
+ or a random graph. Negative values mean G is similar to a lattice whereas
333
+ positive values mean G is a random graph.
334
+ Values close to 0 mean that G has small-world characteristics.
335
+
336
+ Parameters
337
+ ----------
338
+ G : NetworkX graph
339
+ An undirected graph.
340
+
341
+ niter: integer (optional, default=5)
342
+ Approximate number of rewiring per edge to compute the equivalent
343
+ random graph.
344
+
345
+ nrand: integer (optional, default=10)
346
+ Number of random graphs generated to compute the maximal clustering
347
+ coefficient (Cr) and average shortest path length (Lr).
348
+
349
+ seed : integer, random_state, or None (default)
350
+ Indicator of random number generation state.
351
+ See :ref:`Randomness<randomness>`.
352
+
353
+
354
+ Returns
355
+ -------
356
+ omega : float
357
+ The small-world coefficient (omega)
358
+
359
+ Notes
360
+ -----
361
+ The implementation is adapted from the algorithm by Telesford et al. [1]_.
362
+
363
+ References
364
+ ----------
365
+ .. [1] Telesford, Joyce, Hayasaka, Burdette, and Laurienti (2011).
366
+ "The Ubiquity of Small-World Networks".
367
+ Brain Connectivity. 1 (0038): 367-75. PMC 3604768. PMID 22432451.
368
+ doi:10.1089/brain.2011.0038.
369
+ """
370
+ import numpy as np
371
+
372
+ # Compute the mean clustering coefficient and average shortest path length
373
+ # for an equivalent random graph
374
+ randMetrics = {"C": [], "L": []}
375
+
376
+ # Calculate initial average clustering coefficient which potentially will
377
+ # get replaced by higher clustering coefficients from generated lattice
378
+ # reference graphs
379
+ Cl = nx.average_clustering(G)
380
+
381
+ niter_lattice_reference = niter
382
+ niter_random_reference = niter * 2
383
+
384
+ for _ in range(nrand):
385
+ # Generate random graph
386
+ Gr = random_reference(G, niter=niter_random_reference, seed=seed)
387
+ randMetrics["L"].append(nx.average_shortest_path_length(Gr))
388
+
389
+ # Generate lattice graph
390
+ Gl = lattice_reference(G, niter=niter_lattice_reference, seed=seed)
391
+
392
+ # Replace old clustering coefficient, if clustering is higher in
393
+ # generated lattice reference
394
+ Cl_temp = nx.average_clustering(Gl)
395
+ if Cl_temp > Cl:
396
+ Cl = Cl_temp
397
+
398
+ C = nx.average_clustering(G)
399
+ L = nx.average_shortest_path_length(G)
400
+ Lr = np.mean(randMetrics["L"])
401
+
402
+ omega = (Lr / L) - (C / Cl)
403
+
404
+ return float(omega)
.venv/lib/python3.11/site-packages/networkx/algorithms/smetric.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import networkx as nx
2
+
3
+ __all__ = ["s_metric"]
4
+
5
+
6
+ @nx._dispatchable
7
+ def s_metric(G):
8
+ """Returns the s-metric [1]_ of graph.
9
+
10
+ The s-metric is defined as the sum of the products ``deg(u) * deg(v)``
11
+ for every edge ``(u, v)`` in `G`.
12
+
13
+ Parameters
14
+ ----------
15
+ G : graph
16
+ The graph used to compute the s-metric.
17
+
18
+ Returns
19
+ -------
20
+ s : float
21
+ The s-metric of the graph.
22
+
23
+ References
24
+ ----------
25
+ .. [1] Lun Li, David Alderson, John C. Doyle, and Walter Willinger,
26
+ Towards a Theory of Scale-Free Graphs:
27
+ Definition, Properties, and Implications (Extended Version), 2005.
28
+ https://arxiv.org/abs/cond-mat/0501169
29
+ """
30
+ return float(sum(G.degree(u) * G.degree(v) for (u, v) in G.edges()))
.venv/lib/python3.11/site-packages/networkx/algorithms/sparsifiers.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions for computing sparsifiers of graphs."""
2
+
3
+ import math
4
+
5
+ import networkx as nx
6
+ from networkx.utils import not_implemented_for, py_random_state
7
+
8
+ __all__ = ["spanner"]
9
+
10
+
11
+ @not_implemented_for("directed")
12
+ @not_implemented_for("multigraph")
13
+ @py_random_state(3)
14
+ @nx._dispatchable(edge_attrs="weight", returns_graph=True)
15
+ def spanner(G, stretch, weight=None, seed=None):
16
+ """Returns a spanner of the given graph with the given stretch.
17
+
18
+ A spanner of a graph G = (V, E) with stretch t is a subgraph
19
+ H = (V, E_S) such that E_S is a subset of E and the distance between
20
+ any pair of nodes in H is at most t times the distance between the
21
+ nodes in G.
22
+
23
+ Parameters
24
+ ----------
25
+ G : NetworkX graph
26
+ An undirected simple graph.
27
+
28
+ stretch : float
29
+ The stretch of the spanner.
30
+
31
+ weight : object
32
+ The edge attribute to use as distance.
33
+
34
+ seed : integer, random_state, or None (default)
35
+ Indicator of random number generation state.
36
+ See :ref:`Randomness<randomness>`.
37
+
38
+ Returns
39
+ -------
40
+ NetworkX graph
41
+ A spanner of the given graph with the given stretch.
42
+
43
+ Raises
44
+ ------
45
+ ValueError
46
+ If a stretch less than 1 is given.
47
+
48
+ Notes
49
+ -----
50
+ This function implements the spanner algorithm by Baswana and Sen,
51
+ see [1].
52
+
53
+ This algorithm is a randomized las vegas algorithm: The expected
54
+ running time is O(km) where k = (stretch + 1) // 2 and m is the
55
+ number of edges in G. The returned graph is always a spanner of the
56
+ given graph with the specified stretch. For weighted graphs the
57
+ number of edges in the spanner is O(k * n^(1 + 1 / k)) where k is
58
+ defined as above and n is the number of nodes in G. For unweighted
59
+ graphs the number of edges is O(n^(1 + 1 / k) + kn).
60
+
61
+ References
62
+ ----------
63
+ [1] S. Baswana, S. Sen. A Simple and Linear Time Randomized
64
+ Algorithm for Computing Sparse Spanners in Weighted Graphs.
65
+ Random Struct. Algorithms 30(4): 532-563 (2007).
66
+ """
67
+ if stretch < 1:
68
+ raise ValueError("stretch must be at least 1")
69
+
70
+ k = (stretch + 1) // 2
71
+
72
+ # initialize spanner H with empty edge set
73
+ H = nx.empty_graph()
74
+ H.add_nodes_from(G.nodes)
75
+
76
+ # phase 1: forming the clusters
77
+ # the residual graph has V' from the paper as its node set
78
+ # and E' from the paper as its edge set
79
+ residual_graph = _setup_residual_graph(G, weight)
80
+ # clustering is a dictionary that maps nodes in a cluster to the
81
+ # cluster center
82
+ clustering = {v: v for v in G.nodes}
83
+ sample_prob = math.pow(G.number_of_nodes(), -1 / k)
84
+ size_limit = 2 * math.pow(G.number_of_nodes(), 1 + 1 / k)
85
+
86
+ i = 0
87
+ while i < k - 1:
88
+ # step 1: sample centers
89
+ sampled_centers = set()
90
+ for center in set(clustering.values()):
91
+ if seed.random() < sample_prob:
92
+ sampled_centers.add(center)
93
+
94
+ # combined loop for steps 2 and 3
95
+ edges_to_add = set()
96
+ edges_to_remove = set()
97
+ new_clustering = {}
98
+ for v in residual_graph.nodes:
99
+ if clustering[v] in sampled_centers:
100
+ continue
101
+
102
+ # step 2: find neighboring (sampled) clusters and
103
+ # lightest edges to them
104
+ lightest_edge_neighbor, lightest_edge_weight = _lightest_edge_dicts(
105
+ residual_graph, clustering, v
106
+ )
107
+ neighboring_sampled_centers = (
108
+ set(lightest_edge_weight.keys()) & sampled_centers
109
+ )
110
+
111
+ # step 3: add edges to spanner
112
+ if not neighboring_sampled_centers:
113
+ # connect to each neighboring center via lightest edge
114
+ for neighbor in lightest_edge_neighbor.values():
115
+ edges_to_add.add((v, neighbor))
116
+ # remove all incident edges
117
+ for neighbor in residual_graph.adj[v]:
118
+ edges_to_remove.add((v, neighbor))
119
+
120
+ else: # there is a neighboring sampled center
121
+ closest_center = min(
122
+ neighboring_sampled_centers, key=lightest_edge_weight.get
123
+ )
124
+ closest_center_weight = lightest_edge_weight[closest_center]
125
+ closest_center_neighbor = lightest_edge_neighbor[closest_center]
126
+
127
+ edges_to_add.add((v, closest_center_neighbor))
128
+ new_clustering[v] = closest_center
129
+
130
+ # connect to centers with edge weight less than
131
+ # closest_center_weight
132
+ for center, edge_weight in lightest_edge_weight.items():
133
+ if edge_weight < closest_center_weight:
134
+ neighbor = lightest_edge_neighbor[center]
135
+ edges_to_add.add((v, neighbor))
136
+
137
+ # remove edges to centers with edge weight less than
138
+ # closest_center_weight
139
+ for neighbor in residual_graph.adj[v]:
140
+ nbr_cluster = clustering[neighbor]
141
+ nbr_weight = lightest_edge_weight[nbr_cluster]
142
+ if (
143
+ nbr_cluster == closest_center
144
+ or nbr_weight < closest_center_weight
145
+ ):
146
+ edges_to_remove.add((v, neighbor))
147
+
148
+ # check whether iteration added too many edges to spanner,
149
+ # if so repeat
150
+ if len(edges_to_add) > size_limit:
151
+ # an iteration is repeated O(1) times on expectation
152
+ continue
153
+
154
+ # iteration succeeded
155
+ i = i + 1
156
+
157
+ # actually add edges to spanner
158
+ for u, v in edges_to_add:
159
+ _add_edge_to_spanner(H, residual_graph, u, v, weight)
160
+
161
+ # actually delete edges from residual graph
162
+ residual_graph.remove_edges_from(edges_to_remove)
163
+
164
+ # copy old clustering data to new_clustering
165
+ for node, center in clustering.items():
166
+ if center in sampled_centers:
167
+ new_clustering[node] = center
168
+ clustering = new_clustering
169
+
170
+ # step 4: remove intra-cluster edges
171
+ for u in residual_graph.nodes:
172
+ for v in list(residual_graph.adj[u]):
173
+ if clustering[u] == clustering[v]:
174
+ residual_graph.remove_edge(u, v)
175
+
176
+ # update residual graph node set
177
+ for v in list(residual_graph.nodes):
178
+ if v not in clustering:
179
+ residual_graph.remove_node(v)
180
+
181
+ # phase 2: vertex-cluster joining
182
+ for v in residual_graph.nodes:
183
+ lightest_edge_neighbor, _ = _lightest_edge_dicts(residual_graph, clustering, v)
184
+ for neighbor in lightest_edge_neighbor.values():
185
+ _add_edge_to_spanner(H, residual_graph, v, neighbor, weight)
186
+
187
+ return H
188
+
189
+
190
+ def _setup_residual_graph(G, weight):
191
+ """Setup residual graph as a copy of G with unique edges weights.
192
+
193
+ The node set of the residual graph corresponds to the set V' from
194
+ the Baswana-Sen paper and the edge set corresponds to the set E'
195
+ from the paper.
196
+
197
+ This function associates distinct weights to the edges of the
198
+ residual graph (even for unweighted input graphs), as required by
199
+ the algorithm.
200
+
201
+ Parameters
202
+ ----------
203
+ G : NetworkX graph
204
+ An undirected simple graph.
205
+
206
+ weight : object
207
+ The edge attribute to use as distance.
208
+
209
+ Returns
210
+ -------
211
+ NetworkX graph
212
+ The residual graph used for the Baswana-Sen algorithm.
213
+ """
214
+ residual_graph = G.copy()
215
+
216
+ # establish unique edge weights, even for unweighted graphs
217
+ for u, v in G.edges():
218
+ if not weight:
219
+ residual_graph[u][v]["weight"] = (id(u), id(v))
220
+ else:
221
+ residual_graph[u][v]["weight"] = (G[u][v][weight], id(u), id(v))
222
+
223
+ return residual_graph
224
+
225
+
226
+ def _lightest_edge_dicts(residual_graph, clustering, node):
227
+ """Find the lightest edge to each cluster.
228
+
229
+ Searches for the minimum-weight edge to each cluster adjacent to
230
+ the given node.
231
+
232
+ Parameters
233
+ ----------
234
+ residual_graph : NetworkX graph
235
+ The residual graph used by the Baswana-Sen algorithm.
236
+
237
+ clustering : dictionary
238
+ The current clustering of the nodes.
239
+
240
+ node : node
241
+ The node from which the search originates.
242
+
243
+ Returns
244
+ -------
245
+ lightest_edge_neighbor, lightest_edge_weight : dictionary, dictionary
246
+ lightest_edge_neighbor is a dictionary that maps a center C to
247
+ a node v in the corresponding cluster such that the edge from
248
+ the given node to v is the lightest edge from the given node to
249
+ any node in cluster. lightest_edge_weight maps a center C to the
250
+ weight of the aforementioned edge.
251
+
252
+ Notes
253
+ -----
254
+ If a cluster has no node that is adjacent to the given node in the
255
+ residual graph then the center of the cluster is not a key in the
256
+ returned dictionaries.
257
+ """
258
+ lightest_edge_neighbor = {}
259
+ lightest_edge_weight = {}
260
+ for neighbor in residual_graph.adj[node]:
261
+ nbr_center = clustering[neighbor]
262
+ weight = residual_graph[node][neighbor]["weight"]
263
+ if (
264
+ nbr_center not in lightest_edge_weight
265
+ or weight < lightest_edge_weight[nbr_center]
266
+ ):
267
+ lightest_edge_neighbor[nbr_center] = neighbor
268
+ lightest_edge_weight[nbr_center] = weight
269
+ return lightest_edge_neighbor, lightest_edge_weight
270
+
271
+
272
+ def _add_edge_to_spanner(H, residual_graph, u, v, weight):
273
+ """Add the edge {u, v} to the spanner H and take weight from
274
+ the residual graph.
275
+
276
+ Parameters
277
+ ----------
278
+ H : NetworkX graph
279
+ The spanner under construction.
280
+
281
+ residual_graph : NetworkX graph
282
+ The residual graph used by the Baswana-Sen algorithm. The weight
283
+ for the edge is taken from this graph.
284
+
285
+ u : node
286
+ One endpoint of the edge.
287
+
288
+ v : node
289
+ The other endpoint of the edge.
290
+
291
+ weight : object
292
+ The edge attribute to use as distance.
293
+ """
294
+ H.add_edge(u, v)
295
+ if weight:
296
+ H[u][v][weight] = residual_graph[u][v]["weight"][0]