Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/__pycache__/test_vf2pp_helpers.cpython-311.pyc +3 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_asteroidal.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_chordal.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_covering.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_cuts.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_hierarchy.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_link_prediction.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_max_weight_clique.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_mis.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_planarity.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_polynomials.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_sparsifiers.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_time_dependent.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_triads.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_vitality.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_voronoi.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_walks.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_wiener.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_asteroidal.py +23 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_boundary.py +154 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_bridges.py +144 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_broadcasting.py +82 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_chains.py +141 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_chordal.py +129 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_clique.py +291 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_cluster.py +549 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_communicability.py +80 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_core.py +266 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_covering.py +85 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_cuts.py +171 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_cycles.py +974 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_d_separation.py +348 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_dag.py +835 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_distance_measures.py +774 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_distance_regular.py +85 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_dominance.py +286 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_dominating.py +46 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_efficiency.py +58 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_euler.py +314 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_graph_hashing.py +686 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_graphical.py +163 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_hierarchy.py +46 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_hybrid.py +24 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_isolate.py +26 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_link_prediction.py +586 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_lowest_common_ancestors.py +427 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_matching.py +605 -0
.gitattributes
CHANGED
|
@@ -324,3 +324,4 @@ tuning-competition-baseline/.venv/lib/python3.11/site-packages/nvidia/cudnn/lib/
|
|
| 324 |
.venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 325 |
.venv/lib/python3.11/site-packages/OpenSSL/__pycache__/crypto.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 326 |
.venv/lib/python3.11/site-packages/OpenSSL/__pycache__/SSL.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 324 |
.venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 325 |
.venv/lib/python3.11/site-packages/OpenSSL/__pycache__/crypto.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 326 |
.venv/lib/python3.11/site-packages/OpenSSL/__pycache__/SSL.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 327 |
+
.venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/__pycache__/test_vf2pp_helpers.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
.venv/lib/python3.11/site-packages/networkx/algorithms/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (7.15 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/__pycache__/test_vf2pp_helpers.cpython-311.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:270f44360c8ab4b1f0be841758bba811380aa06c53463afbc9669ec165a6e26c
|
| 3 |
+
size 118495
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_asteroidal.cpython-311.pyc
ADDED
|
Binary file (1.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_chordal.cpython-311.pyc
ADDED
|
Binary file (9.24 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_covering.cpython-311.pyc
ADDED
|
Binary file (6.53 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_cuts.cpython-311.pyc
ADDED
|
Binary file (10.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_hierarchy.cpython-311.pyc
ADDED
|
Binary file (3.14 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_link_prediction.cpython-311.pyc
ADDED
|
Binary file (42.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_max_weight_clique.cpython-311.pyc
ADDED
|
Binary file (10.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_mis.cpython-311.pyc
ADDED
|
Binary file (4.74 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_planarity.cpython-311.pyc
ADDED
|
Binary file (27.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_polynomials.cpython-311.pyc
ADDED
|
Binary file (3.85 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_sparsifiers.cpython-311.pyc
ADDED
|
Binary file (7.33 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_time_dependent.cpython-311.pyc
ADDED
|
Binary file (24.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_triads.cpython-311.pyc
ADDED
|
Binary file (24.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_vitality.cpython-311.pyc
ADDED
|
Binary file (3.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_voronoi.cpython-311.pyc
ADDED
|
Binary file (6.48 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_walks.cpython-311.pyc
ADDED
|
Binary file (3.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_wiener.cpython-311.pyc
ADDED
|
Binary file (5.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_asteroidal.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import networkx as nx
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_is_at_free():
|
| 5 |
+
is_at_free = nx.asteroidal.is_at_free
|
| 6 |
+
|
| 7 |
+
cycle = nx.cycle_graph(6)
|
| 8 |
+
assert not is_at_free(cycle)
|
| 9 |
+
|
| 10 |
+
path = nx.path_graph(6)
|
| 11 |
+
assert is_at_free(path)
|
| 12 |
+
|
| 13 |
+
small_graph = nx.complete_graph(2)
|
| 14 |
+
assert is_at_free(small_graph)
|
| 15 |
+
|
| 16 |
+
petersen = nx.petersen_graph()
|
| 17 |
+
assert not is_at_free(petersen)
|
| 18 |
+
|
| 19 |
+
clique = nx.complete_graph(6)
|
| 20 |
+
assert is_at_free(clique)
|
| 21 |
+
|
| 22 |
+
line_clique = nx.line_graph(clique)
|
| 23 |
+
assert not is_at_free(line_clique)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_boundary.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for the :mod:`networkx.algorithms.boundary` module."""
|
| 2 |
+
|
| 3 |
+
from itertools import combinations
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
from networkx import convert_node_labels_to_integers as cnlti
|
| 9 |
+
from networkx.utils import edges_equal
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class TestNodeBoundary:
|
| 13 |
+
"""Unit tests for the :func:`~networkx.node_boundary` function."""
|
| 14 |
+
|
| 15 |
+
def test_null_graph(self):
|
| 16 |
+
"""Tests that the null graph has empty node boundaries."""
|
| 17 |
+
null = nx.null_graph()
|
| 18 |
+
assert nx.node_boundary(null, []) == set()
|
| 19 |
+
assert nx.node_boundary(null, [], []) == set()
|
| 20 |
+
assert nx.node_boundary(null, [1, 2, 3]) == set()
|
| 21 |
+
assert nx.node_boundary(null, [1, 2, 3], [4, 5, 6]) == set()
|
| 22 |
+
assert nx.node_boundary(null, [1, 2, 3], [3, 4, 5]) == set()
|
| 23 |
+
|
| 24 |
+
def test_path_graph(self):
|
| 25 |
+
P10 = cnlti(nx.path_graph(10), first_label=1)
|
| 26 |
+
assert nx.node_boundary(P10, []) == set()
|
| 27 |
+
assert nx.node_boundary(P10, [], []) == set()
|
| 28 |
+
assert nx.node_boundary(P10, [1, 2, 3]) == {4}
|
| 29 |
+
assert nx.node_boundary(P10, [4, 5, 6]) == {3, 7}
|
| 30 |
+
assert nx.node_boundary(P10, [3, 4, 5, 6, 7]) == {2, 8}
|
| 31 |
+
assert nx.node_boundary(P10, [8, 9, 10]) == {7}
|
| 32 |
+
assert nx.node_boundary(P10, [4, 5, 6], [9, 10]) == set()
|
| 33 |
+
|
| 34 |
+
def test_complete_graph(self):
|
| 35 |
+
K10 = cnlti(nx.complete_graph(10), first_label=1)
|
| 36 |
+
assert nx.node_boundary(K10, []) == set()
|
| 37 |
+
assert nx.node_boundary(K10, [], []) == set()
|
| 38 |
+
assert nx.node_boundary(K10, [1, 2, 3]) == {4, 5, 6, 7, 8, 9, 10}
|
| 39 |
+
assert nx.node_boundary(K10, [4, 5, 6]) == {1, 2, 3, 7, 8, 9, 10}
|
| 40 |
+
assert nx.node_boundary(K10, [3, 4, 5, 6, 7]) == {1, 2, 8, 9, 10}
|
| 41 |
+
assert nx.node_boundary(K10, [4, 5, 6], []) == set()
|
| 42 |
+
assert nx.node_boundary(K10, K10) == set()
|
| 43 |
+
assert nx.node_boundary(K10, [1, 2, 3], [3, 4, 5]) == {4, 5}
|
| 44 |
+
|
| 45 |
+
def test_petersen(self):
|
| 46 |
+
"""Check boundaries in the petersen graph
|
| 47 |
+
|
| 48 |
+
cheeger(G,k)=min(|bdy(S)|/|S| for |S|=k, 0<k<=|V(G)|/2)
|
| 49 |
+
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def cheeger(G, k):
|
| 53 |
+
return min(len(nx.node_boundary(G, nn)) / k for nn in combinations(G, k))
|
| 54 |
+
|
| 55 |
+
P = nx.petersen_graph()
|
| 56 |
+
assert cheeger(P, 1) == pytest.approx(3.00, abs=1e-2)
|
| 57 |
+
assert cheeger(P, 2) == pytest.approx(2.00, abs=1e-2)
|
| 58 |
+
assert cheeger(P, 3) == pytest.approx(1.67, abs=1e-2)
|
| 59 |
+
assert cheeger(P, 4) == pytest.approx(1.00, abs=1e-2)
|
| 60 |
+
assert cheeger(P, 5) == pytest.approx(0.80, abs=1e-2)
|
| 61 |
+
|
| 62 |
+
def test_directed(self):
|
| 63 |
+
"""Tests the node boundary of a directed graph."""
|
| 64 |
+
G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)])
|
| 65 |
+
S = {0, 1}
|
| 66 |
+
boundary = nx.node_boundary(G, S)
|
| 67 |
+
expected = {2}
|
| 68 |
+
assert boundary == expected
|
| 69 |
+
|
| 70 |
+
def test_multigraph(self):
|
| 71 |
+
"""Tests the node boundary of a multigraph."""
|
| 72 |
+
G = nx.MultiGraph(list(nx.cycle_graph(5).edges()) * 2)
|
| 73 |
+
S = {0, 1}
|
| 74 |
+
boundary = nx.node_boundary(G, S)
|
| 75 |
+
expected = {2, 4}
|
| 76 |
+
assert boundary == expected
|
| 77 |
+
|
| 78 |
+
def test_multidigraph(self):
|
| 79 |
+
"""Tests the edge boundary of a multidigraph."""
|
| 80 |
+
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]
|
| 81 |
+
G = nx.MultiDiGraph(edges * 2)
|
| 82 |
+
S = {0, 1}
|
| 83 |
+
boundary = nx.node_boundary(G, S)
|
| 84 |
+
expected = {2}
|
| 85 |
+
assert boundary == expected
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class TestEdgeBoundary:
|
| 89 |
+
"""Unit tests for the :func:`~networkx.edge_boundary` function."""
|
| 90 |
+
|
| 91 |
+
def test_null_graph(self):
|
| 92 |
+
null = nx.null_graph()
|
| 93 |
+
assert list(nx.edge_boundary(null, [])) == []
|
| 94 |
+
assert list(nx.edge_boundary(null, [], [])) == []
|
| 95 |
+
assert list(nx.edge_boundary(null, [1, 2, 3])) == []
|
| 96 |
+
assert list(nx.edge_boundary(null, [1, 2, 3], [4, 5, 6])) == []
|
| 97 |
+
assert list(nx.edge_boundary(null, [1, 2, 3], [3, 4, 5])) == []
|
| 98 |
+
|
| 99 |
+
def test_path_graph(self):
|
| 100 |
+
P10 = cnlti(nx.path_graph(10), first_label=1)
|
| 101 |
+
assert list(nx.edge_boundary(P10, [])) == []
|
| 102 |
+
assert list(nx.edge_boundary(P10, [], [])) == []
|
| 103 |
+
assert list(nx.edge_boundary(P10, [1, 2, 3])) == [(3, 4)]
|
| 104 |
+
assert sorted(nx.edge_boundary(P10, [4, 5, 6])) == [(4, 3), (6, 7)]
|
| 105 |
+
assert sorted(nx.edge_boundary(P10, [3, 4, 5, 6, 7])) == [(3, 2), (7, 8)]
|
| 106 |
+
assert list(nx.edge_boundary(P10, [8, 9, 10])) == [(8, 7)]
|
| 107 |
+
assert sorted(nx.edge_boundary(P10, [4, 5, 6], [9, 10])) == []
|
| 108 |
+
assert list(nx.edge_boundary(P10, [1, 2, 3], [3, 4, 5])) == [(2, 3), (3, 4)]
|
| 109 |
+
|
| 110 |
+
def test_complete_graph(self):
|
| 111 |
+
K10 = cnlti(nx.complete_graph(10), first_label=1)
|
| 112 |
+
|
| 113 |
+
def ilen(iterable):
|
| 114 |
+
return sum(1 for i in iterable)
|
| 115 |
+
|
| 116 |
+
assert list(nx.edge_boundary(K10, [])) == []
|
| 117 |
+
assert list(nx.edge_boundary(K10, [], [])) == []
|
| 118 |
+
assert ilen(nx.edge_boundary(K10, [1, 2, 3])) == 21
|
| 119 |
+
assert ilen(nx.edge_boundary(K10, [4, 5, 6, 7])) == 24
|
| 120 |
+
assert ilen(nx.edge_boundary(K10, [3, 4, 5, 6, 7])) == 25
|
| 121 |
+
assert ilen(nx.edge_boundary(K10, [8, 9, 10])) == 21
|
| 122 |
+
assert edges_equal(
|
| 123 |
+
nx.edge_boundary(K10, [4, 5, 6], [9, 10]),
|
| 124 |
+
[(4, 9), (4, 10), (5, 9), (5, 10), (6, 9), (6, 10)],
|
| 125 |
+
)
|
| 126 |
+
assert edges_equal(
|
| 127 |
+
nx.edge_boundary(K10, [1, 2, 3], [3, 4, 5]),
|
| 128 |
+
[(1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5)],
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
def test_directed(self):
|
| 132 |
+
"""Tests the edge boundary of a directed graph."""
|
| 133 |
+
G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)])
|
| 134 |
+
S = {0, 1}
|
| 135 |
+
boundary = list(nx.edge_boundary(G, S))
|
| 136 |
+
expected = [(1, 2)]
|
| 137 |
+
assert boundary == expected
|
| 138 |
+
|
| 139 |
+
def test_multigraph(self):
|
| 140 |
+
"""Tests the edge boundary of a multigraph."""
|
| 141 |
+
G = nx.MultiGraph(list(nx.cycle_graph(5).edges()) * 2)
|
| 142 |
+
S = {0, 1}
|
| 143 |
+
boundary = list(nx.edge_boundary(G, S))
|
| 144 |
+
expected = [(0, 4), (0, 4), (1, 2), (1, 2)]
|
| 145 |
+
assert boundary == expected
|
| 146 |
+
|
| 147 |
+
def test_multidigraph(self):
|
| 148 |
+
"""Tests the edge boundary of a multidigraph."""
|
| 149 |
+
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]
|
| 150 |
+
G = nx.MultiDiGraph(edges * 2)
|
| 151 |
+
S = {0, 1}
|
| 152 |
+
boundary = list(nx.edge_boundary(G, S))
|
| 153 |
+
expected = [(1, 2), (1, 2)]
|
| 154 |
+
assert boundary == expected
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_bridges.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for bridge-finding algorithms."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class TestBridges:
|
| 9 |
+
"""Unit tests for the bridge-finding function."""
|
| 10 |
+
|
| 11 |
+
def test_single_bridge(self):
|
| 12 |
+
edges = [
|
| 13 |
+
# DFS tree edges.
|
| 14 |
+
(1, 2),
|
| 15 |
+
(2, 3),
|
| 16 |
+
(3, 4),
|
| 17 |
+
(3, 5),
|
| 18 |
+
(5, 6),
|
| 19 |
+
(6, 7),
|
| 20 |
+
(7, 8),
|
| 21 |
+
(5, 9),
|
| 22 |
+
(9, 10),
|
| 23 |
+
# Nontree edges.
|
| 24 |
+
(1, 3),
|
| 25 |
+
(1, 4),
|
| 26 |
+
(2, 5),
|
| 27 |
+
(5, 10),
|
| 28 |
+
(6, 8),
|
| 29 |
+
]
|
| 30 |
+
G = nx.Graph(edges)
|
| 31 |
+
source = 1
|
| 32 |
+
bridges = list(nx.bridges(G, source))
|
| 33 |
+
assert bridges == [(5, 6)]
|
| 34 |
+
|
| 35 |
+
def test_barbell_graph(self):
|
| 36 |
+
# The (3, 0) barbell graph has two triangles joined by a single edge.
|
| 37 |
+
G = nx.barbell_graph(3, 0)
|
| 38 |
+
source = 0
|
| 39 |
+
bridges = list(nx.bridges(G, source))
|
| 40 |
+
assert bridges == [(2, 3)]
|
| 41 |
+
|
| 42 |
+
def test_multiedge_bridge(self):
|
| 43 |
+
edges = [
|
| 44 |
+
(0, 1),
|
| 45 |
+
(0, 2),
|
| 46 |
+
(1, 2),
|
| 47 |
+
(1, 2),
|
| 48 |
+
(2, 3),
|
| 49 |
+
(3, 4),
|
| 50 |
+
(3, 4),
|
| 51 |
+
]
|
| 52 |
+
G = nx.MultiGraph(edges)
|
| 53 |
+
assert list(nx.bridges(G)) == [(2, 3)]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class TestHasBridges:
|
| 57 |
+
"""Unit tests for the has bridges function."""
|
| 58 |
+
|
| 59 |
+
def test_single_bridge(self):
|
| 60 |
+
edges = [
|
| 61 |
+
# DFS tree edges.
|
| 62 |
+
(1, 2),
|
| 63 |
+
(2, 3),
|
| 64 |
+
(3, 4),
|
| 65 |
+
(3, 5),
|
| 66 |
+
(5, 6), # The only bridge edge
|
| 67 |
+
(6, 7),
|
| 68 |
+
(7, 8),
|
| 69 |
+
(5, 9),
|
| 70 |
+
(9, 10),
|
| 71 |
+
# Nontree edges.
|
| 72 |
+
(1, 3),
|
| 73 |
+
(1, 4),
|
| 74 |
+
(2, 5),
|
| 75 |
+
(5, 10),
|
| 76 |
+
(6, 8),
|
| 77 |
+
]
|
| 78 |
+
G = nx.Graph(edges)
|
| 79 |
+
assert nx.has_bridges(G) # Default root
|
| 80 |
+
assert nx.has_bridges(G, root=1) # arbitrary root in G
|
| 81 |
+
|
| 82 |
+
def test_has_bridges_raises_root_not_in_G(self):
|
| 83 |
+
G = nx.Graph()
|
| 84 |
+
G.add_nodes_from([1, 2, 3])
|
| 85 |
+
with pytest.raises(nx.NodeNotFound):
|
| 86 |
+
nx.has_bridges(G, root=6)
|
| 87 |
+
|
| 88 |
+
def test_multiedge_bridge(self):
|
| 89 |
+
edges = [
|
| 90 |
+
(0, 1),
|
| 91 |
+
(0, 2),
|
| 92 |
+
(1, 2),
|
| 93 |
+
(1, 2),
|
| 94 |
+
(2, 3),
|
| 95 |
+
(3, 4),
|
| 96 |
+
(3, 4),
|
| 97 |
+
]
|
| 98 |
+
G = nx.MultiGraph(edges)
|
| 99 |
+
assert nx.has_bridges(G)
|
| 100 |
+
# Make every edge a multiedge
|
| 101 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 102 |
+
assert not nx.has_bridges(G)
|
| 103 |
+
|
| 104 |
+
def test_bridges_multiple_components(self):
|
| 105 |
+
G = nx.Graph()
|
| 106 |
+
nx.add_path(G, [0, 1, 2]) # One connected component
|
| 107 |
+
nx.add_path(G, [4, 5, 6]) # Another connected component
|
| 108 |
+
assert list(nx.bridges(G, root=4)) == [(4, 5), (5, 6)]
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class TestLocalBridges:
|
| 112 |
+
"""Unit tests for the local_bridge function."""
|
| 113 |
+
|
| 114 |
+
@classmethod
|
| 115 |
+
def setup_class(cls):
|
| 116 |
+
cls.BB = nx.barbell_graph(4, 0)
|
| 117 |
+
cls.square = nx.cycle_graph(4)
|
| 118 |
+
cls.tri = nx.cycle_graph(3)
|
| 119 |
+
|
| 120 |
+
def test_nospan(self):
|
| 121 |
+
expected = {(3, 4), (4, 3)}
|
| 122 |
+
assert next(nx.local_bridges(self.BB, with_span=False)) in expected
|
| 123 |
+
assert set(nx.local_bridges(self.square, with_span=False)) == self.square.edges
|
| 124 |
+
assert list(nx.local_bridges(self.tri, with_span=False)) == []
|
| 125 |
+
|
| 126 |
+
def test_no_weight(self):
|
| 127 |
+
inf = float("inf")
|
| 128 |
+
expected = {(3, 4, inf), (4, 3, inf)}
|
| 129 |
+
assert next(nx.local_bridges(self.BB)) in expected
|
| 130 |
+
expected = {(u, v, 3) for u, v in self.square.edges}
|
| 131 |
+
assert set(nx.local_bridges(self.square)) == expected
|
| 132 |
+
assert list(nx.local_bridges(self.tri)) == []
|
| 133 |
+
|
| 134 |
+
def test_weight(self):
|
| 135 |
+
inf = float("inf")
|
| 136 |
+
G = self.square.copy()
|
| 137 |
+
|
| 138 |
+
G.edges[1, 2]["weight"] = 2
|
| 139 |
+
expected = {(u, v, 5 - wt) for u, v, wt in G.edges(data="weight", default=1)}
|
| 140 |
+
assert set(nx.local_bridges(G, weight="weight")) == expected
|
| 141 |
+
|
| 142 |
+
expected = {(u, v, 6) for u, v in G.edges}
|
| 143 |
+
lb = nx.local_bridges(G, weight=lambda u, v, d: 2)
|
| 144 |
+
assert set(lb) == expected
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_broadcasting.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for the broadcasting module."""
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_example_tree_broadcast():
|
| 9 |
+
"""
|
| 10 |
+
Test the BROADCAST algorithm on the example in the paper titled: "Information Dissemination in Trees"
|
| 11 |
+
"""
|
| 12 |
+
edge_list = [
|
| 13 |
+
(0, 1),
|
| 14 |
+
(1, 2),
|
| 15 |
+
(2, 7),
|
| 16 |
+
(3, 4),
|
| 17 |
+
(5, 4),
|
| 18 |
+
(4, 7),
|
| 19 |
+
(6, 7),
|
| 20 |
+
(7, 9),
|
| 21 |
+
(8, 9),
|
| 22 |
+
(9, 13),
|
| 23 |
+
(13, 14),
|
| 24 |
+
(14, 15),
|
| 25 |
+
(14, 16),
|
| 26 |
+
(14, 17),
|
| 27 |
+
(13, 11),
|
| 28 |
+
(11, 10),
|
| 29 |
+
(11, 12),
|
| 30 |
+
(13, 18),
|
| 31 |
+
(18, 19),
|
| 32 |
+
(18, 20),
|
| 33 |
+
]
|
| 34 |
+
G = nx.Graph(edge_list)
|
| 35 |
+
b_T, b_C = nx.tree_broadcast_center(G)
|
| 36 |
+
assert b_T == 6
|
| 37 |
+
assert b_C == {13, 9}
|
| 38 |
+
# test broadcast time from specific vertex
|
| 39 |
+
assert nx.tree_broadcast_time(G, 17) == 8
|
| 40 |
+
assert nx.tree_broadcast_time(G, 3) == 9
|
| 41 |
+
# test broadcast time of entire tree
|
| 42 |
+
assert nx.tree_broadcast_time(G) == 10
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_path_broadcast():
|
| 46 |
+
for i in range(2, 12):
|
| 47 |
+
G = nx.path_graph(i)
|
| 48 |
+
b_T, b_C = nx.tree_broadcast_center(G)
|
| 49 |
+
assert b_T == math.ceil(i / 2)
|
| 50 |
+
assert b_C == {
|
| 51 |
+
math.ceil(i / 2),
|
| 52 |
+
math.floor(i / 2),
|
| 53 |
+
math.ceil(i / 2 - 1),
|
| 54 |
+
math.floor(i / 2 - 1),
|
| 55 |
+
}
|
| 56 |
+
assert nx.tree_broadcast_time(G) == i - 1
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_empty_graph_broadcast():
|
| 60 |
+
H = nx.empty_graph(1)
|
| 61 |
+
b_T, b_C = nx.tree_broadcast_center(H)
|
| 62 |
+
assert b_T == 0
|
| 63 |
+
assert b_C == {0}
|
| 64 |
+
assert nx.tree_broadcast_time(H) == 0
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def test_star_broadcast():
|
| 68 |
+
for i in range(4, 12):
|
| 69 |
+
G = nx.star_graph(i)
|
| 70 |
+
b_T, b_C = nx.tree_broadcast_center(G)
|
| 71 |
+
assert b_T == i
|
| 72 |
+
assert b_C == set(G.nodes())
|
| 73 |
+
assert nx.tree_broadcast_time(G) == b_T
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_binomial_tree_broadcast():
|
| 77 |
+
for i in range(2, 8):
|
| 78 |
+
G = nx.binomial_tree(i)
|
| 79 |
+
b_T, b_C = nx.tree_broadcast_center(G)
|
| 80 |
+
assert b_T == i
|
| 81 |
+
assert b_C == {0, 2 ** (i - 1)}
|
| 82 |
+
assert nx.tree_broadcast_time(G) == 2 * i - 1
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_chains.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for the chain decomposition functions."""
|
| 2 |
+
|
| 3 |
+
from itertools import cycle, islice
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def cycles(seq):
|
| 11 |
+
"""Yields cyclic permutations of the given sequence.
|
| 12 |
+
|
| 13 |
+
For example::
|
| 14 |
+
|
| 15 |
+
>>> list(cycles("abc"))
|
| 16 |
+
[('a', 'b', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b')]
|
| 17 |
+
|
| 18 |
+
"""
|
| 19 |
+
n = len(seq)
|
| 20 |
+
cycled_seq = cycle(seq)
|
| 21 |
+
for x in seq:
|
| 22 |
+
yield tuple(islice(cycled_seq, n))
|
| 23 |
+
next(cycled_seq)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def cyclic_equals(seq1, seq2):
|
| 27 |
+
"""Decide whether two sequences are equal up to cyclic permutations.
|
| 28 |
+
|
| 29 |
+
For example::
|
| 30 |
+
|
| 31 |
+
>>> cyclic_equals("xyz", "zxy")
|
| 32 |
+
True
|
| 33 |
+
>>> cyclic_equals("xyz", "zyx")
|
| 34 |
+
False
|
| 35 |
+
|
| 36 |
+
"""
|
| 37 |
+
# Cast seq2 to a tuple since `cycles()` yields tuples.
|
| 38 |
+
seq2 = tuple(seq2)
|
| 39 |
+
return any(x == tuple(seq2) for x in cycles(seq1))
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class TestChainDecomposition:
|
| 43 |
+
"""Unit tests for the chain decomposition function."""
|
| 44 |
+
|
| 45 |
+
def assertContainsChain(self, chain, expected):
|
| 46 |
+
# A cycle could be expressed in two different orientations, one
|
| 47 |
+
# forward and one backward, so we need to check for cyclic
|
| 48 |
+
# equality in both orientations.
|
| 49 |
+
reversed_chain = list(reversed([tuple(reversed(e)) for e in chain]))
|
| 50 |
+
for candidate in expected:
|
| 51 |
+
if cyclic_equals(chain, candidate):
|
| 52 |
+
break
|
| 53 |
+
if cyclic_equals(reversed_chain, candidate):
|
| 54 |
+
break
|
| 55 |
+
else:
|
| 56 |
+
self.fail("chain not found")
|
| 57 |
+
|
| 58 |
+
def test_decomposition(self):
|
| 59 |
+
edges = [
|
| 60 |
+
# DFS tree edges.
|
| 61 |
+
(1, 2),
|
| 62 |
+
(2, 3),
|
| 63 |
+
(3, 4),
|
| 64 |
+
(3, 5),
|
| 65 |
+
(5, 6),
|
| 66 |
+
(6, 7),
|
| 67 |
+
(7, 8),
|
| 68 |
+
(5, 9),
|
| 69 |
+
(9, 10),
|
| 70 |
+
# Nontree edges.
|
| 71 |
+
(1, 3),
|
| 72 |
+
(1, 4),
|
| 73 |
+
(2, 5),
|
| 74 |
+
(5, 10),
|
| 75 |
+
(6, 8),
|
| 76 |
+
]
|
| 77 |
+
G = nx.Graph(edges)
|
| 78 |
+
expected = [
|
| 79 |
+
[(1, 3), (3, 2), (2, 1)],
|
| 80 |
+
[(1, 4), (4, 3)],
|
| 81 |
+
[(2, 5), (5, 3)],
|
| 82 |
+
[(5, 10), (10, 9), (9, 5)],
|
| 83 |
+
[(6, 8), (8, 7), (7, 6)],
|
| 84 |
+
]
|
| 85 |
+
chains = list(nx.chain_decomposition(G, root=1))
|
| 86 |
+
assert len(chains) == len(expected)
|
| 87 |
+
|
| 88 |
+
# This chain decomposition isn't unique
|
| 89 |
+
# for chain in chains:
|
| 90 |
+
# print(chain)
|
| 91 |
+
# self.assertContainsChain(chain, expected)
|
| 92 |
+
|
| 93 |
+
def test_barbell_graph(self):
|
| 94 |
+
# The (3, 0) barbell graph has two triangles joined by a single edge.
|
| 95 |
+
G = nx.barbell_graph(3, 0)
|
| 96 |
+
chains = list(nx.chain_decomposition(G, root=0))
|
| 97 |
+
expected = [[(0, 1), (1, 2), (2, 0)], [(3, 4), (4, 5), (5, 3)]]
|
| 98 |
+
assert len(chains) == len(expected)
|
| 99 |
+
for chain in chains:
|
| 100 |
+
self.assertContainsChain(chain, expected)
|
| 101 |
+
|
| 102 |
+
def test_disconnected_graph(self):
|
| 103 |
+
"""Test for a graph with multiple connected components."""
|
| 104 |
+
G = nx.barbell_graph(3, 0)
|
| 105 |
+
H = nx.barbell_graph(3, 0)
|
| 106 |
+
mapping = dict(zip(range(6), "abcdef"))
|
| 107 |
+
nx.relabel_nodes(H, mapping, copy=False)
|
| 108 |
+
G = nx.union(G, H)
|
| 109 |
+
chains = list(nx.chain_decomposition(G))
|
| 110 |
+
expected = [
|
| 111 |
+
[(0, 1), (1, 2), (2, 0)],
|
| 112 |
+
[(3, 4), (4, 5), (5, 3)],
|
| 113 |
+
[("a", "b"), ("b", "c"), ("c", "a")],
|
| 114 |
+
[("d", "e"), ("e", "f"), ("f", "d")],
|
| 115 |
+
]
|
| 116 |
+
assert len(chains) == len(expected)
|
| 117 |
+
for chain in chains:
|
| 118 |
+
self.assertContainsChain(chain, expected)
|
| 119 |
+
|
| 120 |
+
def test_disconnected_graph_root_node(self):
|
| 121 |
+
"""Test for a single component of a disconnected graph."""
|
| 122 |
+
G = nx.barbell_graph(3, 0)
|
| 123 |
+
H = nx.barbell_graph(3, 0)
|
| 124 |
+
mapping = dict(zip(range(6), "abcdef"))
|
| 125 |
+
nx.relabel_nodes(H, mapping, copy=False)
|
| 126 |
+
G = nx.union(G, H)
|
| 127 |
+
chains = list(nx.chain_decomposition(G, root="a"))
|
| 128 |
+
expected = [
|
| 129 |
+
[("a", "b"), ("b", "c"), ("c", "a")],
|
| 130 |
+
[("d", "e"), ("e", "f"), ("f", "d")],
|
| 131 |
+
]
|
| 132 |
+
assert len(chains) == len(expected)
|
| 133 |
+
for chain in chains:
|
| 134 |
+
self.assertContainsChain(chain, expected)
|
| 135 |
+
|
| 136 |
+
def test_chain_decomposition_root_not_in_G(self):
|
| 137 |
+
"""Test chain decomposition when root is not in graph"""
|
| 138 |
+
G = nx.Graph()
|
| 139 |
+
G.add_nodes_from([1, 2, 3])
|
| 140 |
+
with pytest.raises(nx.NodeNotFound):
|
| 141 |
+
nx.has_bridges(G, root=6)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_chordal.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class TestMCS:
|
| 7 |
+
@classmethod
|
| 8 |
+
def setup_class(cls):
|
| 9 |
+
# simple graph
|
| 10 |
+
connected_chordal_G = nx.Graph()
|
| 11 |
+
connected_chordal_G.add_edges_from(
|
| 12 |
+
[
|
| 13 |
+
(1, 2),
|
| 14 |
+
(1, 3),
|
| 15 |
+
(2, 3),
|
| 16 |
+
(2, 4),
|
| 17 |
+
(3, 4),
|
| 18 |
+
(3, 5),
|
| 19 |
+
(3, 6),
|
| 20 |
+
(4, 5),
|
| 21 |
+
(4, 6),
|
| 22 |
+
(5, 6),
|
| 23 |
+
]
|
| 24 |
+
)
|
| 25 |
+
cls.connected_chordal_G = connected_chordal_G
|
| 26 |
+
|
| 27 |
+
chordal_G = nx.Graph()
|
| 28 |
+
chordal_G.add_edges_from(
|
| 29 |
+
[
|
| 30 |
+
(1, 2),
|
| 31 |
+
(1, 3),
|
| 32 |
+
(2, 3),
|
| 33 |
+
(2, 4),
|
| 34 |
+
(3, 4),
|
| 35 |
+
(3, 5),
|
| 36 |
+
(3, 6),
|
| 37 |
+
(4, 5),
|
| 38 |
+
(4, 6),
|
| 39 |
+
(5, 6),
|
| 40 |
+
(7, 8),
|
| 41 |
+
]
|
| 42 |
+
)
|
| 43 |
+
chordal_G.add_node(9)
|
| 44 |
+
cls.chordal_G = chordal_G
|
| 45 |
+
|
| 46 |
+
non_chordal_G = nx.Graph()
|
| 47 |
+
non_chordal_G.add_edges_from([(1, 2), (1, 3), (2, 4), (2, 5), (3, 4), (3, 5)])
|
| 48 |
+
cls.non_chordal_G = non_chordal_G
|
| 49 |
+
|
| 50 |
+
self_loop_G = nx.Graph()
|
| 51 |
+
self_loop_G.add_edges_from([(1, 1)])
|
| 52 |
+
cls.self_loop_G = self_loop_G
|
| 53 |
+
|
| 54 |
+
@pytest.mark.parametrize("G", (nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()))
|
| 55 |
+
def test_is_chordal_not_implemented(self, G):
|
| 56 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 57 |
+
nx.is_chordal(G)
|
| 58 |
+
|
| 59 |
+
def test_is_chordal(self):
|
| 60 |
+
assert not nx.is_chordal(self.non_chordal_G)
|
| 61 |
+
assert nx.is_chordal(self.chordal_G)
|
| 62 |
+
assert nx.is_chordal(self.connected_chordal_G)
|
| 63 |
+
assert nx.is_chordal(nx.Graph())
|
| 64 |
+
assert nx.is_chordal(nx.complete_graph(3))
|
| 65 |
+
assert nx.is_chordal(nx.cycle_graph(3))
|
| 66 |
+
assert not nx.is_chordal(nx.cycle_graph(5))
|
| 67 |
+
assert nx.is_chordal(self.self_loop_G)
|
| 68 |
+
|
| 69 |
+
def test_induced_nodes(self):
|
| 70 |
+
G = nx.generators.classic.path_graph(10)
|
| 71 |
+
Induced_nodes = nx.find_induced_nodes(G, 1, 9, 2)
|
| 72 |
+
assert Induced_nodes == {1, 2, 3, 4, 5, 6, 7, 8, 9}
|
| 73 |
+
pytest.raises(
|
| 74 |
+
nx.NetworkXTreewidthBoundExceeded, nx.find_induced_nodes, G, 1, 9, 1
|
| 75 |
+
)
|
| 76 |
+
Induced_nodes = nx.find_induced_nodes(self.chordal_G, 1, 6)
|
| 77 |
+
assert Induced_nodes == {1, 2, 4, 6}
|
| 78 |
+
pytest.raises(nx.NetworkXError, nx.find_induced_nodes, self.non_chordal_G, 1, 5)
|
| 79 |
+
|
| 80 |
+
def test_graph_treewidth(self):
|
| 81 |
+
with pytest.raises(nx.NetworkXError, match="Input graph is not chordal"):
|
| 82 |
+
nx.chordal_graph_treewidth(self.non_chordal_G)
|
| 83 |
+
|
| 84 |
+
def test_chordal_find_cliques(self):
|
| 85 |
+
cliques = {
|
| 86 |
+
frozenset([9]),
|
| 87 |
+
frozenset([7, 8]),
|
| 88 |
+
frozenset([1, 2, 3]),
|
| 89 |
+
frozenset([2, 3, 4]),
|
| 90 |
+
frozenset([3, 4, 5, 6]),
|
| 91 |
+
}
|
| 92 |
+
assert set(nx.chordal_graph_cliques(self.chordal_G)) == cliques
|
| 93 |
+
with pytest.raises(nx.NetworkXError, match="Input graph is not chordal"):
|
| 94 |
+
set(nx.chordal_graph_cliques(self.non_chordal_G))
|
| 95 |
+
with pytest.raises(nx.NetworkXError, match="Input graph is not chordal"):
|
| 96 |
+
set(nx.chordal_graph_cliques(self.self_loop_G))
|
| 97 |
+
|
| 98 |
+
def test_chordal_find_cliques_path(self):
|
| 99 |
+
G = nx.path_graph(10)
|
| 100 |
+
cliqueset = nx.chordal_graph_cliques(G)
|
| 101 |
+
for u, v in G.edges():
|
| 102 |
+
assert frozenset([u, v]) in cliqueset or frozenset([v, u]) in cliqueset
|
| 103 |
+
|
| 104 |
+
def test_chordal_find_cliquesCC(self):
|
| 105 |
+
cliques = {frozenset([1, 2, 3]), frozenset([2, 3, 4]), frozenset([3, 4, 5, 6])}
|
| 106 |
+
cgc = nx.chordal_graph_cliques
|
| 107 |
+
assert set(cgc(self.connected_chordal_G)) == cliques
|
| 108 |
+
|
| 109 |
+
def test_complete_to_chordal_graph(self):
|
| 110 |
+
fgrg = nx.fast_gnp_random_graph
|
| 111 |
+
test_graphs = [
|
| 112 |
+
nx.barbell_graph(6, 2),
|
| 113 |
+
nx.cycle_graph(15),
|
| 114 |
+
nx.wheel_graph(20),
|
| 115 |
+
nx.grid_graph([10, 4]),
|
| 116 |
+
nx.ladder_graph(15),
|
| 117 |
+
nx.star_graph(5),
|
| 118 |
+
nx.bull_graph(),
|
| 119 |
+
fgrg(20, 0.3, seed=1),
|
| 120 |
+
]
|
| 121 |
+
for G in test_graphs:
|
| 122 |
+
H, a = nx.complete_to_chordal_graph(G)
|
| 123 |
+
assert nx.is_chordal(H)
|
| 124 |
+
assert len(a) == H.number_of_nodes()
|
| 125 |
+
if nx.is_chordal(G):
|
| 126 |
+
assert G.number_of_edges() == H.number_of_edges()
|
| 127 |
+
assert set(a.values()) == {0}
|
| 128 |
+
else:
|
| 129 |
+
assert len(set(a.values())) == H.number_of_nodes()
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_clique.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from networkx import convert_node_labels_to_integers as cnlti
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class TestCliques:
|
| 8 |
+
def setup_method(self):
|
| 9 |
+
z = [3, 4, 3, 4, 2, 4, 2, 1, 1, 1, 1]
|
| 10 |
+
self.G = cnlti(nx.generators.havel_hakimi_graph(z), first_label=1)
|
| 11 |
+
self.cl = list(nx.find_cliques(self.G))
|
| 12 |
+
H = nx.complete_graph(6)
|
| 13 |
+
H = nx.relabel_nodes(H, {i: i + 1 for i in range(6)})
|
| 14 |
+
H.remove_edges_from([(2, 6), (2, 5), (2, 4), (1, 3), (5, 3)])
|
| 15 |
+
self.H = H
|
| 16 |
+
|
| 17 |
+
def test_find_cliques1(self):
|
| 18 |
+
cl = list(nx.find_cliques(self.G))
|
| 19 |
+
rcl = nx.find_cliques_recursive(self.G)
|
| 20 |
+
expected = [[2, 6, 1, 3], [2, 6, 4], [5, 4, 7], [8, 9], [10, 11]]
|
| 21 |
+
assert sorted(map(sorted, cl)) == sorted(map(sorted, rcl))
|
| 22 |
+
assert sorted(map(sorted, cl)) == sorted(map(sorted, expected))
|
| 23 |
+
|
| 24 |
+
def test_selfloops(self):
|
| 25 |
+
self.G.add_edge(1, 1)
|
| 26 |
+
cl = list(nx.find_cliques(self.G))
|
| 27 |
+
rcl = list(nx.find_cliques_recursive(self.G))
|
| 28 |
+
assert set(map(frozenset, cl)) == set(map(frozenset, rcl))
|
| 29 |
+
answer = [{2, 6, 1, 3}, {2, 6, 4}, {5, 4, 7}, {8, 9}, {10, 11}]
|
| 30 |
+
assert len(answer) == len(cl)
|
| 31 |
+
assert all(set(c) in answer for c in cl)
|
| 32 |
+
|
| 33 |
+
def test_find_cliques2(self):
|
| 34 |
+
hcl = list(nx.find_cliques(self.H))
|
| 35 |
+
assert sorted(map(sorted, hcl)) == [[1, 2], [1, 4, 5, 6], [2, 3], [3, 4, 6]]
|
| 36 |
+
|
| 37 |
+
def test_find_cliques3(self):
|
| 38 |
+
# all cliques are [[2, 6, 1, 3], [2, 6, 4], [5, 4, 7], [8, 9], [10, 11]]
|
| 39 |
+
|
| 40 |
+
cl = list(nx.find_cliques(self.G, [2]))
|
| 41 |
+
rcl = nx.find_cliques_recursive(self.G, [2])
|
| 42 |
+
expected = [[2, 6, 1, 3], [2, 6, 4]]
|
| 43 |
+
assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected))
|
| 44 |
+
assert sorted(map(sorted, cl)) == sorted(map(sorted, expected))
|
| 45 |
+
|
| 46 |
+
cl = list(nx.find_cliques(self.G, [2, 3]))
|
| 47 |
+
rcl = nx.find_cliques_recursive(self.G, [2, 3])
|
| 48 |
+
expected = [[2, 6, 1, 3]]
|
| 49 |
+
assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected))
|
| 50 |
+
assert sorted(map(sorted, cl)) == sorted(map(sorted, expected))
|
| 51 |
+
|
| 52 |
+
cl = list(nx.find_cliques(self.G, [2, 6, 4]))
|
| 53 |
+
rcl = nx.find_cliques_recursive(self.G, [2, 6, 4])
|
| 54 |
+
expected = [[2, 6, 4]]
|
| 55 |
+
assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected))
|
| 56 |
+
assert sorted(map(sorted, cl)) == sorted(map(sorted, expected))
|
| 57 |
+
|
| 58 |
+
cl = list(nx.find_cliques(self.G, [2, 6, 4]))
|
| 59 |
+
rcl = nx.find_cliques_recursive(self.G, [2, 6, 4])
|
| 60 |
+
expected = [[2, 6, 4]]
|
| 61 |
+
assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected))
|
| 62 |
+
assert sorted(map(sorted, cl)) == sorted(map(sorted, expected))
|
| 63 |
+
|
| 64 |
+
with pytest.raises(ValueError):
|
| 65 |
+
list(nx.find_cliques(self.G, [2, 6, 4, 1]))
|
| 66 |
+
|
| 67 |
+
with pytest.raises(ValueError):
|
| 68 |
+
list(nx.find_cliques_recursive(self.G, [2, 6, 4, 1]))
|
| 69 |
+
|
| 70 |
+
def test_number_of_cliques(self):
|
| 71 |
+
G = self.G
|
| 72 |
+
assert nx.number_of_cliques(G, 1) == 1
|
| 73 |
+
assert list(nx.number_of_cliques(G, [1]).values()) == [1]
|
| 74 |
+
assert list(nx.number_of_cliques(G, [1, 2]).values()) == [1, 2]
|
| 75 |
+
assert nx.number_of_cliques(G, [1, 2]) == {1: 1, 2: 2}
|
| 76 |
+
assert nx.number_of_cliques(G, 2) == 2
|
| 77 |
+
assert nx.number_of_cliques(G) == {
|
| 78 |
+
1: 1,
|
| 79 |
+
2: 2,
|
| 80 |
+
3: 1,
|
| 81 |
+
4: 2,
|
| 82 |
+
5: 1,
|
| 83 |
+
6: 2,
|
| 84 |
+
7: 1,
|
| 85 |
+
8: 1,
|
| 86 |
+
9: 1,
|
| 87 |
+
10: 1,
|
| 88 |
+
11: 1,
|
| 89 |
+
}
|
| 90 |
+
assert nx.number_of_cliques(G, nodes=list(G)) == {
|
| 91 |
+
1: 1,
|
| 92 |
+
2: 2,
|
| 93 |
+
3: 1,
|
| 94 |
+
4: 2,
|
| 95 |
+
5: 1,
|
| 96 |
+
6: 2,
|
| 97 |
+
7: 1,
|
| 98 |
+
8: 1,
|
| 99 |
+
9: 1,
|
| 100 |
+
10: 1,
|
| 101 |
+
11: 1,
|
| 102 |
+
}
|
| 103 |
+
assert nx.number_of_cliques(G, nodes=[2, 3, 4]) == {2: 2, 3: 1, 4: 2}
|
| 104 |
+
assert nx.number_of_cliques(G, cliques=self.cl) == {
|
| 105 |
+
1: 1,
|
| 106 |
+
2: 2,
|
| 107 |
+
3: 1,
|
| 108 |
+
4: 2,
|
| 109 |
+
5: 1,
|
| 110 |
+
6: 2,
|
| 111 |
+
7: 1,
|
| 112 |
+
8: 1,
|
| 113 |
+
9: 1,
|
| 114 |
+
10: 1,
|
| 115 |
+
11: 1,
|
| 116 |
+
}
|
| 117 |
+
assert nx.number_of_cliques(G, list(G), cliques=self.cl) == {
|
| 118 |
+
1: 1,
|
| 119 |
+
2: 2,
|
| 120 |
+
3: 1,
|
| 121 |
+
4: 2,
|
| 122 |
+
5: 1,
|
| 123 |
+
6: 2,
|
| 124 |
+
7: 1,
|
| 125 |
+
8: 1,
|
| 126 |
+
9: 1,
|
| 127 |
+
10: 1,
|
| 128 |
+
11: 1,
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
def test_node_clique_number(self):
|
| 132 |
+
G = self.G
|
| 133 |
+
assert nx.node_clique_number(G, 1) == 4
|
| 134 |
+
assert list(nx.node_clique_number(G, [1]).values()) == [4]
|
| 135 |
+
assert list(nx.node_clique_number(G, [1, 2]).values()) == [4, 4]
|
| 136 |
+
assert nx.node_clique_number(G, [1, 2]) == {1: 4, 2: 4}
|
| 137 |
+
assert nx.node_clique_number(G, 1) == 4
|
| 138 |
+
assert nx.node_clique_number(G) == {
|
| 139 |
+
1: 4,
|
| 140 |
+
2: 4,
|
| 141 |
+
3: 4,
|
| 142 |
+
4: 3,
|
| 143 |
+
5: 3,
|
| 144 |
+
6: 4,
|
| 145 |
+
7: 3,
|
| 146 |
+
8: 2,
|
| 147 |
+
9: 2,
|
| 148 |
+
10: 2,
|
| 149 |
+
11: 2,
|
| 150 |
+
}
|
| 151 |
+
assert nx.node_clique_number(G, cliques=self.cl) == {
|
| 152 |
+
1: 4,
|
| 153 |
+
2: 4,
|
| 154 |
+
3: 4,
|
| 155 |
+
4: 3,
|
| 156 |
+
5: 3,
|
| 157 |
+
6: 4,
|
| 158 |
+
7: 3,
|
| 159 |
+
8: 2,
|
| 160 |
+
9: 2,
|
| 161 |
+
10: 2,
|
| 162 |
+
11: 2,
|
| 163 |
+
}
|
| 164 |
+
assert nx.node_clique_number(G, [1, 2], cliques=self.cl) == {1: 4, 2: 4}
|
| 165 |
+
assert nx.node_clique_number(G, 1, cliques=self.cl) == 4
|
| 166 |
+
|
| 167 |
+
def test_make_clique_bipartite(self):
|
| 168 |
+
G = self.G
|
| 169 |
+
B = nx.make_clique_bipartite(G)
|
| 170 |
+
assert sorted(B) == [-5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
| 171 |
+
# Project onto the nodes of the original graph.
|
| 172 |
+
H = nx.projected_graph(B, range(1, 12))
|
| 173 |
+
assert H.adj == G.adj
|
| 174 |
+
# Project onto the nodes representing the cliques.
|
| 175 |
+
H1 = nx.projected_graph(B, range(-5, 0))
|
| 176 |
+
# Relabel the negative numbers as positive ones.
|
| 177 |
+
H1 = nx.relabel_nodes(H1, {-v: v for v in range(1, 6)})
|
| 178 |
+
assert sorted(H1) == [1, 2, 3, 4, 5]
|
| 179 |
+
|
| 180 |
+
def test_make_max_clique_graph(self):
|
| 181 |
+
"""Tests that the maximal clique graph is the same as the bipartite
|
| 182 |
+
clique graph after being projected onto the nodes representing the
|
| 183 |
+
cliques.
|
| 184 |
+
|
| 185 |
+
"""
|
| 186 |
+
G = self.G
|
| 187 |
+
B = nx.make_clique_bipartite(G)
|
| 188 |
+
# Project onto the nodes representing the cliques.
|
| 189 |
+
H1 = nx.projected_graph(B, range(-5, 0))
|
| 190 |
+
# Relabel the negative numbers as nonnegative ones, starting at
|
| 191 |
+
# 0.
|
| 192 |
+
H1 = nx.relabel_nodes(H1, {-v: v - 1 for v in range(1, 6)})
|
| 193 |
+
H2 = nx.make_max_clique_graph(G)
|
| 194 |
+
assert H1.adj == H2.adj
|
| 195 |
+
|
| 196 |
+
def test_directed(self):
|
| 197 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 198 |
+
next(nx.find_cliques(nx.DiGraph()))
|
| 199 |
+
|
| 200 |
+
def test_find_cliques_trivial(self):
|
| 201 |
+
G = nx.Graph()
|
| 202 |
+
assert sorted(nx.find_cliques(G)) == []
|
| 203 |
+
assert sorted(nx.find_cliques_recursive(G)) == []
|
| 204 |
+
|
| 205 |
+
def test_make_max_clique_graph_create_using(self):
|
| 206 |
+
G = nx.Graph([(1, 2), (3, 1), (4, 1), (5, 6)])
|
| 207 |
+
E = nx.Graph([(0, 1), (0, 2), (1, 2)])
|
| 208 |
+
E.add_node(3)
|
| 209 |
+
assert nx.is_isomorphic(nx.make_max_clique_graph(G, create_using=nx.Graph), E)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class TestEnumerateAllCliques:
|
| 213 |
+
def test_paper_figure_4(self):
|
| 214 |
+
# Same graph as given in Fig. 4 of paper enumerate_all_cliques is
|
| 215 |
+
# based on.
|
| 216 |
+
# http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=1559964&isnumber=33129
|
| 217 |
+
G = nx.Graph()
|
| 218 |
+
edges_fig_4 = [
|
| 219 |
+
("a", "b"),
|
| 220 |
+
("a", "c"),
|
| 221 |
+
("a", "d"),
|
| 222 |
+
("a", "e"),
|
| 223 |
+
("b", "c"),
|
| 224 |
+
("b", "d"),
|
| 225 |
+
("b", "e"),
|
| 226 |
+
("c", "d"),
|
| 227 |
+
("c", "e"),
|
| 228 |
+
("d", "e"),
|
| 229 |
+
("f", "b"),
|
| 230 |
+
("f", "c"),
|
| 231 |
+
("f", "g"),
|
| 232 |
+
("g", "f"),
|
| 233 |
+
("g", "c"),
|
| 234 |
+
("g", "d"),
|
| 235 |
+
("g", "e"),
|
| 236 |
+
]
|
| 237 |
+
G.add_edges_from(edges_fig_4)
|
| 238 |
+
|
| 239 |
+
cliques = list(nx.enumerate_all_cliques(G))
|
| 240 |
+
clique_sizes = list(map(len, cliques))
|
| 241 |
+
assert sorted(clique_sizes) == clique_sizes
|
| 242 |
+
|
| 243 |
+
expected_cliques = [
|
| 244 |
+
["a"],
|
| 245 |
+
["b"],
|
| 246 |
+
["c"],
|
| 247 |
+
["d"],
|
| 248 |
+
["e"],
|
| 249 |
+
["f"],
|
| 250 |
+
["g"],
|
| 251 |
+
["a", "b"],
|
| 252 |
+
["a", "b", "d"],
|
| 253 |
+
["a", "b", "d", "e"],
|
| 254 |
+
["a", "b", "e"],
|
| 255 |
+
["a", "c"],
|
| 256 |
+
["a", "c", "d"],
|
| 257 |
+
["a", "c", "d", "e"],
|
| 258 |
+
["a", "c", "e"],
|
| 259 |
+
["a", "d"],
|
| 260 |
+
["a", "d", "e"],
|
| 261 |
+
["a", "e"],
|
| 262 |
+
["b", "c"],
|
| 263 |
+
["b", "c", "d"],
|
| 264 |
+
["b", "c", "d", "e"],
|
| 265 |
+
["b", "c", "e"],
|
| 266 |
+
["b", "c", "f"],
|
| 267 |
+
["b", "d"],
|
| 268 |
+
["b", "d", "e"],
|
| 269 |
+
["b", "e"],
|
| 270 |
+
["b", "f"],
|
| 271 |
+
["c", "d"],
|
| 272 |
+
["c", "d", "e"],
|
| 273 |
+
["c", "d", "e", "g"],
|
| 274 |
+
["c", "d", "g"],
|
| 275 |
+
["c", "e"],
|
| 276 |
+
["c", "e", "g"],
|
| 277 |
+
["c", "f"],
|
| 278 |
+
["c", "f", "g"],
|
| 279 |
+
["c", "g"],
|
| 280 |
+
["d", "e"],
|
| 281 |
+
["d", "e", "g"],
|
| 282 |
+
["d", "g"],
|
| 283 |
+
["e", "g"],
|
| 284 |
+
["f", "g"],
|
| 285 |
+
["a", "b", "c"],
|
| 286 |
+
["a", "b", "c", "d"],
|
| 287 |
+
["a", "b", "c", "d", "e"],
|
| 288 |
+
["a", "b", "c", "e"],
|
| 289 |
+
]
|
| 290 |
+
|
| 291 |
+
assert sorted(map(sorted, cliques)) == sorted(map(sorted, expected_cliques))
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_cluster.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class TestTriangles:
|
| 7 |
+
def test_empty(self):
|
| 8 |
+
G = nx.Graph()
|
| 9 |
+
assert list(nx.triangles(G).values()) == []
|
| 10 |
+
|
| 11 |
+
def test_path(self):
|
| 12 |
+
G = nx.path_graph(10)
|
| 13 |
+
assert list(nx.triangles(G).values()) == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
| 14 |
+
assert nx.triangles(G) == {
|
| 15 |
+
0: 0,
|
| 16 |
+
1: 0,
|
| 17 |
+
2: 0,
|
| 18 |
+
3: 0,
|
| 19 |
+
4: 0,
|
| 20 |
+
5: 0,
|
| 21 |
+
6: 0,
|
| 22 |
+
7: 0,
|
| 23 |
+
8: 0,
|
| 24 |
+
9: 0,
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
def test_cubical(self):
|
| 28 |
+
G = nx.cubical_graph()
|
| 29 |
+
assert list(nx.triangles(G).values()) == [0, 0, 0, 0, 0, 0, 0, 0]
|
| 30 |
+
assert nx.triangles(G, 1) == 0
|
| 31 |
+
assert list(nx.triangles(G, [1, 2]).values()) == [0, 0]
|
| 32 |
+
assert nx.triangles(G, 1) == 0
|
| 33 |
+
assert nx.triangles(G, [1, 2]) == {1: 0, 2: 0}
|
| 34 |
+
|
| 35 |
+
def test_k5(self):
|
| 36 |
+
G = nx.complete_graph(5)
|
| 37 |
+
assert list(nx.triangles(G).values()) == [6, 6, 6, 6, 6]
|
| 38 |
+
assert sum(nx.triangles(G).values()) / 3 == 10
|
| 39 |
+
assert nx.triangles(G, 1) == 6
|
| 40 |
+
G.remove_edge(1, 2)
|
| 41 |
+
assert list(nx.triangles(G).values()) == [5, 3, 3, 5, 5]
|
| 42 |
+
assert nx.triangles(G, 1) == 3
|
| 43 |
+
G.add_edge(3, 3) # ignore self-edges
|
| 44 |
+
assert list(nx.triangles(G).values()) == [5, 3, 3, 5, 5]
|
| 45 |
+
assert nx.triangles(G, 3) == 5
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class TestDirectedClustering:
|
| 49 |
+
def test_clustering(self):
|
| 50 |
+
G = nx.DiGraph()
|
| 51 |
+
assert list(nx.clustering(G).values()) == []
|
| 52 |
+
assert nx.clustering(G) == {}
|
| 53 |
+
|
| 54 |
+
def test_path(self):
|
| 55 |
+
G = nx.path_graph(10, create_using=nx.DiGraph())
|
| 56 |
+
assert list(nx.clustering(G).values()) == [
|
| 57 |
+
0,
|
| 58 |
+
0,
|
| 59 |
+
0,
|
| 60 |
+
0,
|
| 61 |
+
0,
|
| 62 |
+
0,
|
| 63 |
+
0,
|
| 64 |
+
0,
|
| 65 |
+
0,
|
| 66 |
+
0,
|
| 67 |
+
]
|
| 68 |
+
assert nx.clustering(G) == {
|
| 69 |
+
0: 0,
|
| 70 |
+
1: 0,
|
| 71 |
+
2: 0,
|
| 72 |
+
3: 0,
|
| 73 |
+
4: 0,
|
| 74 |
+
5: 0,
|
| 75 |
+
6: 0,
|
| 76 |
+
7: 0,
|
| 77 |
+
8: 0,
|
| 78 |
+
9: 0,
|
| 79 |
+
}
|
| 80 |
+
assert nx.clustering(G, 0) == 0
|
| 81 |
+
|
| 82 |
+
def test_k5(self):
|
| 83 |
+
G = nx.complete_graph(5, create_using=nx.DiGraph())
|
| 84 |
+
assert list(nx.clustering(G).values()) == [1, 1, 1, 1, 1]
|
| 85 |
+
assert nx.average_clustering(G) == 1
|
| 86 |
+
G.remove_edge(1, 2)
|
| 87 |
+
assert list(nx.clustering(G).values()) == [
|
| 88 |
+
11 / 12,
|
| 89 |
+
1,
|
| 90 |
+
1,
|
| 91 |
+
11 / 12,
|
| 92 |
+
11 / 12,
|
| 93 |
+
]
|
| 94 |
+
assert nx.clustering(G, [1, 4]) == {1: 1, 4: 11 / 12}
|
| 95 |
+
G.remove_edge(2, 1)
|
| 96 |
+
assert list(nx.clustering(G).values()) == [
|
| 97 |
+
5 / 6,
|
| 98 |
+
1,
|
| 99 |
+
1,
|
| 100 |
+
5 / 6,
|
| 101 |
+
5 / 6,
|
| 102 |
+
]
|
| 103 |
+
assert nx.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337}
|
| 104 |
+
assert nx.clustering(G, 4) == 5 / 6
|
| 105 |
+
|
| 106 |
+
def test_triangle_and_edge(self):
|
| 107 |
+
G = nx.cycle_graph(3, create_using=nx.DiGraph())
|
| 108 |
+
G.add_edge(0, 4)
|
| 109 |
+
assert nx.clustering(G)[0] == 1 / 6
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class TestDirectedWeightedClustering:
|
| 113 |
+
@classmethod
|
| 114 |
+
def setup_class(cls):
|
| 115 |
+
global np
|
| 116 |
+
np = pytest.importorskip("numpy")
|
| 117 |
+
|
| 118 |
+
def test_clustering(self):
|
| 119 |
+
G = nx.DiGraph()
|
| 120 |
+
assert list(nx.clustering(G, weight="weight").values()) == []
|
| 121 |
+
assert nx.clustering(G) == {}
|
| 122 |
+
|
| 123 |
+
def test_path(self):
|
| 124 |
+
G = nx.path_graph(10, create_using=nx.DiGraph())
|
| 125 |
+
assert list(nx.clustering(G, weight="weight").values()) == [
|
| 126 |
+
0,
|
| 127 |
+
0,
|
| 128 |
+
0,
|
| 129 |
+
0,
|
| 130 |
+
0,
|
| 131 |
+
0,
|
| 132 |
+
0,
|
| 133 |
+
0,
|
| 134 |
+
0,
|
| 135 |
+
0,
|
| 136 |
+
]
|
| 137 |
+
assert nx.clustering(G, weight="weight") == {
|
| 138 |
+
0: 0,
|
| 139 |
+
1: 0,
|
| 140 |
+
2: 0,
|
| 141 |
+
3: 0,
|
| 142 |
+
4: 0,
|
| 143 |
+
5: 0,
|
| 144 |
+
6: 0,
|
| 145 |
+
7: 0,
|
| 146 |
+
8: 0,
|
| 147 |
+
9: 0,
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
def test_k5(self):
|
| 151 |
+
G = nx.complete_graph(5, create_using=nx.DiGraph())
|
| 152 |
+
assert list(nx.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1]
|
| 153 |
+
assert nx.average_clustering(G, weight="weight") == 1
|
| 154 |
+
G.remove_edge(1, 2)
|
| 155 |
+
assert list(nx.clustering(G, weight="weight").values()) == [
|
| 156 |
+
11 / 12,
|
| 157 |
+
1,
|
| 158 |
+
1,
|
| 159 |
+
11 / 12,
|
| 160 |
+
11 / 12,
|
| 161 |
+
]
|
| 162 |
+
assert nx.clustering(G, [1, 4], weight="weight") == {1: 1, 4: 11 / 12}
|
| 163 |
+
G.remove_edge(2, 1)
|
| 164 |
+
assert list(nx.clustering(G, weight="weight").values()) == [
|
| 165 |
+
5 / 6,
|
| 166 |
+
1,
|
| 167 |
+
1,
|
| 168 |
+
5 / 6,
|
| 169 |
+
5 / 6,
|
| 170 |
+
]
|
| 171 |
+
assert nx.clustering(G, [1, 4], weight="weight") == {
|
| 172 |
+
1: 1,
|
| 173 |
+
4: 0.83333333333333337,
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
def test_triangle_and_edge(self):
|
| 177 |
+
G = nx.cycle_graph(3, create_using=nx.DiGraph())
|
| 178 |
+
G.add_edge(0, 4, weight=2)
|
| 179 |
+
assert nx.clustering(G)[0] == 1 / 6
|
| 180 |
+
# Relaxed comparisons to allow graphblas-algorithms to pass tests
|
| 181 |
+
np.testing.assert_allclose(nx.clustering(G, weight="weight")[0], 1 / 12)
|
| 182 |
+
np.testing.assert_allclose(nx.clustering(G, 0, weight="weight"), 1 / 12)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class TestWeightedClustering:
|
| 186 |
+
@classmethod
|
| 187 |
+
def setup_class(cls):
|
| 188 |
+
global np
|
| 189 |
+
np = pytest.importorskip("numpy")
|
| 190 |
+
|
| 191 |
+
def test_clustering(self):
|
| 192 |
+
G = nx.Graph()
|
| 193 |
+
assert list(nx.clustering(G, weight="weight").values()) == []
|
| 194 |
+
assert nx.clustering(G) == {}
|
| 195 |
+
|
| 196 |
+
def test_path(self):
|
| 197 |
+
G = nx.path_graph(10)
|
| 198 |
+
assert list(nx.clustering(G, weight="weight").values()) == [
|
| 199 |
+
0,
|
| 200 |
+
0,
|
| 201 |
+
0,
|
| 202 |
+
0,
|
| 203 |
+
0,
|
| 204 |
+
0,
|
| 205 |
+
0,
|
| 206 |
+
0,
|
| 207 |
+
0,
|
| 208 |
+
0,
|
| 209 |
+
]
|
| 210 |
+
assert nx.clustering(G, weight="weight") == {
|
| 211 |
+
0: 0,
|
| 212 |
+
1: 0,
|
| 213 |
+
2: 0,
|
| 214 |
+
3: 0,
|
| 215 |
+
4: 0,
|
| 216 |
+
5: 0,
|
| 217 |
+
6: 0,
|
| 218 |
+
7: 0,
|
| 219 |
+
8: 0,
|
| 220 |
+
9: 0,
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
def test_cubical(self):
|
| 224 |
+
G = nx.cubical_graph()
|
| 225 |
+
assert list(nx.clustering(G, weight="weight").values()) == [
|
| 226 |
+
0,
|
| 227 |
+
0,
|
| 228 |
+
0,
|
| 229 |
+
0,
|
| 230 |
+
0,
|
| 231 |
+
0,
|
| 232 |
+
0,
|
| 233 |
+
0,
|
| 234 |
+
]
|
| 235 |
+
assert nx.clustering(G, 1) == 0
|
| 236 |
+
assert list(nx.clustering(G, [1, 2], weight="weight").values()) == [0, 0]
|
| 237 |
+
assert nx.clustering(G, 1, weight="weight") == 0
|
| 238 |
+
assert nx.clustering(G, [1, 2], weight="weight") == {1: 0, 2: 0}
|
| 239 |
+
|
| 240 |
+
def test_k5(self):
|
| 241 |
+
G = nx.complete_graph(5)
|
| 242 |
+
assert list(nx.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1]
|
| 243 |
+
assert nx.average_clustering(G, weight="weight") == 1
|
| 244 |
+
G.remove_edge(1, 2)
|
| 245 |
+
assert list(nx.clustering(G, weight="weight").values()) == [
|
| 246 |
+
5 / 6,
|
| 247 |
+
1,
|
| 248 |
+
1,
|
| 249 |
+
5 / 6,
|
| 250 |
+
5 / 6,
|
| 251 |
+
]
|
| 252 |
+
assert nx.clustering(G, [1, 4], weight="weight") == {
|
| 253 |
+
1: 1,
|
| 254 |
+
4: 0.83333333333333337,
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
def test_triangle_and_edge(self):
|
| 258 |
+
G = nx.cycle_graph(3)
|
| 259 |
+
G.add_edge(0, 4, weight=2)
|
| 260 |
+
assert nx.clustering(G)[0] == 1 / 3
|
| 261 |
+
np.testing.assert_allclose(nx.clustering(G, weight="weight")[0], 1 / 6)
|
| 262 |
+
np.testing.assert_allclose(nx.clustering(G, 0, weight="weight"), 1 / 6)
|
| 263 |
+
|
| 264 |
+
def test_triangle_and_signed_edge(self):
|
| 265 |
+
G = nx.cycle_graph(3)
|
| 266 |
+
G.add_edge(0, 1, weight=-1)
|
| 267 |
+
G.add_edge(3, 0, weight=0)
|
| 268 |
+
assert nx.clustering(G)[0] == 1 / 3
|
| 269 |
+
assert nx.clustering(G, weight="weight")[0] == -1 / 3
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
class TestClustering:
|
| 273 |
+
@classmethod
|
| 274 |
+
def setup_class(cls):
|
| 275 |
+
pytest.importorskip("numpy")
|
| 276 |
+
|
| 277 |
+
def test_clustering(self):
|
| 278 |
+
G = nx.Graph()
|
| 279 |
+
assert list(nx.clustering(G).values()) == []
|
| 280 |
+
assert nx.clustering(G) == {}
|
| 281 |
+
|
| 282 |
+
def test_path(self):
|
| 283 |
+
G = nx.path_graph(10)
|
| 284 |
+
assert list(nx.clustering(G).values()) == [
|
| 285 |
+
0,
|
| 286 |
+
0,
|
| 287 |
+
0,
|
| 288 |
+
0,
|
| 289 |
+
0,
|
| 290 |
+
0,
|
| 291 |
+
0,
|
| 292 |
+
0,
|
| 293 |
+
0,
|
| 294 |
+
0,
|
| 295 |
+
]
|
| 296 |
+
assert nx.clustering(G) == {
|
| 297 |
+
0: 0,
|
| 298 |
+
1: 0,
|
| 299 |
+
2: 0,
|
| 300 |
+
3: 0,
|
| 301 |
+
4: 0,
|
| 302 |
+
5: 0,
|
| 303 |
+
6: 0,
|
| 304 |
+
7: 0,
|
| 305 |
+
8: 0,
|
| 306 |
+
9: 0,
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
def test_cubical(self):
|
| 310 |
+
G = nx.cubical_graph()
|
| 311 |
+
assert list(nx.clustering(G).values()) == [0, 0, 0, 0, 0, 0, 0, 0]
|
| 312 |
+
assert nx.clustering(G, 1) == 0
|
| 313 |
+
assert list(nx.clustering(G, [1, 2]).values()) == [0, 0]
|
| 314 |
+
assert nx.clustering(G, 1) == 0
|
| 315 |
+
assert nx.clustering(G, [1, 2]) == {1: 0, 2: 0}
|
| 316 |
+
|
| 317 |
+
def test_k5(self):
|
| 318 |
+
G = nx.complete_graph(5)
|
| 319 |
+
assert list(nx.clustering(G).values()) == [1, 1, 1, 1, 1]
|
| 320 |
+
assert nx.average_clustering(G) == 1
|
| 321 |
+
G.remove_edge(1, 2)
|
| 322 |
+
assert list(nx.clustering(G).values()) == [
|
| 323 |
+
5 / 6,
|
| 324 |
+
1,
|
| 325 |
+
1,
|
| 326 |
+
5 / 6,
|
| 327 |
+
5 / 6,
|
| 328 |
+
]
|
| 329 |
+
assert nx.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337}
|
| 330 |
+
|
| 331 |
+
def test_k5_signed(self):
|
| 332 |
+
G = nx.complete_graph(5)
|
| 333 |
+
assert list(nx.clustering(G).values()) == [1, 1, 1, 1, 1]
|
| 334 |
+
assert nx.average_clustering(G) == 1
|
| 335 |
+
G.remove_edge(1, 2)
|
| 336 |
+
G.add_edge(0, 1, weight=-1)
|
| 337 |
+
assert list(nx.clustering(G, weight="weight").values()) == [
|
| 338 |
+
1 / 6,
|
| 339 |
+
-1 / 3,
|
| 340 |
+
1,
|
| 341 |
+
3 / 6,
|
| 342 |
+
3 / 6,
|
| 343 |
+
]
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
class TestTransitivity:
|
| 347 |
+
def test_transitivity(self):
|
| 348 |
+
G = nx.Graph()
|
| 349 |
+
assert nx.transitivity(G) == 0
|
| 350 |
+
|
| 351 |
+
def test_path(self):
|
| 352 |
+
G = nx.path_graph(10)
|
| 353 |
+
assert nx.transitivity(G) == 0
|
| 354 |
+
|
| 355 |
+
def test_cubical(self):
|
| 356 |
+
G = nx.cubical_graph()
|
| 357 |
+
assert nx.transitivity(G) == 0
|
| 358 |
+
|
| 359 |
+
def test_k5(self):
|
| 360 |
+
G = nx.complete_graph(5)
|
| 361 |
+
assert nx.transitivity(G) == 1
|
| 362 |
+
G.remove_edge(1, 2)
|
| 363 |
+
assert nx.transitivity(G) == 0.875
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
class TestSquareClustering:
|
| 367 |
+
def test_clustering(self):
|
| 368 |
+
G = nx.Graph()
|
| 369 |
+
assert list(nx.square_clustering(G).values()) == []
|
| 370 |
+
assert nx.square_clustering(G) == {}
|
| 371 |
+
|
| 372 |
+
def test_path(self):
|
| 373 |
+
G = nx.path_graph(10)
|
| 374 |
+
assert list(nx.square_clustering(G).values()) == [
|
| 375 |
+
0,
|
| 376 |
+
0,
|
| 377 |
+
0,
|
| 378 |
+
0,
|
| 379 |
+
0,
|
| 380 |
+
0,
|
| 381 |
+
0,
|
| 382 |
+
0,
|
| 383 |
+
0,
|
| 384 |
+
0,
|
| 385 |
+
]
|
| 386 |
+
assert nx.square_clustering(G) == {
|
| 387 |
+
0: 0,
|
| 388 |
+
1: 0,
|
| 389 |
+
2: 0,
|
| 390 |
+
3: 0,
|
| 391 |
+
4: 0,
|
| 392 |
+
5: 0,
|
| 393 |
+
6: 0,
|
| 394 |
+
7: 0,
|
| 395 |
+
8: 0,
|
| 396 |
+
9: 0,
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
def test_cubical(self):
|
| 400 |
+
G = nx.cubical_graph()
|
| 401 |
+
assert list(nx.square_clustering(G).values()) == [
|
| 402 |
+
1 / 3,
|
| 403 |
+
1 / 3,
|
| 404 |
+
1 / 3,
|
| 405 |
+
1 / 3,
|
| 406 |
+
1 / 3,
|
| 407 |
+
1 / 3,
|
| 408 |
+
1 / 3,
|
| 409 |
+
1 / 3,
|
| 410 |
+
]
|
| 411 |
+
assert list(nx.square_clustering(G, [1, 2]).values()) == [1 / 3, 1 / 3]
|
| 412 |
+
assert nx.square_clustering(G, [1])[1] == 1 / 3
|
| 413 |
+
assert nx.square_clustering(G, 1) == 1 / 3
|
| 414 |
+
assert nx.square_clustering(G, [1, 2]) == {1: 1 / 3, 2: 1 / 3}
|
| 415 |
+
|
| 416 |
+
def test_k5(self):
|
| 417 |
+
G = nx.complete_graph(5)
|
| 418 |
+
assert list(nx.square_clustering(G).values()) == [1, 1, 1, 1, 1]
|
| 419 |
+
|
| 420 |
+
def test_bipartite_k5(self):
|
| 421 |
+
G = nx.complete_bipartite_graph(5, 5)
|
| 422 |
+
assert list(nx.square_clustering(G).values()) == [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
|
| 423 |
+
|
| 424 |
+
def test_lind_square_clustering(self):
|
| 425 |
+
"""Test C4 for figure 1 Lind et al (2005)"""
|
| 426 |
+
G = nx.Graph(
|
| 427 |
+
[
|
| 428 |
+
(1, 2),
|
| 429 |
+
(1, 3),
|
| 430 |
+
(1, 6),
|
| 431 |
+
(1, 7),
|
| 432 |
+
(2, 4),
|
| 433 |
+
(2, 5),
|
| 434 |
+
(3, 4),
|
| 435 |
+
(3, 5),
|
| 436 |
+
(6, 7),
|
| 437 |
+
(7, 8),
|
| 438 |
+
(6, 8),
|
| 439 |
+
(7, 9),
|
| 440 |
+
(7, 10),
|
| 441 |
+
(6, 11),
|
| 442 |
+
(6, 12),
|
| 443 |
+
(2, 13),
|
| 444 |
+
(2, 14),
|
| 445 |
+
(3, 15),
|
| 446 |
+
(3, 16),
|
| 447 |
+
]
|
| 448 |
+
)
|
| 449 |
+
G1 = G.subgraph([1, 2, 3, 4, 5, 13, 14, 15, 16])
|
| 450 |
+
G2 = G.subgraph([1, 6, 7, 8, 9, 10, 11, 12])
|
| 451 |
+
assert nx.square_clustering(G, [1])[1] == 3 / 43
|
| 452 |
+
assert nx.square_clustering(G1, [1])[1] == 2 / 6
|
| 453 |
+
assert nx.square_clustering(G2, [1])[1] == 1 / 5
|
| 454 |
+
|
| 455 |
+
def test_peng_square_clustering(self):
|
| 456 |
+
"""Test eq2 for figure 1 Peng et al (2008)"""
|
| 457 |
+
G = nx.Graph([(1, 2), (1, 3), (2, 4), (3, 4), (3, 5), (3, 6)])
|
| 458 |
+
assert nx.square_clustering(G, [1])[1] == 1 / 3
|
| 459 |
+
|
| 460 |
+
def test_self_loops_square_clustering(self):
|
| 461 |
+
G = nx.path_graph(5)
|
| 462 |
+
assert nx.square_clustering(G) == {0: 0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0}
|
| 463 |
+
G.add_edges_from([(0, 0), (1, 1), (2, 2)])
|
| 464 |
+
assert nx.square_clustering(G) == {0: 1, 1: 0.5, 2: 0.2, 3: 0.0, 4: 0}
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
class TestAverageClustering:
|
| 468 |
+
@classmethod
|
| 469 |
+
def setup_class(cls):
|
| 470 |
+
pytest.importorskip("numpy")
|
| 471 |
+
|
| 472 |
+
def test_empty(self):
|
| 473 |
+
G = nx.Graph()
|
| 474 |
+
with pytest.raises(ZeroDivisionError):
|
| 475 |
+
nx.average_clustering(G)
|
| 476 |
+
|
| 477 |
+
def test_average_clustering(self):
|
| 478 |
+
G = nx.cycle_graph(3)
|
| 479 |
+
G.add_edge(2, 3)
|
| 480 |
+
assert nx.average_clustering(G) == (1 + 1 + 1 / 3) / 4
|
| 481 |
+
assert nx.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 4
|
| 482 |
+
assert nx.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 3
|
| 483 |
+
assert nx.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 3
|
| 484 |
+
assert nx.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 3
|
| 485 |
+
assert nx.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 2
|
| 486 |
+
|
| 487 |
+
def test_average_clustering_signed(self):
|
| 488 |
+
G = nx.cycle_graph(3)
|
| 489 |
+
G.add_edge(2, 3)
|
| 490 |
+
G.add_edge(0, 1, weight=-1)
|
| 491 |
+
assert nx.average_clustering(G, weight="weight") == (-1 - 1 - 1 / 3) / 4
|
| 492 |
+
assert (
|
| 493 |
+
nx.average_clustering(G, weight="weight", count_zeros=True)
|
| 494 |
+
== (-1 - 1 - 1 / 3) / 4
|
| 495 |
+
)
|
| 496 |
+
assert (
|
| 497 |
+
nx.average_clustering(G, weight="weight", count_zeros=False)
|
| 498 |
+
== (-1 - 1 - 1 / 3) / 3
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
class TestDirectedAverageClustering:
|
| 503 |
+
@classmethod
|
| 504 |
+
def setup_class(cls):
|
| 505 |
+
pytest.importorskip("numpy")
|
| 506 |
+
|
| 507 |
+
def test_empty(self):
|
| 508 |
+
G = nx.DiGraph()
|
| 509 |
+
with pytest.raises(ZeroDivisionError):
|
| 510 |
+
nx.average_clustering(G)
|
| 511 |
+
|
| 512 |
+
def test_average_clustering(self):
|
| 513 |
+
G = nx.cycle_graph(3, create_using=nx.DiGraph())
|
| 514 |
+
G.add_edge(2, 3)
|
| 515 |
+
assert nx.average_clustering(G) == (1 + 1 + 1 / 3) / 8
|
| 516 |
+
assert nx.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 8
|
| 517 |
+
assert nx.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 6
|
| 518 |
+
assert nx.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 6
|
| 519 |
+
assert nx.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 6
|
| 520 |
+
assert nx.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 4
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
class TestGeneralizedDegree:
|
| 524 |
+
def test_generalized_degree(self):
|
| 525 |
+
G = nx.Graph()
|
| 526 |
+
assert nx.generalized_degree(G) == {}
|
| 527 |
+
|
| 528 |
+
def test_path(self):
|
| 529 |
+
G = nx.path_graph(5)
|
| 530 |
+
assert nx.generalized_degree(G, 0) == {0: 1}
|
| 531 |
+
assert nx.generalized_degree(G, 1) == {0: 2}
|
| 532 |
+
|
| 533 |
+
def test_cubical(self):
|
| 534 |
+
G = nx.cubical_graph()
|
| 535 |
+
assert nx.generalized_degree(G, 0) == {0: 3}
|
| 536 |
+
|
| 537 |
+
def test_k5(self):
|
| 538 |
+
G = nx.complete_graph(5)
|
| 539 |
+
assert nx.generalized_degree(G, 0) == {3: 4}
|
| 540 |
+
G.remove_edge(0, 1)
|
| 541 |
+
assert nx.generalized_degree(G, 0) == {2: 3}
|
| 542 |
+
assert nx.generalized_degree(G, [1, 2]) == {1: {2: 3}, 2: {2: 2, 3: 2}}
|
| 543 |
+
assert nx.generalized_degree(G) == {
|
| 544 |
+
0: {2: 3},
|
| 545 |
+
1: {2: 3},
|
| 546 |
+
2: {2: 2, 3: 2},
|
| 547 |
+
3: {2: 2, 3: 2},
|
| 548 |
+
4: {2: 2, 3: 2},
|
| 549 |
+
}
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_communicability.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections import defaultdict
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
pytest.importorskip("numpy")
|
| 6 |
+
pytest.importorskip("scipy")
|
| 7 |
+
|
| 8 |
+
import networkx as nx
|
| 9 |
+
from networkx.algorithms.communicability_alg import communicability, communicability_exp
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class TestCommunicability:
|
| 13 |
+
def test_communicability(self):
|
| 14 |
+
answer = {
|
| 15 |
+
0: {0: 1.5430806348152435, 1: 1.1752011936438012},
|
| 16 |
+
1: {0: 1.1752011936438012, 1: 1.5430806348152435},
|
| 17 |
+
}
|
| 18 |
+
# answer={(0, 0): 1.5430806348152435,
|
| 19 |
+
# (0, 1): 1.1752011936438012,
|
| 20 |
+
# (1, 0): 1.1752011936438012,
|
| 21 |
+
# (1, 1): 1.5430806348152435}
|
| 22 |
+
|
| 23 |
+
result = communicability(nx.path_graph(2))
|
| 24 |
+
for k1, val in result.items():
|
| 25 |
+
for k2 in val:
|
| 26 |
+
assert answer[k1][k2] == pytest.approx(result[k1][k2], abs=1e-7)
|
| 27 |
+
|
| 28 |
+
def test_communicability2(self):
|
| 29 |
+
answer_orig = {
|
| 30 |
+
("1", "1"): 1.6445956054135658,
|
| 31 |
+
("1", "Albert"): 0.7430186221096251,
|
| 32 |
+
("1", "Aric"): 0.7430186221096251,
|
| 33 |
+
("1", "Dan"): 1.6208126320442937,
|
| 34 |
+
("1", "Franck"): 0.42639707170035257,
|
| 35 |
+
("Albert", "1"): 0.7430186221096251,
|
| 36 |
+
("Albert", "Albert"): 2.4368257358712189,
|
| 37 |
+
("Albert", "Aric"): 1.4368257358712191,
|
| 38 |
+
("Albert", "Dan"): 2.0472097037446453,
|
| 39 |
+
("Albert", "Franck"): 1.8340111678944691,
|
| 40 |
+
("Aric", "1"): 0.7430186221096251,
|
| 41 |
+
("Aric", "Albert"): 1.4368257358712191,
|
| 42 |
+
("Aric", "Aric"): 2.4368257358712193,
|
| 43 |
+
("Aric", "Dan"): 2.0472097037446457,
|
| 44 |
+
("Aric", "Franck"): 1.8340111678944691,
|
| 45 |
+
("Dan", "1"): 1.6208126320442937,
|
| 46 |
+
("Dan", "Albert"): 2.0472097037446453,
|
| 47 |
+
("Dan", "Aric"): 2.0472097037446457,
|
| 48 |
+
("Dan", "Dan"): 3.1306328496328168,
|
| 49 |
+
("Dan", "Franck"): 1.4860372442192515,
|
| 50 |
+
("Franck", "1"): 0.42639707170035257,
|
| 51 |
+
("Franck", "Albert"): 1.8340111678944691,
|
| 52 |
+
("Franck", "Aric"): 1.8340111678944691,
|
| 53 |
+
("Franck", "Dan"): 1.4860372442192515,
|
| 54 |
+
("Franck", "Franck"): 2.3876142275231915,
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
answer = defaultdict(dict)
|
| 58 |
+
for (k1, k2), v in answer_orig.items():
|
| 59 |
+
answer[k1][k2] = v
|
| 60 |
+
|
| 61 |
+
G1 = nx.Graph(
|
| 62 |
+
[
|
| 63 |
+
("Franck", "Aric"),
|
| 64 |
+
("Aric", "Dan"),
|
| 65 |
+
("Dan", "Albert"),
|
| 66 |
+
("Albert", "Franck"),
|
| 67 |
+
("Dan", "1"),
|
| 68 |
+
("Franck", "Albert"),
|
| 69 |
+
]
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
result = communicability(G1)
|
| 73 |
+
for k1, val in result.items():
|
| 74 |
+
for k2 in val:
|
| 75 |
+
assert answer[k1][k2] == pytest.approx(result[k1][k2], abs=1e-7)
|
| 76 |
+
|
| 77 |
+
result = communicability_exp(G1)
|
| 78 |
+
for k1, val in result.items():
|
| 79 |
+
for k2 in val:
|
| 80 |
+
assert answer[k1][k2] == pytest.approx(result[k1][k2], abs=1e-7)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_core.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from networkx.utils import nodes_equal
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class TestCore:
|
| 8 |
+
@classmethod
|
| 9 |
+
def setup_class(cls):
|
| 10 |
+
# G is the example graph in Figure 1 from Batagelj and
|
| 11 |
+
# Zaversnik's paper titled An O(m) Algorithm for Cores
|
| 12 |
+
# Decomposition of Networks, 2003,
|
| 13 |
+
# http://arXiv.org/abs/cs/0310049. With nodes labeled as
|
| 14 |
+
# shown, the 3-core is given by nodes 1-8, the 2-core by nodes
|
| 15 |
+
# 9-16, the 1-core by nodes 17-20 and node 21 is in the
|
| 16 |
+
# 0-core.
|
| 17 |
+
t1 = nx.convert_node_labels_to_integers(nx.tetrahedral_graph(), 1)
|
| 18 |
+
t2 = nx.convert_node_labels_to_integers(t1, 5)
|
| 19 |
+
G = nx.union(t1, t2)
|
| 20 |
+
G.add_edges_from(
|
| 21 |
+
[
|
| 22 |
+
(3, 7),
|
| 23 |
+
(2, 11),
|
| 24 |
+
(11, 5),
|
| 25 |
+
(11, 12),
|
| 26 |
+
(5, 12),
|
| 27 |
+
(12, 19),
|
| 28 |
+
(12, 18),
|
| 29 |
+
(3, 9),
|
| 30 |
+
(7, 9),
|
| 31 |
+
(7, 10),
|
| 32 |
+
(9, 10),
|
| 33 |
+
(9, 20),
|
| 34 |
+
(17, 13),
|
| 35 |
+
(13, 14),
|
| 36 |
+
(14, 15),
|
| 37 |
+
(15, 16),
|
| 38 |
+
(16, 13),
|
| 39 |
+
]
|
| 40 |
+
)
|
| 41 |
+
G.add_node(21)
|
| 42 |
+
cls.G = G
|
| 43 |
+
|
| 44 |
+
# Create the graph H resulting from the degree sequence
|
| 45 |
+
# [0, 1, 2, 2, 2, 2, 3] when using the Havel-Hakimi algorithm.
|
| 46 |
+
|
| 47 |
+
degseq = [0, 1, 2, 2, 2, 2, 3]
|
| 48 |
+
H = nx.havel_hakimi_graph(degseq)
|
| 49 |
+
mapping = {6: 0, 0: 1, 4: 3, 5: 6, 3: 4, 1: 2, 2: 5}
|
| 50 |
+
cls.H = nx.relabel_nodes(H, mapping)
|
| 51 |
+
|
| 52 |
+
def test_trivial(self):
|
| 53 |
+
"""Empty graph"""
|
| 54 |
+
G = nx.Graph()
|
| 55 |
+
assert nx.core_number(G) == {}
|
| 56 |
+
|
| 57 |
+
def test_core_number(self):
|
| 58 |
+
core = nx.core_number(self.G)
|
| 59 |
+
nodes_by_core = [sorted(n for n in core if core[n] == val) for val in range(4)]
|
| 60 |
+
assert nodes_equal(nodes_by_core[0], [21])
|
| 61 |
+
assert nodes_equal(nodes_by_core[1], [17, 18, 19, 20])
|
| 62 |
+
assert nodes_equal(nodes_by_core[2], [9, 10, 11, 12, 13, 14, 15, 16])
|
| 63 |
+
assert nodes_equal(nodes_by_core[3], [1, 2, 3, 4, 5, 6, 7, 8])
|
| 64 |
+
|
| 65 |
+
def test_core_number2(self):
|
| 66 |
+
core = nx.core_number(self.H)
|
| 67 |
+
nodes_by_core = [sorted(n for n in core if core[n] == val) for val in range(3)]
|
| 68 |
+
assert nodes_equal(nodes_by_core[0], [0])
|
| 69 |
+
assert nodes_equal(nodes_by_core[1], [1, 3])
|
| 70 |
+
assert nodes_equal(nodes_by_core[2], [2, 4, 5, 6])
|
| 71 |
+
|
| 72 |
+
def test_core_number_multigraph(self):
|
| 73 |
+
G = nx.complete_graph(3)
|
| 74 |
+
G = nx.MultiGraph(G)
|
| 75 |
+
G.add_edge(1, 2)
|
| 76 |
+
with pytest.raises(
|
| 77 |
+
nx.NetworkXNotImplemented, match="not implemented for multigraph type"
|
| 78 |
+
):
|
| 79 |
+
nx.core_number(G)
|
| 80 |
+
|
| 81 |
+
def test_core_number_self_loop(self):
|
| 82 |
+
G = nx.cycle_graph(3)
|
| 83 |
+
G.add_edge(0, 0)
|
| 84 |
+
with pytest.raises(
|
| 85 |
+
nx.NetworkXNotImplemented, match="Input graph has self loops"
|
| 86 |
+
):
|
| 87 |
+
nx.core_number(G)
|
| 88 |
+
|
| 89 |
+
def test_directed_core_number(self):
|
| 90 |
+
"""core number had a bug for directed graphs found in issue #1959"""
|
| 91 |
+
# small example where too timid edge removal can make cn[2] = 3
|
| 92 |
+
G = nx.DiGraph()
|
| 93 |
+
edges = [(1, 2), (2, 1), (2, 3), (2, 4), (3, 4), (4, 3)]
|
| 94 |
+
G.add_edges_from(edges)
|
| 95 |
+
assert nx.core_number(G) == {1: 2, 2: 2, 3: 2, 4: 2}
|
| 96 |
+
# small example where too aggressive edge removal can make cn[2] = 2
|
| 97 |
+
more_edges = [(1, 5), (3, 5), (4, 5), (3, 6), (4, 6), (5, 6)]
|
| 98 |
+
G.add_edges_from(more_edges)
|
| 99 |
+
assert nx.core_number(G) == {1: 3, 2: 3, 3: 3, 4: 3, 5: 3, 6: 3}
|
| 100 |
+
|
| 101 |
+
def test_main_core(self):
|
| 102 |
+
main_core_subgraph = nx.k_core(self.H)
|
| 103 |
+
assert sorted(main_core_subgraph.nodes()) == [2, 4, 5, 6]
|
| 104 |
+
|
| 105 |
+
def test_k_core(self):
|
| 106 |
+
# k=0
|
| 107 |
+
k_core_subgraph = nx.k_core(self.H, k=0)
|
| 108 |
+
assert sorted(k_core_subgraph.nodes()) == sorted(self.H.nodes())
|
| 109 |
+
# k=1
|
| 110 |
+
k_core_subgraph = nx.k_core(self.H, k=1)
|
| 111 |
+
assert sorted(k_core_subgraph.nodes()) == [1, 2, 3, 4, 5, 6]
|
| 112 |
+
# k = 2
|
| 113 |
+
k_core_subgraph = nx.k_core(self.H, k=2)
|
| 114 |
+
assert sorted(k_core_subgraph.nodes()) == [2, 4, 5, 6]
|
| 115 |
+
|
| 116 |
+
def test_k_core_multigraph(self):
|
| 117 |
+
core_number = nx.core_number(self.H)
|
| 118 |
+
H = nx.MultiGraph(self.H)
|
| 119 |
+
with pytest.deprecated_call():
|
| 120 |
+
nx.k_core(H, k=0, core_number=core_number)
|
| 121 |
+
|
| 122 |
+
def test_main_crust(self):
|
| 123 |
+
main_crust_subgraph = nx.k_crust(self.H)
|
| 124 |
+
assert sorted(main_crust_subgraph.nodes()) == [0, 1, 3]
|
| 125 |
+
|
| 126 |
+
def test_k_crust(self):
|
| 127 |
+
# k = 0
|
| 128 |
+
k_crust_subgraph = nx.k_crust(self.H, k=2)
|
| 129 |
+
assert sorted(k_crust_subgraph.nodes()) == sorted(self.H.nodes())
|
| 130 |
+
# k=1
|
| 131 |
+
k_crust_subgraph = nx.k_crust(self.H, k=1)
|
| 132 |
+
assert sorted(k_crust_subgraph.nodes()) == [0, 1, 3]
|
| 133 |
+
# k=2
|
| 134 |
+
k_crust_subgraph = nx.k_crust(self.H, k=0)
|
| 135 |
+
assert sorted(k_crust_subgraph.nodes()) == [0]
|
| 136 |
+
|
| 137 |
+
def test_k_crust_multigraph(self):
|
| 138 |
+
core_number = nx.core_number(self.H)
|
| 139 |
+
H = nx.MultiGraph(self.H)
|
| 140 |
+
with pytest.deprecated_call():
|
| 141 |
+
nx.k_crust(H, k=0, core_number=core_number)
|
| 142 |
+
|
| 143 |
+
def test_main_shell(self):
|
| 144 |
+
main_shell_subgraph = nx.k_shell(self.H)
|
| 145 |
+
assert sorted(main_shell_subgraph.nodes()) == [2, 4, 5, 6]
|
| 146 |
+
|
| 147 |
+
def test_k_shell(self):
|
| 148 |
+
# k=0
|
| 149 |
+
k_shell_subgraph = nx.k_shell(self.H, k=2)
|
| 150 |
+
assert sorted(k_shell_subgraph.nodes()) == [2, 4, 5, 6]
|
| 151 |
+
# k=1
|
| 152 |
+
k_shell_subgraph = nx.k_shell(self.H, k=1)
|
| 153 |
+
assert sorted(k_shell_subgraph.nodes()) == [1, 3]
|
| 154 |
+
# k=2
|
| 155 |
+
k_shell_subgraph = nx.k_shell(self.H, k=0)
|
| 156 |
+
assert sorted(k_shell_subgraph.nodes()) == [0]
|
| 157 |
+
|
| 158 |
+
def test_k_shell_multigraph(self):
|
| 159 |
+
core_number = nx.core_number(self.H)
|
| 160 |
+
H = nx.MultiGraph(self.H)
|
| 161 |
+
with pytest.deprecated_call():
|
| 162 |
+
nx.k_shell(H, k=0, core_number=core_number)
|
| 163 |
+
|
| 164 |
+
def test_k_corona(self):
|
| 165 |
+
# k=0
|
| 166 |
+
k_corona_subgraph = nx.k_corona(self.H, k=2)
|
| 167 |
+
assert sorted(k_corona_subgraph.nodes()) == [2, 4, 5, 6]
|
| 168 |
+
# k=1
|
| 169 |
+
k_corona_subgraph = nx.k_corona(self.H, k=1)
|
| 170 |
+
assert sorted(k_corona_subgraph.nodes()) == [1]
|
| 171 |
+
# k=2
|
| 172 |
+
k_corona_subgraph = nx.k_corona(self.H, k=0)
|
| 173 |
+
assert sorted(k_corona_subgraph.nodes()) == [0]
|
| 174 |
+
|
| 175 |
+
def test_k_corona_multigraph(self):
|
| 176 |
+
core_number = nx.core_number(self.H)
|
| 177 |
+
H = nx.MultiGraph(self.H)
|
| 178 |
+
with pytest.deprecated_call():
|
| 179 |
+
nx.k_corona(H, k=0, core_number=core_number)
|
| 180 |
+
|
| 181 |
+
def test_k_truss(self):
|
| 182 |
+
# k=-1
|
| 183 |
+
k_truss_subgraph = nx.k_truss(self.G, -1)
|
| 184 |
+
assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21))
|
| 185 |
+
# k=0
|
| 186 |
+
k_truss_subgraph = nx.k_truss(self.G, 0)
|
| 187 |
+
assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21))
|
| 188 |
+
# k=1
|
| 189 |
+
k_truss_subgraph = nx.k_truss(self.G, 1)
|
| 190 |
+
assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21))
|
| 191 |
+
# k=2
|
| 192 |
+
k_truss_subgraph = nx.k_truss(self.G, 2)
|
| 193 |
+
assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21))
|
| 194 |
+
# k=3
|
| 195 |
+
k_truss_subgraph = nx.k_truss(self.G, 3)
|
| 196 |
+
assert sorted(k_truss_subgraph.nodes()) == list(range(1, 13))
|
| 197 |
+
|
| 198 |
+
k_truss_subgraph = nx.k_truss(self.G, 4)
|
| 199 |
+
assert sorted(k_truss_subgraph.nodes()) == list(range(1, 9))
|
| 200 |
+
|
| 201 |
+
k_truss_subgraph = nx.k_truss(self.G, 5)
|
| 202 |
+
assert sorted(k_truss_subgraph.nodes()) == []
|
| 203 |
+
|
| 204 |
+
def test_k_truss_digraph(self):
|
| 205 |
+
G = nx.complete_graph(3)
|
| 206 |
+
G = nx.DiGraph(G)
|
| 207 |
+
G.add_edge(2, 1)
|
| 208 |
+
with pytest.raises(
|
| 209 |
+
nx.NetworkXNotImplemented, match="not implemented for directed type"
|
| 210 |
+
):
|
| 211 |
+
nx.k_truss(G, k=1)
|
| 212 |
+
|
| 213 |
+
def test_k_truss_multigraph(self):
|
| 214 |
+
G = nx.complete_graph(3)
|
| 215 |
+
G = nx.MultiGraph(G)
|
| 216 |
+
G.add_edge(1, 2)
|
| 217 |
+
with pytest.raises(
|
| 218 |
+
nx.NetworkXNotImplemented, match="not implemented for multigraph type"
|
| 219 |
+
):
|
| 220 |
+
nx.k_truss(G, k=1)
|
| 221 |
+
|
| 222 |
+
def test_k_truss_self_loop(self):
|
| 223 |
+
G = nx.cycle_graph(3)
|
| 224 |
+
G.add_edge(0, 0)
|
| 225 |
+
with pytest.raises(
|
| 226 |
+
nx.NetworkXNotImplemented, match="Input graph has self loops"
|
| 227 |
+
):
|
| 228 |
+
nx.k_truss(G, k=1)
|
| 229 |
+
|
| 230 |
+
def test_onion_layers(self):
|
| 231 |
+
layers = nx.onion_layers(self.G)
|
| 232 |
+
nodes_by_layer = [
|
| 233 |
+
sorted(n for n in layers if layers[n] == val) for val in range(1, 7)
|
| 234 |
+
]
|
| 235 |
+
assert nodes_equal(nodes_by_layer[0], [21])
|
| 236 |
+
assert nodes_equal(nodes_by_layer[1], [17, 18, 19, 20])
|
| 237 |
+
assert nodes_equal(nodes_by_layer[2], [10, 12, 13, 14, 15, 16])
|
| 238 |
+
assert nodes_equal(nodes_by_layer[3], [9, 11])
|
| 239 |
+
assert nodes_equal(nodes_by_layer[4], [1, 2, 4, 5, 6, 8])
|
| 240 |
+
assert nodes_equal(nodes_by_layer[5], [3, 7])
|
| 241 |
+
|
| 242 |
+
def test_onion_digraph(self):
|
| 243 |
+
G = nx.complete_graph(3)
|
| 244 |
+
G = nx.DiGraph(G)
|
| 245 |
+
G.add_edge(2, 1)
|
| 246 |
+
with pytest.raises(
|
| 247 |
+
nx.NetworkXNotImplemented, match="not implemented for directed type"
|
| 248 |
+
):
|
| 249 |
+
nx.onion_layers(G)
|
| 250 |
+
|
| 251 |
+
def test_onion_multigraph(self):
|
| 252 |
+
G = nx.complete_graph(3)
|
| 253 |
+
G = nx.MultiGraph(G)
|
| 254 |
+
G.add_edge(1, 2)
|
| 255 |
+
with pytest.raises(
|
| 256 |
+
nx.NetworkXNotImplemented, match="not implemented for multigraph type"
|
| 257 |
+
):
|
| 258 |
+
nx.onion_layers(G)
|
| 259 |
+
|
| 260 |
+
def test_onion_self_loop(self):
|
| 261 |
+
G = nx.cycle_graph(3)
|
| 262 |
+
G.add_edge(0, 0)
|
| 263 |
+
with pytest.raises(
|
| 264 |
+
nx.NetworkXNotImplemented, match="Input graph contains self loops"
|
| 265 |
+
):
|
| 266 |
+
nx.onion_layers(G)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_covering.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class TestMinEdgeCover:
|
| 7 |
+
"""Tests for :func:`networkx.algorithms.min_edge_cover`"""
|
| 8 |
+
|
| 9 |
+
def test_empty_graph(self):
|
| 10 |
+
G = nx.Graph()
|
| 11 |
+
assert nx.min_edge_cover(G) == set()
|
| 12 |
+
|
| 13 |
+
def test_graph_with_loop(self):
|
| 14 |
+
G = nx.Graph()
|
| 15 |
+
G.add_edge(0, 0)
|
| 16 |
+
assert nx.min_edge_cover(G) == {(0, 0)}
|
| 17 |
+
|
| 18 |
+
def test_graph_with_isolated_v(self):
|
| 19 |
+
G = nx.Graph()
|
| 20 |
+
G.add_node(1)
|
| 21 |
+
with pytest.raises(
|
| 22 |
+
nx.NetworkXException,
|
| 23 |
+
match="Graph has a node with no edge incident on it, so no edge cover exists.",
|
| 24 |
+
):
|
| 25 |
+
nx.min_edge_cover(G)
|
| 26 |
+
|
| 27 |
+
def test_graph_single_edge(self):
|
| 28 |
+
G = nx.Graph([(0, 1)])
|
| 29 |
+
assert nx.min_edge_cover(G) in ({(0, 1)}, {(1, 0)})
|
| 30 |
+
|
| 31 |
+
def test_graph_two_edge_path(self):
|
| 32 |
+
G = nx.path_graph(3)
|
| 33 |
+
min_cover = nx.min_edge_cover(G)
|
| 34 |
+
assert len(min_cover) == 2
|
| 35 |
+
for u, v in G.edges:
|
| 36 |
+
assert (u, v) in min_cover or (v, u) in min_cover
|
| 37 |
+
|
| 38 |
+
def test_bipartite_explicit(self):
|
| 39 |
+
G = nx.Graph()
|
| 40 |
+
G.add_nodes_from([1, 2, 3, 4], bipartite=0)
|
| 41 |
+
G.add_nodes_from(["a", "b", "c"], bipartite=1)
|
| 42 |
+
G.add_edges_from([(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c"), (4, "a")])
|
| 43 |
+
# Use bipartite method by prescribing the algorithm
|
| 44 |
+
min_cover = nx.min_edge_cover(
|
| 45 |
+
G, nx.algorithms.bipartite.matching.eppstein_matching
|
| 46 |
+
)
|
| 47 |
+
assert nx.is_edge_cover(G, min_cover)
|
| 48 |
+
assert len(min_cover) == 8
|
| 49 |
+
# Use the default method which is not specialized for bipartite
|
| 50 |
+
min_cover2 = nx.min_edge_cover(G)
|
| 51 |
+
assert nx.is_edge_cover(G, min_cover2)
|
| 52 |
+
assert len(min_cover2) == 4
|
| 53 |
+
|
| 54 |
+
def test_complete_graph_even(self):
|
| 55 |
+
G = nx.complete_graph(10)
|
| 56 |
+
min_cover = nx.min_edge_cover(G)
|
| 57 |
+
assert nx.is_edge_cover(G, min_cover)
|
| 58 |
+
assert len(min_cover) == 5
|
| 59 |
+
|
| 60 |
+
def test_complete_graph_odd(self):
|
| 61 |
+
G = nx.complete_graph(11)
|
| 62 |
+
min_cover = nx.min_edge_cover(G)
|
| 63 |
+
assert nx.is_edge_cover(G, min_cover)
|
| 64 |
+
assert len(min_cover) == 6
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class TestIsEdgeCover:
|
| 68 |
+
"""Tests for :func:`networkx.algorithms.is_edge_cover`"""
|
| 69 |
+
|
| 70 |
+
def test_empty_graph(self):
|
| 71 |
+
G = nx.Graph()
|
| 72 |
+
assert nx.is_edge_cover(G, set())
|
| 73 |
+
|
| 74 |
+
def test_graph_with_loop(self):
|
| 75 |
+
G = nx.Graph()
|
| 76 |
+
G.add_edge(1, 1)
|
| 77 |
+
assert nx.is_edge_cover(G, {(1, 1)})
|
| 78 |
+
|
| 79 |
+
def test_graph_single_edge(self):
|
| 80 |
+
G = nx.Graph()
|
| 81 |
+
G.add_edge(0, 1)
|
| 82 |
+
assert nx.is_edge_cover(G, {(0, 0), (1, 1)})
|
| 83 |
+
assert nx.is_edge_cover(G, {(0, 1), (1, 0)})
|
| 84 |
+
assert nx.is_edge_cover(G, {(0, 1)})
|
| 85 |
+
assert not nx.is_edge_cover(G, {(0, 0)})
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_cuts.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for the :mod:`networkx.algorithms.cuts` module."""
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class TestCutSize:
|
| 7 |
+
"""Unit tests for the :func:`~networkx.cut_size` function."""
|
| 8 |
+
|
| 9 |
+
def test_symmetric(self):
|
| 10 |
+
"""Tests that the cut size is symmetric."""
|
| 11 |
+
G = nx.barbell_graph(3, 0)
|
| 12 |
+
S = {0, 1, 4}
|
| 13 |
+
T = {2, 3, 5}
|
| 14 |
+
assert nx.cut_size(G, S, T) == 4
|
| 15 |
+
assert nx.cut_size(G, T, S) == 4
|
| 16 |
+
|
| 17 |
+
def test_single_edge(self):
|
| 18 |
+
"""Tests for a cut of a single edge."""
|
| 19 |
+
G = nx.barbell_graph(3, 0)
|
| 20 |
+
S = {0, 1, 2}
|
| 21 |
+
T = {3, 4, 5}
|
| 22 |
+
assert nx.cut_size(G, S, T) == 1
|
| 23 |
+
assert nx.cut_size(G, T, S) == 1
|
| 24 |
+
|
| 25 |
+
def test_directed(self):
|
| 26 |
+
"""Tests that each directed edge is counted once in the cut."""
|
| 27 |
+
G = nx.barbell_graph(3, 0).to_directed()
|
| 28 |
+
S = {0, 1, 2}
|
| 29 |
+
T = {3, 4, 5}
|
| 30 |
+
assert nx.cut_size(G, S, T) == 2
|
| 31 |
+
assert nx.cut_size(G, T, S) == 2
|
| 32 |
+
|
| 33 |
+
def test_directed_symmetric(self):
|
| 34 |
+
"""Tests that a cut in a directed graph is symmetric."""
|
| 35 |
+
G = nx.barbell_graph(3, 0).to_directed()
|
| 36 |
+
S = {0, 1, 4}
|
| 37 |
+
T = {2, 3, 5}
|
| 38 |
+
assert nx.cut_size(G, S, T) == 8
|
| 39 |
+
assert nx.cut_size(G, T, S) == 8
|
| 40 |
+
|
| 41 |
+
def test_multigraph(self):
|
| 42 |
+
"""Tests that parallel edges are each counted for a cut."""
|
| 43 |
+
G = nx.MultiGraph(["ab", "ab"])
|
| 44 |
+
assert nx.cut_size(G, {"a"}, {"b"}) == 2
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class TestVolume:
|
| 48 |
+
"""Unit tests for the :func:`~networkx.volume` function."""
|
| 49 |
+
|
| 50 |
+
def test_graph(self):
|
| 51 |
+
G = nx.cycle_graph(4)
|
| 52 |
+
assert nx.volume(G, {0, 1}) == 4
|
| 53 |
+
|
| 54 |
+
def test_digraph(self):
|
| 55 |
+
G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 0)])
|
| 56 |
+
assert nx.volume(G, {0, 1}) == 2
|
| 57 |
+
|
| 58 |
+
def test_multigraph(self):
|
| 59 |
+
edges = list(nx.cycle_graph(4).edges())
|
| 60 |
+
G = nx.MultiGraph(edges * 2)
|
| 61 |
+
assert nx.volume(G, {0, 1}) == 8
|
| 62 |
+
|
| 63 |
+
def test_multidigraph(self):
|
| 64 |
+
edges = [(0, 1), (1, 2), (2, 3), (3, 0)]
|
| 65 |
+
G = nx.MultiDiGraph(edges * 2)
|
| 66 |
+
assert nx.volume(G, {0, 1}) == 4
|
| 67 |
+
|
| 68 |
+
def test_barbell(self):
|
| 69 |
+
G = nx.barbell_graph(3, 0)
|
| 70 |
+
assert nx.volume(G, {0, 1, 2}) == 7
|
| 71 |
+
assert nx.volume(G, {3, 4, 5}) == 7
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class TestNormalizedCutSize:
|
| 75 |
+
"""Unit tests for the :func:`~networkx.normalized_cut_size` function."""
|
| 76 |
+
|
| 77 |
+
def test_graph(self):
|
| 78 |
+
G = nx.path_graph(4)
|
| 79 |
+
S = {1, 2}
|
| 80 |
+
T = set(G) - S
|
| 81 |
+
size = nx.normalized_cut_size(G, S, T)
|
| 82 |
+
# The cut looks like this: o-{-o--o-}-o
|
| 83 |
+
expected = 2 * ((1 / 4) + (1 / 2))
|
| 84 |
+
assert expected == size
|
| 85 |
+
# Test with no input T
|
| 86 |
+
assert expected == nx.normalized_cut_size(G, S)
|
| 87 |
+
|
| 88 |
+
def test_directed(self):
|
| 89 |
+
G = nx.DiGraph([(0, 1), (1, 2), (2, 3)])
|
| 90 |
+
S = {1, 2}
|
| 91 |
+
T = set(G) - S
|
| 92 |
+
size = nx.normalized_cut_size(G, S, T)
|
| 93 |
+
# The cut looks like this: o-{->o-->o-}->o
|
| 94 |
+
expected = 2 * ((1 / 2) + (1 / 1))
|
| 95 |
+
assert expected == size
|
| 96 |
+
# Test with no input T
|
| 97 |
+
assert expected == nx.normalized_cut_size(G, S)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class TestConductance:
|
| 101 |
+
"""Unit tests for the :func:`~networkx.conductance` function."""
|
| 102 |
+
|
| 103 |
+
def test_graph(self):
|
| 104 |
+
G = nx.barbell_graph(5, 0)
|
| 105 |
+
# Consider the singleton sets containing the "bridge" nodes.
|
| 106 |
+
# There is only one cut edge, and each set has volume five.
|
| 107 |
+
S = {4}
|
| 108 |
+
T = {5}
|
| 109 |
+
conductance = nx.conductance(G, S, T)
|
| 110 |
+
expected = 1 / 5
|
| 111 |
+
assert expected == conductance
|
| 112 |
+
# Test with no input T
|
| 113 |
+
G2 = nx.barbell_graph(3, 0)
|
| 114 |
+
# There is only one cut edge, and each set has volume seven.
|
| 115 |
+
S2 = {0, 1, 2}
|
| 116 |
+
assert nx.conductance(G2, S2) == 1 / 7
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class TestEdgeExpansion:
|
| 120 |
+
"""Unit tests for the :func:`~networkx.edge_expansion` function."""
|
| 121 |
+
|
| 122 |
+
def test_graph(self):
|
| 123 |
+
G = nx.barbell_graph(5, 0)
|
| 124 |
+
S = set(range(5))
|
| 125 |
+
T = set(G) - S
|
| 126 |
+
expansion = nx.edge_expansion(G, S, T)
|
| 127 |
+
expected = 1 / 5
|
| 128 |
+
assert expected == expansion
|
| 129 |
+
# Test with no input T
|
| 130 |
+
assert expected == nx.edge_expansion(G, S)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class TestNodeExpansion:
|
| 134 |
+
"""Unit tests for the :func:`~networkx.node_expansion` function."""
|
| 135 |
+
|
| 136 |
+
def test_graph(self):
|
| 137 |
+
G = nx.path_graph(8)
|
| 138 |
+
S = {3, 4, 5}
|
| 139 |
+
expansion = nx.node_expansion(G, S)
|
| 140 |
+
# The neighborhood of S has cardinality five, and S has
|
| 141 |
+
# cardinality three.
|
| 142 |
+
expected = 5 / 3
|
| 143 |
+
assert expected == expansion
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class TestBoundaryExpansion:
|
| 147 |
+
"""Unit tests for the :func:`~networkx.boundary_expansion` function."""
|
| 148 |
+
|
| 149 |
+
def test_graph(self):
|
| 150 |
+
G = nx.complete_graph(10)
|
| 151 |
+
S = set(range(4))
|
| 152 |
+
expansion = nx.boundary_expansion(G, S)
|
| 153 |
+
# The node boundary of S has cardinality six, and S has
|
| 154 |
+
# cardinality three.
|
| 155 |
+
expected = 6 / 4
|
| 156 |
+
assert expected == expansion
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class TestMixingExpansion:
|
| 160 |
+
"""Unit tests for the :func:`~networkx.mixing_expansion` function."""
|
| 161 |
+
|
| 162 |
+
def test_graph(self):
|
| 163 |
+
G = nx.barbell_graph(5, 0)
|
| 164 |
+
S = set(range(5))
|
| 165 |
+
T = set(G) - S
|
| 166 |
+
expansion = nx.mixing_expansion(G, S, T)
|
| 167 |
+
# There is one cut edge, and the total number of edges in the
|
| 168 |
+
# graph is twice the total number of edges in a clique of size
|
| 169 |
+
# five, plus one more for the bridge.
|
| 170 |
+
expected = 1 / (2 * (5 * 4 + 1))
|
| 171 |
+
assert expected == expansion
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_cycles.py
ADDED
|
@@ -0,0 +1,974 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from itertools import chain, islice, tee
|
| 2 |
+
from math import inf
|
| 3 |
+
from random import shuffle
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
from networkx.algorithms.traversal.edgedfs import FORWARD, REVERSE
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def check_independent(basis):
|
| 12 |
+
if len(basis) == 0:
|
| 13 |
+
return
|
| 14 |
+
|
| 15 |
+
np = pytest.importorskip("numpy")
|
| 16 |
+
sp = pytest.importorskip("scipy") # Required by incidence_matrix
|
| 17 |
+
|
| 18 |
+
H = nx.Graph()
|
| 19 |
+
for b in basis:
|
| 20 |
+
nx.add_cycle(H, b)
|
| 21 |
+
inc = nx.incidence_matrix(H, oriented=True)
|
| 22 |
+
rank = np.linalg.matrix_rank(inc.toarray(), tol=None, hermitian=False)
|
| 23 |
+
assert inc.shape[1] - rank == len(basis)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class TestCycles:
|
| 27 |
+
@classmethod
|
| 28 |
+
def setup_class(cls):
|
| 29 |
+
G = nx.Graph()
|
| 30 |
+
nx.add_cycle(G, [0, 1, 2, 3])
|
| 31 |
+
nx.add_cycle(G, [0, 3, 4, 5])
|
| 32 |
+
nx.add_cycle(G, [0, 1, 6, 7, 8])
|
| 33 |
+
G.add_edge(8, 9)
|
| 34 |
+
cls.G = G
|
| 35 |
+
|
| 36 |
+
def is_cyclic_permutation(self, a, b):
|
| 37 |
+
n = len(a)
|
| 38 |
+
if len(b) != n:
|
| 39 |
+
return False
|
| 40 |
+
l = a + a
|
| 41 |
+
return any(l[i : i + n] == b for i in range(n))
|
| 42 |
+
|
| 43 |
+
def test_cycle_basis(self):
|
| 44 |
+
G = self.G
|
| 45 |
+
cy = nx.cycle_basis(G, 0)
|
| 46 |
+
sort_cy = sorted(sorted(c) for c in cy)
|
| 47 |
+
assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
|
| 48 |
+
cy = nx.cycle_basis(G, 1)
|
| 49 |
+
sort_cy = sorted(sorted(c) for c in cy)
|
| 50 |
+
assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
|
| 51 |
+
cy = nx.cycle_basis(G, 9)
|
| 52 |
+
sort_cy = sorted(sorted(c) for c in cy)
|
| 53 |
+
assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
|
| 54 |
+
# test disconnected graphs
|
| 55 |
+
nx.add_cycle(G, "ABC")
|
| 56 |
+
cy = nx.cycle_basis(G, 9)
|
| 57 |
+
sort_cy = sorted(sorted(c) for c in cy[:-1]) + [sorted(cy[-1])]
|
| 58 |
+
assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5], ["A", "B", "C"]]
|
| 59 |
+
|
| 60 |
+
def test_cycle_basis2(self):
|
| 61 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 62 |
+
G = nx.DiGraph()
|
| 63 |
+
cy = nx.cycle_basis(G, 0)
|
| 64 |
+
|
| 65 |
+
def test_cycle_basis3(self):
|
| 66 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 67 |
+
G = nx.MultiGraph()
|
| 68 |
+
cy = nx.cycle_basis(G, 0)
|
| 69 |
+
|
| 70 |
+
def test_cycle_basis_ordered(self):
|
| 71 |
+
# see gh-6654 replace sets with (ordered) dicts
|
| 72 |
+
G = nx.cycle_graph(5)
|
| 73 |
+
G.update(nx.cycle_graph(range(3, 8)))
|
| 74 |
+
cbG = nx.cycle_basis(G)
|
| 75 |
+
|
| 76 |
+
perm = {1: 0, 0: 1} # switch 0 and 1
|
| 77 |
+
H = nx.relabel_nodes(G, perm)
|
| 78 |
+
cbH = [[perm.get(n, n) for n in cyc] for cyc in nx.cycle_basis(H)]
|
| 79 |
+
assert cbG == cbH
|
| 80 |
+
|
| 81 |
+
def test_cycle_basis_self_loop(self):
|
| 82 |
+
"""Tests the function for graphs with self loops"""
|
| 83 |
+
G = nx.Graph()
|
| 84 |
+
nx.add_cycle(G, [0, 1, 2, 3])
|
| 85 |
+
nx.add_cycle(G, [0, 0, 6, 2])
|
| 86 |
+
cy = nx.cycle_basis(G)
|
| 87 |
+
sort_cy = sorted(sorted(c) for c in cy)
|
| 88 |
+
assert sort_cy == [[0], [0, 1, 2], [0, 2, 3], [0, 2, 6]]
|
| 89 |
+
|
| 90 |
+
def test_simple_cycles(self):
|
| 91 |
+
edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
|
| 92 |
+
G = nx.DiGraph(edges)
|
| 93 |
+
cc = sorted(nx.simple_cycles(G))
|
| 94 |
+
ca = [[0], [0, 1, 2], [0, 2], [1, 2], [2]]
|
| 95 |
+
assert len(cc) == len(ca)
|
| 96 |
+
for c in cc:
|
| 97 |
+
assert any(self.is_cyclic_permutation(c, rc) for rc in ca)
|
| 98 |
+
|
| 99 |
+
def test_simple_cycles_singleton(self):
|
| 100 |
+
G = nx.Graph([(0, 0)]) # self-loop
|
| 101 |
+
assert list(nx.simple_cycles(G)) == [[0]]
|
| 102 |
+
|
| 103 |
+
def test_unsortable(self):
|
| 104 |
+
# this test ensures that graphs whose nodes without an intrinsic
|
| 105 |
+
# ordering do not cause issues
|
| 106 |
+
G = nx.DiGraph()
|
| 107 |
+
nx.add_cycle(G, ["a", 1])
|
| 108 |
+
c = list(nx.simple_cycles(G))
|
| 109 |
+
assert len(c) == 1
|
| 110 |
+
|
| 111 |
+
def test_simple_cycles_small(self):
|
| 112 |
+
G = nx.DiGraph()
|
| 113 |
+
nx.add_cycle(G, [1, 2, 3])
|
| 114 |
+
c = sorted(nx.simple_cycles(G))
|
| 115 |
+
assert len(c) == 1
|
| 116 |
+
assert self.is_cyclic_permutation(c[0], [1, 2, 3])
|
| 117 |
+
nx.add_cycle(G, [10, 20, 30])
|
| 118 |
+
cc = sorted(nx.simple_cycles(G))
|
| 119 |
+
assert len(cc) == 2
|
| 120 |
+
ca = [[1, 2, 3], [10, 20, 30]]
|
| 121 |
+
for c in cc:
|
| 122 |
+
assert any(self.is_cyclic_permutation(c, rc) for rc in ca)
|
| 123 |
+
|
| 124 |
+
def test_simple_cycles_empty(self):
|
| 125 |
+
G = nx.DiGraph()
|
| 126 |
+
assert list(nx.simple_cycles(G)) == []
|
| 127 |
+
|
| 128 |
+
def worst_case_graph(self, k):
|
| 129 |
+
# see figure 1 in Johnson's paper
|
| 130 |
+
# this graph has exactly 3k simple cycles
|
| 131 |
+
G = nx.DiGraph()
|
| 132 |
+
for n in range(2, k + 2):
|
| 133 |
+
G.add_edge(1, n)
|
| 134 |
+
G.add_edge(n, k + 2)
|
| 135 |
+
G.add_edge(2 * k + 1, 1)
|
| 136 |
+
for n in range(k + 2, 2 * k + 2):
|
| 137 |
+
G.add_edge(n, 2 * k + 2)
|
| 138 |
+
G.add_edge(n, n + 1)
|
| 139 |
+
G.add_edge(2 * k + 3, k + 2)
|
| 140 |
+
for n in range(2 * k + 3, 3 * k + 3):
|
| 141 |
+
G.add_edge(2 * k + 2, n)
|
| 142 |
+
G.add_edge(n, 3 * k + 3)
|
| 143 |
+
G.add_edge(3 * k + 3, 2 * k + 2)
|
| 144 |
+
return G
|
| 145 |
+
|
| 146 |
+
def test_worst_case_graph(self):
|
| 147 |
+
# see figure 1 in Johnson's paper
|
| 148 |
+
for k in range(3, 10):
|
| 149 |
+
G = self.worst_case_graph(k)
|
| 150 |
+
l = len(list(nx.simple_cycles(G)))
|
| 151 |
+
assert l == 3 * k
|
| 152 |
+
|
| 153 |
+
def test_recursive_simple_and_not(self):
|
| 154 |
+
for k in range(2, 10):
|
| 155 |
+
G = self.worst_case_graph(k)
|
| 156 |
+
cc = sorted(nx.simple_cycles(G))
|
| 157 |
+
rcc = sorted(nx.recursive_simple_cycles(G))
|
| 158 |
+
assert len(cc) == len(rcc)
|
| 159 |
+
for c in cc:
|
| 160 |
+
assert any(self.is_cyclic_permutation(c, r) for r in rcc)
|
| 161 |
+
for rc in rcc:
|
| 162 |
+
assert any(self.is_cyclic_permutation(rc, c) for c in cc)
|
| 163 |
+
|
| 164 |
+
def test_simple_graph_with_reported_bug(self):
|
| 165 |
+
G = nx.DiGraph()
|
| 166 |
+
edges = [
|
| 167 |
+
(0, 2),
|
| 168 |
+
(0, 3),
|
| 169 |
+
(1, 0),
|
| 170 |
+
(1, 3),
|
| 171 |
+
(2, 1),
|
| 172 |
+
(2, 4),
|
| 173 |
+
(3, 2),
|
| 174 |
+
(3, 4),
|
| 175 |
+
(4, 0),
|
| 176 |
+
(4, 1),
|
| 177 |
+
(4, 5),
|
| 178 |
+
(5, 0),
|
| 179 |
+
(5, 1),
|
| 180 |
+
(5, 2),
|
| 181 |
+
(5, 3),
|
| 182 |
+
]
|
| 183 |
+
G.add_edges_from(edges)
|
| 184 |
+
cc = sorted(nx.simple_cycles(G))
|
| 185 |
+
assert len(cc) == 26
|
| 186 |
+
rcc = sorted(nx.recursive_simple_cycles(G))
|
| 187 |
+
assert len(cc) == len(rcc)
|
| 188 |
+
for c in cc:
|
| 189 |
+
assert any(self.is_cyclic_permutation(c, rc) for rc in rcc)
|
| 190 |
+
for rc in rcc:
|
| 191 |
+
assert any(self.is_cyclic_permutation(rc, c) for c in cc)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def pairwise(iterable):
|
| 195 |
+
a, b = tee(iterable)
|
| 196 |
+
next(b, None)
|
| 197 |
+
return zip(a, b)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def cycle_edges(c):
|
| 201 |
+
return pairwise(chain(c, islice(c, 1)))
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def directed_cycle_edgeset(c):
|
| 205 |
+
return frozenset(cycle_edges(c))
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def undirected_cycle_edgeset(c):
|
| 209 |
+
if len(c) == 1:
|
| 210 |
+
return frozenset(cycle_edges(c))
|
| 211 |
+
return frozenset(map(frozenset, cycle_edges(c)))
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def multigraph_cycle_edgeset(c):
|
| 215 |
+
if len(c) <= 2:
|
| 216 |
+
return frozenset(cycle_edges(c))
|
| 217 |
+
else:
|
| 218 |
+
return frozenset(map(frozenset, cycle_edges(c)))
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
class TestCycleEnumeration:
|
| 222 |
+
@staticmethod
|
| 223 |
+
def K(n):
|
| 224 |
+
return nx.complete_graph(n)
|
| 225 |
+
|
| 226 |
+
@staticmethod
|
| 227 |
+
def D(n):
|
| 228 |
+
return nx.complete_graph(n).to_directed()
|
| 229 |
+
|
| 230 |
+
@staticmethod
|
| 231 |
+
def edgeset_function(g):
|
| 232 |
+
if g.is_directed():
|
| 233 |
+
return directed_cycle_edgeset
|
| 234 |
+
elif g.is_multigraph():
|
| 235 |
+
return multigraph_cycle_edgeset
|
| 236 |
+
else:
|
| 237 |
+
return undirected_cycle_edgeset
|
| 238 |
+
|
| 239 |
+
def check_cycle(self, g, c, es, cache, source, original_c, length_bound, chordless):
|
| 240 |
+
if length_bound is not None and len(c) > length_bound:
|
| 241 |
+
raise RuntimeError(
|
| 242 |
+
f"computed cycle {original_c} exceeds length bound {length_bound}"
|
| 243 |
+
)
|
| 244 |
+
if source == "computed":
|
| 245 |
+
if es in cache:
|
| 246 |
+
raise RuntimeError(
|
| 247 |
+
f"computed cycle {original_c} has already been found!"
|
| 248 |
+
)
|
| 249 |
+
else:
|
| 250 |
+
cache[es] = tuple(original_c)
|
| 251 |
+
else:
|
| 252 |
+
if es in cache:
|
| 253 |
+
cache.pop(es)
|
| 254 |
+
else:
|
| 255 |
+
raise RuntimeError(f"expected cycle {original_c} was not computed")
|
| 256 |
+
|
| 257 |
+
if not all(g.has_edge(*e) for e in es):
|
| 258 |
+
raise RuntimeError(
|
| 259 |
+
f"{source} claimed cycle {original_c} is not a cycle of g"
|
| 260 |
+
)
|
| 261 |
+
if chordless and len(g.subgraph(c).edges) > len(c):
|
| 262 |
+
raise RuntimeError(f"{source} cycle {original_c} is not chordless")
|
| 263 |
+
|
| 264 |
+
def check_cycle_algorithm(
|
| 265 |
+
self,
|
| 266 |
+
g,
|
| 267 |
+
expected_cycles,
|
| 268 |
+
length_bound=None,
|
| 269 |
+
chordless=False,
|
| 270 |
+
algorithm=None,
|
| 271 |
+
):
|
| 272 |
+
if algorithm is None:
|
| 273 |
+
algorithm = nx.chordless_cycles if chordless else nx.simple_cycles
|
| 274 |
+
|
| 275 |
+
# note: we shuffle the labels of g to rule out accidentally-correct
|
| 276 |
+
# behavior which occurred during the development of chordless cycle
|
| 277 |
+
# enumeration algorithms
|
| 278 |
+
|
| 279 |
+
relabel = list(range(len(g)))
|
| 280 |
+
shuffle(relabel)
|
| 281 |
+
label = dict(zip(g, relabel))
|
| 282 |
+
unlabel = dict(zip(relabel, g))
|
| 283 |
+
h = nx.relabel_nodes(g, label, copy=True)
|
| 284 |
+
|
| 285 |
+
edgeset = self.edgeset_function(h)
|
| 286 |
+
|
| 287 |
+
params = {}
|
| 288 |
+
if length_bound is not None:
|
| 289 |
+
params["length_bound"] = length_bound
|
| 290 |
+
|
| 291 |
+
cycle_cache = {}
|
| 292 |
+
for c in algorithm(h, **params):
|
| 293 |
+
original_c = [unlabel[x] for x in c]
|
| 294 |
+
es = edgeset(c)
|
| 295 |
+
self.check_cycle(
|
| 296 |
+
h, c, es, cycle_cache, "computed", original_c, length_bound, chordless
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
if isinstance(expected_cycles, int):
|
| 300 |
+
if len(cycle_cache) != expected_cycles:
|
| 301 |
+
raise RuntimeError(
|
| 302 |
+
f"expected {expected_cycles} cycles, got {len(cycle_cache)}"
|
| 303 |
+
)
|
| 304 |
+
return
|
| 305 |
+
for original_c in expected_cycles:
|
| 306 |
+
c = [label[x] for x in original_c]
|
| 307 |
+
es = edgeset(c)
|
| 308 |
+
self.check_cycle(
|
| 309 |
+
h, c, es, cycle_cache, "expected", original_c, length_bound, chordless
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
if len(cycle_cache):
|
| 313 |
+
for c in cycle_cache.values():
|
| 314 |
+
raise RuntimeError(
|
| 315 |
+
f"computed cycle {c} is valid but not in the expected cycle set!"
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
def check_cycle_enumeration_integer_sequence(
|
| 319 |
+
self,
|
| 320 |
+
g_family,
|
| 321 |
+
cycle_counts,
|
| 322 |
+
length_bound=None,
|
| 323 |
+
chordless=False,
|
| 324 |
+
algorithm=None,
|
| 325 |
+
):
|
| 326 |
+
for g, num_cycles in zip(g_family, cycle_counts):
|
| 327 |
+
self.check_cycle_algorithm(
|
| 328 |
+
g,
|
| 329 |
+
num_cycles,
|
| 330 |
+
length_bound=length_bound,
|
| 331 |
+
chordless=chordless,
|
| 332 |
+
algorithm=algorithm,
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
def test_directed_chordless_cycle_digons(self):
|
| 336 |
+
g = nx.DiGraph()
|
| 337 |
+
nx.add_cycle(g, range(5))
|
| 338 |
+
nx.add_cycle(g, range(5)[::-1])
|
| 339 |
+
g.add_edge(0, 0)
|
| 340 |
+
expected_cycles = [(0,), (1, 2), (2, 3), (3, 4)]
|
| 341 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True)
|
| 342 |
+
|
| 343 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=2)
|
| 344 |
+
|
| 345 |
+
expected_cycles = [c for c in expected_cycles if len(c) < 2]
|
| 346 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=1)
|
| 347 |
+
|
| 348 |
+
def test_directed_chordless_cycle_undirected(self):
|
| 349 |
+
g = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 5), (5, 0), (5, 1), (0, 2)])
|
| 350 |
+
expected_cycles = [(0, 2, 3, 4, 5), (1, 2, 3, 4, 5)]
|
| 351 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True)
|
| 352 |
+
|
| 353 |
+
g = nx.DiGraph()
|
| 354 |
+
nx.add_cycle(g, range(5))
|
| 355 |
+
nx.add_cycle(g, range(4, 9))
|
| 356 |
+
g.add_edge(7, 3)
|
| 357 |
+
expected_cycles = [(0, 1, 2, 3, 4), (3, 4, 5, 6, 7), (4, 5, 6, 7, 8)]
|
| 358 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True)
|
| 359 |
+
|
| 360 |
+
g.add_edge(3, 7)
|
| 361 |
+
expected_cycles = [(0, 1, 2, 3, 4), (3, 7), (4, 5, 6, 7, 8)]
|
| 362 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True)
|
| 363 |
+
|
| 364 |
+
expected_cycles = [(3, 7)]
|
| 365 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=4)
|
| 366 |
+
|
| 367 |
+
g.remove_edge(7, 3)
|
| 368 |
+
expected_cycles = [(0, 1, 2, 3, 4), (4, 5, 6, 7, 8)]
|
| 369 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True)
|
| 370 |
+
|
| 371 |
+
g = nx.DiGraph((i, j) for i in range(10) for j in range(i))
|
| 372 |
+
expected_cycles = []
|
| 373 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True)
|
| 374 |
+
|
| 375 |
+
def test_chordless_cycles_directed(self):
|
| 376 |
+
G = nx.DiGraph()
|
| 377 |
+
nx.add_cycle(G, range(5))
|
| 378 |
+
nx.add_cycle(G, range(4, 12))
|
| 379 |
+
expected = [[*range(5)], [*range(4, 12)]]
|
| 380 |
+
self.check_cycle_algorithm(G, expected, chordless=True)
|
| 381 |
+
self.check_cycle_algorithm(
|
| 382 |
+
G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
G.add_edge(7, 3)
|
| 386 |
+
expected.append([*range(3, 8)])
|
| 387 |
+
self.check_cycle_algorithm(G, expected, chordless=True)
|
| 388 |
+
self.check_cycle_algorithm(
|
| 389 |
+
G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
G.add_edge(3, 7)
|
| 393 |
+
expected[-1] = [7, 3]
|
| 394 |
+
self.check_cycle_algorithm(G, expected, chordless=True)
|
| 395 |
+
self.check_cycle_algorithm(
|
| 396 |
+
G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
expected.pop()
|
| 400 |
+
G.remove_edge(7, 3)
|
| 401 |
+
self.check_cycle_algorithm(G, expected, chordless=True)
|
| 402 |
+
self.check_cycle_algorithm(
|
| 403 |
+
G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
def test_directed_chordless_cycle_diclique(self):
|
| 407 |
+
g_family = [self.D(n) for n in range(10)]
|
| 408 |
+
expected_cycles = [(n * n - n) // 2 for n in range(10)]
|
| 409 |
+
self.check_cycle_enumeration_integer_sequence(
|
| 410 |
+
g_family, expected_cycles, chordless=True
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
expected_cycles = [(n * n - n) // 2 for n in range(10)]
|
| 414 |
+
self.check_cycle_enumeration_integer_sequence(
|
| 415 |
+
g_family, expected_cycles, length_bound=2
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
def test_directed_chordless_loop_blockade(self):
|
| 419 |
+
g = nx.DiGraph((i, i) for i in range(10))
|
| 420 |
+
nx.add_cycle(g, range(10))
|
| 421 |
+
expected_cycles = [(i,) for i in range(10)]
|
| 422 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True)
|
| 423 |
+
|
| 424 |
+
self.check_cycle_algorithm(g, expected_cycles, length_bound=1)
|
| 425 |
+
|
| 426 |
+
g = nx.MultiDiGraph(g)
|
| 427 |
+
g.add_edges_from((i, i) for i in range(0, 10, 2))
|
| 428 |
+
expected_cycles = [(i,) for i in range(1, 10, 2)]
|
| 429 |
+
self.check_cycle_algorithm(g, expected_cycles, chordless=True)
|
| 430 |
+
|
| 431 |
+
def test_simple_cycles_notable_clique_sequences(self):
|
| 432 |
+
# A000292: Number of labeled graphs on n+3 nodes that are triangles.
|
| 433 |
+
g_family = [self.K(n) for n in range(2, 12)]
|
| 434 |
+
expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220]
|
| 435 |
+
self.check_cycle_enumeration_integer_sequence(
|
| 436 |
+
g_family, expected, length_bound=3
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
def triangles(g, **kwargs):
|
| 440 |
+
yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 3)
|
| 441 |
+
|
| 442 |
+
# directed complete graphs have twice as many triangles thanks to reversal
|
| 443 |
+
g_family = [self.D(n) for n in range(2, 12)]
|
| 444 |
+
expected = [2 * e for e in expected]
|
| 445 |
+
self.check_cycle_enumeration_integer_sequence(
|
| 446 |
+
g_family, expected, length_bound=3, algorithm=triangles
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
def four_cycles(g, **kwargs):
|
| 450 |
+
yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 4)
|
| 451 |
+
|
| 452 |
+
# A050534: the number of 4-cycles in the complete graph K_{n+1}
|
| 453 |
+
expected = [0, 0, 0, 3, 15, 45, 105, 210, 378, 630, 990]
|
| 454 |
+
g_family = [self.K(n) for n in range(1, 12)]
|
| 455 |
+
self.check_cycle_enumeration_integer_sequence(
|
| 456 |
+
g_family, expected, length_bound=4, algorithm=four_cycles
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
# directed complete graphs have twice as many 4-cycles thanks to reversal
|
| 460 |
+
expected = [2 * e for e in expected]
|
| 461 |
+
g_family = [self.D(n) for n in range(1, 15)]
|
| 462 |
+
self.check_cycle_enumeration_integer_sequence(
|
| 463 |
+
g_family, expected, length_bound=4, algorithm=four_cycles
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
+
# A006231: the number of elementary circuits in a complete directed graph with n nodes
|
| 467 |
+
expected = [0, 1, 5, 20, 84, 409, 2365]
|
| 468 |
+
g_family = [self.D(n) for n in range(1, 8)]
|
| 469 |
+
self.check_cycle_enumeration_integer_sequence(g_family, expected)
|
| 470 |
+
|
| 471 |
+
# A002807: Number of cycles in the complete graph on n nodes K_{n}.
|
| 472 |
+
expected = [0, 0, 0, 1, 7, 37, 197, 1172]
|
| 473 |
+
g_family = [self.K(n) for n in range(8)]
|
| 474 |
+
self.check_cycle_enumeration_integer_sequence(g_family, expected)
|
| 475 |
+
|
| 476 |
+
def test_directed_chordless_cycle_parallel_multiedges(self):
|
| 477 |
+
g = nx.MultiGraph()
|
| 478 |
+
|
| 479 |
+
nx.add_cycle(g, range(5))
|
| 480 |
+
expected = [[*range(5)]]
|
| 481 |
+
self.check_cycle_algorithm(g, expected, chordless=True)
|
| 482 |
+
|
| 483 |
+
nx.add_cycle(g, range(5))
|
| 484 |
+
expected = [*cycle_edges(range(5))]
|
| 485 |
+
self.check_cycle_algorithm(g, expected, chordless=True)
|
| 486 |
+
|
| 487 |
+
nx.add_cycle(g, range(5))
|
| 488 |
+
expected = []
|
| 489 |
+
self.check_cycle_algorithm(g, expected, chordless=True)
|
| 490 |
+
|
| 491 |
+
g = nx.MultiDiGraph()
|
| 492 |
+
|
| 493 |
+
nx.add_cycle(g, range(5))
|
| 494 |
+
expected = [[*range(5)]]
|
| 495 |
+
self.check_cycle_algorithm(g, expected, chordless=True)
|
| 496 |
+
|
| 497 |
+
nx.add_cycle(g, range(5))
|
| 498 |
+
self.check_cycle_algorithm(g, [], chordless=True)
|
| 499 |
+
|
| 500 |
+
nx.add_cycle(g, range(5))
|
| 501 |
+
self.check_cycle_algorithm(g, [], chordless=True)
|
| 502 |
+
|
| 503 |
+
g = nx.MultiDiGraph()
|
| 504 |
+
|
| 505 |
+
nx.add_cycle(g, range(5))
|
| 506 |
+
nx.add_cycle(g, range(5)[::-1])
|
| 507 |
+
expected = [*cycle_edges(range(5))]
|
| 508 |
+
self.check_cycle_algorithm(g, expected, chordless=True)
|
| 509 |
+
|
| 510 |
+
nx.add_cycle(g, range(5))
|
| 511 |
+
self.check_cycle_algorithm(g, [], chordless=True)
|
| 512 |
+
|
| 513 |
+
def test_chordless_cycles_graph(self):
|
| 514 |
+
G = nx.Graph()
|
| 515 |
+
nx.add_cycle(G, range(5))
|
| 516 |
+
nx.add_cycle(G, range(4, 12))
|
| 517 |
+
expected = [[*range(5)], [*range(4, 12)]]
|
| 518 |
+
self.check_cycle_algorithm(G, expected, chordless=True)
|
| 519 |
+
self.check_cycle_algorithm(
|
| 520 |
+
G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
G.add_edge(7, 3)
|
| 524 |
+
expected.append([*range(3, 8)])
|
| 525 |
+
expected.append([4, 3, 7, 8, 9, 10, 11])
|
| 526 |
+
self.check_cycle_algorithm(G, expected, chordless=True)
|
| 527 |
+
self.check_cycle_algorithm(
|
| 528 |
+
G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
def test_chordless_cycles_giant_hamiltonian(self):
|
| 532 |
+
# ... o - e - o - e - o ... # o = odd, e = even
|
| 533 |
+
# ... ---/ \-----/ \--- ... # <-- "long" edges
|
| 534 |
+
#
|
| 535 |
+
# each long edge belongs to exactly one triangle, and one giant cycle
|
| 536 |
+
# of length n/2. The remaining edges each belong to a triangle
|
| 537 |
+
|
| 538 |
+
n = 1000
|
| 539 |
+
assert n % 2 == 0
|
| 540 |
+
G = nx.Graph()
|
| 541 |
+
for v in range(n):
|
| 542 |
+
if not v % 2:
|
| 543 |
+
G.add_edge(v, (v + 2) % n)
|
| 544 |
+
G.add_edge(v, (v + 1) % n)
|
| 545 |
+
|
| 546 |
+
expected = [[*range(0, n, 2)]] + [
|
| 547 |
+
[x % n for x in range(i, i + 3)] for i in range(0, n, 2)
|
| 548 |
+
]
|
| 549 |
+
self.check_cycle_algorithm(G, expected, chordless=True)
|
| 550 |
+
self.check_cycle_algorithm(
|
| 551 |
+
G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
# ... o -> e -> o -> e -> o ... # o = odd, e = even
|
| 555 |
+
# ... <---/ \---<---/ \---< ... # <-- "long" edges
|
| 556 |
+
#
|
| 557 |
+
# this time, we orient the short and long edges in opposition
|
| 558 |
+
# the cycle structure of this graph is the same, but we need to reverse
|
| 559 |
+
# the long one in our representation. Also, we need to drop the size
|
| 560 |
+
# because our partitioning algorithm uses strongly connected components
|
| 561 |
+
# instead of separating graphs by their strong articulation points
|
| 562 |
+
|
| 563 |
+
n = 100
|
| 564 |
+
assert n % 2 == 0
|
| 565 |
+
G = nx.DiGraph()
|
| 566 |
+
for v in range(n):
|
| 567 |
+
G.add_edge(v, (v + 1) % n)
|
| 568 |
+
if not v % 2:
|
| 569 |
+
G.add_edge((v + 2) % n, v)
|
| 570 |
+
|
| 571 |
+
expected = [[*range(n - 2, -2, -2)]] + [
|
| 572 |
+
[x % n for x in range(i, i + 3)] for i in range(0, n, 2)
|
| 573 |
+
]
|
| 574 |
+
self.check_cycle_algorithm(G, expected, chordless=True)
|
| 575 |
+
self.check_cycle_algorithm(
|
| 576 |
+
G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
def test_simple_cycles_acyclic_tournament(self):
|
| 580 |
+
n = 10
|
| 581 |
+
G = nx.DiGraph((x, y) for x in range(n) for y in range(x))
|
| 582 |
+
self.check_cycle_algorithm(G, [])
|
| 583 |
+
self.check_cycle_algorithm(G, [], chordless=True)
|
| 584 |
+
|
| 585 |
+
for k in range(n + 1):
|
| 586 |
+
self.check_cycle_algorithm(G, [], length_bound=k)
|
| 587 |
+
self.check_cycle_algorithm(G, [], length_bound=k, chordless=True)
|
| 588 |
+
|
| 589 |
+
def test_simple_cycles_graph(self):
|
| 590 |
+
testG = nx.cycle_graph(8)
|
| 591 |
+
cyc1 = tuple(range(8))
|
| 592 |
+
self.check_cycle_algorithm(testG, [cyc1])
|
| 593 |
+
|
| 594 |
+
testG.add_edge(4, -1)
|
| 595 |
+
nx.add_path(testG, [3, -2, -3, -4])
|
| 596 |
+
self.check_cycle_algorithm(testG, [cyc1])
|
| 597 |
+
|
| 598 |
+
testG.update(nx.cycle_graph(range(8, 16)))
|
| 599 |
+
cyc2 = tuple(range(8, 16))
|
| 600 |
+
self.check_cycle_algorithm(testG, [cyc1, cyc2])
|
| 601 |
+
|
| 602 |
+
testG.update(nx.cycle_graph(range(4, 12)))
|
| 603 |
+
cyc3 = tuple(range(4, 12))
|
| 604 |
+
expected = {
|
| 605 |
+
(0, 1, 2, 3, 4, 5, 6, 7), # cyc1
|
| 606 |
+
(8, 9, 10, 11, 12, 13, 14, 15), # cyc2
|
| 607 |
+
(4, 5, 6, 7, 8, 9, 10, 11), # cyc3
|
| 608 |
+
(4, 5, 6, 7, 8, 15, 14, 13, 12, 11), # cyc2 + cyc3
|
| 609 |
+
(0, 1, 2, 3, 4, 11, 10, 9, 8, 7), # cyc1 + cyc3
|
| 610 |
+
(0, 1, 2, 3, 4, 11, 12, 13, 14, 15, 8, 7), # cyc1 + cyc2 + cyc3
|
| 611 |
+
}
|
| 612 |
+
self.check_cycle_algorithm(testG, expected)
|
| 613 |
+
assert len(expected) == (2**3 - 1) - 1 # 1 disjoint comb: cyc1 + cyc2
|
| 614 |
+
|
| 615 |
+
# Basis size = 5 (2 loops overlapping gives 5 small loops
|
| 616 |
+
# E
|
| 617 |
+
# / \ Note: A-F = 10-15
|
| 618 |
+
# 1-2-3-4-5
|
| 619 |
+
# / | | \ cyc1=012DAB -- left
|
| 620 |
+
# 0 D F 6 cyc2=234E -- top
|
| 621 |
+
# \ | | / cyc3=45678F -- right
|
| 622 |
+
# B-A-9-8-7 cyc4=89AC -- bottom
|
| 623 |
+
# \ / cyc5=234F89AD -- middle
|
| 624 |
+
# C
|
| 625 |
+
#
|
| 626 |
+
# combinations of 5 basis elements: 2^5 - 1 (one includes no cycles)
|
| 627 |
+
#
|
| 628 |
+
# disjoint combs: (11 total) not simple cycles
|
| 629 |
+
# Any pair not including cyc5 => choose(4, 2) = 6
|
| 630 |
+
# Any triple not including cyc5 => choose(4, 3) = 4
|
| 631 |
+
# Any quad not including cyc5 => choose(4, 4) = 1
|
| 632 |
+
#
|
| 633 |
+
# we expect 31 - 11 = 20 simple cycles
|
| 634 |
+
#
|
| 635 |
+
testG = nx.cycle_graph(12)
|
| 636 |
+
testG.update(nx.cycle_graph([12, 10, 13, 2, 14, 4, 15, 8]).edges)
|
| 637 |
+
expected = (2**5 - 1) - 11 # 11 disjoint combinations
|
| 638 |
+
self.check_cycle_algorithm(testG, expected)
|
| 639 |
+
|
| 640 |
+
def test_simple_cycles_bounded(self):
|
| 641 |
+
# iteratively construct a cluster of nested cycles running in the same direction
|
| 642 |
+
# there should be one cycle of every length
|
| 643 |
+
d = nx.DiGraph()
|
| 644 |
+
expected = []
|
| 645 |
+
for n in range(10):
|
| 646 |
+
nx.add_cycle(d, range(n))
|
| 647 |
+
expected.append(n)
|
| 648 |
+
for k, e in enumerate(expected):
|
| 649 |
+
self.check_cycle_algorithm(d, e, length_bound=k)
|
| 650 |
+
|
| 651 |
+
# iteratively construct a path of undirected cycles, connected at articulation
|
| 652 |
+
# points. there should be one cycle of every length except 2: no digons
|
| 653 |
+
g = nx.Graph()
|
| 654 |
+
top = 0
|
| 655 |
+
expected = []
|
| 656 |
+
for n in range(10):
|
| 657 |
+
expected.append(n if n < 2 else n - 1)
|
| 658 |
+
if n == 2:
|
| 659 |
+
# no digons in undirected graphs
|
| 660 |
+
continue
|
| 661 |
+
nx.add_cycle(g, range(top, top + n))
|
| 662 |
+
top += n
|
| 663 |
+
for k, e in enumerate(expected):
|
| 664 |
+
self.check_cycle_algorithm(g, e, length_bound=k)
|
| 665 |
+
|
| 666 |
+
def test_simple_cycles_bound_corner_cases(self):
|
| 667 |
+
G = nx.cycle_graph(4)
|
| 668 |
+
DG = nx.cycle_graph(4, create_using=nx.DiGraph)
|
| 669 |
+
assert list(nx.simple_cycles(G, length_bound=0)) == []
|
| 670 |
+
assert list(nx.simple_cycles(DG, length_bound=0)) == []
|
| 671 |
+
assert list(nx.chordless_cycles(G, length_bound=0)) == []
|
| 672 |
+
assert list(nx.chordless_cycles(DG, length_bound=0)) == []
|
| 673 |
+
|
| 674 |
+
def test_simple_cycles_bound_error(self):
|
| 675 |
+
with pytest.raises(ValueError):
|
| 676 |
+
G = nx.DiGraph()
|
| 677 |
+
for c in nx.simple_cycles(G, -1):
|
| 678 |
+
assert False
|
| 679 |
+
|
| 680 |
+
with pytest.raises(ValueError):
|
| 681 |
+
G = nx.Graph()
|
| 682 |
+
for c in nx.simple_cycles(G, -1):
|
| 683 |
+
assert False
|
| 684 |
+
|
| 685 |
+
with pytest.raises(ValueError):
|
| 686 |
+
G = nx.Graph()
|
| 687 |
+
for c in nx.chordless_cycles(G, -1):
|
| 688 |
+
assert False
|
| 689 |
+
|
| 690 |
+
with pytest.raises(ValueError):
|
| 691 |
+
G = nx.DiGraph()
|
| 692 |
+
for c in nx.chordless_cycles(G, -1):
|
| 693 |
+
assert False
|
| 694 |
+
|
| 695 |
+
def test_chordless_cycles_clique(self):
|
| 696 |
+
g_family = [self.K(n) for n in range(2, 15)]
|
| 697 |
+
expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220, 286, 364]
|
| 698 |
+
self.check_cycle_enumeration_integer_sequence(
|
| 699 |
+
g_family, expected, chordless=True
|
| 700 |
+
)
|
| 701 |
+
|
| 702 |
+
# directed cliques have as many digons as undirected graphs have edges
|
| 703 |
+
expected = [(n * n - n) // 2 for n in range(15)]
|
| 704 |
+
g_family = [self.D(n) for n in range(15)]
|
| 705 |
+
self.check_cycle_enumeration_integer_sequence(
|
| 706 |
+
g_family, expected, chordless=True
|
| 707 |
+
)
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
# These tests might fail with hash randomization since they depend on
|
| 711 |
+
# edge_dfs. For more information, see the comments in:
|
| 712 |
+
# networkx/algorithms/traversal/tests/test_edgedfs.py
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
class TestFindCycle:
|
| 716 |
+
@classmethod
|
| 717 |
+
def setup_class(cls):
|
| 718 |
+
cls.nodes = [0, 1, 2, 3]
|
| 719 |
+
cls.edges = [(-1, 0), (0, 1), (1, 0), (1, 0), (2, 1), (3, 1)]
|
| 720 |
+
|
| 721 |
+
def test_graph_nocycle(self):
|
| 722 |
+
G = nx.Graph(self.edges)
|
| 723 |
+
pytest.raises(nx.exception.NetworkXNoCycle, nx.find_cycle, G, self.nodes)
|
| 724 |
+
|
| 725 |
+
def test_graph_cycle(self):
|
| 726 |
+
G = nx.Graph(self.edges)
|
| 727 |
+
G.add_edge(2, 0)
|
| 728 |
+
x = list(nx.find_cycle(G, self.nodes))
|
| 729 |
+
x_ = [(0, 1), (1, 2), (2, 0)]
|
| 730 |
+
assert x == x_
|
| 731 |
+
|
| 732 |
+
def test_graph_orientation_none(self):
|
| 733 |
+
G = nx.Graph(self.edges)
|
| 734 |
+
G.add_edge(2, 0)
|
| 735 |
+
x = list(nx.find_cycle(G, self.nodes, orientation=None))
|
| 736 |
+
x_ = [(0, 1), (1, 2), (2, 0)]
|
| 737 |
+
assert x == x_
|
| 738 |
+
|
| 739 |
+
def test_graph_orientation_original(self):
|
| 740 |
+
G = nx.Graph(self.edges)
|
| 741 |
+
G.add_edge(2, 0)
|
| 742 |
+
x = list(nx.find_cycle(G, self.nodes, orientation="original"))
|
| 743 |
+
x_ = [(0, 1, FORWARD), (1, 2, FORWARD), (2, 0, FORWARD)]
|
| 744 |
+
assert x == x_
|
| 745 |
+
|
| 746 |
+
def test_digraph(self):
|
| 747 |
+
G = nx.DiGraph(self.edges)
|
| 748 |
+
x = list(nx.find_cycle(G, self.nodes))
|
| 749 |
+
x_ = [(0, 1), (1, 0)]
|
| 750 |
+
assert x == x_
|
| 751 |
+
|
| 752 |
+
def test_digraph_orientation_none(self):
|
| 753 |
+
G = nx.DiGraph(self.edges)
|
| 754 |
+
x = list(nx.find_cycle(G, self.nodes, orientation=None))
|
| 755 |
+
x_ = [(0, 1), (1, 0)]
|
| 756 |
+
assert x == x_
|
| 757 |
+
|
| 758 |
+
def test_digraph_orientation_original(self):
|
| 759 |
+
G = nx.DiGraph(self.edges)
|
| 760 |
+
x = list(nx.find_cycle(G, self.nodes, orientation="original"))
|
| 761 |
+
x_ = [(0, 1, FORWARD), (1, 0, FORWARD)]
|
| 762 |
+
assert x == x_
|
| 763 |
+
|
| 764 |
+
def test_multigraph(self):
|
| 765 |
+
G = nx.MultiGraph(self.edges)
|
| 766 |
+
x = list(nx.find_cycle(G, self.nodes))
|
| 767 |
+
x_ = [(0, 1, 0), (1, 0, 1)] # or (1, 0, 2)
|
| 768 |
+
# Hash randomization...could be any edge.
|
| 769 |
+
assert x[0] == x_[0]
|
| 770 |
+
assert x[1][:2] == x_[1][:2]
|
| 771 |
+
|
| 772 |
+
def test_multidigraph(self):
|
| 773 |
+
G = nx.MultiDiGraph(self.edges)
|
| 774 |
+
x = list(nx.find_cycle(G, self.nodes))
|
| 775 |
+
x_ = [(0, 1, 0), (1, 0, 0)] # (1, 0, 1)
|
| 776 |
+
assert x[0] == x_[0]
|
| 777 |
+
assert x[1][:2] == x_[1][:2]
|
| 778 |
+
|
| 779 |
+
def test_digraph_ignore(self):
|
| 780 |
+
G = nx.DiGraph(self.edges)
|
| 781 |
+
x = list(nx.find_cycle(G, self.nodes, orientation="ignore"))
|
| 782 |
+
x_ = [(0, 1, FORWARD), (1, 0, FORWARD)]
|
| 783 |
+
assert x == x_
|
| 784 |
+
|
| 785 |
+
def test_digraph_reverse(self):
|
| 786 |
+
G = nx.DiGraph(self.edges)
|
| 787 |
+
x = list(nx.find_cycle(G, self.nodes, orientation="reverse"))
|
| 788 |
+
x_ = [(1, 0, REVERSE), (0, 1, REVERSE)]
|
| 789 |
+
assert x == x_
|
| 790 |
+
|
| 791 |
+
def test_multidigraph_ignore(self):
|
| 792 |
+
G = nx.MultiDiGraph(self.edges)
|
| 793 |
+
x = list(nx.find_cycle(G, self.nodes, orientation="ignore"))
|
| 794 |
+
x_ = [(0, 1, 0, FORWARD), (1, 0, 0, FORWARD)] # or (1, 0, 1, 1)
|
| 795 |
+
assert x[0] == x_[0]
|
| 796 |
+
assert x[1][:2] == x_[1][:2]
|
| 797 |
+
assert x[1][3] == x_[1][3]
|
| 798 |
+
|
| 799 |
+
def test_multidigraph_ignore2(self):
|
| 800 |
+
# Loop traversed an edge while ignoring its orientation.
|
| 801 |
+
G = nx.MultiDiGraph([(0, 1), (1, 2), (1, 2)])
|
| 802 |
+
x = list(nx.find_cycle(G, [0, 1, 2], orientation="ignore"))
|
| 803 |
+
x_ = [(1, 2, 0, FORWARD), (1, 2, 1, REVERSE)]
|
| 804 |
+
assert x == x_
|
| 805 |
+
|
| 806 |
+
def test_multidigraph_original(self):
|
| 807 |
+
# Node 2 doesn't need to be searched again from visited from 4.
|
| 808 |
+
# The goal here is to cover the case when 2 to be researched from 4,
|
| 809 |
+
# when 4 is visited from the first time (so we must make sure that 4
|
| 810 |
+
# is not visited from 2, and hence, we respect the edge orientation).
|
| 811 |
+
G = nx.MultiDiGraph([(0, 1), (1, 2), (2, 3), (4, 2)])
|
| 812 |
+
pytest.raises(
|
| 813 |
+
nx.exception.NetworkXNoCycle,
|
| 814 |
+
nx.find_cycle,
|
| 815 |
+
G,
|
| 816 |
+
[0, 1, 2, 3, 4],
|
| 817 |
+
orientation="original",
|
| 818 |
+
)
|
| 819 |
+
|
| 820 |
+
def test_dag(self):
|
| 821 |
+
G = nx.DiGraph([(0, 1), (0, 2), (1, 2)])
|
| 822 |
+
pytest.raises(
|
| 823 |
+
nx.exception.NetworkXNoCycle, nx.find_cycle, G, orientation="original"
|
| 824 |
+
)
|
| 825 |
+
x = list(nx.find_cycle(G, orientation="ignore"))
|
| 826 |
+
assert x == [(0, 1, FORWARD), (1, 2, FORWARD), (0, 2, REVERSE)]
|
| 827 |
+
|
| 828 |
+
def test_prev_explored(self):
|
| 829 |
+
# https://github.com/networkx/networkx/issues/2323
|
| 830 |
+
|
| 831 |
+
G = nx.DiGraph()
|
| 832 |
+
G.add_edges_from([(1, 0), (2, 0), (1, 2), (2, 1)])
|
| 833 |
+
pytest.raises(nx.NetworkXNoCycle, nx.find_cycle, G, source=0)
|
| 834 |
+
x = list(nx.find_cycle(G, 1))
|
| 835 |
+
x_ = [(1, 2), (2, 1)]
|
| 836 |
+
assert x == x_
|
| 837 |
+
|
| 838 |
+
x = list(nx.find_cycle(G, 2))
|
| 839 |
+
x_ = [(2, 1), (1, 2)]
|
| 840 |
+
assert x == x_
|
| 841 |
+
|
| 842 |
+
x = list(nx.find_cycle(G))
|
| 843 |
+
x_ = [(1, 2), (2, 1)]
|
| 844 |
+
assert x == x_
|
| 845 |
+
|
| 846 |
+
def test_no_cycle(self):
|
| 847 |
+
# https://github.com/networkx/networkx/issues/2439
|
| 848 |
+
|
| 849 |
+
G = nx.DiGraph()
|
| 850 |
+
G.add_edges_from([(1, 2), (2, 0), (3, 1), (3, 2)])
|
| 851 |
+
pytest.raises(nx.NetworkXNoCycle, nx.find_cycle, G, source=0)
|
| 852 |
+
pytest.raises(nx.NetworkXNoCycle, nx.find_cycle, G)
|
| 853 |
+
|
| 854 |
+
|
| 855 |
+
def assert_basis_equal(a, b):
|
| 856 |
+
assert sorted(a) == sorted(b)
|
| 857 |
+
|
| 858 |
+
|
| 859 |
+
class TestMinimumCycleBasis:
|
| 860 |
+
@classmethod
|
| 861 |
+
def setup_class(cls):
|
| 862 |
+
T = nx.Graph()
|
| 863 |
+
nx.add_cycle(T, [1, 2, 3, 4], weight=1)
|
| 864 |
+
T.add_edge(2, 4, weight=5)
|
| 865 |
+
cls.diamond_graph = T
|
| 866 |
+
|
| 867 |
+
def test_unweighted_diamond(self):
|
| 868 |
+
mcb = nx.minimum_cycle_basis(self.diamond_graph)
|
| 869 |
+
assert_basis_equal(mcb, [[2, 4, 1], [3, 4, 2]])
|
| 870 |
+
|
| 871 |
+
def test_weighted_diamond(self):
|
| 872 |
+
mcb = nx.minimum_cycle_basis(self.diamond_graph, weight="weight")
|
| 873 |
+
assert_basis_equal(mcb, [[2, 4, 1], [4, 3, 2, 1]])
|
| 874 |
+
|
| 875 |
+
def test_dimensionality(self):
|
| 876 |
+
# checks |MCB|=|E|-|V|+|NC|
|
| 877 |
+
ntrial = 10
|
| 878 |
+
for seed in range(1234, 1234 + ntrial):
|
| 879 |
+
rg = nx.erdos_renyi_graph(10, 0.3, seed=seed)
|
| 880 |
+
nnodes = rg.number_of_nodes()
|
| 881 |
+
nedges = rg.number_of_edges()
|
| 882 |
+
ncomp = nx.number_connected_components(rg)
|
| 883 |
+
|
| 884 |
+
mcb = nx.minimum_cycle_basis(rg)
|
| 885 |
+
assert len(mcb) == nedges - nnodes + ncomp
|
| 886 |
+
check_independent(mcb)
|
| 887 |
+
|
| 888 |
+
def test_complete_graph(self):
|
| 889 |
+
cg = nx.complete_graph(5)
|
| 890 |
+
mcb = nx.minimum_cycle_basis(cg)
|
| 891 |
+
assert all(len(cycle) == 3 for cycle in mcb)
|
| 892 |
+
check_independent(mcb)
|
| 893 |
+
|
| 894 |
+
def test_tree_graph(self):
|
| 895 |
+
tg = nx.balanced_tree(3, 3)
|
| 896 |
+
assert not nx.minimum_cycle_basis(tg)
|
| 897 |
+
|
| 898 |
+
def test_petersen_graph(self):
|
| 899 |
+
G = nx.petersen_graph()
|
| 900 |
+
mcb = list(nx.minimum_cycle_basis(G))
|
| 901 |
+
expected = [
|
| 902 |
+
[4, 9, 7, 5, 0],
|
| 903 |
+
[1, 2, 3, 4, 0],
|
| 904 |
+
[1, 6, 8, 5, 0],
|
| 905 |
+
[4, 3, 8, 5, 0],
|
| 906 |
+
[1, 6, 9, 4, 0],
|
| 907 |
+
[1, 2, 7, 5, 0],
|
| 908 |
+
]
|
| 909 |
+
assert len(mcb) == len(expected)
|
| 910 |
+
assert all(c in expected for c in mcb)
|
| 911 |
+
|
| 912 |
+
# check that order of the nodes is a path
|
| 913 |
+
for c in mcb:
|
| 914 |
+
assert all(G.has_edge(u, v) for u, v in nx.utils.pairwise(c, cyclic=True))
|
| 915 |
+
# check independence of the basis
|
| 916 |
+
check_independent(mcb)
|
| 917 |
+
|
| 918 |
+
def test_gh6787_variable_weighted_complete_graph(self):
|
| 919 |
+
N = 8
|
| 920 |
+
cg = nx.complete_graph(N)
|
| 921 |
+
cg.add_weighted_edges_from([(u, v, 9) for u, v in cg.edges])
|
| 922 |
+
cg.add_weighted_edges_from([(u, v, 1) for u, v in nx.cycle_graph(N).edges])
|
| 923 |
+
mcb = nx.minimum_cycle_basis(cg, weight="weight")
|
| 924 |
+
check_independent(mcb)
|
| 925 |
+
|
| 926 |
+
def test_gh6787_and_edge_attribute_names(self):
|
| 927 |
+
G = nx.cycle_graph(4)
|
| 928 |
+
G.add_weighted_edges_from([(0, 2, 10), (1, 3, 10)], weight="dist")
|
| 929 |
+
expected = [[1, 3, 0], [3, 2, 1, 0], [1, 2, 0]]
|
| 930 |
+
mcb = list(nx.minimum_cycle_basis(G, weight="dist"))
|
| 931 |
+
assert len(mcb) == len(expected)
|
| 932 |
+
assert all(c in expected for c in mcb)
|
| 933 |
+
|
| 934 |
+
# test not using a weight with weight attributes
|
| 935 |
+
expected = [[1, 3, 0], [1, 2, 0], [3, 2, 0]]
|
| 936 |
+
mcb = list(nx.minimum_cycle_basis(G))
|
| 937 |
+
assert len(mcb) == len(expected)
|
| 938 |
+
assert all(c in expected for c in mcb)
|
| 939 |
+
|
| 940 |
+
|
| 941 |
+
class TestGirth:
|
| 942 |
+
@pytest.mark.parametrize(
|
| 943 |
+
("G", "expected"),
|
| 944 |
+
(
|
| 945 |
+
(nx.chvatal_graph(), 4),
|
| 946 |
+
(nx.tutte_graph(), 4),
|
| 947 |
+
(nx.petersen_graph(), 5),
|
| 948 |
+
(nx.heawood_graph(), 6),
|
| 949 |
+
(nx.pappus_graph(), 6),
|
| 950 |
+
(nx.random_labeled_tree(10, seed=42), inf),
|
| 951 |
+
(nx.empty_graph(10), inf),
|
| 952 |
+
(nx.Graph(chain(cycle_edges(range(5)), cycle_edges(range(6, 10)))), 4),
|
| 953 |
+
(
|
| 954 |
+
nx.Graph(
|
| 955 |
+
[
|
| 956 |
+
(0, 6),
|
| 957 |
+
(0, 8),
|
| 958 |
+
(0, 9),
|
| 959 |
+
(1, 8),
|
| 960 |
+
(2, 8),
|
| 961 |
+
(2, 9),
|
| 962 |
+
(4, 9),
|
| 963 |
+
(5, 9),
|
| 964 |
+
(6, 8),
|
| 965 |
+
(6, 9),
|
| 966 |
+
(7, 8),
|
| 967 |
+
]
|
| 968 |
+
),
|
| 969 |
+
3,
|
| 970 |
+
),
|
| 971 |
+
),
|
| 972 |
+
)
|
| 973 |
+
def test_girth(self, G, expected):
|
| 974 |
+
assert nx.girth(G) == expected
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_d_separation.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from itertools import combinations
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def path_graph():
|
| 9 |
+
"""Return a path graph of length three."""
|
| 10 |
+
G = nx.path_graph(3, create_using=nx.DiGraph)
|
| 11 |
+
G.graph["name"] = "path"
|
| 12 |
+
nx.freeze(G)
|
| 13 |
+
return G
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def fork_graph():
|
| 17 |
+
"""Return a three node fork graph."""
|
| 18 |
+
G = nx.DiGraph(name="fork")
|
| 19 |
+
G.add_edges_from([(0, 1), (0, 2)])
|
| 20 |
+
nx.freeze(G)
|
| 21 |
+
return G
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def collider_graph():
|
| 25 |
+
"""Return a collider/v-structure graph with three nodes."""
|
| 26 |
+
G = nx.DiGraph(name="collider")
|
| 27 |
+
G.add_edges_from([(0, 2), (1, 2)])
|
| 28 |
+
nx.freeze(G)
|
| 29 |
+
return G
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def naive_bayes_graph():
|
| 33 |
+
"""Return a simply Naive Bayes PGM graph."""
|
| 34 |
+
G = nx.DiGraph(name="naive_bayes")
|
| 35 |
+
G.add_edges_from([(0, 1), (0, 2), (0, 3), (0, 4)])
|
| 36 |
+
nx.freeze(G)
|
| 37 |
+
return G
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def asia_graph():
|
| 41 |
+
"""Return the 'Asia' PGM graph."""
|
| 42 |
+
G = nx.DiGraph(name="asia")
|
| 43 |
+
G.add_edges_from(
|
| 44 |
+
[
|
| 45 |
+
("asia", "tuberculosis"),
|
| 46 |
+
("smoking", "cancer"),
|
| 47 |
+
("smoking", "bronchitis"),
|
| 48 |
+
("tuberculosis", "either"),
|
| 49 |
+
("cancer", "either"),
|
| 50 |
+
("either", "xray"),
|
| 51 |
+
("either", "dyspnea"),
|
| 52 |
+
("bronchitis", "dyspnea"),
|
| 53 |
+
]
|
| 54 |
+
)
|
| 55 |
+
nx.freeze(G)
|
| 56 |
+
return G
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@pytest.fixture(name="path_graph")
|
| 60 |
+
def path_graph_fixture():
|
| 61 |
+
return path_graph()
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@pytest.fixture(name="fork_graph")
|
| 65 |
+
def fork_graph_fixture():
|
| 66 |
+
return fork_graph()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@pytest.fixture(name="collider_graph")
|
| 70 |
+
def collider_graph_fixture():
|
| 71 |
+
return collider_graph()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@pytest.fixture(name="naive_bayes_graph")
|
| 75 |
+
def naive_bayes_graph_fixture():
|
| 76 |
+
return naive_bayes_graph()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@pytest.fixture(name="asia_graph")
|
| 80 |
+
def asia_graph_fixture():
|
| 81 |
+
return asia_graph()
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@pytest.fixture()
|
| 85 |
+
def large_collider_graph():
|
| 86 |
+
edge_list = [("A", "B"), ("C", "B"), ("B", "D"), ("D", "E"), ("B", "F"), ("G", "E")]
|
| 87 |
+
G = nx.DiGraph(edge_list)
|
| 88 |
+
return G
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@pytest.fixture()
|
| 92 |
+
def chain_and_fork_graph():
|
| 93 |
+
edge_list = [("A", "B"), ("B", "C"), ("B", "D"), ("D", "C")]
|
| 94 |
+
G = nx.DiGraph(edge_list)
|
| 95 |
+
return G
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@pytest.fixture()
|
| 99 |
+
def no_separating_set_graph():
|
| 100 |
+
edge_list = [("A", "B")]
|
| 101 |
+
G = nx.DiGraph(edge_list)
|
| 102 |
+
return G
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@pytest.fixture()
|
| 106 |
+
def large_no_separating_set_graph():
|
| 107 |
+
edge_list = [("A", "B"), ("C", "A"), ("C", "B")]
|
| 108 |
+
G = nx.DiGraph(edge_list)
|
| 109 |
+
return G
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@pytest.fixture()
|
| 113 |
+
def collider_trek_graph():
|
| 114 |
+
edge_list = [("A", "B"), ("C", "B"), ("C", "D")]
|
| 115 |
+
G = nx.DiGraph(edge_list)
|
| 116 |
+
return G
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@pytest.mark.parametrize(
|
| 120 |
+
"graph",
|
| 121 |
+
[path_graph(), fork_graph(), collider_graph(), naive_bayes_graph(), asia_graph()],
|
| 122 |
+
)
|
| 123 |
+
def test_markov_condition(graph):
|
| 124 |
+
"""Test that the Markov condition holds for each PGM graph."""
|
| 125 |
+
for node in graph.nodes:
|
| 126 |
+
parents = set(graph.predecessors(node))
|
| 127 |
+
non_descendants = graph.nodes - nx.descendants(graph, node) - {node} - parents
|
| 128 |
+
assert nx.is_d_separator(graph, {node}, non_descendants, parents)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def test_path_graph_dsep(path_graph):
|
| 132 |
+
"""Example-based test of d-separation for path_graph."""
|
| 133 |
+
assert nx.is_d_separator(path_graph, {0}, {2}, {1})
|
| 134 |
+
assert not nx.is_d_separator(path_graph, {0}, {2}, set())
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def test_fork_graph_dsep(fork_graph):
|
| 138 |
+
"""Example-based test of d-separation for fork_graph."""
|
| 139 |
+
assert nx.is_d_separator(fork_graph, {1}, {2}, {0})
|
| 140 |
+
assert not nx.is_d_separator(fork_graph, {1}, {2}, set())
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def test_collider_graph_dsep(collider_graph):
|
| 144 |
+
"""Example-based test of d-separation for collider_graph."""
|
| 145 |
+
assert nx.is_d_separator(collider_graph, {0}, {1}, set())
|
| 146 |
+
assert not nx.is_d_separator(collider_graph, {0}, {1}, {2})
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def test_naive_bayes_dsep(naive_bayes_graph):
|
| 150 |
+
"""Example-based test of d-separation for naive_bayes_graph."""
|
| 151 |
+
for u, v in combinations(range(1, 5), 2):
|
| 152 |
+
assert nx.is_d_separator(naive_bayes_graph, {u}, {v}, {0})
|
| 153 |
+
assert not nx.is_d_separator(naive_bayes_graph, {u}, {v}, set())
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def test_asia_graph_dsep(asia_graph):
|
| 157 |
+
"""Example-based test of d-separation for asia_graph."""
|
| 158 |
+
assert nx.is_d_separator(
|
| 159 |
+
asia_graph, {"asia", "smoking"}, {"dyspnea", "xray"}, {"bronchitis", "either"}
|
| 160 |
+
)
|
| 161 |
+
assert nx.is_d_separator(
|
| 162 |
+
asia_graph, {"tuberculosis", "cancer"}, {"bronchitis"}, {"smoking", "xray"}
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def test_undirected_graphs_are_not_supported():
|
| 167 |
+
"""
|
| 168 |
+
Test that undirected graphs are not supported.
|
| 169 |
+
|
| 170 |
+
d-separation and its related algorithms do not apply in
|
| 171 |
+
the case of undirected graphs.
|
| 172 |
+
"""
|
| 173 |
+
g = nx.path_graph(3, nx.Graph)
|
| 174 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 175 |
+
nx.is_d_separator(g, {0}, {1}, {2})
|
| 176 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 177 |
+
nx.is_minimal_d_separator(g, {0}, {1}, {2})
|
| 178 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 179 |
+
nx.find_minimal_d_separator(g, {0}, {1})
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def test_cyclic_graphs_raise_error():
|
| 183 |
+
"""
|
| 184 |
+
Test that cycle graphs should cause erroring.
|
| 185 |
+
|
| 186 |
+
This is because PGMs assume a directed acyclic graph.
|
| 187 |
+
"""
|
| 188 |
+
g = nx.cycle_graph(3, nx.DiGraph)
|
| 189 |
+
with pytest.raises(nx.NetworkXError):
|
| 190 |
+
nx.is_d_separator(g, {0}, {1}, {2})
|
| 191 |
+
with pytest.raises(nx.NetworkXError):
|
| 192 |
+
nx.find_minimal_d_separator(g, {0}, {1})
|
| 193 |
+
with pytest.raises(nx.NetworkXError):
|
| 194 |
+
nx.is_minimal_d_separator(g, {0}, {1}, {2})
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def test_invalid_nodes_raise_error(asia_graph):
|
| 198 |
+
"""
|
| 199 |
+
Test that graphs that have invalid nodes passed in raise errors.
|
| 200 |
+
"""
|
| 201 |
+
# Check both set and node arguments
|
| 202 |
+
with pytest.raises(nx.NodeNotFound):
|
| 203 |
+
nx.is_d_separator(asia_graph, {0}, {1}, {2})
|
| 204 |
+
with pytest.raises(nx.NodeNotFound):
|
| 205 |
+
nx.is_d_separator(asia_graph, 0, 1, 2)
|
| 206 |
+
with pytest.raises(nx.NodeNotFound):
|
| 207 |
+
nx.is_minimal_d_separator(asia_graph, {0}, {1}, {2})
|
| 208 |
+
with pytest.raises(nx.NodeNotFound):
|
| 209 |
+
nx.is_minimal_d_separator(asia_graph, 0, 1, 2)
|
| 210 |
+
with pytest.raises(nx.NodeNotFound):
|
| 211 |
+
nx.find_minimal_d_separator(asia_graph, {0}, {1})
|
| 212 |
+
with pytest.raises(nx.NodeNotFound):
|
| 213 |
+
nx.find_minimal_d_separator(asia_graph, 0, 1)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def test_nondisjoint_node_sets_raise_error(collider_graph):
|
| 217 |
+
"""
|
| 218 |
+
Test that error is raised when node sets aren't disjoint.
|
| 219 |
+
"""
|
| 220 |
+
with pytest.raises(nx.NetworkXError):
|
| 221 |
+
nx.is_d_separator(collider_graph, 0, 1, 0)
|
| 222 |
+
with pytest.raises(nx.NetworkXError):
|
| 223 |
+
nx.is_d_separator(collider_graph, 0, 2, 0)
|
| 224 |
+
with pytest.raises(nx.NetworkXError):
|
| 225 |
+
nx.is_d_separator(collider_graph, 0, 0, 1)
|
| 226 |
+
with pytest.raises(nx.NetworkXError):
|
| 227 |
+
nx.is_d_separator(collider_graph, 1, 0, 0)
|
| 228 |
+
with pytest.raises(nx.NetworkXError):
|
| 229 |
+
nx.find_minimal_d_separator(collider_graph, 0, 0)
|
| 230 |
+
with pytest.raises(nx.NetworkXError):
|
| 231 |
+
nx.find_minimal_d_separator(collider_graph, 0, 1, included=0)
|
| 232 |
+
with pytest.raises(nx.NetworkXError):
|
| 233 |
+
nx.find_minimal_d_separator(collider_graph, 1, 0, included=0)
|
| 234 |
+
with pytest.raises(nx.NetworkXError):
|
| 235 |
+
nx.is_minimal_d_separator(collider_graph, 0, 0, set())
|
| 236 |
+
with pytest.raises(nx.NetworkXError):
|
| 237 |
+
nx.is_minimal_d_separator(collider_graph, 0, 1, set(), included=0)
|
| 238 |
+
with pytest.raises(nx.NetworkXError):
|
| 239 |
+
nx.is_minimal_d_separator(collider_graph, 1, 0, set(), included=0)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def test_is_minimal_d_separator(
|
| 243 |
+
large_collider_graph,
|
| 244 |
+
chain_and_fork_graph,
|
| 245 |
+
no_separating_set_graph,
|
| 246 |
+
large_no_separating_set_graph,
|
| 247 |
+
collider_trek_graph,
|
| 248 |
+
):
|
| 249 |
+
# Case 1:
|
| 250 |
+
# create a graph A -> B <- C
|
| 251 |
+
# B -> D -> E;
|
| 252 |
+
# B -> F;
|
| 253 |
+
# G -> E;
|
| 254 |
+
assert not nx.is_d_separator(large_collider_graph, {"B"}, {"E"}, set())
|
| 255 |
+
|
| 256 |
+
# minimal set of the corresponding graph
|
| 257 |
+
# for B and E should be (D,)
|
| 258 |
+
Zmin = nx.find_minimal_d_separator(large_collider_graph, "B", "E")
|
| 259 |
+
# check that the minimal d-separator is a d-separating set
|
| 260 |
+
assert nx.is_d_separator(large_collider_graph, "B", "E", Zmin)
|
| 261 |
+
# the minimal separating set should also pass the test for minimality
|
| 262 |
+
assert nx.is_minimal_d_separator(large_collider_graph, "B", "E", Zmin)
|
| 263 |
+
# function should also work with set arguments
|
| 264 |
+
assert nx.is_minimal_d_separator(large_collider_graph, {"A", "B"}, {"G", "E"}, Zmin)
|
| 265 |
+
assert Zmin == {"D"}
|
| 266 |
+
|
| 267 |
+
# Case 2:
|
| 268 |
+
# create a graph A -> B -> C
|
| 269 |
+
# B -> D -> C;
|
| 270 |
+
assert not nx.is_d_separator(chain_and_fork_graph, {"A"}, {"C"}, set())
|
| 271 |
+
Zmin = nx.find_minimal_d_separator(chain_and_fork_graph, "A", "C")
|
| 272 |
+
|
| 273 |
+
# the minimal separating set should pass the test for minimality
|
| 274 |
+
assert nx.is_minimal_d_separator(chain_and_fork_graph, "A", "C", Zmin)
|
| 275 |
+
assert Zmin == {"B"}
|
| 276 |
+
Znotmin = Zmin.union({"D"})
|
| 277 |
+
assert not nx.is_minimal_d_separator(chain_and_fork_graph, "A", "C", Znotmin)
|
| 278 |
+
|
| 279 |
+
# Case 3:
|
| 280 |
+
# create a graph A -> B
|
| 281 |
+
|
| 282 |
+
# there is no m-separating set between A and B at all, so
|
| 283 |
+
# no minimal m-separating set can exist
|
| 284 |
+
assert not nx.is_d_separator(no_separating_set_graph, {"A"}, {"B"}, set())
|
| 285 |
+
assert nx.find_minimal_d_separator(no_separating_set_graph, "A", "B") is None
|
| 286 |
+
|
| 287 |
+
# Case 4:
|
| 288 |
+
# create a graph A -> B with A <- C -> B
|
| 289 |
+
|
| 290 |
+
# there is no m-separating set between A and B at all, so
|
| 291 |
+
# no minimal m-separating set can exist
|
| 292 |
+
# however, the algorithm will initially propose C as a
|
| 293 |
+
# minimal (but invalid) separating set
|
| 294 |
+
assert not nx.is_d_separator(large_no_separating_set_graph, {"A"}, {"B"}, {"C"})
|
| 295 |
+
assert nx.find_minimal_d_separator(large_no_separating_set_graph, "A", "B") is None
|
| 296 |
+
|
| 297 |
+
# Test `included` and `excluded` args
|
| 298 |
+
# create graph A -> B <- C -> D
|
| 299 |
+
assert nx.find_minimal_d_separator(collider_trek_graph, "A", "D", included="B") == {
|
| 300 |
+
"B",
|
| 301 |
+
"C",
|
| 302 |
+
}
|
| 303 |
+
assert (
|
| 304 |
+
nx.find_minimal_d_separator(
|
| 305 |
+
collider_trek_graph, "A", "D", included="B", restricted="B"
|
| 306 |
+
)
|
| 307 |
+
is None
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def test_is_minimal_d_separator_checks_dsep():
|
| 312 |
+
"""Test that is_minimal_d_separator checks for d-separation as well."""
|
| 313 |
+
g = nx.DiGraph()
|
| 314 |
+
g.add_edges_from(
|
| 315 |
+
[
|
| 316 |
+
("A", "B"),
|
| 317 |
+
("A", "E"),
|
| 318 |
+
("B", "C"),
|
| 319 |
+
("B", "D"),
|
| 320 |
+
("D", "C"),
|
| 321 |
+
("D", "F"),
|
| 322 |
+
("E", "D"),
|
| 323 |
+
("E", "F"),
|
| 324 |
+
]
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
assert not nx.is_d_separator(g, {"C"}, {"F"}, {"D"})
|
| 328 |
+
|
| 329 |
+
# since {'D'} and {} are not d-separators, we return false
|
| 330 |
+
assert not nx.is_minimal_d_separator(g, "C", "F", {"D"})
|
| 331 |
+
assert not nx.is_minimal_d_separator(g, "C", "F", set())
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def test__reachable(large_collider_graph):
|
| 335 |
+
reachable = nx.algorithms.d_separation._reachable
|
| 336 |
+
g = large_collider_graph
|
| 337 |
+
x = {"F", "D"}
|
| 338 |
+
ancestors = {"A", "B", "C", "D", "F"}
|
| 339 |
+
assert reachable(g, x, ancestors, {"B"}) == {"B", "F", "D"}
|
| 340 |
+
assert reachable(g, x, ancestors, set()) == ancestors
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def test_deprecations():
|
| 344 |
+
G = nx.DiGraph([(0, 1), (1, 2)])
|
| 345 |
+
with pytest.deprecated_call():
|
| 346 |
+
nx.d_separated(G, 0, 2, {1})
|
| 347 |
+
with pytest.deprecated_call():
|
| 348 |
+
z = nx.minimal_d_separator(G, 0, 2)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_dag.py
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections import deque
|
| 2 |
+
from itertools import combinations, permutations
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from networkx.utils import edges_equal, pairwise
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# Recipe from the itertools documentation.
|
| 11 |
+
def _consume(iterator):
|
| 12 |
+
"Consume the iterator entirely."
|
| 13 |
+
# Feed the entire iterator into a zero-length deque.
|
| 14 |
+
deque(iterator, maxlen=0)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class TestDagLongestPath:
|
| 18 |
+
"""Unit tests computing the longest path in a directed acyclic graph."""
|
| 19 |
+
|
| 20 |
+
def test_empty(self):
|
| 21 |
+
G = nx.DiGraph()
|
| 22 |
+
assert nx.dag_longest_path(G) == []
|
| 23 |
+
|
| 24 |
+
def test_unweighted1(self):
|
| 25 |
+
edges = [(1, 2), (2, 3), (2, 4), (3, 5), (5, 6), (3, 7)]
|
| 26 |
+
G = nx.DiGraph(edges)
|
| 27 |
+
assert nx.dag_longest_path(G) == [1, 2, 3, 5, 6]
|
| 28 |
+
|
| 29 |
+
def test_unweighted2(self):
|
| 30 |
+
edges = [(1, 2), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)]
|
| 31 |
+
G = nx.DiGraph(edges)
|
| 32 |
+
assert nx.dag_longest_path(G) == [1, 2, 3, 4, 5]
|
| 33 |
+
|
| 34 |
+
def test_weighted(self):
|
| 35 |
+
G = nx.DiGraph()
|
| 36 |
+
edges = [(1, 2, -5), (2, 3, 1), (3, 4, 1), (4, 5, 0), (3, 5, 4), (1, 6, 2)]
|
| 37 |
+
G.add_weighted_edges_from(edges)
|
| 38 |
+
assert nx.dag_longest_path(G) == [2, 3, 5]
|
| 39 |
+
|
| 40 |
+
def test_undirected_not_implemented(self):
|
| 41 |
+
G = nx.Graph()
|
| 42 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.dag_longest_path, G)
|
| 43 |
+
|
| 44 |
+
def test_unorderable_nodes(self):
|
| 45 |
+
"""Tests that computing the longest path does not depend on
|
| 46 |
+
nodes being orderable.
|
| 47 |
+
|
| 48 |
+
For more information, see issue #1989.
|
| 49 |
+
|
| 50 |
+
"""
|
| 51 |
+
# Create the directed path graph on four nodes in a diamond shape,
|
| 52 |
+
# with nodes represented as (unorderable) Python objects.
|
| 53 |
+
nodes = [object() for n in range(4)]
|
| 54 |
+
G = nx.DiGraph()
|
| 55 |
+
G.add_edge(nodes[0], nodes[1])
|
| 56 |
+
G.add_edge(nodes[0], nodes[2])
|
| 57 |
+
G.add_edge(nodes[2], nodes[3])
|
| 58 |
+
G.add_edge(nodes[1], nodes[3])
|
| 59 |
+
|
| 60 |
+
# this will raise NotImplementedError when nodes need to be ordered
|
| 61 |
+
nx.dag_longest_path(G)
|
| 62 |
+
|
| 63 |
+
def test_multigraph_unweighted(self):
|
| 64 |
+
edges = [(1, 2), (2, 3), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)]
|
| 65 |
+
G = nx.MultiDiGraph(edges)
|
| 66 |
+
assert nx.dag_longest_path(G) == [1, 2, 3, 4, 5]
|
| 67 |
+
|
| 68 |
+
def test_multigraph_weighted(self):
|
| 69 |
+
G = nx.MultiDiGraph()
|
| 70 |
+
edges = [
|
| 71 |
+
(1, 2, 2),
|
| 72 |
+
(2, 3, 2),
|
| 73 |
+
(1, 3, 1),
|
| 74 |
+
(1, 3, 5),
|
| 75 |
+
(1, 3, 2),
|
| 76 |
+
]
|
| 77 |
+
G.add_weighted_edges_from(edges)
|
| 78 |
+
assert nx.dag_longest_path(G) == [1, 3]
|
| 79 |
+
|
| 80 |
+
def test_multigraph_weighted_default_weight(self):
|
| 81 |
+
G = nx.MultiDiGraph([(1, 2), (2, 3)]) # Unweighted edges
|
| 82 |
+
G.add_weighted_edges_from([(1, 3, 1), (1, 3, 5), (1, 3, 2)])
|
| 83 |
+
|
| 84 |
+
# Default value for default weight is 1
|
| 85 |
+
assert nx.dag_longest_path(G) == [1, 3]
|
| 86 |
+
assert nx.dag_longest_path(G, default_weight=3) == [1, 2, 3]
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class TestDagLongestPathLength:
|
| 90 |
+
"""Unit tests for computing the length of a longest path in a
|
| 91 |
+
directed acyclic graph.
|
| 92 |
+
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
def test_unweighted(self):
|
| 96 |
+
edges = [(1, 2), (2, 3), (2, 4), (3, 5), (5, 6), (5, 7)]
|
| 97 |
+
G = nx.DiGraph(edges)
|
| 98 |
+
assert nx.dag_longest_path_length(G) == 4
|
| 99 |
+
|
| 100 |
+
edges = [(1, 2), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)]
|
| 101 |
+
G = nx.DiGraph(edges)
|
| 102 |
+
assert nx.dag_longest_path_length(G) == 4
|
| 103 |
+
|
| 104 |
+
# test degenerate graphs
|
| 105 |
+
G = nx.DiGraph()
|
| 106 |
+
G.add_node(1)
|
| 107 |
+
assert nx.dag_longest_path_length(G) == 0
|
| 108 |
+
|
| 109 |
+
def test_undirected_not_implemented(self):
|
| 110 |
+
G = nx.Graph()
|
| 111 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.dag_longest_path_length, G)
|
| 112 |
+
|
| 113 |
+
def test_weighted(self):
|
| 114 |
+
edges = [(1, 2, -5), (2, 3, 1), (3, 4, 1), (4, 5, 0), (3, 5, 4), (1, 6, 2)]
|
| 115 |
+
G = nx.DiGraph()
|
| 116 |
+
G.add_weighted_edges_from(edges)
|
| 117 |
+
assert nx.dag_longest_path_length(G) == 5
|
| 118 |
+
|
| 119 |
+
def test_multigraph_unweighted(self):
|
| 120 |
+
edges = [(1, 2), (2, 3), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)]
|
| 121 |
+
G = nx.MultiDiGraph(edges)
|
| 122 |
+
assert nx.dag_longest_path_length(G) == 4
|
| 123 |
+
|
| 124 |
+
def test_multigraph_weighted(self):
|
| 125 |
+
G = nx.MultiDiGraph()
|
| 126 |
+
edges = [
|
| 127 |
+
(1, 2, 2),
|
| 128 |
+
(2, 3, 2),
|
| 129 |
+
(1, 3, 1),
|
| 130 |
+
(1, 3, 5),
|
| 131 |
+
(1, 3, 2),
|
| 132 |
+
]
|
| 133 |
+
G.add_weighted_edges_from(edges)
|
| 134 |
+
assert nx.dag_longest_path_length(G) == 5
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
class TestDAG:
|
| 138 |
+
@classmethod
|
| 139 |
+
def setup_class(cls):
|
| 140 |
+
pass
|
| 141 |
+
|
| 142 |
+
def test_topological_sort1(self):
|
| 143 |
+
DG = nx.DiGraph([(1, 2), (1, 3), (2, 3)])
|
| 144 |
+
|
| 145 |
+
for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]:
|
| 146 |
+
assert tuple(algorithm(DG)) == (1, 2, 3)
|
| 147 |
+
|
| 148 |
+
DG.add_edge(3, 2)
|
| 149 |
+
|
| 150 |
+
for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]:
|
| 151 |
+
pytest.raises(nx.NetworkXUnfeasible, _consume, algorithm(DG))
|
| 152 |
+
|
| 153 |
+
DG.remove_edge(2, 3)
|
| 154 |
+
|
| 155 |
+
for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]:
|
| 156 |
+
assert tuple(algorithm(DG)) == (1, 3, 2)
|
| 157 |
+
|
| 158 |
+
DG.remove_edge(3, 2)
|
| 159 |
+
|
| 160 |
+
assert tuple(nx.topological_sort(DG)) in {(1, 2, 3), (1, 3, 2)}
|
| 161 |
+
assert tuple(nx.lexicographical_topological_sort(DG)) == (1, 2, 3)
|
| 162 |
+
|
| 163 |
+
def test_is_directed_acyclic_graph(self):
|
| 164 |
+
G = nx.generators.complete_graph(2)
|
| 165 |
+
assert not nx.is_directed_acyclic_graph(G)
|
| 166 |
+
assert not nx.is_directed_acyclic_graph(G.to_directed())
|
| 167 |
+
assert not nx.is_directed_acyclic_graph(nx.Graph([(3, 4), (4, 5)]))
|
| 168 |
+
assert nx.is_directed_acyclic_graph(nx.DiGraph([(3, 4), (4, 5)]))
|
| 169 |
+
|
| 170 |
+
def test_topological_sort2(self):
|
| 171 |
+
DG = nx.DiGraph(
|
| 172 |
+
{
|
| 173 |
+
1: [2],
|
| 174 |
+
2: [3],
|
| 175 |
+
3: [4],
|
| 176 |
+
4: [5],
|
| 177 |
+
5: [1],
|
| 178 |
+
11: [12],
|
| 179 |
+
12: [13],
|
| 180 |
+
13: [14],
|
| 181 |
+
14: [15],
|
| 182 |
+
}
|
| 183 |
+
)
|
| 184 |
+
pytest.raises(nx.NetworkXUnfeasible, _consume, nx.topological_sort(DG))
|
| 185 |
+
|
| 186 |
+
assert not nx.is_directed_acyclic_graph(DG)
|
| 187 |
+
|
| 188 |
+
DG.remove_edge(1, 2)
|
| 189 |
+
_consume(nx.topological_sort(DG))
|
| 190 |
+
assert nx.is_directed_acyclic_graph(DG)
|
| 191 |
+
|
| 192 |
+
def test_topological_sort3(self):
|
| 193 |
+
DG = nx.DiGraph()
|
| 194 |
+
DG.add_edges_from([(1, i) for i in range(2, 5)])
|
| 195 |
+
DG.add_edges_from([(2, i) for i in range(5, 9)])
|
| 196 |
+
DG.add_edges_from([(6, i) for i in range(9, 12)])
|
| 197 |
+
DG.add_edges_from([(4, i) for i in range(12, 15)])
|
| 198 |
+
|
| 199 |
+
def validate(order):
|
| 200 |
+
assert isinstance(order, list)
|
| 201 |
+
assert set(order) == set(DG)
|
| 202 |
+
for u, v in combinations(order, 2):
|
| 203 |
+
assert not nx.has_path(DG, v, u)
|
| 204 |
+
|
| 205 |
+
validate(list(nx.topological_sort(DG)))
|
| 206 |
+
|
| 207 |
+
DG.add_edge(14, 1)
|
| 208 |
+
pytest.raises(nx.NetworkXUnfeasible, _consume, nx.topological_sort(DG))
|
| 209 |
+
|
| 210 |
+
def test_topological_sort4(self):
|
| 211 |
+
G = nx.Graph()
|
| 212 |
+
G.add_edge(1, 2)
|
| 213 |
+
# Only directed graphs can be topologically sorted.
|
| 214 |
+
pytest.raises(nx.NetworkXError, _consume, nx.topological_sort(G))
|
| 215 |
+
|
| 216 |
+
def test_topological_sort5(self):
|
| 217 |
+
G = nx.DiGraph()
|
| 218 |
+
G.add_edge(0, 1)
|
| 219 |
+
assert list(nx.topological_sort(G)) == [0, 1]
|
| 220 |
+
|
| 221 |
+
def test_topological_sort6(self):
|
| 222 |
+
for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]:
|
| 223 |
+
|
| 224 |
+
def runtime_error():
|
| 225 |
+
DG = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
|
| 226 |
+
first = True
|
| 227 |
+
for x in algorithm(DG):
|
| 228 |
+
if first:
|
| 229 |
+
first = False
|
| 230 |
+
DG.add_edge(5 - x, 5)
|
| 231 |
+
|
| 232 |
+
def unfeasible_error():
|
| 233 |
+
DG = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
|
| 234 |
+
first = True
|
| 235 |
+
for x in algorithm(DG):
|
| 236 |
+
if first:
|
| 237 |
+
first = False
|
| 238 |
+
DG.remove_node(4)
|
| 239 |
+
|
| 240 |
+
def runtime_error2():
|
| 241 |
+
DG = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
|
| 242 |
+
first = True
|
| 243 |
+
for x in algorithm(DG):
|
| 244 |
+
if first:
|
| 245 |
+
first = False
|
| 246 |
+
DG.remove_node(2)
|
| 247 |
+
|
| 248 |
+
pytest.raises(RuntimeError, runtime_error)
|
| 249 |
+
pytest.raises(RuntimeError, runtime_error2)
|
| 250 |
+
pytest.raises(nx.NetworkXUnfeasible, unfeasible_error)
|
| 251 |
+
|
| 252 |
+
def test_all_topological_sorts_1(self):
|
| 253 |
+
DG = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 5)])
|
| 254 |
+
assert list(nx.all_topological_sorts(DG)) == [[1, 2, 3, 4, 5]]
|
| 255 |
+
|
| 256 |
+
def test_all_topological_sorts_2(self):
|
| 257 |
+
DG = nx.DiGraph([(1, 3), (2, 1), (2, 4), (4, 3), (4, 5)])
|
| 258 |
+
assert sorted(nx.all_topological_sorts(DG)) == [
|
| 259 |
+
[2, 1, 4, 3, 5],
|
| 260 |
+
[2, 1, 4, 5, 3],
|
| 261 |
+
[2, 4, 1, 3, 5],
|
| 262 |
+
[2, 4, 1, 5, 3],
|
| 263 |
+
[2, 4, 5, 1, 3],
|
| 264 |
+
]
|
| 265 |
+
|
| 266 |
+
def test_all_topological_sorts_3(self):
|
| 267 |
+
def unfeasible():
|
| 268 |
+
DG = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 2), (4, 5)])
|
| 269 |
+
# convert to list to execute generator
|
| 270 |
+
list(nx.all_topological_sorts(DG))
|
| 271 |
+
|
| 272 |
+
def not_implemented():
|
| 273 |
+
G = nx.Graph([(1, 2), (2, 3)])
|
| 274 |
+
# convert to list to execute generator
|
| 275 |
+
list(nx.all_topological_sorts(G))
|
| 276 |
+
|
| 277 |
+
def not_implemented_2():
|
| 278 |
+
G = nx.MultiGraph([(1, 2), (1, 2), (2, 3)])
|
| 279 |
+
list(nx.all_topological_sorts(G))
|
| 280 |
+
|
| 281 |
+
pytest.raises(nx.NetworkXUnfeasible, unfeasible)
|
| 282 |
+
pytest.raises(nx.NetworkXNotImplemented, not_implemented)
|
| 283 |
+
pytest.raises(nx.NetworkXNotImplemented, not_implemented_2)
|
| 284 |
+
|
| 285 |
+
def test_all_topological_sorts_4(self):
|
| 286 |
+
DG = nx.DiGraph()
|
| 287 |
+
for i in range(7):
|
| 288 |
+
DG.add_node(i)
|
| 289 |
+
assert sorted(map(list, permutations(DG.nodes))) == sorted(
|
| 290 |
+
nx.all_topological_sorts(DG)
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
def test_all_topological_sorts_multigraph_1(self):
|
| 294 |
+
DG = nx.MultiDiGraph([(1, 2), (1, 2), (2, 3), (3, 4), (3, 5), (3, 5), (3, 5)])
|
| 295 |
+
assert sorted(nx.all_topological_sorts(DG)) == sorted(
|
| 296 |
+
[[1, 2, 3, 4, 5], [1, 2, 3, 5, 4]]
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
def test_all_topological_sorts_multigraph_2(self):
|
| 300 |
+
N = 9
|
| 301 |
+
edges = []
|
| 302 |
+
for i in range(1, N):
|
| 303 |
+
edges.extend([(i, i + 1)] * i)
|
| 304 |
+
DG = nx.MultiDiGraph(edges)
|
| 305 |
+
assert list(nx.all_topological_sorts(DG)) == [list(range(1, N + 1))]
|
| 306 |
+
|
| 307 |
+
def test_ancestors(self):
|
| 308 |
+
G = nx.DiGraph()
|
| 309 |
+
ancestors = nx.algorithms.dag.ancestors
|
| 310 |
+
G.add_edges_from([(1, 2), (1, 3), (4, 2), (4, 3), (4, 5), (2, 6), (5, 6)])
|
| 311 |
+
assert ancestors(G, 6) == {1, 2, 4, 5}
|
| 312 |
+
assert ancestors(G, 3) == {1, 4}
|
| 313 |
+
assert ancestors(G, 1) == set()
|
| 314 |
+
pytest.raises(nx.NetworkXError, ancestors, G, 8)
|
| 315 |
+
|
| 316 |
+
def test_descendants(self):
|
| 317 |
+
G = nx.DiGraph()
|
| 318 |
+
descendants = nx.algorithms.dag.descendants
|
| 319 |
+
G.add_edges_from([(1, 2), (1, 3), (4, 2), (4, 3), (4, 5), (2, 6), (5, 6)])
|
| 320 |
+
assert descendants(G, 1) == {2, 3, 6}
|
| 321 |
+
assert descendants(G, 4) == {2, 3, 5, 6}
|
| 322 |
+
assert descendants(G, 3) == set()
|
| 323 |
+
pytest.raises(nx.NetworkXError, descendants, G, 8)
|
| 324 |
+
|
| 325 |
+
def test_transitive_closure(self):
|
| 326 |
+
G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
|
| 327 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
|
| 328 |
+
assert edges_equal(nx.transitive_closure(G).edges(), solution)
|
| 329 |
+
G = nx.DiGraph([(1, 2), (2, 3), (2, 4)])
|
| 330 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]
|
| 331 |
+
assert edges_equal(nx.transitive_closure(G).edges(), solution)
|
| 332 |
+
G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
|
| 333 |
+
solution = [(1, 2), (2, 1), (2, 3), (3, 2), (1, 3), (3, 1)]
|
| 334 |
+
soln = sorted(solution + [(n, n) for n in G])
|
| 335 |
+
assert edges_equal(sorted(nx.transitive_closure(G).edges()), soln)
|
| 336 |
+
|
| 337 |
+
G = nx.Graph([(1, 2), (2, 3), (3, 4)])
|
| 338 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
|
| 339 |
+
assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution)
|
| 340 |
+
|
| 341 |
+
G = nx.MultiGraph([(1, 2), (2, 3), (3, 4)])
|
| 342 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
|
| 343 |
+
assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution)
|
| 344 |
+
|
| 345 |
+
G = nx.MultiDiGraph([(1, 2), (2, 3), (3, 4)])
|
| 346 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
|
| 347 |
+
assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution)
|
| 348 |
+
|
| 349 |
+
# test if edge data is copied
|
| 350 |
+
G = nx.DiGraph([(1, 2, {"a": 3}), (2, 3, {"b": 0}), (3, 4)])
|
| 351 |
+
H = nx.transitive_closure(G)
|
| 352 |
+
for u, v in G.edges():
|
| 353 |
+
assert G.get_edge_data(u, v) == H.get_edge_data(u, v)
|
| 354 |
+
|
| 355 |
+
k = 10
|
| 356 |
+
G = nx.DiGraph((i, i + 1, {"f": "b", "weight": i}) for i in range(k))
|
| 357 |
+
H = nx.transitive_closure(G)
|
| 358 |
+
for u, v in G.edges():
|
| 359 |
+
assert G.get_edge_data(u, v) == H.get_edge_data(u, v)
|
| 360 |
+
|
| 361 |
+
G = nx.Graph()
|
| 362 |
+
with pytest.raises(nx.NetworkXError):
|
| 363 |
+
nx.transitive_closure(G, reflexive="wrong input")
|
| 364 |
+
|
| 365 |
+
def test_reflexive_transitive_closure(self):
|
| 366 |
+
G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
|
| 367 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
|
| 368 |
+
soln = sorted(solution + [(n, n) for n in G])
|
| 369 |
+
assert edges_equal(nx.transitive_closure(G).edges(), solution)
|
| 370 |
+
assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
|
| 371 |
+
assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
|
| 372 |
+
assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
|
| 373 |
+
|
| 374 |
+
G = nx.DiGraph([(1, 2), (2, 3), (2, 4)])
|
| 375 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]
|
| 376 |
+
soln = sorted(solution + [(n, n) for n in G])
|
| 377 |
+
assert edges_equal(nx.transitive_closure(G).edges(), solution)
|
| 378 |
+
assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
|
| 379 |
+
assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
|
| 380 |
+
assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
|
| 381 |
+
|
| 382 |
+
G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
|
| 383 |
+
solution = sorted([(1, 2), (2, 1), (2, 3), (3, 2), (1, 3), (3, 1)])
|
| 384 |
+
soln = sorted(solution + [(n, n) for n in G])
|
| 385 |
+
assert edges_equal(sorted(nx.transitive_closure(G).edges()), soln)
|
| 386 |
+
assert edges_equal(sorted(nx.transitive_closure(G, False).edges()), soln)
|
| 387 |
+
assert edges_equal(sorted(nx.transitive_closure(G, None).edges()), solution)
|
| 388 |
+
assert edges_equal(sorted(nx.transitive_closure(G, True).edges()), soln)
|
| 389 |
+
|
| 390 |
+
G = nx.Graph([(1, 2), (2, 3), (3, 4)])
|
| 391 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
|
| 392 |
+
soln = sorted(solution + [(n, n) for n in G])
|
| 393 |
+
assert edges_equal(nx.transitive_closure(G).edges(), solution)
|
| 394 |
+
assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
|
| 395 |
+
assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
|
| 396 |
+
assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
|
| 397 |
+
|
| 398 |
+
G = nx.MultiGraph([(1, 2), (2, 3), (3, 4)])
|
| 399 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
|
| 400 |
+
soln = sorted(solution + [(n, n) for n in G])
|
| 401 |
+
assert edges_equal(nx.transitive_closure(G).edges(), solution)
|
| 402 |
+
assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
|
| 403 |
+
assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
|
| 404 |
+
assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
|
| 405 |
+
|
| 406 |
+
G = nx.MultiDiGraph([(1, 2), (2, 3), (3, 4)])
|
| 407 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
|
| 408 |
+
soln = sorted(solution + [(n, n) for n in G])
|
| 409 |
+
assert edges_equal(nx.transitive_closure(G).edges(), solution)
|
| 410 |
+
assert edges_equal(nx.transitive_closure(G, False).edges(), solution)
|
| 411 |
+
assert edges_equal(nx.transitive_closure(G, True).edges(), soln)
|
| 412 |
+
assert edges_equal(nx.transitive_closure(G, None).edges(), solution)
|
| 413 |
+
|
| 414 |
+
def test_transitive_closure_dag(self):
|
| 415 |
+
G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
|
| 416 |
+
transitive_closure = nx.algorithms.dag.transitive_closure_dag
|
| 417 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
|
| 418 |
+
assert edges_equal(transitive_closure(G).edges(), solution)
|
| 419 |
+
G = nx.DiGraph([(1, 2), (2, 3), (2, 4)])
|
| 420 |
+
solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]
|
| 421 |
+
assert edges_equal(transitive_closure(G).edges(), solution)
|
| 422 |
+
G = nx.Graph([(1, 2), (2, 3), (3, 4)])
|
| 423 |
+
pytest.raises(nx.NetworkXNotImplemented, transitive_closure, G)
|
| 424 |
+
|
| 425 |
+
# test if edge data is copied
|
| 426 |
+
G = nx.DiGraph([(1, 2, {"a": 3}), (2, 3, {"b": 0}), (3, 4)])
|
| 427 |
+
H = transitive_closure(G)
|
| 428 |
+
for u, v in G.edges():
|
| 429 |
+
assert G.get_edge_data(u, v) == H.get_edge_data(u, v)
|
| 430 |
+
|
| 431 |
+
k = 10
|
| 432 |
+
G = nx.DiGraph((i, i + 1, {"foo": "bar", "weight": i}) for i in range(k))
|
| 433 |
+
H = transitive_closure(G)
|
| 434 |
+
for u, v in G.edges():
|
| 435 |
+
assert G.get_edge_data(u, v) == H.get_edge_data(u, v)
|
| 436 |
+
|
| 437 |
+
def test_transitive_reduction(self):
|
| 438 |
+
G = nx.DiGraph([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)])
|
| 439 |
+
transitive_reduction = nx.algorithms.dag.transitive_reduction
|
| 440 |
+
solution = [(1, 2), (2, 3), (3, 4)]
|
| 441 |
+
assert edges_equal(transitive_reduction(G).edges(), solution)
|
| 442 |
+
G = nx.DiGraph([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)])
|
| 443 |
+
transitive_reduction = nx.algorithms.dag.transitive_reduction
|
| 444 |
+
solution = [(1, 2), (2, 3), (2, 4)]
|
| 445 |
+
assert edges_equal(transitive_reduction(G).edges(), solution)
|
| 446 |
+
G = nx.Graph([(1, 2), (2, 3), (3, 4)])
|
| 447 |
+
pytest.raises(nx.NetworkXNotImplemented, transitive_reduction, G)
|
| 448 |
+
|
| 449 |
+
def _check_antichains(self, solution, result):
|
| 450 |
+
sol = [frozenset(a) for a in solution]
|
| 451 |
+
res = [frozenset(a) for a in result]
|
| 452 |
+
assert set(sol) == set(res)
|
| 453 |
+
|
| 454 |
+
def test_antichains(self):
|
| 455 |
+
antichains = nx.algorithms.dag.antichains
|
| 456 |
+
G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
|
| 457 |
+
solution = [[], [4], [3], [2], [1]]
|
| 458 |
+
self._check_antichains(list(antichains(G)), solution)
|
| 459 |
+
G = nx.DiGraph([(1, 2), (2, 3), (2, 4), (3, 5), (5, 6), (5, 7)])
|
| 460 |
+
solution = [
|
| 461 |
+
[],
|
| 462 |
+
[4],
|
| 463 |
+
[7],
|
| 464 |
+
[7, 4],
|
| 465 |
+
[6],
|
| 466 |
+
[6, 4],
|
| 467 |
+
[6, 7],
|
| 468 |
+
[6, 7, 4],
|
| 469 |
+
[5],
|
| 470 |
+
[5, 4],
|
| 471 |
+
[3],
|
| 472 |
+
[3, 4],
|
| 473 |
+
[2],
|
| 474 |
+
[1],
|
| 475 |
+
]
|
| 476 |
+
self._check_antichains(list(antichains(G)), solution)
|
| 477 |
+
G = nx.DiGraph([(1, 2), (1, 3), (3, 4), (3, 5), (5, 6)])
|
| 478 |
+
solution = [
|
| 479 |
+
[],
|
| 480 |
+
[6],
|
| 481 |
+
[5],
|
| 482 |
+
[4],
|
| 483 |
+
[4, 6],
|
| 484 |
+
[4, 5],
|
| 485 |
+
[3],
|
| 486 |
+
[2],
|
| 487 |
+
[2, 6],
|
| 488 |
+
[2, 5],
|
| 489 |
+
[2, 4],
|
| 490 |
+
[2, 4, 6],
|
| 491 |
+
[2, 4, 5],
|
| 492 |
+
[2, 3],
|
| 493 |
+
[1],
|
| 494 |
+
]
|
| 495 |
+
self._check_antichains(list(antichains(G)), solution)
|
| 496 |
+
G = nx.DiGraph({0: [1, 2], 1: [4], 2: [3], 3: [4]})
|
| 497 |
+
solution = [[], [4], [3], [2], [1], [1, 3], [1, 2], [0]]
|
| 498 |
+
self._check_antichains(list(antichains(G)), solution)
|
| 499 |
+
G = nx.DiGraph()
|
| 500 |
+
self._check_antichains(list(antichains(G)), [[]])
|
| 501 |
+
G = nx.DiGraph()
|
| 502 |
+
G.add_nodes_from([0, 1, 2])
|
| 503 |
+
solution = [[], [0], [1], [1, 0], [2], [2, 0], [2, 1], [2, 1, 0]]
|
| 504 |
+
self._check_antichains(list(antichains(G)), solution)
|
| 505 |
+
|
| 506 |
+
def f(x):
|
| 507 |
+
return list(antichains(x))
|
| 508 |
+
|
| 509 |
+
G = nx.Graph([(1, 2), (2, 3), (3, 4)])
|
| 510 |
+
pytest.raises(nx.NetworkXNotImplemented, f, G)
|
| 511 |
+
G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
|
| 512 |
+
pytest.raises(nx.NetworkXUnfeasible, f, G)
|
| 513 |
+
|
| 514 |
+
def test_lexicographical_topological_sort(self):
|
| 515 |
+
G = nx.DiGraph([(1, 2), (2, 3), (1, 4), (1, 5), (2, 6)])
|
| 516 |
+
assert list(nx.lexicographical_topological_sort(G)) == [1, 2, 3, 4, 5, 6]
|
| 517 |
+
assert list(nx.lexicographical_topological_sort(G, key=lambda x: x)) == [
|
| 518 |
+
1,
|
| 519 |
+
2,
|
| 520 |
+
3,
|
| 521 |
+
4,
|
| 522 |
+
5,
|
| 523 |
+
6,
|
| 524 |
+
]
|
| 525 |
+
assert list(nx.lexicographical_topological_sort(G, key=lambda x: -x)) == [
|
| 526 |
+
1,
|
| 527 |
+
5,
|
| 528 |
+
4,
|
| 529 |
+
2,
|
| 530 |
+
6,
|
| 531 |
+
3,
|
| 532 |
+
]
|
| 533 |
+
|
| 534 |
+
def test_lexicographical_topological_sort2(self):
|
| 535 |
+
"""
|
| 536 |
+
Check the case of two or more nodes with same key value.
|
| 537 |
+
Want to avoid exception raised due to comparing nodes directly.
|
| 538 |
+
See Issue #3493
|
| 539 |
+
"""
|
| 540 |
+
|
| 541 |
+
class Test_Node:
|
| 542 |
+
def __init__(self, n):
|
| 543 |
+
self.label = n
|
| 544 |
+
self.priority = 1
|
| 545 |
+
|
| 546 |
+
def __repr__(self):
|
| 547 |
+
return f"Node({self.label})"
|
| 548 |
+
|
| 549 |
+
def sorting_key(node):
|
| 550 |
+
return node.priority
|
| 551 |
+
|
| 552 |
+
test_nodes = [Test_Node(n) for n in range(4)]
|
| 553 |
+
G = nx.DiGraph()
|
| 554 |
+
edges = [(0, 1), (0, 2), (0, 3), (2, 3)]
|
| 555 |
+
G.add_edges_from((test_nodes[a], test_nodes[b]) for a, b in edges)
|
| 556 |
+
|
| 557 |
+
sorting = list(nx.lexicographical_topological_sort(G, key=sorting_key))
|
| 558 |
+
assert sorting == test_nodes
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def test_topological_generations():
|
| 562 |
+
G = nx.DiGraph(
|
| 563 |
+
{1: [2, 3], 2: [4, 5], 3: [7], 4: [], 5: [6, 7], 6: [], 7: []}
|
| 564 |
+
).reverse()
|
| 565 |
+
# order within each generation is inconsequential
|
| 566 |
+
generations = [sorted(gen) for gen in nx.topological_generations(G)]
|
| 567 |
+
expected = [[4, 6, 7], [3, 5], [2], [1]]
|
| 568 |
+
assert generations == expected
|
| 569 |
+
|
| 570 |
+
MG = nx.MultiDiGraph(G.edges)
|
| 571 |
+
MG.add_edge(2, 1)
|
| 572 |
+
generations = [sorted(gen) for gen in nx.topological_generations(MG)]
|
| 573 |
+
assert generations == expected
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
def test_topological_generations_empty():
|
| 577 |
+
G = nx.DiGraph()
|
| 578 |
+
assert list(nx.topological_generations(G)) == []
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
def test_topological_generations_cycle():
|
| 582 |
+
G = nx.DiGraph([[2, 1], [3, 1], [1, 2]])
|
| 583 |
+
with pytest.raises(nx.NetworkXUnfeasible):
|
| 584 |
+
list(nx.topological_generations(G))
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
def test_is_aperiodic_cycle():
|
| 588 |
+
G = nx.DiGraph()
|
| 589 |
+
nx.add_cycle(G, [1, 2, 3, 4])
|
| 590 |
+
assert not nx.is_aperiodic(G)
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
def test_is_aperiodic_cycle2():
|
| 594 |
+
G = nx.DiGraph()
|
| 595 |
+
nx.add_cycle(G, [1, 2, 3, 4])
|
| 596 |
+
nx.add_cycle(G, [3, 4, 5, 6, 7])
|
| 597 |
+
assert nx.is_aperiodic(G)
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
def test_is_aperiodic_cycle3():
|
| 601 |
+
G = nx.DiGraph()
|
| 602 |
+
nx.add_cycle(G, [1, 2, 3, 4])
|
| 603 |
+
nx.add_cycle(G, [3, 4, 5, 6])
|
| 604 |
+
assert not nx.is_aperiodic(G)
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
def test_is_aperiodic_cycle4():
|
| 608 |
+
G = nx.DiGraph()
|
| 609 |
+
nx.add_cycle(G, [1, 2, 3, 4])
|
| 610 |
+
G.add_edge(1, 3)
|
| 611 |
+
assert nx.is_aperiodic(G)
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
def test_is_aperiodic_selfloop():
|
| 615 |
+
G = nx.DiGraph()
|
| 616 |
+
nx.add_cycle(G, [1, 2, 3, 4])
|
| 617 |
+
G.add_edge(1, 1)
|
| 618 |
+
assert nx.is_aperiodic(G)
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
def test_is_aperiodic_undirected_raises():
|
| 622 |
+
G = nx.Graph()
|
| 623 |
+
pytest.raises(nx.NetworkXError, nx.is_aperiodic, G)
|
| 624 |
+
|
| 625 |
+
|
| 626 |
+
def test_is_aperiodic_empty_graph():
|
| 627 |
+
G = nx.empty_graph(create_using=nx.DiGraph)
|
| 628 |
+
with pytest.raises(nx.NetworkXPointlessConcept, match="Graph has no nodes."):
|
| 629 |
+
nx.is_aperiodic(G)
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
def test_is_aperiodic_bipartite():
|
| 633 |
+
# Bipartite graph
|
| 634 |
+
G = nx.DiGraph(nx.davis_southern_women_graph())
|
| 635 |
+
assert not nx.is_aperiodic(G)
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
def test_is_aperiodic_rary_tree():
|
| 639 |
+
G = nx.full_rary_tree(3, 27, create_using=nx.DiGraph())
|
| 640 |
+
assert not nx.is_aperiodic(G)
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
def test_is_aperiodic_disconnected():
|
| 644 |
+
# disconnected graph
|
| 645 |
+
G = nx.DiGraph()
|
| 646 |
+
nx.add_cycle(G, [1, 2, 3, 4])
|
| 647 |
+
nx.add_cycle(G, [5, 6, 7, 8])
|
| 648 |
+
assert not nx.is_aperiodic(G)
|
| 649 |
+
G.add_edge(1, 3)
|
| 650 |
+
G.add_edge(5, 7)
|
| 651 |
+
assert nx.is_aperiodic(G)
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
def test_is_aperiodic_disconnected2():
|
| 655 |
+
G = nx.DiGraph()
|
| 656 |
+
nx.add_cycle(G, [0, 1, 2])
|
| 657 |
+
G.add_edge(3, 3)
|
| 658 |
+
assert not nx.is_aperiodic(G)
|
| 659 |
+
|
| 660 |
+
|
| 661 |
+
class TestDagToBranching:
|
| 662 |
+
"""Unit tests for the :func:`networkx.dag_to_branching` function."""
|
| 663 |
+
|
| 664 |
+
def test_single_root(self):
|
| 665 |
+
"""Tests that a directed acyclic graph with a single degree
|
| 666 |
+
zero node produces an arborescence.
|
| 667 |
+
|
| 668 |
+
"""
|
| 669 |
+
G = nx.DiGraph([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 670 |
+
B = nx.dag_to_branching(G)
|
| 671 |
+
expected = nx.DiGraph([(0, 1), (1, 3), (0, 2), (2, 4)])
|
| 672 |
+
assert nx.is_arborescence(B)
|
| 673 |
+
assert nx.is_isomorphic(B, expected)
|
| 674 |
+
|
| 675 |
+
def test_multiple_roots(self):
|
| 676 |
+
"""Tests that a directed acyclic graph with multiple degree zero
|
| 677 |
+
nodes creates an arborescence with multiple (weakly) connected
|
| 678 |
+
components.
|
| 679 |
+
|
| 680 |
+
"""
|
| 681 |
+
G = nx.DiGraph([(0, 1), (0, 2), (1, 3), (2, 3), (5, 2)])
|
| 682 |
+
B = nx.dag_to_branching(G)
|
| 683 |
+
expected = nx.DiGraph([(0, 1), (1, 3), (0, 2), (2, 4), (5, 6), (6, 7)])
|
| 684 |
+
assert nx.is_branching(B)
|
| 685 |
+
assert not nx.is_arborescence(B)
|
| 686 |
+
assert nx.is_isomorphic(B, expected)
|
| 687 |
+
|
| 688 |
+
# # Attributes are not copied by this function. If they were, this would
|
| 689 |
+
# # be a good test to uncomment.
|
| 690 |
+
# def test_copy_attributes(self):
|
| 691 |
+
# """Tests that node attributes are copied in the branching."""
|
| 692 |
+
# G = nx.DiGraph([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 693 |
+
# for v in G:
|
| 694 |
+
# G.node[v]['label'] = str(v)
|
| 695 |
+
# B = nx.dag_to_branching(G)
|
| 696 |
+
# # Determine the root node of the branching.
|
| 697 |
+
# root = next(v for v, d in B.in_degree() if d == 0)
|
| 698 |
+
# assert_equal(B.node[root]['label'], '0')
|
| 699 |
+
# children = B[root]
|
| 700 |
+
# # Get the left and right children, nodes 1 and 2, respectively.
|
| 701 |
+
# left, right = sorted(children, key=lambda v: B.node[v]['label'])
|
| 702 |
+
# assert_equal(B.node[left]['label'], '1')
|
| 703 |
+
# assert_equal(B.node[right]['label'], '2')
|
| 704 |
+
# # Get the left grandchild.
|
| 705 |
+
# children = B[left]
|
| 706 |
+
# assert_equal(len(children), 1)
|
| 707 |
+
# left_grandchild = arbitrary_element(children)
|
| 708 |
+
# assert_equal(B.node[left_grandchild]['label'], '3')
|
| 709 |
+
# # Get the right grandchild.
|
| 710 |
+
# children = B[right]
|
| 711 |
+
# assert_equal(len(children), 1)
|
| 712 |
+
# right_grandchild = arbitrary_element(children)
|
| 713 |
+
# assert_equal(B.node[right_grandchild]['label'], '3')
|
| 714 |
+
|
| 715 |
+
def test_already_arborescence(self):
|
| 716 |
+
"""Tests that a directed acyclic graph that is already an
|
| 717 |
+
arborescence produces an isomorphic arborescence as output.
|
| 718 |
+
|
| 719 |
+
"""
|
| 720 |
+
A = nx.balanced_tree(2, 2, create_using=nx.DiGraph())
|
| 721 |
+
B = nx.dag_to_branching(A)
|
| 722 |
+
assert nx.is_isomorphic(A, B)
|
| 723 |
+
|
| 724 |
+
def test_already_branching(self):
|
| 725 |
+
"""Tests that a directed acyclic graph that is already a
|
| 726 |
+
branching produces an isomorphic branching as output.
|
| 727 |
+
|
| 728 |
+
"""
|
| 729 |
+
T1 = nx.balanced_tree(2, 2, create_using=nx.DiGraph())
|
| 730 |
+
T2 = nx.balanced_tree(2, 2, create_using=nx.DiGraph())
|
| 731 |
+
G = nx.disjoint_union(T1, T2)
|
| 732 |
+
B = nx.dag_to_branching(G)
|
| 733 |
+
assert nx.is_isomorphic(G, B)
|
| 734 |
+
|
| 735 |
+
def test_not_acyclic(self):
|
| 736 |
+
"""Tests that a non-acyclic graph causes an exception."""
|
| 737 |
+
with pytest.raises(nx.HasACycle):
|
| 738 |
+
G = nx.DiGraph(pairwise("abc", cyclic=True))
|
| 739 |
+
nx.dag_to_branching(G)
|
| 740 |
+
|
| 741 |
+
def test_undirected(self):
|
| 742 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 743 |
+
nx.dag_to_branching(nx.Graph())
|
| 744 |
+
|
| 745 |
+
def test_multigraph(self):
|
| 746 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 747 |
+
nx.dag_to_branching(nx.MultiGraph())
|
| 748 |
+
|
| 749 |
+
def test_multidigraph(self):
|
| 750 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 751 |
+
nx.dag_to_branching(nx.MultiDiGraph())
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
def test_ancestors_descendants_undirected():
|
| 755 |
+
"""Regression test to ensure ancestors and descendants work as expected on
|
| 756 |
+
undirected graphs."""
|
| 757 |
+
G = nx.path_graph(5)
|
| 758 |
+
nx.ancestors(G, 2) == nx.descendants(G, 2) == {0, 1, 3, 4}
|
| 759 |
+
|
| 760 |
+
|
| 761 |
+
def test_compute_v_structures_raise():
|
| 762 |
+
G = nx.Graph()
|
| 763 |
+
with pytest.raises(nx.NetworkXNotImplemented, match="for undirected type"):
|
| 764 |
+
nx.compute_v_structures(G)
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def test_compute_v_structures():
|
| 768 |
+
edges = [(0, 1), (0, 2), (3, 2)]
|
| 769 |
+
G = nx.DiGraph(edges)
|
| 770 |
+
|
| 771 |
+
v_structs = set(nx.compute_v_structures(G))
|
| 772 |
+
assert len(v_structs) == 1
|
| 773 |
+
assert (0, 2, 3) in v_structs
|
| 774 |
+
|
| 775 |
+
edges = [("A", "B"), ("C", "B"), ("B", "D"), ("D", "E"), ("G", "E")]
|
| 776 |
+
G = nx.DiGraph(edges)
|
| 777 |
+
v_structs = set(nx.compute_v_structures(G))
|
| 778 |
+
assert len(v_structs) == 2
|
| 779 |
+
|
| 780 |
+
|
| 781 |
+
def test_compute_v_structures_deprecated():
|
| 782 |
+
G = nx.DiGraph()
|
| 783 |
+
with pytest.deprecated_call():
|
| 784 |
+
nx.compute_v_structures(G)
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
def test_v_structures_raise():
|
| 788 |
+
G = nx.Graph()
|
| 789 |
+
with pytest.raises(nx.NetworkXNotImplemented, match="for undirected type"):
|
| 790 |
+
nx.dag.v_structures(G)
|
| 791 |
+
|
| 792 |
+
|
| 793 |
+
@pytest.mark.parametrize(
|
| 794 |
+
("edgelist", "expected"),
|
| 795 |
+
(
|
| 796 |
+
(
|
| 797 |
+
[(0, 1), (0, 2), (3, 2)],
|
| 798 |
+
{(0, 2, 3)},
|
| 799 |
+
),
|
| 800 |
+
(
|
| 801 |
+
[("A", "B"), ("C", "B"), ("D", "G"), ("D", "E"), ("G", "E")],
|
| 802 |
+
{("A", "B", "C")},
|
| 803 |
+
),
|
| 804 |
+
([(0, 1), (2, 1), (0, 2)], set()), # adjacent parents case: see gh-7385
|
| 805 |
+
),
|
| 806 |
+
)
|
| 807 |
+
def test_v_structures(edgelist, expected):
|
| 808 |
+
G = nx.DiGraph(edgelist)
|
| 809 |
+
v_structs = set(nx.dag.v_structures(G))
|
| 810 |
+
assert v_structs == expected
|
| 811 |
+
|
| 812 |
+
|
| 813 |
+
def test_colliders_raise():
|
| 814 |
+
G = nx.Graph()
|
| 815 |
+
with pytest.raises(nx.NetworkXNotImplemented, match="for undirected type"):
|
| 816 |
+
nx.dag.colliders(G)
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
@pytest.mark.parametrize(
|
| 820 |
+
("edgelist", "expected"),
|
| 821 |
+
(
|
| 822 |
+
(
|
| 823 |
+
[(0, 1), (0, 2), (3, 2)],
|
| 824 |
+
{(0, 2, 3)},
|
| 825 |
+
),
|
| 826 |
+
(
|
| 827 |
+
[("A", "B"), ("C", "B"), ("D", "G"), ("D", "E"), ("G", "E")],
|
| 828 |
+
{("A", "B", "C"), ("D", "E", "G")},
|
| 829 |
+
),
|
| 830 |
+
),
|
| 831 |
+
)
|
| 832 |
+
def test_colliders(edgelist, expected):
|
| 833 |
+
G = nx.DiGraph(edgelist)
|
| 834 |
+
colliders = set(nx.dag.colliders(G))
|
| 835 |
+
assert colliders == expected
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_distance_measures.py
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from random import Random
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from networkx import convert_node_labels_to_integers as cnlti
|
| 8 |
+
from networkx.algorithms.distance_measures import _extrema_bounding
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test__extrema_bounding_invalid_compute_kwarg():
|
| 12 |
+
G = nx.path_graph(3)
|
| 13 |
+
with pytest.raises(ValueError, match="compute must be one of"):
|
| 14 |
+
_extrema_bounding(G, compute="spam")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class TestDistance:
|
| 18 |
+
def setup_method(self):
|
| 19 |
+
G = cnlti(nx.grid_2d_graph(4, 4), first_label=1, ordering="sorted")
|
| 20 |
+
self.G = G
|
| 21 |
+
|
| 22 |
+
def test_eccentricity(self):
|
| 23 |
+
assert nx.eccentricity(self.G, 1) == 6
|
| 24 |
+
e = nx.eccentricity(self.G)
|
| 25 |
+
assert e[1] == 6
|
| 26 |
+
|
| 27 |
+
sp = dict(nx.shortest_path_length(self.G))
|
| 28 |
+
e = nx.eccentricity(self.G, sp=sp)
|
| 29 |
+
assert e[1] == 6
|
| 30 |
+
|
| 31 |
+
e = nx.eccentricity(self.G, v=1)
|
| 32 |
+
assert e == 6
|
| 33 |
+
|
| 34 |
+
# This behavior changed in version 1.8 (ticket #739)
|
| 35 |
+
e = nx.eccentricity(self.G, v=[1, 1])
|
| 36 |
+
assert e[1] == 6
|
| 37 |
+
e = nx.eccentricity(self.G, v=[1, 2])
|
| 38 |
+
assert e[1] == 6
|
| 39 |
+
|
| 40 |
+
# test against graph with one node
|
| 41 |
+
G = nx.path_graph(1)
|
| 42 |
+
e = nx.eccentricity(G)
|
| 43 |
+
assert e[0] == 0
|
| 44 |
+
e = nx.eccentricity(G, v=0)
|
| 45 |
+
assert e == 0
|
| 46 |
+
pytest.raises(nx.NetworkXError, nx.eccentricity, G, 1)
|
| 47 |
+
|
| 48 |
+
# test against empty graph
|
| 49 |
+
G = nx.empty_graph()
|
| 50 |
+
e = nx.eccentricity(G)
|
| 51 |
+
assert e == {}
|
| 52 |
+
|
| 53 |
+
def test_diameter(self):
|
| 54 |
+
assert nx.diameter(self.G) == 6
|
| 55 |
+
|
| 56 |
+
def test_harmonic_diameter(self):
|
| 57 |
+
assert abs(nx.harmonic_diameter(self.G) - 2.0477815699658715) < 1e-12
|
| 58 |
+
|
| 59 |
+
def test_harmonic_diameter_empty(self):
|
| 60 |
+
assert math.isnan(nx.harmonic_diameter(nx.empty_graph()))
|
| 61 |
+
|
| 62 |
+
def test_harmonic_diameter_single_node(self):
|
| 63 |
+
assert math.isnan(nx.harmonic_diameter(nx.empty_graph(1)))
|
| 64 |
+
|
| 65 |
+
def test_harmonic_diameter_discrete(self):
|
| 66 |
+
assert math.isinf(nx.harmonic_diameter(nx.empty_graph(3)))
|
| 67 |
+
|
| 68 |
+
def test_harmonic_diameter_not_strongly_connected(self):
|
| 69 |
+
DG = nx.DiGraph()
|
| 70 |
+
DG.add_edge(0, 1)
|
| 71 |
+
assert nx.harmonic_diameter(DG) == 2
|
| 72 |
+
|
| 73 |
+
def test_radius(self):
|
| 74 |
+
assert nx.radius(self.G) == 4
|
| 75 |
+
|
| 76 |
+
def test_periphery(self):
|
| 77 |
+
assert set(nx.periphery(self.G)) == {1, 4, 13, 16}
|
| 78 |
+
|
| 79 |
+
def test_center(self):
|
| 80 |
+
assert set(nx.center(self.G)) == {6, 7, 10, 11}
|
| 81 |
+
|
| 82 |
+
def test_bound_diameter(self):
|
| 83 |
+
assert nx.diameter(self.G, usebounds=True) == 6
|
| 84 |
+
|
| 85 |
+
def test_bound_radius(self):
|
| 86 |
+
assert nx.radius(self.G, usebounds=True) == 4
|
| 87 |
+
|
| 88 |
+
def test_bound_periphery(self):
|
| 89 |
+
result = {1, 4, 13, 16}
|
| 90 |
+
assert set(nx.periphery(self.G, usebounds=True)) == result
|
| 91 |
+
|
| 92 |
+
def test_bound_center(self):
|
| 93 |
+
result = {6, 7, 10, 11}
|
| 94 |
+
assert set(nx.center(self.G, usebounds=True)) == result
|
| 95 |
+
|
| 96 |
+
def test_radius_exception(self):
|
| 97 |
+
G = nx.Graph()
|
| 98 |
+
G.add_edge(1, 2)
|
| 99 |
+
G.add_edge(3, 4)
|
| 100 |
+
pytest.raises(nx.NetworkXError, nx.diameter, G)
|
| 101 |
+
|
| 102 |
+
def test_eccentricity_infinite(self):
|
| 103 |
+
with pytest.raises(nx.NetworkXError):
|
| 104 |
+
G = nx.Graph([(1, 2), (3, 4)])
|
| 105 |
+
e = nx.eccentricity(G)
|
| 106 |
+
|
| 107 |
+
def test_eccentricity_undirected_not_connected(self):
|
| 108 |
+
with pytest.raises(nx.NetworkXError):
|
| 109 |
+
G = nx.Graph([(1, 2), (3, 4)])
|
| 110 |
+
e = nx.eccentricity(G, sp=1)
|
| 111 |
+
|
| 112 |
+
def test_eccentricity_directed_weakly_connected(self):
|
| 113 |
+
with pytest.raises(nx.NetworkXError):
|
| 114 |
+
DG = nx.DiGraph([(1, 2), (1, 3)])
|
| 115 |
+
nx.eccentricity(DG)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class TestWeightedDistance:
|
| 119 |
+
def setup_method(self):
|
| 120 |
+
G = nx.Graph()
|
| 121 |
+
G.add_edge(0, 1, weight=0.6, cost=0.6, high_cost=6)
|
| 122 |
+
G.add_edge(0, 2, weight=0.2, cost=0.2, high_cost=2)
|
| 123 |
+
G.add_edge(2, 3, weight=0.1, cost=0.1, high_cost=1)
|
| 124 |
+
G.add_edge(2, 4, weight=0.7, cost=0.7, high_cost=7)
|
| 125 |
+
G.add_edge(2, 5, weight=0.9, cost=0.9, high_cost=9)
|
| 126 |
+
G.add_edge(1, 5, weight=0.3, cost=0.3, high_cost=3)
|
| 127 |
+
self.G = G
|
| 128 |
+
self.weight_fn = lambda v, u, e: 2
|
| 129 |
+
|
| 130 |
+
def test_eccentricity_weight_None(self):
|
| 131 |
+
assert nx.eccentricity(self.G, 1, weight=None) == 3
|
| 132 |
+
e = nx.eccentricity(self.G, weight=None)
|
| 133 |
+
assert e[1] == 3
|
| 134 |
+
|
| 135 |
+
e = nx.eccentricity(self.G, v=1, weight=None)
|
| 136 |
+
assert e == 3
|
| 137 |
+
|
| 138 |
+
# This behavior changed in version 1.8 (ticket #739)
|
| 139 |
+
e = nx.eccentricity(self.G, v=[1, 1], weight=None)
|
| 140 |
+
assert e[1] == 3
|
| 141 |
+
e = nx.eccentricity(self.G, v=[1, 2], weight=None)
|
| 142 |
+
assert e[1] == 3
|
| 143 |
+
|
| 144 |
+
def test_eccentricity_weight_attr(self):
|
| 145 |
+
assert nx.eccentricity(self.G, 1, weight="weight") == 1.5
|
| 146 |
+
e = nx.eccentricity(self.G, weight="weight")
|
| 147 |
+
assert (
|
| 148 |
+
e
|
| 149 |
+
== nx.eccentricity(self.G, weight="cost")
|
| 150 |
+
!= nx.eccentricity(self.G, weight="high_cost")
|
| 151 |
+
)
|
| 152 |
+
assert e[1] == 1.5
|
| 153 |
+
|
| 154 |
+
e = nx.eccentricity(self.G, v=1, weight="weight")
|
| 155 |
+
assert e == 1.5
|
| 156 |
+
|
| 157 |
+
# This behavior changed in version 1.8 (ticket #739)
|
| 158 |
+
e = nx.eccentricity(self.G, v=[1, 1], weight="weight")
|
| 159 |
+
assert e[1] == 1.5
|
| 160 |
+
e = nx.eccentricity(self.G, v=[1, 2], weight="weight")
|
| 161 |
+
assert e[1] == 1.5
|
| 162 |
+
|
| 163 |
+
def test_eccentricity_weight_fn(self):
|
| 164 |
+
assert nx.eccentricity(self.G, 1, weight=self.weight_fn) == 6
|
| 165 |
+
e = nx.eccentricity(self.G, weight=self.weight_fn)
|
| 166 |
+
assert e[1] == 6
|
| 167 |
+
|
| 168 |
+
e = nx.eccentricity(self.G, v=1, weight=self.weight_fn)
|
| 169 |
+
assert e == 6
|
| 170 |
+
|
| 171 |
+
# This behavior changed in version 1.8 (ticket #739)
|
| 172 |
+
e = nx.eccentricity(self.G, v=[1, 1], weight=self.weight_fn)
|
| 173 |
+
assert e[1] == 6
|
| 174 |
+
e = nx.eccentricity(self.G, v=[1, 2], weight=self.weight_fn)
|
| 175 |
+
assert e[1] == 6
|
| 176 |
+
|
| 177 |
+
def test_diameter_weight_None(self):
|
| 178 |
+
assert nx.diameter(self.G, weight=None) == 3
|
| 179 |
+
|
| 180 |
+
def test_diameter_weight_attr(self):
|
| 181 |
+
assert (
|
| 182 |
+
nx.diameter(self.G, weight="weight")
|
| 183 |
+
== nx.diameter(self.G, weight="cost")
|
| 184 |
+
== 1.6
|
| 185 |
+
!= nx.diameter(self.G, weight="high_cost")
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
def test_diameter_weight_fn(self):
|
| 189 |
+
assert nx.diameter(self.G, weight=self.weight_fn) == 6
|
| 190 |
+
|
| 191 |
+
def test_radius_weight_None(self):
|
| 192 |
+
assert pytest.approx(nx.radius(self.G, weight=None)) == 2
|
| 193 |
+
|
| 194 |
+
def test_radius_weight_attr(self):
|
| 195 |
+
assert (
|
| 196 |
+
pytest.approx(nx.radius(self.G, weight="weight"))
|
| 197 |
+
== pytest.approx(nx.radius(self.G, weight="cost"))
|
| 198 |
+
== 0.9
|
| 199 |
+
!= nx.radius(self.G, weight="high_cost")
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
def test_radius_weight_fn(self):
|
| 203 |
+
assert nx.radius(self.G, weight=self.weight_fn) == 4
|
| 204 |
+
|
| 205 |
+
def test_periphery_weight_None(self):
|
| 206 |
+
for v in set(nx.periphery(self.G, weight=None)):
|
| 207 |
+
assert nx.eccentricity(self.G, v, weight=None) == nx.diameter(
|
| 208 |
+
self.G, weight=None
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
def test_periphery_weight_attr(self):
|
| 212 |
+
periphery = set(nx.periphery(self.G, weight="weight"))
|
| 213 |
+
assert (
|
| 214 |
+
periphery
|
| 215 |
+
== set(nx.periphery(self.G, weight="cost"))
|
| 216 |
+
== set(nx.periphery(self.G, weight="high_cost"))
|
| 217 |
+
)
|
| 218 |
+
for v in periphery:
|
| 219 |
+
assert (
|
| 220 |
+
nx.eccentricity(self.G, v, weight="high_cost")
|
| 221 |
+
!= nx.eccentricity(self.G, v, weight="weight")
|
| 222 |
+
== nx.eccentricity(self.G, v, weight="cost")
|
| 223 |
+
== nx.diameter(self.G, weight="weight")
|
| 224 |
+
== nx.diameter(self.G, weight="cost")
|
| 225 |
+
!= nx.diameter(self.G, weight="high_cost")
|
| 226 |
+
)
|
| 227 |
+
assert nx.eccentricity(self.G, v, weight="high_cost") == nx.diameter(
|
| 228 |
+
self.G, weight="high_cost"
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def test_periphery_weight_fn(self):
|
| 232 |
+
for v in set(nx.periphery(self.G, weight=self.weight_fn)):
|
| 233 |
+
assert nx.eccentricity(self.G, v, weight=self.weight_fn) == nx.diameter(
|
| 234 |
+
self.G, weight=self.weight_fn
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
def test_center_weight_None(self):
|
| 238 |
+
for v in set(nx.center(self.G, weight=None)):
|
| 239 |
+
assert pytest.approx(nx.eccentricity(self.G, v, weight=None)) == nx.radius(
|
| 240 |
+
self.G, weight=None
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
def test_center_weight_attr(self):
|
| 244 |
+
center = set(nx.center(self.G, weight="weight"))
|
| 245 |
+
assert (
|
| 246 |
+
center
|
| 247 |
+
== set(nx.center(self.G, weight="cost"))
|
| 248 |
+
!= set(nx.center(self.G, weight="high_cost"))
|
| 249 |
+
)
|
| 250 |
+
for v in center:
|
| 251 |
+
assert (
|
| 252 |
+
nx.eccentricity(self.G, v, weight="high_cost")
|
| 253 |
+
!= pytest.approx(nx.eccentricity(self.G, v, weight="weight"))
|
| 254 |
+
== pytest.approx(nx.eccentricity(self.G, v, weight="cost"))
|
| 255 |
+
== nx.radius(self.G, weight="weight")
|
| 256 |
+
== nx.radius(self.G, weight="cost")
|
| 257 |
+
!= nx.radius(self.G, weight="high_cost")
|
| 258 |
+
)
|
| 259 |
+
assert nx.eccentricity(self.G, v, weight="high_cost") == nx.radius(
|
| 260 |
+
self.G, weight="high_cost"
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
def test_center_weight_fn(self):
|
| 264 |
+
for v in set(nx.center(self.G, weight=self.weight_fn)):
|
| 265 |
+
assert nx.eccentricity(self.G, v, weight=self.weight_fn) == nx.radius(
|
| 266 |
+
self.G, weight=self.weight_fn
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
def test_bound_diameter_weight_None(self):
|
| 270 |
+
assert nx.diameter(self.G, usebounds=True, weight=None) == 3
|
| 271 |
+
|
| 272 |
+
def test_bound_diameter_weight_attr(self):
|
| 273 |
+
assert (
|
| 274 |
+
nx.diameter(self.G, usebounds=True, weight="high_cost")
|
| 275 |
+
!= nx.diameter(self.G, usebounds=True, weight="weight")
|
| 276 |
+
== nx.diameter(self.G, usebounds=True, weight="cost")
|
| 277 |
+
== 1.6
|
| 278 |
+
!= nx.diameter(self.G, usebounds=True, weight="high_cost")
|
| 279 |
+
)
|
| 280 |
+
assert nx.diameter(self.G, usebounds=True, weight="high_cost") == nx.diameter(
|
| 281 |
+
self.G, usebounds=True, weight="high_cost"
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
def test_bound_diameter_weight_fn(self):
|
| 285 |
+
assert nx.diameter(self.G, usebounds=True, weight=self.weight_fn) == 6
|
| 286 |
+
|
| 287 |
+
def test_bound_radius_weight_None(self):
|
| 288 |
+
assert pytest.approx(nx.radius(self.G, usebounds=True, weight=None)) == 2
|
| 289 |
+
|
| 290 |
+
def test_bound_radius_weight_attr(self):
|
| 291 |
+
assert (
|
| 292 |
+
nx.radius(self.G, usebounds=True, weight="high_cost")
|
| 293 |
+
!= pytest.approx(nx.radius(self.G, usebounds=True, weight="weight"))
|
| 294 |
+
== pytest.approx(nx.radius(self.G, usebounds=True, weight="cost"))
|
| 295 |
+
== 0.9
|
| 296 |
+
!= nx.radius(self.G, usebounds=True, weight="high_cost")
|
| 297 |
+
)
|
| 298 |
+
assert nx.radius(self.G, usebounds=True, weight="high_cost") == nx.radius(
|
| 299 |
+
self.G, usebounds=True, weight="high_cost"
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
def test_bound_radius_weight_fn(self):
|
| 303 |
+
assert nx.radius(self.G, usebounds=True, weight=self.weight_fn) == 4
|
| 304 |
+
|
| 305 |
+
def test_bound_periphery_weight_None(self):
|
| 306 |
+
result = {1, 3, 4}
|
| 307 |
+
assert set(nx.periphery(self.G, usebounds=True, weight=None)) == result
|
| 308 |
+
|
| 309 |
+
def test_bound_periphery_weight_attr(self):
|
| 310 |
+
result = {4, 5}
|
| 311 |
+
assert (
|
| 312 |
+
set(nx.periphery(self.G, usebounds=True, weight="weight"))
|
| 313 |
+
== set(nx.periphery(self.G, usebounds=True, weight="cost"))
|
| 314 |
+
== result
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
def test_bound_periphery_weight_fn(self):
|
| 318 |
+
result = {1, 3, 4}
|
| 319 |
+
assert (
|
| 320 |
+
set(nx.periphery(self.G, usebounds=True, weight=self.weight_fn)) == result
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
def test_bound_center_weight_None(self):
|
| 324 |
+
result = {0, 2, 5}
|
| 325 |
+
assert set(nx.center(self.G, usebounds=True, weight=None)) == result
|
| 326 |
+
|
| 327 |
+
def test_bound_center_weight_attr(self):
|
| 328 |
+
result = {0}
|
| 329 |
+
assert (
|
| 330 |
+
set(nx.center(self.G, usebounds=True, weight="weight"))
|
| 331 |
+
== set(nx.center(self.G, usebounds=True, weight="cost"))
|
| 332 |
+
== result
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
def test_bound_center_weight_fn(self):
|
| 336 |
+
result = {0, 2, 5}
|
| 337 |
+
assert set(nx.center(self.G, usebounds=True, weight=self.weight_fn)) == result
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class TestResistanceDistance:
|
| 341 |
+
@classmethod
|
| 342 |
+
def setup_class(cls):
|
| 343 |
+
global np
|
| 344 |
+
np = pytest.importorskip("numpy")
|
| 345 |
+
sp = pytest.importorskip("scipy")
|
| 346 |
+
|
| 347 |
+
def setup_method(self):
|
| 348 |
+
G = nx.Graph()
|
| 349 |
+
G.add_edge(1, 2, weight=2)
|
| 350 |
+
G.add_edge(2, 3, weight=4)
|
| 351 |
+
G.add_edge(3, 4, weight=1)
|
| 352 |
+
G.add_edge(1, 4, weight=3)
|
| 353 |
+
self.G = G
|
| 354 |
+
|
| 355 |
+
def test_resistance_distance_directed_graph(self):
|
| 356 |
+
G = nx.DiGraph()
|
| 357 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 358 |
+
nx.resistance_distance(G)
|
| 359 |
+
|
| 360 |
+
def test_resistance_distance_empty(self):
|
| 361 |
+
G = nx.Graph()
|
| 362 |
+
with pytest.raises(nx.NetworkXError):
|
| 363 |
+
nx.resistance_distance(G)
|
| 364 |
+
|
| 365 |
+
def test_resistance_distance_not_connected(self):
|
| 366 |
+
with pytest.raises(nx.NetworkXError):
|
| 367 |
+
self.G.add_node(5)
|
| 368 |
+
nx.resistance_distance(self.G, 1, 5)
|
| 369 |
+
|
| 370 |
+
def test_resistance_distance_nodeA_not_in_graph(self):
|
| 371 |
+
with pytest.raises(nx.NetworkXError):
|
| 372 |
+
nx.resistance_distance(self.G, 9, 1)
|
| 373 |
+
|
| 374 |
+
def test_resistance_distance_nodeB_not_in_graph(self):
|
| 375 |
+
with pytest.raises(nx.NetworkXError):
|
| 376 |
+
nx.resistance_distance(self.G, 1, 9)
|
| 377 |
+
|
| 378 |
+
def test_resistance_distance(self):
|
| 379 |
+
rd = nx.resistance_distance(self.G, 1, 3, "weight", True)
|
| 380 |
+
test_data = 1 / (1 / (2 + 4) + 1 / (1 + 3))
|
| 381 |
+
assert round(rd, 5) == round(test_data, 5)
|
| 382 |
+
|
| 383 |
+
def test_resistance_distance_noinv(self):
|
| 384 |
+
rd = nx.resistance_distance(self.G, 1, 3, "weight", False)
|
| 385 |
+
test_data = 1 / (1 / (1 / 2 + 1 / 4) + 1 / (1 / 1 + 1 / 3))
|
| 386 |
+
assert round(rd, 5) == round(test_data, 5)
|
| 387 |
+
|
| 388 |
+
def test_resistance_distance_no_weight(self):
|
| 389 |
+
rd = nx.resistance_distance(self.G, 1, 3)
|
| 390 |
+
assert round(rd, 5) == 1
|
| 391 |
+
|
| 392 |
+
def test_resistance_distance_neg_weight(self):
|
| 393 |
+
self.G[2][3]["weight"] = -4
|
| 394 |
+
rd = nx.resistance_distance(self.G, 1, 3, "weight", True)
|
| 395 |
+
test_data = 1 / (1 / (2 + -4) + 1 / (1 + 3))
|
| 396 |
+
assert round(rd, 5) == round(test_data, 5)
|
| 397 |
+
|
| 398 |
+
def test_multigraph(self):
|
| 399 |
+
G = nx.MultiGraph()
|
| 400 |
+
G.add_edge(1, 2, weight=2)
|
| 401 |
+
G.add_edge(2, 3, weight=4)
|
| 402 |
+
G.add_edge(3, 4, weight=1)
|
| 403 |
+
G.add_edge(1, 4, weight=3)
|
| 404 |
+
rd = nx.resistance_distance(G, 1, 3, "weight", True)
|
| 405 |
+
assert np.isclose(rd, 1 / (1 / (2 + 4) + 1 / (1 + 3)))
|
| 406 |
+
|
| 407 |
+
def test_resistance_distance_div0(self):
|
| 408 |
+
with pytest.raises(ZeroDivisionError):
|
| 409 |
+
self.G[1][2]["weight"] = 0
|
| 410 |
+
nx.resistance_distance(self.G, 1, 3, "weight")
|
| 411 |
+
|
| 412 |
+
def test_resistance_distance_same_node(self):
|
| 413 |
+
assert nx.resistance_distance(self.G, 1, 1) == 0
|
| 414 |
+
|
| 415 |
+
def test_resistance_distance_only_nodeA(self):
|
| 416 |
+
rd = nx.resistance_distance(self.G, nodeA=1)
|
| 417 |
+
test_data = {}
|
| 418 |
+
test_data[1] = 0
|
| 419 |
+
test_data[2] = 0.75
|
| 420 |
+
test_data[3] = 1
|
| 421 |
+
test_data[4] = 0.75
|
| 422 |
+
assert type(rd) == dict
|
| 423 |
+
assert sorted(rd.keys()) == sorted(test_data.keys())
|
| 424 |
+
for key in rd:
|
| 425 |
+
assert np.isclose(rd[key], test_data[key])
|
| 426 |
+
|
| 427 |
+
def test_resistance_distance_only_nodeB(self):
|
| 428 |
+
rd = nx.resistance_distance(self.G, nodeB=1)
|
| 429 |
+
test_data = {}
|
| 430 |
+
test_data[1] = 0
|
| 431 |
+
test_data[2] = 0.75
|
| 432 |
+
test_data[3] = 1
|
| 433 |
+
test_data[4] = 0.75
|
| 434 |
+
assert type(rd) == dict
|
| 435 |
+
assert sorted(rd.keys()) == sorted(test_data.keys())
|
| 436 |
+
for key in rd:
|
| 437 |
+
assert np.isclose(rd[key], test_data[key])
|
| 438 |
+
|
| 439 |
+
def test_resistance_distance_all(self):
|
| 440 |
+
rd = nx.resistance_distance(self.G)
|
| 441 |
+
assert type(rd) == dict
|
| 442 |
+
assert round(rd[1][3], 5) == 1
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
class TestEffectiveGraphResistance:
|
| 446 |
+
@classmethod
|
| 447 |
+
def setup_class(cls):
|
| 448 |
+
global np
|
| 449 |
+
np = pytest.importorskip("numpy")
|
| 450 |
+
sp = pytest.importorskip("scipy")
|
| 451 |
+
|
| 452 |
+
def setup_method(self):
|
| 453 |
+
G = nx.Graph()
|
| 454 |
+
G.add_edge(1, 2, weight=2)
|
| 455 |
+
G.add_edge(1, 3, weight=1)
|
| 456 |
+
G.add_edge(2, 3, weight=4)
|
| 457 |
+
self.G = G
|
| 458 |
+
|
| 459 |
+
def test_effective_graph_resistance_directed_graph(self):
|
| 460 |
+
G = nx.DiGraph()
|
| 461 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 462 |
+
nx.effective_graph_resistance(G)
|
| 463 |
+
|
| 464 |
+
def test_effective_graph_resistance_empty(self):
|
| 465 |
+
G = nx.Graph()
|
| 466 |
+
with pytest.raises(nx.NetworkXError):
|
| 467 |
+
nx.effective_graph_resistance(G)
|
| 468 |
+
|
| 469 |
+
def test_effective_graph_resistance_not_connected(self):
|
| 470 |
+
G = nx.Graph([(1, 2), (3, 4)])
|
| 471 |
+
RG = nx.effective_graph_resistance(G)
|
| 472 |
+
assert np.isinf(RG)
|
| 473 |
+
|
| 474 |
+
def test_effective_graph_resistance(self):
|
| 475 |
+
RG = nx.effective_graph_resistance(self.G, "weight", True)
|
| 476 |
+
rd12 = 1 / (1 / (1 + 4) + 1 / 2)
|
| 477 |
+
rd13 = 1 / (1 / (1 + 2) + 1 / 4)
|
| 478 |
+
rd23 = 1 / (1 / (2 + 4) + 1 / 1)
|
| 479 |
+
assert np.isclose(RG, rd12 + rd13 + rd23)
|
| 480 |
+
|
| 481 |
+
def test_effective_graph_resistance_noinv(self):
|
| 482 |
+
RG = nx.effective_graph_resistance(self.G, "weight", False)
|
| 483 |
+
rd12 = 1 / (1 / (1 / 1 + 1 / 4) + 1 / (1 / 2))
|
| 484 |
+
rd13 = 1 / (1 / (1 / 1 + 1 / 2) + 1 / (1 / 4))
|
| 485 |
+
rd23 = 1 / (1 / (1 / 2 + 1 / 4) + 1 / (1 / 1))
|
| 486 |
+
assert np.isclose(RG, rd12 + rd13 + rd23)
|
| 487 |
+
|
| 488 |
+
def test_effective_graph_resistance_no_weight(self):
|
| 489 |
+
RG = nx.effective_graph_resistance(self.G)
|
| 490 |
+
assert np.isclose(RG, 2)
|
| 491 |
+
|
| 492 |
+
def test_effective_graph_resistance_neg_weight(self):
|
| 493 |
+
self.G[2][3]["weight"] = -4
|
| 494 |
+
RG = nx.effective_graph_resistance(self.G, "weight", True)
|
| 495 |
+
rd12 = 1 / (1 / (1 + -4) + 1 / 2)
|
| 496 |
+
rd13 = 1 / (1 / (1 + 2) + 1 / (-4))
|
| 497 |
+
rd23 = 1 / (1 / (2 + -4) + 1 / 1)
|
| 498 |
+
assert np.isclose(RG, rd12 + rd13 + rd23)
|
| 499 |
+
|
| 500 |
+
def test_effective_graph_resistance_multigraph(self):
|
| 501 |
+
G = nx.MultiGraph()
|
| 502 |
+
G.add_edge(1, 2, weight=2)
|
| 503 |
+
G.add_edge(1, 3, weight=1)
|
| 504 |
+
G.add_edge(2, 3, weight=1)
|
| 505 |
+
G.add_edge(2, 3, weight=3)
|
| 506 |
+
RG = nx.effective_graph_resistance(G, "weight", True)
|
| 507 |
+
edge23 = 1 / (1 / 1 + 1 / 3)
|
| 508 |
+
rd12 = 1 / (1 / (1 + edge23) + 1 / 2)
|
| 509 |
+
rd13 = 1 / (1 / (1 + 2) + 1 / edge23)
|
| 510 |
+
rd23 = 1 / (1 / (2 + edge23) + 1 / 1)
|
| 511 |
+
assert np.isclose(RG, rd12 + rd13 + rd23)
|
| 512 |
+
|
| 513 |
+
def test_effective_graph_resistance_div0(self):
|
| 514 |
+
with pytest.raises(ZeroDivisionError):
|
| 515 |
+
self.G[1][2]["weight"] = 0
|
| 516 |
+
nx.effective_graph_resistance(self.G, "weight")
|
| 517 |
+
|
| 518 |
+
def test_effective_graph_resistance_complete_graph(self):
|
| 519 |
+
N = 10
|
| 520 |
+
G = nx.complete_graph(N)
|
| 521 |
+
RG = nx.effective_graph_resistance(G)
|
| 522 |
+
assert np.isclose(RG, N - 1)
|
| 523 |
+
|
| 524 |
+
def test_effective_graph_resistance_path_graph(self):
|
| 525 |
+
N = 10
|
| 526 |
+
G = nx.path_graph(N)
|
| 527 |
+
RG = nx.effective_graph_resistance(G)
|
| 528 |
+
assert np.isclose(RG, (N - 1) * N * (N + 1) // 6)
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
class TestBarycenter:
|
| 532 |
+
"""Test :func:`networkx.algorithms.distance_measures.barycenter`."""
|
| 533 |
+
|
| 534 |
+
def barycenter_as_subgraph(self, g, **kwargs):
|
| 535 |
+
"""Return the subgraph induced on the barycenter of g"""
|
| 536 |
+
b = nx.barycenter(g, **kwargs)
|
| 537 |
+
assert isinstance(b, list)
|
| 538 |
+
assert set(b) <= set(g)
|
| 539 |
+
return g.subgraph(b)
|
| 540 |
+
|
| 541 |
+
def test_must_be_connected(self):
|
| 542 |
+
pytest.raises(nx.NetworkXNoPath, nx.barycenter, nx.empty_graph(5))
|
| 543 |
+
|
| 544 |
+
def test_sp_kwarg(self):
|
| 545 |
+
# Complete graph K_5. Normally it works...
|
| 546 |
+
K_5 = nx.complete_graph(5)
|
| 547 |
+
sp = dict(nx.shortest_path_length(K_5))
|
| 548 |
+
assert nx.barycenter(K_5, sp=sp) == list(K_5)
|
| 549 |
+
|
| 550 |
+
# ...but not with the weight argument
|
| 551 |
+
for u, v, data in K_5.edges.data():
|
| 552 |
+
data["weight"] = 1
|
| 553 |
+
pytest.raises(ValueError, nx.barycenter, K_5, sp=sp, weight="weight")
|
| 554 |
+
|
| 555 |
+
# ...and a corrupted sp can make it seem like K_5 is disconnected
|
| 556 |
+
del sp[0][1]
|
| 557 |
+
pytest.raises(nx.NetworkXNoPath, nx.barycenter, K_5, sp=sp)
|
| 558 |
+
|
| 559 |
+
def test_trees(self):
|
| 560 |
+
"""The barycenter of a tree is a single vertex or an edge.
|
| 561 |
+
|
| 562 |
+
See [West01]_, p. 78.
|
| 563 |
+
"""
|
| 564 |
+
prng = Random(0xDEADBEEF)
|
| 565 |
+
for i in range(50):
|
| 566 |
+
RT = nx.random_labeled_tree(prng.randint(1, 75), seed=prng)
|
| 567 |
+
b = self.barycenter_as_subgraph(RT)
|
| 568 |
+
if len(b) == 2:
|
| 569 |
+
assert b.size() == 1
|
| 570 |
+
else:
|
| 571 |
+
assert len(b) == 1
|
| 572 |
+
assert b.size() == 0
|
| 573 |
+
|
| 574 |
+
def test_this_one_specific_tree(self):
|
| 575 |
+
"""Test the tree pictured at the bottom of [West01]_, p. 78."""
|
| 576 |
+
g = nx.Graph(
|
| 577 |
+
{
|
| 578 |
+
"a": ["b"],
|
| 579 |
+
"b": ["a", "x"],
|
| 580 |
+
"x": ["b", "y"],
|
| 581 |
+
"y": ["x", "z"],
|
| 582 |
+
"z": ["y", 0, 1, 2, 3, 4],
|
| 583 |
+
0: ["z"],
|
| 584 |
+
1: ["z"],
|
| 585 |
+
2: ["z"],
|
| 586 |
+
3: ["z"],
|
| 587 |
+
4: ["z"],
|
| 588 |
+
}
|
| 589 |
+
)
|
| 590 |
+
b = self.barycenter_as_subgraph(g, attr="barycentricity")
|
| 591 |
+
assert list(b) == ["z"]
|
| 592 |
+
assert not b.edges
|
| 593 |
+
expected_barycentricity = {
|
| 594 |
+
0: 23,
|
| 595 |
+
1: 23,
|
| 596 |
+
2: 23,
|
| 597 |
+
3: 23,
|
| 598 |
+
4: 23,
|
| 599 |
+
"a": 35,
|
| 600 |
+
"b": 27,
|
| 601 |
+
"x": 21,
|
| 602 |
+
"y": 17,
|
| 603 |
+
"z": 15,
|
| 604 |
+
}
|
| 605 |
+
for node, barycentricity in expected_barycentricity.items():
|
| 606 |
+
assert g.nodes[node]["barycentricity"] == barycentricity
|
| 607 |
+
|
| 608 |
+
# Doubling weights should do nothing but double the barycentricities
|
| 609 |
+
for edge in g.edges:
|
| 610 |
+
g.edges[edge]["weight"] = 2
|
| 611 |
+
b = self.barycenter_as_subgraph(g, weight="weight", attr="barycentricity2")
|
| 612 |
+
assert list(b) == ["z"]
|
| 613 |
+
assert not b.edges
|
| 614 |
+
for node, barycentricity in expected_barycentricity.items():
|
| 615 |
+
assert g.nodes[node]["barycentricity2"] == barycentricity * 2
|
| 616 |
+
|
| 617 |
+
|
| 618 |
+
class TestKemenyConstant:
|
| 619 |
+
@classmethod
|
| 620 |
+
def setup_class(cls):
|
| 621 |
+
global np
|
| 622 |
+
np = pytest.importorskip("numpy")
|
| 623 |
+
sp = pytest.importorskip("scipy")
|
| 624 |
+
|
| 625 |
+
def setup_method(self):
|
| 626 |
+
G = nx.Graph()
|
| 627 |
+
w12 = 2
|
| 628 |
+
w13 = 3
|
| 629 |
+
w23 = 4
|
| 630 |
+
G.add_edge(1, 2, weight=w12)
|
| 631 |
+
G.add_edge(1, 3, weight=w13)
|
| 632 |
+
G.add_edge(2, 3, weight=w23)
|
| 633 |
+
self.G = G
|
| 634 |
+
|
| 635 |
+
def test_kemeny_constant_directed(self):
|
| 636 |
+
G = nx.DiGraph()
|
| 637 |
+
G.add_edge(1, 2)
|
| 638 |
+
G.add_edge(1, 3)
|
| 639 |
+
G.add_edge(2, 3)
|
| 640 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 641 |
+
nx.kemeny_constant(G)
|
| 642 |
+
|
| 643 |
+
def test_kemeny_constant_not_connected(self):
|
| 644 |
+
self.G.add_node(5)
|
| 645 |
+
with pytest.raises(nx.NetworkXError):
|
| 646 |
+
nx.kemeny_constant(self.G)
|
| 647 |
+
|
| 648 |
+
def test_kemeny_constant_no_nodes(self):
|
| 649 |
+
G = nx.Graph()
|
| 650 |
+
with pytest.raises(nx.NetworkXError):
|
| 651 |
+
nx.kemeny_constant(G)
|
| 652 |
+
|
| 653 |
+
def test_kemeny_constant_negative_weight(self):
|
| 654 |
+
G = nx.Graph()
|
| 655 |
+
w12 = 2
|
| 656 |
+
w13 = 3
|
| 657 |
+
w23 = -10
|
| 658 |
+
G.add_edge(1, 2, weight=w12)
|
| 659 |
+
G.add_edge(1, 3, weight=w13)
|
| 660 |
+
G.add_edge(2, 3, weight=w23)
|
| 661 |
+
with pytest.raises(nx.NetworkXError):
|
| 662 |
+
nx.kemeny_constant(G, weight="weight")
|
| 663 |
+
|
| 664 |
+
def test_kemeny_constant(self):
|
| 665 |
+
K = nx.kemeny_constant(self.G, weight="weight")
|
| 666 |
+
w12 = 2
|
| 667 |
+
w13 = 3
|
| 668 |
+
w23 = 4
|
| 669 |
+
test_data = (
|
| 670 |
+
3
|
| 671 |
+
/ 2
|
| 672 |
+
* (w12 + w13)
|
| 673 |
+
* (w12 + w23)
|
| 674 |
+
* (w13 + w23)
|
| 675 |
+
/ (
|
| 676 |
+
w12**2 * (w13 + w23)
|
| 677 |
+
+ w13**2 * (w12 + w23)
|
| 678 |
+
+ w23**2 * (w12 + w13)
|
| 679 |
+
+ 3 * w12 * w13 * w23
|
| 680 |
+
)
|
| 681 |
+
)
|
| 682 |
+
assert np.isclose(K, test_data)
|
| 683 |
+
|
| 684 |
+
def test_kemeny_constant_no_weight(self):
|
| 685 |
+
K = nx.kemeny_constant(self.G)
|
| 686 |
+
assert np.isclose(K, 4 / 3)
|
| 687 |
+
|
| 688 |
+
def test_kemeny_constant_multigraph(self):
|
| 689 |
+
G = nx.MultiGraph()
|
| 690 |
+
w12_1 = 2
|
| 691 |
+
w12_2 = 1
|
| 692 |
+
w13 = 3
|
| 693 |
+
w23 = 4
|
| 694 |
+
G.add_edge(1, 2, weight=w12_1)
|
| 695 |
+
G.add_edge(1, 2, weight=w12_2)
|
| 696 |
+
G.add_edge(1, 3, weight=w13)
|
| 697 |
+
G.add_edge(2, 3, weight=w23)
|
| 698 |
+
K = nx.kemeny_constant(G, weight="weight")
|
| 699 |
+
w12 = w12_1 + w12_2
|
| 700 |
+
test_data = (
|
| 701 |
+
3
|
| 702 |
+
/ 2
|
| 703 |
+
* (w12 + w13)
|
| 704 |
+
* (w12 + w23)
|
| 705 |
+
* (w13 + w23)
|
| 706 |
+
/ (
|
| 707 |
+
w12**2 * (w13 + w23)
|
| 708 |
+
+ w13**2 * (w12 + w23)
|
| 709 |
+
+ w23**2 * (w12 + w13)
|
| 710 |
+
+ 3 * w12 * w13 * w23
|
| 711 |
+
)
|
| 712 |
+
)
|
| 713 |
+
assert np.isclose(K, test_data)
|
| 714 |
+
|
| 715 |
+
def test_kemeny_constant_weight0(self):
|
| 716 |
+
G = nx.Graph()
|
| 717 |
+
w12 = 0
|
| 718 |
+
w13 = 3
|
| 719 |
+
w23 = 4
|
| 720 |
+
G.add_edge(1, 2, weight=w12)
|
| 721 |
+
G.add_edge(1, 3, weight=w13)
|
| 722 |
+
G.add_edge(2, 3, weight=w23)
|
| 723 |
+
K = nx.kemeny_constant(G, weight="weight")
|
| 724 |
+
test_data = (
|
| 725 |
+
3
|
| 726 |
+
/ 2
|
| 727 |
+
* (w12 + w13)
|
| 728 |
+
* (w12 + w23)
|
| 729 |
+
* (w13 + w23)
|
| 730 |
+
/ (
|
| 731 |
+
w12**2 * (w13 + w23)
|
| 732 |
+
+ w13**2 * (w12 + w23)
|
| 733 |
+
+ w23**2 * (w12 + w13)
|
| 734 |
+
+ 3 * w12 * w13 * w23
|
| 735 |
+
)
|
| 736 |
+
)
|
| 737 |
+
assert np.isclose(K, test_data)
|
| 738 |
+
|
| 739 |
+
def test_kemeny_constant_selfloop(self):
|
| 740 |
+
G = nx.Graph()
|
| 741 |
+
w11 = 1
|
| 742 |
+
w12 = 2
|
| 743 |
+
w13 = 3
|
| 744 |
+
w23 = 4
|
| 745 |
+
G.add_edge(1, 1, weight=w11)
|
| 746 |
+
G.add_edge(1, 2, weight=w12)
|
| 747 |
+
G.add_edge(1, 3, weight=w13)
|
| 748 |
+
G.add_edge(2, 3, weight=w23)
|
| 749 |
+
K = nx.kemeny_constant(G, weight="weight")
|
| 750 |
+
test_data = (
|
| 751 |
+
(2 * w11 + 3 * w12 + 3 * w13)
|
| 752 |
+
* (w12 + w23)
|
| 753 |
+
* (w13 + w23)
|
| 754 |
+
/ (
|
| 755 |
+
(w12 * w13 + w12 * w23 + w13 * w23)
|
| 756 |
+
* (w11 + 2 * w12 + 2 * w13 + 2 * w23)
|
| 757 |
+
)
|
| 758 |
+
)
|
| 759 |
+
assert np.isclose(K, test_data)
|
| 760 |
+
|
| 761 |
+
def test_kemeny_constant_complete_bipartite_graph(self):
|
| 762 |
+
# Theorem 1 in https://www.sciencedirect.com/science/article/pii/S0166218X20302912
|
| 763 |
+
n1 = 5
|
| 764 |
+
n2 = 4
|
| 765 |
+
G = nx.complete_bipartite_graph(n1, n2)
|
| 766 |
+
K = nx.kemeny_constant(G)
|
| 767 |
+
assert np.isclose(K, n1 + n2 - 3 / 2)
|
| 768 |
+
|
| 769 |
+
def test_kemeny_constant_path_graph(self):
|
| 770 |
+
# Theorem 2 in https://www.sciencedirect.com/science/article/pii/S0166218X20302912
|
| 771 |
+
n = 10
|
| 772 |
+
G = nx.path_graph(n)
|
| 773 |
+
K = nx.kemeny_constant(G)
|
| 774 |
+
assert np.isclose(K, n**2 / 3 - 2 * n / 3 + 1 / 2)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_distance_regular.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from networkx import is_strongly_regular
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@pytest.mark.parametrize(
|
| 8 |
+
"f", (nx.is_distance_regular, nx.intersection_array, nx.is_strongly_regular)
|
| 9 |
+
)
|
| 10 |
+
@pytest.mark.parametrize("graph_constructor", (nx.DiGraph, nx.MultiGraph))
|
| 11 |
+
def test_raises_on_directed_and_multigraphs(f, graph_constructor):
|
| 12 |
+
G = graph_constructor([(0, 1), (1, 2)])
|
| 13 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 14 |
+
f(G)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class TestDistanceRegular:
|
| 18 |
+
def test_is_distance_regular(self):
|
| 19 |
+
assert nx.is_distance_regular(nx.icosahedral_graph())
|
| 20 |
+
assert nx.is_distance_regular(nx.petersen_graph())
|
| 21 |
+
assert nx.is_distance_regular(nx.cubical_graph())
|
| 22 |
+
assert nx.is_distance_regular(nx.complete_bipartite_graph(3, 3))
|
| 23 |
+
assert nx.is_distance_regular(nx.tetrahedral_graph())
|
| 24 |
+
assert nx.is_distance_regular(nx.dodecahedral_graph())
|
| 25 |
+
assert nx.is_distance_regular(nx.pappus_graph())
|
| 26 |
+
assert nx.is_distance_regular(nx.heawood_graph())
|
| 27 |
+
assert nx.is_distance_regular(nx.cycle_graph(3))
|
| 28 |
+
# no distance regular
|
| 29 |
+
assert not nx.is_distance_regular(nx.path_graph(4))
|
| 30 |
+
|
| 31 |
+
def test_not_connected(self):
|
| 32 |
+
G = nx.cycle_graph(4)
|
| 33 |
+
nx.add_cycle(G, [5, 6, 7])
|
| 34 |
+
assert not nx.is_distance_regular(G)
|
| 35 |
+
|
| 36 |
+
def test_global_parameters(self):
|
| 37 |
+
b, c = nx.intersection_array(nx.cycle_graph(5))
|
| 38 |
+
g = nx.global_parameters(b, c)
|
| 39 |
+
assert list(g) == [(0, 0, 2), (1, 0, 1), (1, 1, 0)]
|
| 40 |
+
b, c = nx.intersection_array(nx.cycle_graph(3))
|
| 41 |
+
g = nx.global_parameters(b, c)
|
| 42 |
+
assert list(g) == [(0, 0, 2), (1, 1, 0)]
|
| 43 |
+
|
| 44 |
+
def test_intersection_array(self):
|
| 45 |
+
b, c = nx.intersection_array(nx.cycle_graph(5))
|
| 46 |
+
assert b == [2, 1]
|
| 47 |
+
assert c == [1, 1]
|
| 48 |
+
b, c = nx.intersection_array(nx.dodecahedral_graph())
|
| 49 |
+
assert b == [3, 2, 1, 1, 1]
|
| 50 |
+
assert c == [1, 1, 1, 2, 3]
|
| 51 |
+
b, c = nx.intersection_array(nx.icosahedral_graph())
|
| 52 |
+
assert b == [5, 2, 1]
|
| 53 |
+
assert c == [1, 2, 5]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@pytest.mark.parametrize("f", (nx.is_distance_regular, nx.is_strongly_regular))
|
| 57 |
+
def test_empty_graph_raises(f):
|
| 58 |
+
G = nx.Graph()
|
| 59 |
+
with pytest.raises(nx.NetworkXPointlessConcept, match="Graph has no nodes"):
|
| 60 |
+
f(G)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class TestStronglyRegular:
|
| 64 |
+
"""Unit tests for the :func:`~networkx.is_strongly_regular`
|
| 65 |
+
function.
|
| 66 |
+
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
def test_cycle_graph(self):
|
| 70 |
+
"""Tests that the cycle graph on five vertices is strongly
|
| 71 |
+
regular.
|
| 72 |
+
|
| 73 |
+
"""
|
| 74 |
+
G = nx.cycle_graph(5)
|
| 75 |
+
assert is_strongly_regular(G)
|
| 76 |
+
|
| 77 |
+
def test_petersen_graph(self):
|
| 78 |
+
"""Tests that the Petersen graph is strongly regular."""
|
| 79 |
+
G = nx.petersen_graph()
|
| 80 |
+
assert is_strongly_regular(G)
|
| 81 |
+
|
| 82 |
+
def test_path_graph(self):
|
| 83 |
+
"""Tests that the path graph is not strongly regular."""
|
| 84 |
+
G = nx.path_graph(4)
|
| 85 |
+
assert not is_strongly_regular(G)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_dominance.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class TestImmediateDominators:
|
| 7 |
+
def test_exceptions(self):
|
| 8 |
+
G = nx.Graph()
|
| 9 |
+
G.add_node(0)
|
| 10 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.immediate_dominators, G, 0)
|
| 11 |
+
G = nx.MultiGraph(G)
|
| 12 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.immediate_dominators, G, 0)
|
| 13 |
+
G = nx.DiGraph([[0, 0]])
|
| 14 |
+
pytest.raises(nx.NetworkXError, nx.immediate_dominators, G, 1)
|
| 15 |
+
|
| 16 |
+
def test_singleton(self):
|
| 17 |
+
G = nx.DiGraph()
|
| 18 |
+
G.add_node(0)
|
| 19 |
+
assert nx.immediate_dominators(G, 0) == {0: 0}
|
| 20 |
+
G.add_edge(0, 0)
|
| 21 |
+
assert nx.immediate_dominators(G, 0) == {0: 0}
|
| 22 |
+
|
| 23 |
+
def test_path(self):
|
| 24 |
+
n = 5
|
| 25 |
+
G = nx.path_graph(n, create_using=nx.DiGraph())
|
| 26 |
+
assert nx.immediate_dominators(G, 0) == {i: max(i - 1, 0) for i in range(n)}
|
| 27 |
+
|
| 28 |
+
def test_cycle(self):
|
| 29 |
+
n = 5
|
| 30 |
+
G = nx.cycle_graph(n, create_using=nx.DiGraph())
|
| 31 |
+
assert nx.immediate_dominators(G, 0) == {i: max(i - 1, 0) for i in range(n)}
|
| 32 |
+
|
| 33 |
+
def test_unreachable(self):
|
| 34 |
+
n = 5
|
| 35 |
+
assert n > 1
|
| 36 |
+
G = nx.path_graph(n, create_using=nx.DiGraph())
|
| 37 |
+
assert nx.immediate_dominators(G, n // 2) == {
|
| 38 |
+
i: max(i - 1, n // 2) for i in range(n // 2, n)
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
def test_irreducible1(self):
|
| 42 |
+
"""
|
| 43 |
+
Graph taken from figure 2 of "A simple, fast dominance algorithm." (2006).
|
| 44 |
+
https://hdl.handle.net/1911/96345
|
| 45 |
+
"""
|
| 46 |
+
edges = [(1, 2), (2, 1), (3, 2), (4, 1), (5, 3), (5, 4)]
|
| 47 |
+
G = nx.DiGraph(edges)
|
| 48 |
+
assert nx.immediate_dominators(G, 5) == {i: 5 for i in range(1, 6)}
|
| 49 |
+
|
| 50 |
+
def test_irreducible2(self):
|
| 51 |
+
"""
|
| 52 |
+
Graph taken from figure 4 of "A simple, fast dominance algorithm." (2006).
|
| 53 |
+
https://hdl.handle.net/1911/96345
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
edges = [(1, 2), (2, 1), (2, 3), (3, 2), (4, 2), (4, 3), (5, 1), (6, 4), (6, 5)]
|
| 57 |
+
G = nx.DiGraph(edges)
|
| 58 |
+
result = nx.immediate_dominators(G, 6)
|
| 59 |
+
assert result == {i: 6 for i in range(1, 7)}
|
| 60 |
+
|
| 61 |
+
def test_domrel_png(self):
|
| 62 |
+
# Graph taken from https://commons.wikipedia.org/wiki/File:Domrel.png
|
| 63 |
+
edges = [(1, 2), (2, 3), (2, 4), (2, 6), (3, 5), (4, 5), (5, 2)]
|
| 64 |
+
G = nx.DiGraph(edges)
|
| 65 |
+
result = nx.immediate_dominators(G, 1)
|
| 66 |
+
assert result == {1: 1, 2: 1, 3: 2, 4: 2, 5: 2, 6: 2}
|
| 67 |
+
# Test postdominance.
|
| 68 |
+
result = nx.immediate_dominators(G.reverse(copy=False), 6)
|
| 69 |
+
assert result == {1: 2, 2: 6, 3: 5, 4: 5, 5: 2, 6: 6}
|
| 70 |
+
|
| 71 |
+
def test_boost_example(self):
|
| 72 |
+
# Graph taken from Figure 1 of
|
| 73 |
+
# http://www.boost.org/doc/libs/1_56_0/libs/graph/doc/lengauer_tarjan_dominator.htm
|
| 74 |
+
edges = [(0, 1), (1, 2), (1, 3), (2, 7), (3, 4), (4, 5), (4, 6), (5, 7), (6, 4)]
|
| 75 |
+
G = nx.DiGraph(edges)
|
| 76 |
+
result = nx.immediate_dominators(G, 0)
|
| 77 |
+
assert result == {0: 0, 1: 0, 2: 1, 3: 1, 4: 3, 5: 4, 6: 4, 7: 1}
|
| 78 |
+
# Test postdominance.
|
| 79 |
+
result = nx.immediate_dominators(G.reverse(copy=False), 7)
|
| 80 |
+
assert result == {0: 1, 1: 7, 2: 7, 3: 4, 4: 5, 5: 7, 6: 4, 7: 7}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class TestDominanceFrontiers:
|
| 84 |
+
def test_exceptions(self):
|
| 85 |
+
G = nx.Graph()
|
| 86 |
+
G.add_node(0)
|
| 87 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.dominance_frontiers, G, 0)
|
| 88 |
+
G = nx.MultiGraph(G)
|
| 89 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.dominance_frontiers, G, 0)
|
| 90 |
+
G = nx.DiGraph([[0, 0]])
|
| 91 |
+
pytest.raises(nx.NetworkXError, nx.dominance_frontiers, G, 1)
|
| 92 |
+
|
| 93 |
+
def test_singleton(self):
|
| 94 |
+
G = nx.DiGraph()
|
| 95 |
+
G.add_node(0)
|
| 96 |
+
assert nx.dominance_frontiers(G, 0) == {0: set()}
|
| 97 |
+
G.add_edge(0, 0)
|
| 98 |
+
assert nx.dominance_frontiers(G, 0) == {0: set()}
|
| 99 |
+
|
| 100 |
+
def test_path(self):
|
| 101 |
+
n = 5
|
| 102 |
+
G = nx.path_graph(n, create_using=nx.DiGraph())
|
| 103 |
+
assert nx.dominance_frontiers(G, 0) == {i: set() for i in range(n)}
|
| 104 |
+
|
| 105 |
+
def test_cycle(self):
|
| 106 |
+
n = 5
|
| 107 |
+
G = nx.cycle_graph(n, create_using=nx.DiGraph())
|
| 108 |
+
assert nx.dominance_frontiers(G, 0) == {i: set() for i in range(n)}
|
| 109 |
+
|
| 110 |
+
def test_unreachable(self):
|
| 111 |
+
n = 5
|
| 112 |
+
assert n > 1
|
| 113 |
+
G = nx.path_graph(n, create_using=nx.DiGraph())
|
| 114 |
+
assert nx.dominance_frontiers(G, n // 2) == {i: set() for i in range(n // 2, n)}
|
| 115 |
+
|
| 116 |
+
def test_irreducible1(self):
|
| 117 |
+
"""
|
| 118 |
+
Graph taken from figure 2 of "A simple, fast dominance algorithm." (2006).
|
| 119 |
+
https://hdl.handle.net/1911/96345
|
| 120 |
+
"""
|
| 121 |
+
edges = [(1, 2), (2, 1), (3, 2), (4, 1), (5, 3), (5, 4)]
|
| 122 |
+
G = nx.DiGraph(edges)
|
| 123 |
+
assert dict(nx.dominance_frontiers(G, 5).items()) == {
|
| 124 |
+
1: {2},
|
| 125 |
+
2: {1},
|
| 126 |
+
3: {2},
|
| 127 |
+
4: {1},
|
| 128 |
+
5: set(),
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
def test_irreducible2(self):
|
| 132 |
+
"""
|
| 133 |
+
Graph taken from figure 4 of "A simple, fast dominance algorithm." (2006).
|
| 134 |
+
https://hdl.handle.net/1911/96345
|
| 135 |
+
"""
|
| 136 |
+
edges = [(1, 2), (2, 1), (2, 3), (3, 2), (4, 2), (4, 3), (5, 1), (6, 4), (6, 5)]
|
| 137 |
+
G = nx.DiGraph(edges)
|
| 138 |
+
assert nx.dominance_frontiers(G, 6) == {
|
| 139 |
+
1: {2},
|
| 140 |
+
2: {1, 3},
|
| 141 |
+
3: {2},
|
| 142 |
+
4: {2, 3},
|
| 143 |
+
5: {1},
|
| 144 |
+
6: set(),
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
def test_domrel_png(self):
|
| 148 |
+
# Graph taken from https://commons.wikipedia.org/wiki/File:Domrel.png
|
| 149 |
+
edges = [(1, 2), (2, 3), (2, 4), (2, 6), (3, 5), (4, 5), (5, 2)]
|
| 150 |
+
G = nx.DiGraph(edges)
|
| 151 |
+
assert nx.dominance_frontiers(G, 1) == {
|
| 152 |
+
1: set(),
|
| 153 |
+
2: {2},
|
| 154 |
+
3: {5},
|
| 155 |
+
4: {5},
|
| 156 |
+
5: {2},
|
| 157 |
+
6: set(),
|
| 158 |
+
}
|
| 159 |
+
# Test postdominance.
|
| 160 |
+
result = nx.dominance_frontiers(G.reverse(copy=False), 6)
|
| 161 |
+
assert result == {1: set(), 2: {2}, 3: {2}, 4: {2}, 5: {2}, 6: set()}
|
| 162 |
+
|
| 163 |
+
def test_boost_example(self):
|
| 164 |
+
# Graph taken from Figure 1 of
|
| 165 |
+
# http://www.boost.org/doc/libs/1_56_0/libs/graph/doc/lengauer_tarjan_dominator.htm
|
| 166 |
+
edges = [(0, 1), (1, 2), (1, 3), (2, 7), (3, 4), (4, 5), (4, 6), (5, 7), (6, 4)]
|
| 167 |
+
G = nx.DiGraph(edges)
|
| 168 |
+
assert nx.dominance_frontiers(G, 0) == {
|
| 169 |
+
0: set(),
|
| 170 |
+
1: set(),
|
| 171 |
+
2: {7},
|
| 172 |
+
3: {7},
|
| 173 |
+
4: {4, 7},
|
| 174 |
+
5: {7},
|
| 175 |
+
6: {4},
|
| 176 |
+
7: set(),
|
| 177 |
+
}
|
| 178 |
+
# Test postdominance.
|
| 179 |
+
result = nx.dominance_frontiers(G.reverse(copy=False), 7)
|
| 180 |
+
expected = {
|
| 181 |
+
0: set(),
|
| 182 |
+
1: set(),
|
| 183 |
+
2: {1},
|
| 184 |
+
3: {1},
|
| 185 |
+
4: {1, 4},
|
| 186 |
+
5: {1},
|
| 187 |
+
6: {4},
|
| 188 |
+
7: set(),
|
| 189 |
+
}
|
| 190 |
+
assert result == expected
|
| 191 |
+
|
| 192 |
+
def test_discard_issue(self):
|
| 193 |
+
# https://github.com/networkx/networkx/issues/2071
|
| 194 |
+
g = nx.DiGraph()
|
| 195 |
+
g.add_edges_from(
|
| 196 |
+
[
|
| 197 |
+
("b0", "b1"),
|
| 198 |
+
("b1", "b2"),
|
| 199 |
+
("b2", "b3"),
|
| 200 |
+
("b3", "b1"),
|
| 201 |
+
("b1", "b5"),
|
| 202 |
+
("b5", "b6"),
|
| 203 |
+
("b5", "b8"),
|
| 204 |
+
("b6", "b7"),
|
| 205 |
+
("b8", "b7"),
|
| 206 |
+
("b7", "b3"),
|
| 207 |
+
("b3", "b4"),
|
| 208 |
+
]
|
| 209 |
+
)
|
| 210 |
+
df = nx.dominance_frontiers(g, "b0")
|
| 211 |
+
assert df == {
|
| 212 |
+
"b4": set(),
|
| 213 |
+
"b5": {"b3"},
|
| 214 |
+
"b6": {"b7"},
|
| 215 |
+
"b7": {"b3"},
|
| 216 |
+
"b0": set(),
|
| 217 |
+
"b1": {"b1"},
|
| 218 |
+
"b2": {"b3"},
|
| 219 |
+
"b3": {"b1"},
|
| 220 |
+
"b8": {"b7"},
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
def test_loop(self):
|
| 224 |
+
g = nx.DiGraph()
|
| 225 |
+
g.add_edges_from([("a", "b"), ("b", "c"), ("b", "a")])
|
| 226 |
+
df = nx.dominance_frontiers(g, "a")
|
| 227 |
+
assert df == {"a": set(), "b": set(), "c": set()}
|
| 228 |
+
|
| 229 |
+
def test_missing_immediate_doms(self):
|
| 230 |
+
# see https://github.com/networkx/networkx/issues/2070
|
| 231 |
+
g = nx.DiGraph()
|
| 232 |
+
edges = [
|
| 233 |
+
("entry_1", "b1"),
|
| 234 |
+
("b1", "b2"),
|
| 235 |
+
("b2", "b3"),
|
| 236 |
+
("b3", "exit"),
|
| 237 |
+
("entry_2", "b3"),
|
| 238 |
+
]
|
| 239 |
+
|
| 240 |
+
# entry_1
|
| 241 |
+
# |
|
| 242 |
+
# b1
|
| 243 |
+
# |
|
| 244 |
+
# b2 entry_2
|
| 245 |
+
# | /
|
| 246 |
+
# b3
|
| 247 |
+
# |
|
| 248 |
+
# exit
|
| 249 |
+
|
| 250 |
+
g.add_edges_from(edges)
|
| 251 |
+
# formerly raised KeyError on entry_2 when parsing b3
|
| 252 |
+
# because entry_2 does not have immediate doms (no path)
|
| 253 |
+
nx.dominance_frontiers(g, "entry_1")
|
| 254 |
+
|
| 255 |
+
def test_loops_larger(self):
|
| 256 |
+
# from
|
| 257 |
+
# http://ecee.colorado.edu/~waite/Darmstadt/motion.html
|
| 258 |
+
g = nx.DiGraph()
|
| 259 |
+
edges = [
|
| 260 |
+
("entry", "exit"),
|
| 261 |
+
("entry", "1"),
|
| 262 |
+
("1", "2"),
|
| 263 |
+
("2", "3"),
|
| 264 |
+
("3", "4"),
|
| 265 |
+
("4", "5"),
|
| 266 |
+
("5", "6"),
|
| 267 |
+
("6", "exit"),
|
| 268 |
+
("6", "2"),
|
| 269 |
+
("5", "3"),
|
| 270 |
+
("4", "4"),
|
| 271 |
+
]
|
| 272 |
+
|
| 273 |
+
g.add_edges_from(edges)
|
| 274 |
+
df = nx.dominance_frontiers(g, "entry")
|
| 275 |
+
answer = {
|
| 276 |
+
"entry": set(),
|
| 277 |
+
"1": {"exit"},
|
| 278 |
+
"2": {"exit", "2"},
|
| 279 |
+
"3": {"exit", "3", "2"},
|
| 280 |
+
"4": {"exit", "4", "3", "2"},
|
| 281 |
+
"5": {"exit", "3", "2"},
|
| 282 |
+
"6": {"exit", "2"},
|
| 283 |
+
"exit": set(),
|
| 284 |
+
}
|
| 285 |
+
for n in df:
|
| 286 |
+
assert set(df[n]) == set(answer[n])
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_dominating.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_dominating_set():
|
| 7 |
+
G = nx.gnp_random_graph(100, 0.1)
|
| 8 |
+
D = nx.dominating_set(G)
|
| 9 |
+
assert nx.is_dominating_set(G, D)
|
| 10 |
+
D = nx.dominating_set(G, start_with=0)
|
| 11 |
+
assert nx.is_dominating_set(G, D)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_complete():
|
| 15 |
+
"""In complete graphs each node is a dominating set.
|
| 16 |
+
Thus the dominating set has to be of cardinality 1.
|
| 17 |
+
"""
|
| 18 |
+
K4 = nx.complete_graph(4)
|
| 19 |
+
assert len(nx.dominating_set(K4)) == 1
|
| 20 |
+
K5 = nx.complete_graph(5)
|
| 21 |
+
assert len(nx.dominating_set(K5)) == 1
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_raise_dominating_set():
|
| 25 |
+
with pytest.raises(nx.NetworkXError):
|
| 26 |
+
G = nx.path_graph(4)
|
| 27 |
+
D = nx.dominating_set(G, start_with=10)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_is_dominating_set():
|
| 31 |
+
G = nx.path_graph(4)
|
| 32 |
+
d = {1, 3}
|
| 33 |
+
assert nx.is_dominating_set(G, d)
|
| 34 |
+
d = {0, 2}
|
| 35 |
+
assert nx.is_dominating_set(G, d)
|
| 36 |
+
d = {1}
|
| 37 |
+
assert not nx.is_dominating_set(G, d)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_wikipedia_is_dominating_set():
|
| 41 |
+
"""Example from https://en.wikipedia.org/wiki/Dominating_set"""
|
| 42 |
+
G = nx.cycle_graph(4)
|
| 43 |
+
G.add_edges_from([(0, 4), (1, 4), (2, 5)])
|
| 44 |
+
assert nx.is_dominating_set(G, {4, 3, 5})
|
| 45 |
+
assert nx.is_dominating_set(G, {0, 2})
|
| 46 |
+
assert nx.is_dominating_set(G, {1, 2})
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_efficiency.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for the :mod:`networkx.algorithms.efficiency` module."""
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class TestEfficiency:
|
| 7 |
+
def setup_method(self):
|
| 8 |
+
# G1 is a disconnected graph
|
| 9 |
+
self.G1 = nx.Graph()
|
| 10 |
+
self.G1.add_nodes_from([1, 2, 3])
|
| 11 |
+
# G2 is a cycle graph
|
| 12 |
+
self.G2 = nx.cycle_graph(4)
|
| 13 |
+
# G3 is the triangle graph with one additional edge
|
| 14 |
+
self.G3 = nx.lollipop_graph(3, 1)
|
| 15 |
+
|
| 16 |
+
def test_efficiency_disconnected_nodes(self):
|
| 17 |
+
"""
|
| 18 |
+
When nodes are disconnected, efficiency is 0
|
| 19 |
+
"""
|
| 20 |
+
assert nx.efficiency(self.G1, 1, 2) == 0
|
| 21 |
+
|
| 22 |
+
def test_local_efficiency_disconnected_graph(self):
|
| 23 |
+
"""
|
| 24 |
+
In a disconnected graph the efficiency is 0
|
| 25 |
+
"""
|
| 26 |
+
assert nx.local_efficiency(self.G1) == 0
|
| 27 |
+
|
| 28 |
+
def test_efficiency(self):
|
| 29 |
+
assert nx.efficiency(self.G2, 0, 1) == 1
|
| 30 |
+
assert nx.efficiency(self.G2, 0, 2) == 1 / 2
|
| 31 |
+
|
| 32 |
+
def test_global_efficiency(self):
|
| 33 |
+
assert nx.global_efficiency(self.G2) == 5 / 6
|
| 34 |
+
|
| 35 |
+
def test_global_efficiency_complete_graph(self):
|
| 36 |
+
"""
|
| 37 |
+
Tests that the average global efficiency of the complete graph is one.
|
| 38 |
+
"""
|
| 39 |
+
for n in range(2, 10):
|
| 40 |
+
G = nx.complete_graph(n)
|
| 41 |
+
assert nx.global_efficiency(G) == 1
|
| 42 |
+
|
| 43 |
+
def test_local_efficiency_complete_graph(self):
|
| 44 |
+
"""
|
| 45 |
+
Test that the local efficiency for a complete graph with at least 3
|
| 46 |
+
nodes should be one. For a graph with only 2 nodes, the induced
|
| 47 |
+
subgraph has no edges.
|
| 48 |
+
"""
|
| 49 |
+
for n in range(3, 10):
|
| 50 |
+
G = nx.complete_graph(n)
|
| 51 |
+
assert nx.local_efficiency(G) == 1
|
| 52 |
+
|
| 53 |
+
def test_using_ego_graph(self):
|
| 54 |
+
"""
|
| 55 |
+
Test that the ego graph is used when computing local efficiency.
|
| 56 |
+
For more information, see GitHub issue #2710.
|
| 57 |
+
"""
|
| 58 |
+
assert nx.local_efficiency(self.G3) == 7 / 12
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_euler.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@pytest.mark.parametrize("f", (nx.is_eulerian, nx.is_semieulerian))
|
| 9 |
+
def test_empty_graph_raises(f):
|
| 10 |
+
G = nx.Graph()
|
| 11 |
+
with pytest.raises(nx.NetworkXPointlessConcept, match="Connectivity is undefined"):
|
| 12 |
+
f(G)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class TestIsEulerian:
|
| 16 |
+
def test_is_eulerian(self):
|
| 17 |
+
assert nx.is_eulerian(nx.complete_graph(5))
|
| 18 |
+
assert nx.is_eulerian(nx.complete_graph(7))
|
| 19 |
+
assert nx.is_eulerian(nx.hypercube_graph(4))
|
| 20 |
+
assert nx.is_eulerian(nx.hypercube_graph(6))
|
| 21 |
+
|
| 22 |
+
assert not nx.is_eulerian(nx.complete_graph(4))
|
| 23 |
+
assert not nx.is_eulerian(nx.complete_graph(6))
|
| 24 |
+
assert not nx.is_eulerian(nx.hypercube_graph(3))
|
| 25 |
+
assert not nx.is_eulerian(nx.hypercube_graph(5))
|
| 26 |
+
|
| 27 |
+
assert not nx.is_eulerian(nx.petersen_graph())
|
| 28 |
+
assert not nx.is_eulerian(nx.path_graph(4))
|
| 29 |
+
|
| 30 |
+
def test_is_eulerian2(self):
|
| 31 |
+
# not connected
|
| 32 |
+
G = nx.Graph()
|
| 33 |
+
G.add_nodes_from([1, 2, 3])
|
| 34 |
+
assert not nx.is_eulerian(G)
|
| 35 |
+
# not strongly connected
|
| 36 |
+
G = nx.DiGraph()
|
| 37 |
+
G.add_nodes_from([1, 2, 3])
|
| 38 |
+
assert not nx.is_eulerian(G)
|
| 39 |
+
G = nx.MultiDiGraph()
|
| 40 |
+
G.add_edge(1, 2)
|
| 41 |
+
G.add_edge(2, 3)
|
| 42 |
+
G.add_edge(2, 3)
|
| 43 |
+
G.add_edge(3, 1)
|
| 44 |
+
assert not nx.is_eulerian(G)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class TestEulerianCircuit:
|
| 48 |
+
def test_eulerian_circuit_cycle(self):
|
| 49 |
+
G = nx.cycle_graph(4)
|
| 50 |
+
|
| 51 |
+
edges = list(nx.eulerian_circuit(G, source=0))
|
| 52 |
+
nodes = [u for u, v in edges]
|
| 53 |
+
assert nodes == [0, 3, 2, 1]
|
| 54 |
+
assert edges == [(0, 3), (3, 2), (2, 1), (1, 0)]
|
| 55 |
+
|
| 56 |
+
edges = list(nx.eulerian_circuit(G, source=1))
|
| 57 |
+
nodes = [u for u, v in edges]
|
| 58 |
+
assert nodes == [1, 2, 3, 0]
|
| 59 |
+
assert edges == [(1, 2), (2, 3), (3, 0), (0, 1)]
|
| 60 |
+
|
| 61 |
+
G = nx.complete_graph(3)
|
| 62 |
+
|
| 63 |
+
edges = list(nx.eulerian_circuit(G, source=0))
|
| 64 |
+
nodes = [u for u, v in edges]
|
| 65 |
+
assert nodes == [0, 2, 1]
|
| 66 |
+
assert edges == [(0, 2), (2, 1), (1, 0)]
|
| 67 |
+
|
| 68 |
+
edges = list(nx.eulerian_circuit(G, source=1))
|
| 69 |
+
nodes = [u for u, v in edges]
|
| 70 |
+
assert nodes == [1, 2, 0]
|
| 71 |
+
assert edges == [(1, 2), (2, 0), (0, 1)]
|
| 72 |
+
|
| 73 |
+
def test_eulerian_circuit_digraph(self):
|
| 74 |
+
G = nx.DiGraph()
|
| 75 |
+
nx.add_cycle(G, [0, 1, 2, 3])
|
| 76 |
+
|
| 77 |
+
edges = list(nx.eulerian_circuit(G, source=0))
|
| 78 |
+
nodes = [u for u, v in edges]
|
| 79 |
+
assert nodes == [0, 1, 2, 3]
|
| 80 |
+
assert edges == [(0, 1), (1, 2), (2, 3), (3, 0)]
|
| 81 |
+
|
| 82 |
+
edges = list(nx.eulerian_circuit(G, source=1))
|
| 83 |
+
nodes = [u for u, v in edges]
|
| 84 |
+
assert nodes == [1, 2, 3, 0]
|
| 85 |
+
assert edges == [(1, 2), (2, 3), (3, 0), (0, 1)]
|
| 86 |
+
|
| 87 |
+
def test_multigraph(self):
|
| 88 |
+
G = nx.MultiGraph()
|
| 89 |
+
nx.add_cycle(G, [0, 1, 2, 3])
|
| 90 |
+
G.add_edge(1, 2)
|
| 91 |
+
G.add_edge(1, 2)
|
| 92 |
+
edges = list(nx.eulerian_circuit(G, source=0))
|
| 93 |
+
nodes = [u for u, v in edges]
|
| 94 |
+
assert nodes == [0, 3, 2, 1, 2, 1]
|
| 95 |
+
assert edges == [(0, 3), (3, 2), (2, 1), (1, 2), (2, 1), (1, 0)]
|
| 96 |
+
|
| 97 |
+
def test_multigraph_with_keys(self):
|
| 98 |
+
G = nx.MultiGraph()
|
| 99 |
+
nx.add_cycle(G, [0, 1, 2, 3])
|
| 100 |
+
G.add_edge(1, 2)
|
| 101 |
+
G.add_edge(1, 2)
|
| 102 |
+
edges = list(nx.eulerian_circuit(G, source=0, keys=True))
|
| 103 |
+
nodes = [u for u, v, k in edges]
|
| 104 |
+
assert nodes == [0, 3, 2, 1, 2, 1]
|
| 105 |
+
assert edges[:2] == [(0, 3, 0), (3, 2, 0)]
|
| 106 |
+
assert collections.Counter(edges[2:5]) == collections.Counter(
|
| 107 |
+
[(2, 1, 0), (1, 2, 1), (2, 1, 2)]
|
| 108 |
+
)
|
| 109 |
+
assert edges[5:] == [(1, 0, 0)]
|
| 110 |
+
|
| 111 |
+
def test_not_eulerian(self):
|
| 112 |
+
with pytest.raises(nx.NetworkXError):
|
| 113 |
+
f = list(nx.eulerian_circuit(nx.complete_graph(4)))
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class TestIsSemiEulerian:
|
| 117 |
+
def test_is_semieulerian(self):
|
| 118 |
+
# Test graphs with Eulerian paths but no cycles return True.
|
| 119 |
+
assert nx.is_semieulerian(nx.path_graph(4))
|
| 120 |
+
G = nx.path_graph(6, create_using=nx.DiGraph)
|
| 121 |
+
assert nx.is_semieulerian(G)
|
| 122 |
+
|
| 123 |
+
# Test graphs with Eulerian cycles return False.
|
| 124 |
+
assert not nx.is_semieulerian(nx.complete_graph(5))
|
| 125 |
+
assert not nx.is_semieulerian(nx.complete_graph(7))
|
| 126 |
+
assert not nx.is_semieulerian(nx.hypercube_graph(4))
|
| 127 |
+
assert not nx.is_semieulerian(nx.hypercube_graph(6))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class TestHasEulerianPath:
|
| 131 |
+
def test_has_eulerian_path_cyclic(self):
|
| 132 |
+
# Test graphs with Eulerian cycles return True.
|
| 133 |
+
assert nx.has_eulerian_path(nx.complete_graph(5))
|
| 134 |
+
assert nx.has_eulerian_path(nx.complete_graph(7))
|
| 135 |
+
assert nx.has_eulerian_path(nx.hypercube_graph(4))
|
| 136 |
+
assert nx.has_eulerian_path(nx.hypercube_graph(6))
|
| 137 |
+
|
| 138 |
+
def test_has_eulerian_path_non_cyclic(self):
|
| 139 |
+
# Test graphs with Eulerian paths but no cycles return True.
|
| 140 |
+
assert nx.has_eulerian_path(nx.path_graph(4))
|
| 141 |
+
G = nx.path_graph(6, create_using=nx.DiGraph)
|
| 142 |
+
assert nx.has_eulerian_path(G)
|
| 143 |
+
|
| 144 |
+
def test_has_eulerian_path_directed_graph(self):
|
| 145 |
+
# Test directed graphs and returns False
|
| 146 |
+
G = nx.DiGraph()
|
| 147 |
+
G.add_edges_from([(0, 1), (1, 2), (0, 2)])
|
| 148 |
+
assert not nx.has_eulerian_path(G)
|
| 149 |
+
|
| 150 |
+
# Test directed graphs without isolated node returns True
|
| 151 |
+
G = nx.DiGraph()
|
| 152 |
+
G.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
| 153 |
+
assert nx.has_eulerian_path(G)
|
| 154 |
+
|
| 155 |
+
# Test directed graphs with isolated node returns False
|
| 156 |
+
G.add_node(3)
|
| 157 |
+
assert not nx.has_eulerian_path(G)
|
| 158 |
+
|
| 159 |
+
@pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph()))
|
| 160 |
+
def test_has_eulerian_path_not_weakly_connected(self, G):
|
| 161 |
+
G.add_edges_from([(0, 1), (2, 3), (3, 2)])
|
| 162 |
+
assert not nx.has_eulerian_path(G)
|
| 163 |
+
|
| 164 |
+
@pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph()))
|
| 165 |
+
def test_has_eulerian_path_unbalancedins_more_than_one(self, G):
|
| 166 |
+
G.add_edges_from([(0, 1), (2, 3)])
|
| 167 |
+
assert not nx.has_eulerian_path(G)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
class TestFindPathStart:
|
| 171 |
+
def testfind_path_start(self):
|
| 172 |
+
find_path_start = nx.algorithms.euler._find_path_start
|
| 173 |
+
# Test digraphs return correct starting node.
|
| 174 |
+
G = nx.path_graph(6, create_using=nx.DiGraph)
|
| 175 |
+
assert find_path_start(G) == 0
|
| 176 |
+
edges = [(0, 1), (1, 2), (2, 0), (4, 0)]
|
| 177 |
+
assert find_path_start(nx.DiGraph(edges)) == 4
|
| 178 |
+
|
| 179 |
+
# Test graph with no Eulerian path return None.
|
| 180 |
+
edges = [(0, 1), (1, 2), (2, 3), (2, 4)]
|
| 181 |
+
assert find_path_start(nx.DiGraph(edges)) is None
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
class TestEulerianPath:
|
| 185 |
+
def test_eulerian_path(self):
|
| 186 |
+
x = [(4, 0), (0, 1), (1, 2), (2, 0)]
|
| 187 |
+
for e1, e2 in zip(x, nx.eulerian_path(nx.DiGraph(x))):
|
| 188 |
+
assert e1 == e2
|
| 189 |
+
|
| 190 |
+
def test_eulerian_path_straight_link(self):
|
| 191 |
+
G = nx.DiGraph()
|
| 192 |
+
result = [(1, 2), (2, 3), (3, 4), (4, 5)]
|
| 193 |
+
G.add_edges_from(result)
|
| 194 |
+
assert result == list(nx.eulerian_path(G))
|
| 195 |
+
assert result == list(nx.eulerian_path(G, source=1))
|
| 196 |
+
with pytest.raises(nx.NetworkXError):
|
| 197 |
+
list(nx.eulerian_path(G, source=3))
|
| 198 |
+
with pytest.raises(nx.NetworkXError):
|
| 199 |
+
list(nx.eulerian_path(G, source=4))
|
| 200 |
+
with pytest.raises(nx.NetworkXError):
|
| 201 |
+
list(nx.eulerian_path(G, source=5))
|
| 202 |
+
|
| 203 |
+
def test_eulerian_path_multigraph(self):
|
| 204 |
+
G = nx.MultiDiGraph()
|
| 205 |
+
result = [(2, 1), (1, 2), (2, 1), (1, 2), (2, 3), (3, 4), (4, 3)]
|
| 206 |
+
G.add_edges_from(result)
|
| 207 |
+
assert result == list(nx.eulerian_path(G))
|
| 208 |
+
assert result == list(nx.eulerian_path(G, source=2))
|
| 209 |
+
with pytest.raises(nx.NetworkXError):
|
| 210 |
+
list(nx.eulerian_path(G, source=3))
|
| 211 |
+
with pytest.raises(nx.NetworkXError):
|
| 212 |
+
list(nx.eulerian_path(G, source=4))
|
| 213 |
+
|
| 214 |
+
def test_eulerian_path_eulerian_circuit(self):
|
| 215 |
+
G = nx.DiGraph()
|
| 216 |
+
result = [(1, 2), (2, 3), (3, 4), (4, 1)]
|
| 217 |
+
result2 = [(2, 3), (3, 4), (4, 1), (1, 2)]
|
| 218 |
+
result3 = [(3, 4), (4, 1), (1, 2), (2, 3)]
|
| 219 |
+
G.add_edges_from(result)
|
| 220 |
+
assert result == list(nx.eulerian_path(G))
|
| 221 |
+
assert result == list(nx.eulerian_path(G, source=1))
|
| 222 |
+
assert result2 == list(nx.eulerian_path(G, source=2))
|
| 223 |
+
assert result3 == list(nx.eulerian_path(G, source=3))
|
| 224 |
+
|
| 225 |
+
def test_eulerian_path_undirected(self):
|
| 226 |
+
G = nx.Graph()
|
| 227 |
+
result = [(1, 2), (2, 3), (3, 4), (4, 5)]
|
| 228 |
+
result2 = [(5, 4), (4, 3), (3, 2), (2, 1)]
|
| 229 |
+
G.add_edges_from(result)
|
| 230 |
+
assert list(nx.eulerian_path(G)) in (result, result2)
|
| 231 |
+
assert result == list(nx.eulerian_path(G, source=1))
|
| 232 |
+
assert result2 == list(nx.eulerian_path(G, source=5))
|
| 233 |
+
with pytest.raises(nx.NetworkXError):
|
| 234 |
+
list(nx.eulerian_path(G, source=3))
|
| 235 |
+
with pytest.raises(nx.NetworkXError):
|
| 236 |
+
list(nx.eulerian_path(G, source=2))
|
| 237 |
+
|
| 238 |
+
def test_eulerian_path_multigraph_undirected(self):
|
| 239 |
+
G = nx.MultiGraph()
|
| 240 |
+
result = [(2, 1), (1, 2), (2, 1), (1, 2), (2, 3), (3, 4)]
|
| 241 |
+
G.add_edges_from(result)
|
| 242 |
+
assert result == list(nx.eulerian_path(G))
|
| 243 |
+
assert result == list(nx.eulerian_path(G, source=2))
|
| 244 |
+
with pytest.raises(nx.NetworkXError):
|
| 245 |
+
list(nx.eulerian_path(G, source=3))
|
| 246 |
+
with pytest.raises(nx.NetworkXError):
|
| 247 |
+
list(nx.eulerian_path(G, source=1))
|
| 248 |
+
|
| 249 |
+
@pytest.mark.parametrize(
|
| 250 |
+
("graph_type", "result"),
|
| 251 |
+
(
|
| 252 |
+
(nx.MultiGraph, [(0, 1, 0), (1, 0, 1)]),
|
| 253 |
+
(nx.MultiDiGraph, [(0, 1, 0), (1, 0, 0)]),
|
| 254 |
+
),
|
| 255 |
+
)
|
| 256 |
+
def test_eulerian_with_keys(self, graph_type, result):
|
| 257 |
+
G = graph_type([(0, 1), (1, 0)])
|
| 258 |
+
answer = nx.eulerian_path(G, keys=True)
|
| 259 |
+
assert list(answer) == result
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
class TestEulerize:
|
| 263 |
+
def test_disconnected(self):
|
| 264 |
+
with pytest.raises(nx.NetworkXError):
|
| 265 |
+
G = nx.from_edgelist([(0, 1), (2, 3)])
|
| 266 |
+
nx.eulerize(G)
|
| 267 |
+
|
| 268 |
+
def test_null_graph(self):
|
| 269 |
+
with pytest.raises(nx.NetworkXPointlessConcept):
|
| 270 |
+
nx.eulerize(nx.Graph())
|
| 271 |
+
|
| 272 |
+
def test_null_multigraph(self):
|
| 273 |
+
with pytest.raises(nx.NetworkXPointlessConcept):
|
| 274 |
+
nx.eulerize(nx.MultiGraph())
|
| 275 |
+
|
| 276 |
+
def test_on_empty_graph(self):
|
| 277 |
+
with pytest.raises(nx.NetworkXError):
|
| 278 |
+
nx.eulerize(nx.empty_graph(3))
|
| 279 |
+
|
| 280 |
+
def test_on_eulerian(self):
|
| 281 |
+
G = nx.cycle_graph(3)
|
| 282 |
+
H = nx.eulerize(G)
|
| 283 |
+
assert nx.is_isomorphic(G, H)
|
| 284 |
+
|
| 285 |
+
def test_on_eulerian_multigraph(self):
|
| 286 |
+
G = nx.MultiGraph(nx.cycle_graph(3))
|
| 287 |
+
G.add_edge(0, 1)
|
| 288 |
+
H = nx.eulerize(G)
|
| 289 |
+
assert nx.is_eulerian(H)
|
| 290 |
+
|
| 291 |
+
def test_on_complete_graph(self):
|
| 292 |
+
G = nx.complete_graph(4)
|
| 293 |
+
assert nx.is_eulerian(nx.eulerize(G))
|
| 294 |
+
assert nx.is_eulerian(nx.eulerize(nx.MultiGraph(G)))
|
| 295 |
+
|
| 296 |
+
def test_on_non_eulerian_graph(self):
|
| 297 |
+
G = nx.cycle_graph(18)
|
| 298 |
+
G.add_edge(0, 18)
|
| 299 |
+
G.add_edge(18, 19)
|
| 300 |
+
G.add_edge(17, 19)
|
| 301 |
+
G.add_edge(4, 20)
|
| 302 |
+
G.add_edge(20, 21)
|
| 303 |
+
G.add_edge(21, 22)
|
| 304 |
+
G.add_edge(22, 23)
|
| 305 |
+
G.add_edge(23, 24)
|
| 306 |
+
G.add_edge(24, 25)
|
| 307 |
+
G.add_edge(25, 26)
|
| 308 |
+
G.add_edge(26, 27)
|
| 309 |
+
G.add_edge(27, 28)
|
| 310 |
+
G.add_edge(28, 13)
|
| 311 |
+
assert not nx.is_eulerian(G)
|
| 312 |
+
G = nx.eulerize(G)
|
| 313 |
+
assert nx.is_eulerian(G)
|
| 314 |
+
assert nx.number_of_edges(G) == 39
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_graph_hashing.py
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from networkx.generators import directed
|
| 5 |
+
|
| 6 |
+
# Unit tests for the :func:`~networkx.weisfeiler_lehman_graph_hash` function
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_empty_graph_hash():
|
| 10 |
+
"""
|
| 11 |
+
empty graphs should give hashes regardless of other params
|
| 12 |
+
"""
|
| 13 |
+
G1 = nx.empty_graph()
|
| 14 |
+
G2 = nx.empty_graph()
|
| 15 |
+
|
| 16 |
+
h1 = nx.weisfeiler_lehman_graph_hash(G1)
|
| 17 |
+
h2 = nx.weisfeiler_lehman_graph_hash(G2)
|
| 18 |
+
h3 = nx.weisfeiler_lehman_graph_hash(G2, edge_attr="edge_attr1")
|
| 19 |
+
h4 = nx.weisfeiler_lehman_graph_hash(G2, node_attr="node_attr1")
|
| 20 |
+
h5 = nx.weisfeiler_lehman_graph_hash(
|
| 21 |
+
G2, edge_attr="edge_attr1", node_attr="node_attr1"
|
| 22 |
+
)
|
| 23 |
+
h6 = nx.weisfeiler_lehman_graph_hash(G2, iterations=10)
|
| 24 |
+
|
| 25 |
+
assert h1 == h2
|
| 26 |
+
assert h1 == h3
|
| 27 |
+
assert h1 == h4
|
| 28 |
+
assert h1 == h5
|
| 29 |
+
assert h1 == h6
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_directed():
|
| 33 |
+
"""
|
| 34 |
+
A directed graph with no bi-directional edges should yield different a graph hash
|
| 35 |
+
to the same graph taken as undirected if there are no hash collisions.
|
| 36 |
+
"""
|
| 37 |
+
r = 10
|
| 38 |
+
for i in range(r):
|
| 39 |
+
G_directed = nx.gn_graph(10 + r, seed=100 + i)
|
| 40 |
+
G_undirected = nx.to_undirected(G_directed)
|
| 41 |
+
|
| 42 |
+
h_directed = nx.weisfeiler_lehman_graph_hash(G_directed)
|
| 43 |
+
h_undirected = nx.weisfeiler_lehman_graph_hash(G_undirected)
|
| 44 |
+
|
| 45 |
+
assert h_directed != h_undirected
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_reversed():
|
| 49 |
+
"""
|
| 50 |
+
A directed graph with no bi-directional edges should yield different a graph hash
|
| 51 |
+
to the same graph taken with edge directions reversed if there are no hash collisions.
|
| 52 |
+
Here we test a cycle graph which is the minimal counterexample
|
| 53 |
+
"""
|
| 54 |
+
G = nx.cycle_graph(5, create_using=nx.DiGraph)
|
| 55 |
+
nx.set_node_attributes(G, {n: str(n) for n in G.nodes()}, name="label")
|
| 56 |
+
|
| 57 |
+
G_reversed = G.reverse()
|
| 58 |
+
|
| 59 |
+
h = nx.weisfeiler_lehman_graph_hash(G, node_attr="label")
|
| 60 |
+
h_reversed = nx.weisfeiler_lehman_graph_hash(G_reversed, node_attr="label")
|
| 61 |
+
|
| 62 |
+
assert h != h_reversed
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_isomorphic():
|
| 66 |
+
"""
|
| 67 |
+
graph hashes should be invariant to node-relabeling (when the output is reindexed
|
| 68 |
+
by the same mapping)
|
| 69 |
+
"""
|
| 70 |
+
n, r = 100, 10
|
| 71 |
+
p = 1.0 / r
|
| 72 |
+
for i in range(1, r + 1):
|
| 73 |
+
G1 = nx.erdos_renyi_graph(n, p * i, seed=200 + i)
|
| 74 |
+
G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
|
| 75 |
+
|
| 76 |
+
g1_hash = nx.weisfeiler_lehman_graph_hash(G1)
|
| 77 |
+
g2_hash = nx.weisfeiler_lehman_graph_hash(G2)
|
| 78 |
+
|
| 79 |
+
assert g1_hash == g2_hash
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_isomorphic_edge_attr():
|
| 83 |
+
"""
|
| 84 |
+
Isomorphic graphs with differing edge attributes should yield different graph
|
| 85 |
+
hashes if the 'edge_attr' argument is supplied and populated in the graph,
|
| 86 |
+
and there are no hash collisions.
|
| 87 |
+
The output should still be invariant to node-relabeling
|
| 88 |
+
"""
|
| 89 |
+
n, r = 100, 10
|
| 90 |
+
p = 1.0 / r
|
| 91 |
+
for i in range(1, r + 1):
|
| 92 |
+
G1 = nx.erdos_renyi_graph(n, p * i, seed=300 + i)
|
| 93 |
+
|
| 94 |
+
for a, b in G1.edges:
|
| 95 |
+
G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
|
| 96 |
+
G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
|
| 97 |
+
|
| 98 |
+
g1_hash_with_edge_attr1 = nx.weisfeiler_lehman_graph_hash(
|
| 99 |
+
G1, edge_attr="edge_attr1"
|
| 100 |
+
)
|
| 101 |
+
g1_hash_with_edge_attr2 = nx.weisfeiler_lehman_graph_hash(
|
| 102 |
+
G1, edge_attr="edge_attr2"
|
| 103 |
+
)
|
| 104 |
+
g1_hash_no_edge_attr = nx.weisfeiler_lehman_graph_hash(G1, edge_attr=None)
|
| 105 |
+
|
| 106 |
+
assert g1_hash_with_edge_attr1 != g1_hash_no_edge_attr
|
| 107 |
+
assert g1_hash_with_edge_attr2 != g1_hash_no_edge_attr
|
| 108 |
+
assert g1_hash_with_edge_attr1 != g1_hash_with_edge_attr2
|
| 109 |
+
|
| 110 |
+
G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
|
| 111 |
+
|
| 112 |
+
g2_hash_with_edge_attr1 = nx.weisfeiler_lehman_graph_hash(
|
| 113 |
+
G2, edge_attr="edge_attr1"
|
| 114 |
+
)
|
| 115 |
+
g2_hash_with_edge_attr2 = nx.weisfeiler_lehman_graph_hash(
|
| 116 |
+
G2, edge_attr="edge_attr2"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
assert g1_hash_with_edge_attr1 == g2_hash_with_edge_attr1
|
| 120 |
+
assert g1_hash_with_edge_attr2 == g2_hash_with_edge_attr2
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def test_missing_edge_attr():
|
| 124 |
+
"""
|
| 125 |
+
If the 'edge_attr' argument is supplied but is missing from an edge in the graph,
|
| 126 |
+
we should raise a KeyError
|
| 127 |
+
"""
|
| 128 |
+
G = nx.Graph()
|
| 129 |
+
G.add_edges_from([(1, 2, {"edge_attr1": "a"}), (1, 3, {})])
|
| 130 |
+
pytest.raises(KeyError, nx.weisfeiler_lehman_graph_hash, G, edge_attr="edge_attr1")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def test_isomorphic_node_attr():
|
| 134 |
+
"""
|
| 135 |
+
Isomorphic graphs with differing node attributes should yield different graph
|
| 136 |
+
hashes if the 'node_attr' argument is supplied and populated in the graph, and
|
| 137 |
+
there are no hash collisions.
|
| 138 |
+
The output should still be invariant to node-relabeling
|
| 139 |
+
"""
|
| 140 |
+
n, r = 100, 10
|
| 141 |
+
p = 1.0 / r
|
| 142 |
+
for i in range(1, r + 1):
|
| 143 |
+
G1 = nx.erdos_renyi_graph(n, p * i, seed=400 + i)
|
| 144 |
+
|
| 145 |
+
for u in G1.nodes():
|
| 146 |
+
G1.nodes[u]["node_attr1"] = f"{u}-1"
|
| 147 |
+
G1.nodes[u]["node_attr2"] = f"{u}-2"
|
| 148 |
+
|
| 149 |
+
g1_hash_with_node_attr1 = nx.weisfeiler_lehman_graph_hash(
|
| 150 |
+
G1, node_attr="node_attr1"
|
| 151 |
+
)
|
| 152 |
+
g1_hash_with_node_attr2 = nx.weisfeiler_lehman_graph_hash(
|
| 153 |
+
G1, node_attr="node_attr2"
|
| 154 |
+
)
|
| 155 |
+
g1_hash_no_node_attr = nx.weisfeiler_lehman_graph_hash(G1, node_attr=None)
|
| 156 |
+
|
| 157 |
+
assert g1_hash_with_node_attr1 != g1_hash_no_node_attr
|
| 158 |
+
assert g1_hash_with_node_attr2 != g1_hash_no_node_attr
|
| 159 |
+
assert g1_hash_with_node_attr1 != g1_hash_with_node_attr2
|
| 160 |
+
|
| 161 |
+
G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
|
| 162 |
+
|
| 163 |
+
g2_hash_with_node_attr1 = nx.weisfeiler_lehman_graph_hash(
|
| 164 |
+
G2, node_attr="node_attr1"
|
| 165 |
+
)
|
| 166 |
+
g2_hash_with_node_attr2 = nx.weisfeiler_lehman_graph_hash(
|
| 167 |
+
G2, node_attr="node_attr2"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
assert g1_hash_with_node_attr1 == g2_hash_with_node_attr1
|
| 171 |
+
assert g1_hash_with_node_attr2 == g2_hash_with_node_attr2
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def test_missing_node_attr():
|
| 175 |
+
"""
|
| 176 |
+
If the 'node_attr' argument is supplied but is missing from a node in the graph,
|
| 177 |
+
we should raise a KeyError
|
| 178 |
+
"""
|
| 179 |
+
G = nx.Graph()
|
| 180 |
+
G.add_nodes_from([(1, {"node_attr1": "a"}), (2, {})])
|
| 181 |
+
G.add_edges_from([(1, 2), (2, 3), (3, 1), (1, 4)])
|
| 182 |
+
pytest.raises(KeyError, nx.weisfeiler_lehman_graph_hash, G, node_attr="node_attr1")
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def test_isomorphic_edge_attr_and_node_attr():
|
| 186 |
+
"""
|
| 187 |
+
Isomorphic graphs with differing node attributes should yield different graph
|
| 188 |
+
hashes if the 'node_attr' and 'edge_attr' argument is supplied and populated in
|
| 189 |
+
the graph, and there are no hash collisions.
|
| 190 |
+
The output should still be invariant to node-relabeling
|
| 191 |
+
"""
|
| 192 |
+
n, r = 100, 10
|
| 193 |
+
p = 1.0 / r
|
| 194 |
+
for i in range(1, r + 1):
|
| 195 |
+
G1 = nx.erdos_renyi_graph(n, p * i, seed=500 + i)
|
| 196 |
+
|
| 197 |
+
for u in G1.nodes():
|
| 198 |
+
G1.nodes[u]["node_attr1"] = f"{u}-1"
|
| 199 |
+
G1.nodes[u]["node_attr2"] = f"{u}-2"
|
| 200 |
+
|
| 201 |
+
for a, b in G1.edges:
|
| 202 |
+
G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
|
| 203 |
+
G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
|
| 204 |
+
|
| 205 |
+
g1_hash_edge1_node1 = nx.weisfeiler_lehman_graph_hash(
|
| 206 |
+
G1, edge_attr="edge_attr1", node_attr="node_attr1"
|
| 207 |
+
)
|
| 208 |
+
g1_hash_edge2_node2 = nx.weisfeiler_lehman_graph_hash(
|
| 209 |
+
G1, edge_attr="edge_attr2", node_attr="node_attr2"
|
| 210 |
+
)
|
| 211 |
+
g1_hash_edge1_node2 = nx.weisfeiler_lehman_graph_hash(
|
| 212 |
+
G1, edge_attr="edge_attr1", node_attr="node_attr2"
|
| 213 |
+
)
|
| 214 |
+
g1_hash_no_attr = nx.weisfeiler_lehman_graph_hash(G1)
|
| 215 |
+
|
| 216 |
+
assert g1_hash_edge1_node1 != g1_hash_no_attr
|
| 217 |
+
assert g1_hash_edge2_node2 != g1_hash_no_attr
|
| 218 |
+
assert g1_hash_edge1_node1 != g1_hash_edge2_node2
|
| 219 |
+
assert g1_hash_edge1_node2 != g1_hash_edge2_node2
|
| 220 |
+
assert g1_hash_edge1_node2 != g1_hash_edge1_node1
|
| 221 |
+
|
| 222 |
+
G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
|
| 223 |
+
|
| 224 |
+
g2_hash_edge1_node1 = nx.weisfeiler_lehman_graph_hash(
|
| 225 |
+
G2, edge_attr="edge_attr1", node_attr="node_attr1"
|
| 226 |
+
)
|
| 227 |
+
g2_hash_edge2_node2 = nx.weisfeiler_lehman_graph_hash(
|
| 228 |
+
G2, edge_attr="edge_attr2", node_attr="node_attr2"
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
assert g1_hash_edge1_node1 == g2_hash_edge1_node1
|
| 232 |
+
assert g1_hash_edge2_node2 == g2_hash_edge2_node2
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def test_digest_size():
|
| 236 |
+
"""
|
| 237 |
+
The hash string lengths should be as expected for a variety of graphs and
|
| 238 |
+
digest sizes
|
| 239 |
+
"""
|
| 240 |
+
n, r = 100, 10
|
| 241 |
+
p = 1.0 / r
|
| 242 |
+
for i in range(1, r + 1):
|
| 243 |
+
G = nx.erdos_renyi_graph(n, p * i, seed=1000 + i)
|
| 244 |
+
|
| 245 |
+
h16 = nx.weisfeiler_lehman_graph_hash(G)
|
| 246 |
+
h32 = nx.weisfeiler_lehman_graph_hash(G, digest_size=32)
|
| 247 |
+
|
| 248 |
+
assert h16 != h32
|
| 249 |
+
assert len(h16) == 16 * 2
|
| 250 |
+
assert len(h32) == 32 * 2
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
# Unit tests for the :func:`~networkx.weisfeiler_lehman_hash_subgraphs` function
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def is_subiteration(a, b):
|
| 257 |
+
"""
|
| 258 |
+
returns True if that each hash sequence in 'a' is a prefix for
|
| 259 |
+
the corresponding sequence indexed by the same node in 'b'.
|
| 260 |
+
"""
|
| 261 |
+
return all(b[node][: len(hashes)] == hashes for node, hashes in a.items())
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def hexdigest_sizes_correct(a, digest_size):
|
| 265 |
+
"""
|
| 266 |
+
returns True if all hex digest sizes are the expected length in a node:subgraph-hashes
|
| 267 |
+
dictionary. Hex digest string length == 2 * bytes digest length since each pair of hex
|
| 268 |
+
digits encodes 1 byte (https://docs.python.org/3/library/hashlib.html)
|
| 269 |
+
"""
|
| 270 |
+
hexdigest_size = digest_size * 2
|
| 271 |
+
list_digest_sizes_correct = lambda l: all(len(x) == hexdigest_size for x in l)
|
| 272 |
+
return all(list_digest_sizes_correct(hashes) for hashes in a.values())
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def test_empty_graph_subgraph_hash():
|
| 276 |
+
""" "
|
| 277 |
+
empty graphs should give empty dict subgraph hashes regardless of other params
|
| 278 |
+
"""
|
| 279 |
+
G = nx.empty_graph()
|
| 280 |
+
|
| 281 |
+
subgraph_hashes1 = nx.weisfeiler_lehman_subgraph_hashes(G)
|
| 282 |
+
subgraph_hashes2 = nx.weisfeiler_lehman_subgraph_hashes(G, edge_attr="edge_attr")
|
| 283 |
+
subgraph_hashes3 = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="edge_attr")
|
| 284 |
+
subgraph_hashes4 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=2)
|
| 285 |
+
subgraph_hashes5 = nx.weisfeiler_lehman_subgraph_hashes(G, digest_size=64)
|
| 286 |
+
|
| 287 |
+
assert subgraph_hashes1 == {}
|
| 288 |
+
assert subgraph_hashes2 == {}
|
| 289 |
+
assert subgraph_hashes3 == {}
|
| 290 |
+
assert subgraph_hashes4 == {}
|
| 291 |
+
assert subgraph_hashes5 == {}
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def test_directed_subgraph_hash():
|
| 295 |
+
"""
|
| 296 |
+
A directed graph with no bi-directional edges should yield different subgraph hashes
|
| 297 |
+
to the same graph taken as undirected, if all hashes don't collide.
|
| 298 |
+
"""
|
| 299 |
+
r = 10
|
| 300 |
+
for i in range(r):
|
| 301 |
+
G_directed = nx.gn_graph(10 + r, seed=100 + i)
|
| 302 |
+
G_undirected = nx.to_undirected(G_directed)
|
| 303 |
+
|
| 304 |
+
directed_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G_directed)
|
| 305 |
+
undirected_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G_undirected)
|
| 306 |
+
|
| 307 |
+
assert directed_subgraph_hashes != undirected_subgraph_hashes
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def test_reversed_subgraph_hash():
|
| 311 |
+
"""
|
| 312 |
+
A directed graph with no bi-directional edges should yield different subgraph hashes
|
| 313 |
+
to the same graph taken with edge directions reversed if there are no hash collisions.
|
| 314 |
+
Here we test a cycle graph which is the minimal counterexample
|
| 315 |
+
"""
|
| 316 |
+
G = nx.cycle_graph(5, create_using=nx.DiGraph)
|
| 317 |
+
nx.set_node_attributes(G, {n: str(n) for n in G.nodes()}, name="label")
|
| 318 |
+
|
| 319 |
+
G_reversed = G.reverse()
|
| 320 |
+
|
| 321 |
+
h = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="label")
|
| 322 |
+
h_reversed = nx.weisfeiler_lehman_subgraph_hashes(G_reversed, node_attr="label")
|
| 323 |
+
|
| 324 |
+
assert h != h_reversed
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def test_isomorphic_subgraph_hash():
|
| 328 |
+
"""
|
| 329 |
+
the subgraph hashes should be invariant to node-relabeling when the output is reindexed
|
| 330 |
+
by the same mapping and all hashes don't collide.
|
| 331 |
+
"""
|
| 332 |
+
n, r = 100, 10
|
| 333 |
+
p = 1.0 / r
|
| 334 |
+
for i in range(1, r + 1):
|
| 335 |
+
G1 = nx.erdos_renyi_graph(n, p * i, seed=200 + i)
|
| 336 |
+
G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
|
| 337 |
+
|
| 338 |
+
g1_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G1)
|
| 339 |
+
g2_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G2)
|
| 340 |
+
|
| 341 |
+
assert g1_subgraph_hashes == {-1 * k: v for k, v in g2_subgraph_hashes.items()}
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def test_isomorphic_edge_attr_subgraph_hash():
|
| 345 |
+
"""
|
| 346 |
+
Isomorphic graphs with differing edge attributes should yield different subgraph
|
| 347 |
+
hashes if the 'edge_attr' argument is supplied and populated in the graph, and
|
| 348 |
+
all hashes don't collide.
|
| 349 |
+
The output should still be invariant to node-relabeling
|
| 350 |
+
"""
|
| 351 |
+
n, r = 100, 10
|
| 352 |
+
p = 1.0 / r
|
| 353 |
+
for i in range(1, r + 1):
|
| 354 |
+
G1 = nx.erdos_renyi_graph(n, p * i, seed=300 + i)
|
| 355 |
+
|
| 356 |
+
for a, b in G1.edges:
|
| 357 |
+
G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
|
| 358 |
+
G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
|
| 359 |
+
|
| 360 |
+
g1_hash_with_edge_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 361 |
+
G1, edge_attr="edge_attr1"
|
| 362 |
+
)
|
| 363 |
+
g1_hash_with_edge_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 364 |
+
G1, edge_attr="edge_attr2"
|
| 365 |
+
)
|
| 366 |
+
g1_hash_no_edge_attr = nx.weisfeiler_lehman_subgraph_hashes(G1, edge_attr=None)
|
| 367 |
+
|
| 368 |
+
assert g1_hash_with_edge_attr1 != g1_hash_no_edge_attr
|
| 369 |
+
assert g1_hash_with_edge_attr2 != g1_hash_no_edge_attr
|
| 370 |
+
assert g1_hash_with_edge_attr1 != g1_hash_with_edge_attr2
|
| 371 |
+
|
| 372 |
+
G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
|
| 373 |
+
|
| 374 |
+
g2_hash_with_edge_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 375 |
+
G2, edge_attr="edge_attr1"
|
| 376 |
+
)
|
| 377 |
+
g2_hash_with_edge_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 378 |
+
G2, edge_attr="edge_attr2"
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
assert g1_hash_with_edge_attr1 == {
|
| 382 |
+
-1 * k: v for k, v in g2_hash_with_edge_attr1.items()
|
| 383 |
+
}
|
| 384 |
+
assert g1_hash_with_edge_attr2 == {
|
| 385 |
+
-1 * k: v for k, v in g2_hash_with_edge_attr2.items()
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def test_missing_edge_attr_subgraph_hash():
|
| 390 |
+
"""
|
| 391 |
+
If the 'edge_attr' argument is supplied but is missing from an edge in the graph,
|
| 392 |
+
we should raise a KeyError
|
| 393 |
+
"""
|
| 394 |
+
G = nx.Graph()
|
| 395 |
+
G.add_edges_from([(1, 2, {"edge_attr1": "a"}), (1, 3, {})])
|
| 396 |
+
pytest.raises(
|
| 397 |
+
KeyError, nx.weisfeiler_lehman_subgraph_hashes, G, edge_attr="edge_attr1"
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def test_isomorphic_node_attr_subgraph_hash():
|
| 402 |
+
"""
|
| 403 |
+
Isomorphic graphs with differing node attributes should yield different subgraph
|
| 404 |
+
hashes if the 'node_attr' argument is supplied and populated in the graph, and
|
| 405 |
+
all hashes don't collide.
|
| 406 |
+
The output should still be invariant to node-relabeling
|
| 407 |
+
"""
|
| 408 |
+
n, r = 100, 10
|
| 409 |
+
p = 1.0 / r
|
| 410 |
+
for i in range(1, r + 1):
|
| 411 |
+
G1 = nx.erdos_renyi_graph(n, p * i, seed=400 + i)
|
| 412 |
+
|
| 413 |
+
for u in G1.nodes():
|
| 414 |
+
G1.nodes[u]["node_attr1"] = f"{u}-1"
|
| 415 |
+
G1.nodes[u]["node_attr2"] = f"{u}-2"
|
| 416 |
+
|
| 417 |
+
g1_hash_with_node_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 418 |
+
G1, node_attr="node_attr1"
|
| 419 |
+
)
|
| 420 |
+
g1_hash_with_node_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 421 |
+
G1, node_attr="node_attr2"
|
| 422 |
+
)
|
| 423 |
+
g1_hash_no_node_attr = nx.weisfeiler_lehman_subgraph_hashes(G1, node_attr=None)
|
| 424 |
+
|
| 425 |
+
assert g1_hash_with_node_attr1 != g1_hash_no_node_attr
|
| 426 |
+
assert g1_hash_with_node_attr2 != g1_hash_no_node_attr
|
| 427 |
+
assert g1_hash_with_node_attr1 != g1_hash_with_node_attr2
|
| 428 |
+
|
| 429 |
+
G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
|
| 430 |
+
|
| 431 |
+
g2_hash_with_node_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 432 |
+
G2, node_attr="node_attr1"
|
| 433 |
+
)
|
| 434 |
+
g2_hash_with_node_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 435 |
+
G2, node_attr="node_attr2"
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
assert g1_hash_with_node_attr1 == {
|
| 439 |
+
-1 * k: v for k, v in g2_hash_with_node_attr1.items()
|
| 440 |
+
}
|
| 441 |
+
assert g1_hash_with_node_attr2 == {
|
| 442 |
+
-1 * k: v for k, v in g2_hash_with_node_attr2.items()
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def test_missing_node_attr_subgraph_hash():
|
| 447 |
+
"""
|
| 448 |
+
If the 'node_attr' argument is supplied but is missing from a node in the graph,
|
| 449 |
+
we should raise a KeyError
|
| 450 |
+
"""
|
| 451 |
+
G = nx.Graph()
|
| 452 |
+
G.add_nodes_from([(1, {"node_attr1": "a"}), (2, {})])
|
| 453 |
+
G.add_edges_from([(1, 2), (2, 3), (3, 1), (1, 4)])
|
| 454 |
+
pytest.raises(
|
| 455 |
+
KeyError, nx.weisfeiler_lehman_subgraph_hashes, G, node_attr="node_attr1"
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def test_isomorphic_edge_attr_and_node_attr_subgraph_hash():
|
| 460 |
+
"""
|
| 461 |
+
Isomorphic graphs with differing node attributes should yield different subgraph
|
| 462 |
+
hashes if the 'node_attr' and 'edge_attr' argument is supplied and populated in
|
| 463 |
+
the graph, and all hashes don't collide
|
| 464 |
+
The output should still be invariant to node-relabeling
|
| 465 |
+
"""
|
| 466 |
+
n, r = 100, 10
|
| 467 |
+
p = 1.0 / r
|
| 468 |
+
for i in range(1, r + 1):
|
| 469 |
+
G1 = nx.erdos_renyi_graph(n, p * i, seed=500 + i)
|
| 470 |
+
|
| 471 |
+
for u in G1.nodes():
|
| 472 |
+
G1.nodes[u]["node_attr1"] = f"{u}-1"
|
| 473 |
+
G1.nodes[u]["node_attr2"] = f"{u}-2"
|
| 474 |
+
|
| 475 |
+
for a, b in G1.edges:
|
| 476 |
+
G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
|
| 477 |
+
G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
|
| 478 |
+
|
| 479 |
+
g1_hash_edge1_node1 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 480 |
+
G1, edge_attr="edge_attr1", node_attr="node_attr1"
|
| 481 |
+
)
|
| 482 |
+
g1_hash_edge2_node2 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 483 |
+
G1, edge_attr="edge_attr2", node_attr="node_attr2"
|
| 484 |
+
)
|
| 485 |
+
g1_hash_edge1_node2 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 486 |
+
G1, edge_attr="edge_attr1", node_attr="node_attr2"
|
| 487 |
+
)
|
| 488 |
+
g1_hash_no_attr = nx.weisfeiler_lehman_subgraph_hashes(G1)
|
| 489 |
+
|
| 490 |
+
assert g1_hash_edge1_node1 != g1_hash_no_attr
|
| 491 |
+
assert g1_hash_edge2_node2 != g1_hash_no_attr
|
| 492 |
+
assert g1_hash_edge1_node1 != g1_hash_edge2_node2
|
| 493 |
+
assert g1_hash_edge1_node2 != g1_hash_edge2_node2
|
| 494 |
+
assert g1_hash_edge1_node2 != g1_hash_edge1_node1
|
| 495 |
+
|
| 496 |
+
G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
|
| 497 |
+
|
| 498 |
+
g2_hash_edge1_node1 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 499 |
+
G2, edge_attr="edge_attr1", node_attr="node_attr1"
|
| 500 |
+
)
|
| 501 |
+
g2_hash_edge2_node2 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 502 |
+
G2, edge_attr="edge_attr2", node_attr="node_attr2"
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
assert g1_hash_edge1_node1 == {
|
| 506 |
+
-1 * k: v for k, v in g2_hash_edge1_node1.items()
|
| 507 |
+
}
|
| 508 |
+
assert g1_hash_edge2_node2 == {
|
| 509 |
+
-1 * k: v for k, v in g2_hash_edge2_node2.items()
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
def test_iteration_depth():
|
| 514 |
+
"""
|
| 515 |
+
All nodes should have the correct number of subgraph hashes in the output when
|
| 516 |
+
using degree as initial node labels
|
| 517 |
+
Subsequent iteration depths for the same graph should be additive for each node
|
| 518 |
+
"""
|
| 519 |
+
n, r = 100, 10
|
| 520 |
+
p = 1.0 / r
|
| 521 |
+
for i in range(1, r + 1):
|
| 522 |
+
G = nx.erdos_renyi_graph(n, p * i, seed=600 + i)
|
| 523 |
+
|
| 524 |
+
depth3 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=3)
|
| 525 |
+
depth4 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=4)
|
| 526 |
+
depth5 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=5)
|
| 527 |
+
|
| 528 |
+
assert all(len(hashes) == 3 for hashes in depth3.values())
|
| 529 |
+
assert all(len(hashes) == 4 for hashes in depth4.values())
|
| 530 |
+
assert all(len(hashes) == 5 for hashes in depth5.values())
|
| 531 |
+
|
| 532 |
+
assert is_subiteration(depth3, depth4)
|
| 533 |
+
assert is_subiteration(depth4, depth5)
|
| 534 |
+
assert is_subiteration(depth3, depth5)
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
def test_iteration_depth_edge_attr():
|
| 538 |
+
"""
|
| 539 |
+
All nodes should have the correct number of subgraph hashes in the output when
|
| 540 |
+
setting initial node labels empty and using an edge attribute when aggregating
|
| 541 |
+
neighborhoods.
|
| 542 |
+
Subsequent iteration depths for the same graph should be additive for each node
|
| 543 |
+
"""
|
| 544 |
+
n, r = 100, 10
|
| 545 |
+
p = 1.0 / r
|
| 546 |
+
for i in range(1, r + 1):
|
| 547 |
+
G = nx.erdos_renyi_graph(n, p * i, seed=700 + i)
|
| 548 |
+
|
| 549 |
+
for a, b in G.edges:
|
| 550 |
+
G[a][b]["edge_attr1"] = f"{a}-{b}-1"
|
| 551 |
+
|
| 552 |
+
depth3 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 553 |
+
G, edge_attr="edge_attr1", iterations=3
|
| 554 |
+
)
|
| 555 |
+
depth4 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 556 |
+
G, edge_attr="edge_attr1", iterations=4
|
| 557 |
+
)
|
| 558 |
+
depth5 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 559 |
+
G, edge_attr="edge_attr1", iterations=5
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
assert all(len(hashes) == 3 for hashes in depth3.values())
|
| 563 |
+
assert all(len(hashes) == 4 for hashes in depth4.values())
|
| 564 |
+
assert all(len(hashes) == 5 for hashes in depth5.values())
|
| 565 |
+
|
| 566 |
+
assert is_subiteration(depth3, depth4)
|
| 567 |
+
assert is_subiteration(depth4, depth5)
|
| 568 |
+
assert is_subiteration(depth3, depth5)
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def test_iteration_depth_node_attr():
|
| 572 |
+
"""
|
| 573 |
+
All nodes should have the correct number of subgraph hashes in the output when
|
| 574 |
+
setting initial node labels to an attribute.
|
| 575 |
+
Subsequent iteration depths for the same graph should be additive for each node
|
| 576 |
+
"""
|
| 577 |
+
n, r = 100, 10
|
| 578 |
+
p = 1.0 / r
|
| 579 |
+
for i in range(1, r + 1):
|
| 580 |
+
G = nx.erdos_renyi_graph(n, p * i, seed=800 + i)
|
| 581 |
+
|
| 582 |
+
for u in G.nodes():
|
| 583 |
+
G.nodes[u]["node_attr1"] = f"{u}-1"
|
| 584 |
+
|
| 585 |
+
depth3 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 586 |
+
G, node_attr="node_attr1", iterations=3
|
| 587 |
+
)
|
| 588 |
+
depth4 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 589 |
+
G, node_attr="node_attr1", iterations=4
|
| 590 |
+
)
|
| 591 |
+
depth5 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 592 |
+
G, node_attr="node_attr1", iterations=5
|
| 593 |
+
)
|
| 594 |
+
|
| 595 |
+
assert all(len(hashes) == 3 for hashes in depth3.values())
|
| 596 |
+
assert all(len(hashes) == 4 for hashes in depth4.values())
|
| 597 |
+
assert all(len(hashes) == 5 for hashes in depth5.values())
|
| 598 |
+
|
| 599 |
+
assert is_subiteration(depth3, depth4)
|
| 600 |
+
assert is_subiteration(depth4, depth5)
|
| 601 |
+
assert is_subiteration(depth3, depth5)
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
def test_iteration_depth_node_edge_attr():
|
| 605 |
+
"""
|
| 606 |
+
All nodes should have the correct number of subgraph hashes in the output when
|
| 607 |
+
setting initial node labels to an attribute and also using an edge attribute when
|
| 608 |
+
aggregating neighborhoods.
|
| 609 |
+
Subsequent iteration depths for the same graph should be additive for each node
|
| 610 |
+
"""
|
| 611 |
+
n, r = 100, 10
|
| 612 |
+
p = 1.0 / r
|
| 613 |
+
for i in range(1, r + 1):
|
| 614 |
+
G = nx.erdos_renyi_graph(n, p * i, seed=900 + i)
|
| 615 |
+
|
| 616 |
+
for u in G.nodes():
|
| 617 |
+
G.nodes[u]["node_attr1"] = f"{u}-1"
|
| 618 |
+
|
| 619 |
+
for a, b in G.edges:
|
| 620 |
+
G[a][b]["edge_attr1"] = f"{a}-{b}-1"
|
| 621 |
+
|
| 622 |
+
depth3 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 623 |
+
G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=3
|
| 624 |
+
)
|
| 625 |
+
depth4 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 626 |
+
G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=4
|
| 627 |
+
)
|
| 628 |
+
depth5 = nx.weisfeiler_lehman_subgraph_hashes(
|
| 629 |
+
G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=5
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
assert all(len(hashes) == 3 for hashes in depth3.values())
|
| 633 |
+
assert all(len(hashes) == 4 for hashes in depth4.values())
|
| 634 |
+
assert all(len(hashes) == 5 for hashes in depth5.values())
|
| 635 |
+
|
| 636 |
+
assert is_subiteration(depth3, depth4)
|
| 637 |
+
assert is_subiteration(depth4, depth5)
|
| 638 |
+
assert is_subiteration(depth3, depth5)
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
def test_digest_size_subgraph_hash():
|
| 642 |
+
"""
|
| 643 |
+
The hash string lengths should be as expected for a variety of graphs and
|
| 644 |
+
digest sizes
|
| 645 |
+
"""
|
| 646 |
+
n, r = 100, 10
|
| 647 |
+
p = 1.0 / r
|
| 648 |
+
for i in range(1, r + 1):
|
| 649 |
+
G = nx.erdos_renyi_graph(n, p * i, seed=1000 + i)
|
| 650 |
+
|
| 651 |
+
digest_size16_hashes = nx.weisfeiler_lehman_subgraph_hashes(G)
|
| 652 |
+
digest_size32_hashes = nx.weisfeiler_lehman_subgraph_hashes(G, digest_size=32)
|
| 653 |
+
|
| 654 |
+
assert digest_size16_hashes != digest_size32_hashes
|
| 655 |
+
|
| 656 |
+
assert hexdigest_sizes_correct(digest_size16_hashes, 16)
|
| 657 |
+
assert hexdigest_sizes_correct(digest_size32_hashes, 32)
|
| 658 |
+
|
| 659 |
+
|
| 660 |
+
def test_initial_node_labels_subgraph_hash():
|
| 661 |
+
"""
|
| 662 |
+
Including the hashed initial label prepends an extra hash to the lists
|
| 663 |
+
"""
|
| 664 |
+
G = nx.path_graph(5)
|
| 665 |
+
nx.set_node_attributes(G, {i: int(0 < i < 4) for i in G}, "label")
|
| 666 |
+
# initial node labels:
|
| 667 |
+
# 0--1--1--1--0
|
| 668 |
+
|
| 669 |
+
without_initial_label = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="label")
|
| 670 |
+
assert all(len(v) == 3 for v in without_initial_label.values())
|
| 671 |
+
# 3 different 1 hop nhds
|
| 672 |
+
assert len({v[0] for v in without_initial_label.values()}) == 3
|
| 673 |
+
|
| 674 |
+
with_initial_label = nx.weisfeiler_lehman_subgraph_hashes(
|
| 675 |
+
G, node_attr="label", include_initial_labels=True
|
| 676 |
+
)
|
| 677 |
+
assert all(len(v) == 4 for v in with_initial_label.values())
|
| 678 |
+
# 2 different initial labels
|
| 679 |
+
assert len({v[0] for v in with_initial_label.values()}) == 2
|
| 680 |
+
|
| 681 |
+
# check hashes match otherwise
|
| 682 |
+
for u in G:
|
| 683 |
+
for a, b in zip(
|
| 684 |
+
with_initial_label[u][1:], without_initial_label[u], strict=True
|
| 685 |
+
):
|
| 686 |
+
assert a == b
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_graphical.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_valid_degree_sequence1():
|
| 7 |
+
n = 100
|
| 8 |
+
p = 0.3
|
| 9 |
+
for i in range(10):
|
| 10 |
+
G = nx.erdos_renyi_graph(n, p)
|
| 11 |
+
deg = (d for n, d in G.degree())
|
| 12 |
+
assert nx.is_graphical(deg, method="eg")
|
| 13 |
+
assert nx.is_graphical(deg, method="hh")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_valid_degree_sequence2():
|
| 17 |
+
n = 100
|
| 18 |
+
for i in range(10):
|
| 19 |
+
G = nx.barabasi_albert_graph(n, 1)
|
| 20 |
+
deg = (d for n, d in G.degree())
|
| 21 |
+
assert nx.is_graphical(deg, method="eg")
|
| 22 |
+
assert nx.is_graphical(deg, method="hh")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_string_input():
|
| 26 |
+
pytest.raises(nx.NetworkXException, nx.is_graphical, [], "foo")
|
| 27 |
+
pytest.raises(nx.NetworkXException, nx.is_graphical, ["red"], "hh")
|
| 28 |
+
pytest.raises(nx.NetworkXException, nx.is_graphical, ["red"], "eg")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_non_integer_input():
|
| 32 |
+
pytest.raises(nx.NetworkXException, nx.is_graphical, [72.5], "eg")
|
| 33 |
+
pytest.raises(nx.NetworkXException, nx.is_graphical, [72.5], "hh")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def test_negative_input():
|
| 37 |
+
assert not nx.is_graphical([-1], "hh")
|
| 38 |
+
assert not nx.is_graphical([-1], "eg")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class TestAtlas:
|
| 42 |
+
@classmethod
|
| 43 |
+
def setup_class(cls):
|
| 44 |
+
global atlas
|
| 45 |
+
from networkx.generators import atlas
|
| 46 |
+
|
| 47 |
+
cls.GAG = atlas.graph_atlas_g()
|
| 48 |
+
|
| 49 |
+
def test_atlas(self):
|
| 50 |
+
for graph in self.GAG:
|
| 51 |
+
deg = (d for n, d in graph.degree())
|
| 52 |
+
assert nx.is_graphical(deg, method="eg")
|
| 53 |
+
assert nx.is_graphical(deg, method="hh")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def test_small_graph_true():
|
| 57 |
+
z = [5, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
|
| 58 |
+
assert nx.is_graphical(z, method="hh")
|
| 59 |
+
assert nx.is_graphical(z, method="eg")
|
| 60 |
+
z = [10, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2]
|
| 61 |
+
assert nx.is_graphical(z, method="hh")
|
| 62 |
+
assert nx.is_graphical(z, method="eg")
|
| 63 |
+
z = [1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
|
| 64 |
+
assert nx.is_graphical(z, method="hh")
|
| 65 |
+
assert nx.is_graphical(z, method="eg")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def test_small_graph_false():
|
| 69 |
+
z = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
|
| 70 |
+
assert not nx.is_graphical(z, method="hh")
|
| 71 |
+
assert not nx.is_graphical(z, method="eg")
|
| 72 |
+
z = [6, 5, 4, 4, 2, 1, 1, 1]
|
| 73 |
+
assert not nx.is_graphical(z, method="hh")
|
| 74 |
+
assert not nx.is_graphical(z, method="eg")
|
| 75 |
+
z = [1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
|
| 76 |
+
assert not nx.is_graphical(z, method="hh")
|
| 77 |
+
assert not nx.is_graphical(z, method="eg")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_directed_degree_sequence():
|
| 81 |
+
# Test a range of valid directed degree sequences
|
| 82 |
+
n, r = 100, 10
|
| 83 |
+
p = 1.0 / r
|
| 84 |
+
for i in range(r):
|
| 85 |
+
G = nx.erdos_renyi_graph(n, p * (i + 1), None, True)
|
| 86 |
+
din = (d for n, d in G.in_degree())
|
| 87 |
+
dout = (d for n, d in G.out_degree())
|
| 88 |
+
assert nx.is_digraphical(din, dout)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def test_small_directed_sequences():
|
| 92 |
+
dout = [5, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
|
| 93 |
+
din = [3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1]
|
| 94 |
+
assert nx.is_digraphical(din, dout)
|
| 95 |
+
# Test nongraphical directed sequence
|
| 96 |
+
dout = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
|
| 97 |
+
din = [103, 102, 102, 102, 102, 102, 102, 102, 102, 102]
|
| 98 |
+
assert not nx.is_digraphical(din, dout)
|
| 99 |
+
# Test digraphical small sequence
|
| 100 |
+
dout = [1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
|
| 101 |
+
din = [2, 2, 2, 2, 2, 2, 2, 2, 1, 1]
|
| 102 |
+
assert nx.is_digraphical(din, dout)
|
| 103 |
+
# Test nonmatching sum
|
| 104 |
+
din = [2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1]
|
| 105 |
+
assert not nx.is_digraphical(din, dout)
|
| 106 |
+
# Test for negative integer in sequence
|
| 107 |
+
din = [2, 2, 2, -2, 2, 2, 2, 2, 1, 1, 4]
|
| 108 |
+
assert not nx.is_digraphical(din, dout)
|
| 109 |
+
# Test for noninteger
|
| 110 |
+
din = dout = [1, 1, 1.1, 1]
|
| 111 |
+
assert not nx.is_digraphical(din, dout)
|
| 112 |
+
din = dout = [1, 1, "rer", 1]
|
| 113 |
+
assert not nx.is_digraphical(din, dout)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def test_multi_sequence():
|
| 117 |
+
# Test nongraphical multi sequence
|
| 118 |
+
seq = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1]
|
| 119 |
+
assert not nx.is_multigraphical(seq)
|
| 120 |
+
# Test small graphical multi sequence
|
| 121 |
+
seq = [6, 5, 4, 4, 2, 1, 1, 1]
|
| 122 |
+
assert nx.is_multigraphical(seq)
|
| 123 |
+
# Test for negative integer in sequence
|
| 124 |
+
seq = [6, 5, 4, -4, 2, 1, 1, 1]
|
| 125 |
+
assert not nx.is_multigraphical(seq)
|
| 126 |
+
# Test for sequence with odd sum
|
| 127 |
+
seq = [1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
|
| 128 |
+
assert not nx.is_multigraphical(seq)
|
| 129 |
+
# Test for noninteger
|
| 130 |
+
seq = [1, 1, 1.1, 1]
|
| 131 |
+
assert not nx.is_multigraphical(seq)
|
| 132 |
+
seq = [1, 1, "rer", 1]
|
| 133 |
+
assert not nx.is_multigraphical(seq)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def test_pseudo_sequence():
|
| 137 |
+
# Test small valid pseudo sequence
|
| 138 |
+
seq = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1]
|
| 139 |
+
assert nx.is_pseudographical(seq)
|
| 140 |
+
# Test for sequence with odd sum
|
| 141 |
+
seq = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
|
| 142 |
+
assert not nx.is_pseudographical(seq)
|
| 143 |
+
# Test for negative integer in sequence
|
| 144 |
+
seq = [1000, 3, 3, 3, 3, 2, 2, -2, 1, 1]
|
| 145 |
+
assert not nx.is_pseudographical(seq)
|
| 146 |
+
# Test for noninteger
|
| 147 |
+
seq = [1, 1, 1.1, 1]
|
| 148 |
+
assert not nx.is_pseudographical(seq)
|
| 149 |
+
seq = [1, 1, "rer", 1]
|
| 150 |
+
assert not nx.is_pseudographical(seq)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def test_numpy_degree_sequence():
|
| 154 |
+
np = pytest.importorskip("numpy")
|
| 155 |
+
ds = np.array([1, 2, 2, 2, 1], dtype=np.int64)
|
| 156 |
+
assert nx.is_graphical(ds, "eg")
|
| 157 |
+
assert nx.is_graphical(ds, "hh")
|
| 158 |
+
ds = np.array([1, 2, 2, 2, 1], dtype=np.float64)
|
| 159 |
+
assert nx.is_graphical(ds, "eg")
|
| 160 |
+
assert nx.is_graphical(ds, "hh")
|
| 161 |
+
ds = np.array([1.1, 2, 2, 2, 1], dtype=np.float64)
|
| 162 |
+
pytest.raises(nx.NetworkXException, nx.is_graphical, ds, "eg")
|
| 163 |
+
pytest.raises(nx.NetworkXException, nx.is_graphical, ds, "hh")
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_hierarchy.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_hierarchy_undirected():
|
| 7 |
+
G = nx.cycle_graph(5)
|
| 8 |
+
pytest.raises(nx.NetworkXError, nx.flow_hierarchy, G)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_hierarchy_cycle():
|
| 12 |
+
G = nx.cycle_graph(5, create_using=nx.DiGraph())
|
| 13 |
+
assert nx.flow_hierarchy(G) == 0.0
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_hierarchy_tree():
|
| 17 |
+
G = nx.full_rary_tree(2, 16, create_using=nx.DiGraph())
|
| 18 |
+
assert nx.flow_hierarchy(G) == 1.0
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_hierarchy_1():
|
| 22 |
+
G = nx.DiGraph()
|
| 23 |
+
G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 1), (3, 4), (0, 4)])
|
| 24 |
+
assert nx.flow_hierarchy(G) == 0.5
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def test_hierarchy_weight():
|
| 28 |
+
G = nx.DiGraph()
|
| 29 |
+
G.add_edges_from(
|
| 30 |
+
[
|
| 31 |
+
(0, 1, {"weight": 0.3}),
|
| 32 |
+
(1, 2, {"weight": 0.1}),
|
| 33 |
+
(2, 3, {"weight": 0.1}),
|
| 34 |
+
(3, 1, {"weight": 0.1}),
|
| 35 |
+
(3, 4, {"weight": 0.3}),
|
| 36 |
+
(0, 4, {"weight": 0.3}),
|
| 37 |
+
]
|
| 38 |
+
)
|
| 39 |
+
assert nx.flow_hierarchy(G, weight="weight") == 0.75
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@pytest.mark.parametrize("n", (0, 1, 3))
|
| 43 |
+
def test_hierarchy_empty_graph(n):
|
| 44 |
+
G = nx.empty_graph(n, create_using=nx.DiGraph)
|
| 45 |
+
with pytest.raises(nx.NetworkXError, match=".*not applicable to empty graphs"):
|
| 46 |
+
nx.flow_hierarchy(G)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_hybrid.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import networkx as nx
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_2d_grid_graph():
|
| 5 |
+
# FC article claims 2d grid graph of size n is (3,3)-connected
|
| 6 |
+
# and (5,9)-connected, but I don't think it is (5,9)-connected
|
| 7 |
+
G = nx.grid_2d_graph(8, 8, periodic=True)
|
| 8 |
+
assert nx.is_kl_connected(G, 3, 3)
|
| 9 |
+
assert not nx.is_kl_connected(G, 5, 9)
|
| 10 |
+
(H, graphOK) = nx.kl_connected_subgraph(G, 5, 9, same_as_graph=True)
|
| 11 |
+
assert not graphOK
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_small_graph():
|
| 15 |
+
G = nx.Graph()
|
| 16 |
+
G.add_edge(1, 2)
|
| 17 |
+
G.add_edge(1, 3)
|
| 18 |
+
G.add_edge(2, 3)
|
| 19 |
+
assert nx.is_kl_connected(G, 2, 2)
|
| 20 |
+
H = nx.kl_connected_subgraph(G, 2, 2)
|
| 21 |
+
(H, graphOK) = nx.kl_connected_subgraph(
|
| 22 |
+
G, 2, 2, low_memory=True, same_as_graph=True
|
| 23 |
+
)
|
| 24 |
+
assert graphOK
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_isolate.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for the :mod:`networkx.algorithms.isolates` module."""
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_is_isolate():
|
| 7 |
+
G = nx.Graph()
|
| 8 |
+
G.add_edge(0, 1)
|
| 9 |
+
G.add_node(2)
|
| 10 |
+
assert not nx.is_isolate(G, 0)
|
| 11 |
+
assert not nx.is_isolate(G, 1)
|
| 12 |
+
assert nx.is_isolate(G, 2)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_isolates():
|
| 16 |
+
G = nx.Graph()
|
| 17 |
+
G.add_edge(0, 1)
|
| 18 |
+
G.add_nodes_from([2, 3])
|
| 19 |
+
assert sorted(nx.isolates(G)) == [2, 3]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def test_number_of_isolates():
|
| 23 |
+
G = nx.Graph()
|
| 24 |
+
G.add_edge(0, 1)
|
| 25 |
+
G.add_nodes_from([2, 3])
|
| 26 |
+
assert nx.number_of_isolates(G) == 2
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_link_prediction.py
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from functools import partial
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _test_func(G, ebunch, expected, predict_func, **kwargs):
|
| 10 |
+
result = predict_func(G, ebunch, **kwargs)
|
| 11 |
+
exp_dict = {tuple(sorted([u, v])): score for u, v, score in expected}
|
| 12 |
+
res_dict = {tuple(sorted([u, v])): score for u, v, score in result}
|
| 13 |
+
|
| 14 |
+
assert len(exp_dict) == len(res_dict)
|
| 15 |
+
for p in exp_dict:
|
| 16 |
+
assert exp_dict[p] == pytest.approx(res_dict[p], abs=1e-7)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class TestResourceAllocationIndex:
|
| 20 |
+
@classmethod
|
| 21 |
+
def setup_class(cls):
|
| 22 |
+
cls.func = staticmethod(nx.resource_allocation_index)
|
| 23 |
+
cls.test = partial(_test_func, predict_func=cls.func)
|
| 24 |
+
|
| 25 |
+
def test_K5(self):
|
| 26 |
+
G = nx.complete_graph(5)
|
| 27 |
+
self.test(G, [(0, 1)], [(0, 1, 0.75)])
|
| 28 |
+
|
| 29 |
+
def test_P3(self):
|
| 30 |
+
G = nx.path_graph(3)
|
| 31 |
+
self.test(G, [(0, 2)], [(0, 2, 0.5)])
|
| 32 |
+
|
| 33 |
+
def test_S4(self):
|
| 34 |
+
G = nx.star_graph(4)
|
| 35 |
+
self.test(G, [(1, 2)], [(1, 2, 0.25)])
|
| 36 |
+
|
| 37 |
+
@pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
|
| 38 |
+
def test_notimplemented(self, graph_type):
|
| 39 |
+
assert pytest.raises(
|
| 40 |
+
nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
def test_node_not_found(self):
|
| 44 |
+
G = nx.Graph()
|
| 45 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 46 |
+
assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
|
| 47 |
+
|
| 48 |
+
def test_no_common_neighbor(self):
|
| 49 |
+
G = nx.Graph()
|
| 50 |
+
G.add_nodes_from([0, 1])
|
| 51 |
+
self.test(G, [(0, 1)], [(0, 1, 0)])
|
| 52 |
+
|
| 53 |
+
def test_equal_nodes(self):
|
| 54 |
+
G = nx.complete_graph(4)
|
| 55 |
+
self.test(G, [(0, 0)], [(0, 0, 1)])
|
| 56 |
+
|
| 57 |
+
def test_all_nonexistent_edges(self):
|
| 58 |
+
G = nx.Graph()
|
| 59 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 60 |
+
self.test(G, None, [(0, 3, 0.5), (1, 2, 0.5), (1, 3, 0)])
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class TestJaccardCoefficient:
|
| 64 |
+
@classmethod
|
| 65 |
+
def setup_class(cls):
|
| 66 |
+
cls.func = staticmethod(nx.jaccard_coefficient)
|
| 67 |
+
cls.test = partial(_test_func, predict_func=cls.func)
|
| 68 |
+
|
| 69 |
+
def test_K5(self):
|
| 70 |
+
G = nx.complete_graph(5)
|
| 71 |
+
self.test(G, [(0, 1)], [(0, 1, 0.6)])
|
| 72 |
+
|
| 73 |
+
def test_P4(self):
|
| 74 |
+
G = nx.path_graph(4)
|
| 75 |
+
self.test(G, [(0, 2)], [(0, 2, 0.5)])
|
| 76 |
+
|
| 77 |
+
@pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
|
| 78 |
+
def test_notimplemented(self, graph_type):
|
| 79 |
+
assert pytest.raises(
|
| 80 |
+
nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
def test_node_not_found(self):
|
| 84 |
+
G = nx.Graph()
|
| 85 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 86 |
+
assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
|
| 87 |
+
|
| 88 |
+
def test_no_common_neighbor(self):
|
| 89 |
+
G = nx.Graph()
|
| 90 |
+
G.add_edges_from([(0, 1), (2, 3)])
|
| 91 |
+
self.test(G, [(0, 2)], [(0, 2, 0)])
|
| 92 |
+
|
| 93 |
+
def test_isolated_nodes(self):
|
| 94 |
+
G = nx.Graph()
|
| 95 |
+
G.add_nodes_from([0, 1])
|
| 96 |
+
self.test(G, [(0, 1)], [(0, 1, 0)])
|
| 97 |
+
|
| 98 |
+
def test_all_nonexistent_edges(self):
|
| 99 |
+
G = nx.Graph()
|
| 100 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 101 |
+
self.test(G, None, [(0, 3, 0.5), (1, 2, 0.5), (1, 3, 0)])
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class TestAdamicAdarIndex:
|
| 105 |
+
@classmethod
|
| 106 |
+
def setup_class(cls):
|
| 107 |
+
cls.func = staticmethod(nx.adamic_adar_index)
|
| 108 |
+
cls.test = partial(_test_func, predict_func=cls.func)
|
| 109 |
+
|
| 110 |
+
def test_K5(self):
|
| 111 |
+
G = nx.complete_graph(5)
|
| 112 |
+
self.test(G, [(0, 1)], [(0, 1, 3 / math.log(4))])
|
| 113 |
+
|
| 114 |
+
def test_P3(self):
|
| 115 |
+
G = nx.path_graph(3)
|
| 116 |
+
self.test(G, [(0, 2)], [(0, 2, 1 / math.log(2))])
|
| 117 |
+
|
| 118 |
+
def test_S4(self):
|
| 119 |
+
G = nx.star_graph(4)
|
| 120 |
+
self.test(G, [(1, 2)], [(1, 2, 1 / math.log(4))])
|
| 121 |
+
|
| 122 |
+
@pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
|
| 123 |
+
def test_notimplemented(self, graph_type):
|
| 124 |
+
assert pytest.raises(
|
| 125 |
+
nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
def test_node_not_found(self):
|
| 129 |
+
G = nx.Graph()
|
| 130 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 131 |
+
assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
|
| 132 |
+
|
| 133 |
+
def test_no_common_neighbor(self):
|
| 134 |
+
G = nx.Graph()
|
| 135 |
+
G.add_nodes_from([0, 1])
|
| 136 |
+
self.test(G, [(0, 1)], [(0, 1, 0)])
|
| 137 |
+
|
| 138 |
+
def test_equal_nodes(self):
|
| 139 |
+
G = nx.complete_graph(4)
|
| 140 |
+
self.test(G, [(0, 0)], [(0, 0, 3 / math.log(3))])
|
| 141 |
+
|
| 142 |
+
def test_all_nonexistent_edges(self):
|
| 143 |
+
G = nx.Graph()
|
| 144 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 145 |
+
self.test(
|
| 146 |
+
G, None, [(0, 3, 1 / math.log(2)), (1, 2, 1 / math.log(2)), (1, 3, 0)]
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class TestCommonNeighborCentrality:
|
| 151 |
+
@classmethod
|
| 152 |
+
def setup_class(cls):
|
| 153 |
+
cls.func = staticmethod(nx.common_neighbor_centrality)
|
| 154 |
+
cls.test = partial(_test_func, predict_func=cls.func)
|
| 155 |
+
|
| 156 |
+
def test_K5(self):
|
| 157 |
+
G = nx.complete_graph(5)
|
| 158 |
+
self.test(G, [(0, 1)], [(0, 1, 3.0)], alpha=1)
|
| 159 |
+
self.test(G, [(0, 1)], [(0, 1, 5.0)], alpha=0)
|
| 160 |
+
|
| 161 |
+
def test_P3(self):
|
| 162 |
+
G = nx.path_graph(3)
|
| 163 |
+
self.test(G, [(0, 2)], [(0, 2, 1.25)], alpha=0.5)
|
| 164 |
+
|
| 165 |
+
def test_S4(self):
|
| 166 |
+
G = nx.star_graph(4)
|
| 167 |
+
self.test(G, [(1, 2)], [(1, 2, 1.75)], alpha=0.5)
|
| 168 |
+
|
| 169 |
+
@pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
|
| 170 |
+
def test_notimplemented(self, graph_type):
|
| 171 |
+
assert pytest.raises(
|
| 172 |
+
nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
def test_node_u_not_found(self):
|
| 176 |
+
G = nx.Graph()
|
| 177 |
+
G.add_edges_from([(1, 3), (2, 3)])
|
| 178 |
+
assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 1)])
|
| 179 |
+
|
| 180 |
+
def test_node_v_not_found(self):
|
| 181 |
+
G = nx.Graph()
|
| 182 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 183 |
+
assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
|
| 184 |
+
|
| 185 |
+
def test_no_common_neighbor(self):
|
| 186 |
+
G = nx.Graph()
|
| 187 |
+
G.add_nodes_from([0, 1])
|
| 188 |
+
self.test(G, [(0, 1)], [(0, 1, 0)])
|
| 189 |
+
|
| 190 |
+
def test_equal_nodes(self):
|
| 191 |
+
G = nx.complete_graph(4)
|
| 192 |
+
assert pytest.raises(nx.NetworkXAlgorithmError, self.test, G, [(0, 0)], [])
|
| 193 |
+
|
| 194 |
+
def test_equal_nodes_with_alpha_one_raises_error(self):
|
| 195 |
+
G = nx.complete_graph(4)
|
| 196 |
+
assert pytest.raises(
|
| 197 |
+
nx.NetworkXAlgorithmError, self.test, G, [(0, 0)], [], alpha=1.0
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
def test_all_nonexistent_edges(self):
|
| 201 |
+
G = nx.Graph()
|
| 202 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 203 |
+
self.test(G, None, [(0, 3, 1.5), (1, 2, 1.5), (1, 3, 2 / 3)], alpha=0.5)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class TestPreferentialAttachment:
|
| 207 |
+
@classmethod
|
| 208 |
+
def setup_class(cls):
|
| 209 |
+
cls.func = staticmethod(nx.preferential_attachment)
|
| 210 |
+
cls.test = partial(_test_func, predict_func=cls.func)
|
| 211 |
+
|
| 212 |
+
def test_K5(self):
|
| 213 |
+
G = nx.complete_graph(5)
|
| 214 |
+
self.test(G, [(0, 1)], [(0, 1, 16)])
|
| 215 |
+
|
| 216 |
+
def test_P3(self):
|
| 217 |
+
G = nx.path_graph(3)
|
| 218 |
+
self.test(G, [(0, 1)], [(0, 1, 2)])
|
| 219 |
+
|
| 220 |
+
def test_S4(self):
|
| 221 |
+
G = nx.star_graph(4)
|
| 222 |
+
self.test(G, [(0, 2)], [(0, 2, 4)])
|
| 223 |
+
|
| 224 |
+
@pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
|
| 225 |
+
def test_notimplemented(self, graph_type):
|
| 226 |
+
assert pytest.raises(
|
| 227 |
+
nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
def test_node_not_found(self):
|
| 231 |
+
G = nx.Graph()
|
| 232 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 233 |
+
assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
|
| 234 |
+
|
| 235 |
+
def test_zero_degrees(self):
|
| 236 |
+
G = nx.Graph()
|
| 237 |
+
G.add_nodes_from([0, 1])
|
| 238 |
+
self.test(G, [(0, 1)], [(0, 1, 0)])
|
| 239 |
+
|
| 240 |
+
def test_all_nonexistent_edges(self):
|
| 241 |
+
G = nx.Graph()
|
| 242 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 243 |
+
self.test(G, None, [(0, 3, 2), (1, 2, 2), (1, 3, 1)])
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
class TestCNSoundarajanHopcroft:
|
| 247 |
+
@classmethod
|
| 248 |
+
def setup_class(cls):
|
| 249 |
+
cls.func = staticmethod(nx.cn_soundarajan_hopcroft)
|
| 250 |
+
cls.test = partial(_test_func, predict_func=cls.func, community="community")
|
| 251 |
+
|
| 252 |
+
def test_K5(self):
|
| 253 |
+
G = nx.complete_graph(5)
|
| 254 |
+
G.nodes[0]["community"] = 0
|
| 255 |
+
G.nodes[1]["community"] = 0
|
| 256 |
+
G.nodes[2]["community"] = 0
|
| 257 |
+
G.nodes[3]["community"] = 0
|
| 258 |
+
G.nodes[4]["community"] = 1
|
| 259 |
+
self.test(G, [(0, 1)], [(0, 1, 5)])
|
| 260 |
+
|
| 261 |
+
def test_P3(self):
|
| 262 |
+
G = nx.path_graph(3)
|
| 263 |
+
G.nodes[0]["community"] = 0
|
| 264 |
+
G.nodes[1]["community"] = 1
|
| 265 |
+
G.nodes[2]["community"] = 0
|
| 266 |
+
self.test(G, [(0, 2)], [(0, 2, 1)])
|
| 267 |
+
|
| 268 |
+
def test_S4(self):
|
| 269 |
+
G = nx.star_graph(4)
|
| 270 |
+
G.nodes[0]["community"] = 1
|
| 271 |
+
G.nodes[1]["community"] = 1
|
| 272 |
+
G.nodes[2]["community"] = 1
|
| 273 |
+
G.nodes[3]["community"] = 0
|
| 274 |
+
G.nodes[4]["community"] = 0
|
| 275 |
+
self.test(G, [(1, 2)], [(1, 2, 2)])
|
| 276 |
+
|
| 277 |
+
@pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
|
| 278 |
+
def test_notimplemented(self, graph_type):
|
| 279 |
+
G = graph_type([(0, 1), (1, 2)])
|
| 280 |
+
G.add_nodes_from([0, 1, 2], community=0)
|
| 281 |
+
assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
|
| 282 |
+
|
| 283 |
+
def test_node_not_found(self):
|
| 284 |
+
G = nx.Graph()
|
| 285 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 286 |
+
G.nodes[0]["community"] = 0
|
| 287 |
+
G.nodes[1]["community"] = 1
|
| 288 |
+
G.nodes[2]["community"] = 0
|
| 289 |
+
G.nodes[3]["community"] = 0
|
| 290 |
+
assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
|
| 291 |
+
|
| 292 |
+
def test_no_common_neighbor(self):
|
| 293 |
+
G = nx.Graph()
|
| 294 |
+
G.add_nodes_from([0, 1])
|
| 295 |
+
G.nodes[0]["community"] = 0
|
| 296 |
+
G.nodes[1]["community"] = 0
|
| 297 |
+
self.test(G, [(0, 1)], [(0, 1, 0)])
|
| 298 |
+
|
| 299 |
+
def test_equal_nodes(self):
|
| 300 |
+
G = nx.complete_graph(3)
|
| 301 |
+
G.nodes[0]["community"] = 0
|
| 302 |
+
G.nodes[1]["community"] = 0
|
| 303 |
+
G.nodes[2]["community"] = 0
|
| 304 |
+
self.test(G, [(0, 0)], [(0, 0, 4)])
|
| 305 |
+
|
| 306 |
+
def test_different_community(self):
|
| 307 |
+
G = nx.Graph()
|
| 308 |
+
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 309 |
+
G.nodes[0]["community"] = 0
|
| 310 |
+
G.nodes[1]["community"] = 0
|
| 311 |
+
G.nodes[2]["community"] = 0
|
| 312 |
+
G.nodes[3]["community"] = 1
|
| 313 |
+
self.test(G, [(0, 3)], [(0, 3, 2)])
|
| 314 |
+
|
| 315 |
+
def test_no_community_information(self):
|
| 316 |
+
G = nx.complete_graph(5)
|
| 317 |
+
assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 1)]))
|
| 318 |
+
|
| 319 |
+
def test_insufficient_community_information(self):
|
| 320 |
+
G = nx.Graph()
|
| 321 |
+
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 322 |
+
G.nodes[0]["community"] = 0
|
| 323 |
+
G.nodes[1]["community"] = 0
|
| 324 |
+
G.nodes[3]["community"] = 0
|
| 325 |
+
assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 3)]))
|
| 326 |
+
|
| 327 |
+
def test_sufficient_community_information(self):
|
| 328 |
+
G = nx.Graph()
|
| 329 |
+
G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)])
|
| 330 |
+
G.nodes[1]["community"] = 0
|
| 331 |
+
G.nodes[2]["community"] = 0
|
| 332 |
+
G.nodes[3]["community"] = 0
|
| 333 |
+
G.nodes[4]["community"] = 0
|
| 334 |
+
self.test(G, [(1, 4)], [(1, 4, 4)])
|
| 335 |
+
|
| 336 |
+
def test_custom_community_attribute_name(self):
|
| 337 |
+
G = nx.Graph()
|
| 338 |
+
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 339 |
+
G.nodes[0]["cmty"] = 0
|
| 340 |
+
G.nodes[1]["cmty"] = 0
|
| 341 |
+
G.nodes[2]["cmty"] = 0
|
| 342 |
+
G.nodes[3]["cmty"] = 1
|
| 343 |
+
self.test(G, [(0, 3)], [(0, 3, 2)], community="cmty")
|
| 344 |
+
|
| 345 |
+
def test_all_nonexistent_edges(self):
|
| 346 |
+
G = nx.Graph()
|
| 347 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 348 |
+
G.nodes[0]["community"] = 0
|
| 349 |
+
G.nodes[1]["community"] = 1
|
| 350 |
+
G.nodes[2]["community"] = 0
|
| 351 |
+
G.nodes[3]["community"] = 0
|
| 352 |
+
self.test(G, None, [(0, 3, 2), (1, 2, 1), (1, 3, 0)])
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
class TestRAIndexSoundarajanHopcroft:
|
| 356 |
+
@classmethod
|
| 357 |
+
def setup_class(cls):
|
| 358 |
+
cls.func = staticmethod(nx.ra_index_soundarajan_hopcroft)
|
| 359 |
+
cls.test = partial(_test_func, predict_func=cls.func, community="community")
|
| 360 |
+
|
| 361 |
+
def test_K5(self):
|
| 362 |
+
G = nx.complete_graph(5)
|
| 363 |
+
G.nodes[0]["community"] = 0
|
| 364 |
+
G.nodes[1]["community"] = 0
|
| 365 |
+
G.nodes[2]["community"] = 0
|
| 366 |
+
G.nodes[3]["community"] = 0
|
| 367 |
+
G.nodes[4]["community"] = 1
|
| 368 |
+
self.test(G, [(0, 1)], [(0, 1, 0.5)])
|
| 369 |
+
|
| 370 |
+
def test_P3(self):
|
| 371 |
+
G = nx.path_graph(3)
|
| 372 |
+
G.nodes[0]["community"] = 0
|
| 373 |
+
G.nodes[1]["community"] = 1
|
| 374 |
+
G.nodes[2]["community"] = 0
|
| 375 |
+
self.test(G, [(0, 2)], [(0, 2, 0)])
|
| 376 |
+
|
| 377 |
+
def test_S4(self):
|
| 378 |
+
G = nx.star_graph(4)
|
| 379 |
+
G.nodes[0]["community"] = 1
|
| 380 |
+
G.nodes[1]["community"] = 1
|
| 381 |
+
G.nodes[2]["community"] = 1
|
| 382 |
+
G.nodes[3]["community"] = 0
|
| 383 |
+
G.nodes[4]["community"] = 0
|
| 384 |
+
self.test(G, [(1, 2)], [(1, 2, 0.25)])
|
| 385 |
+
|
| 386 |
+
@pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
|
| 387 |
+
def test_notimplemented(self, graph_type):
|
| 388 |
+
G = graph_type([(0, 1), (1, 2)])
|
| 389 |
+
G.add_nodes_from([0, 1, 2], community=0)
|
| 390 |
+
assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
|
| 391 |
+
|
| 392 |
+
def test_node_not_found(self):
|
| 393 |
+
G = nx.Graph()
|
| 394 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 395 |
+
G.nodes[0]["community"] = 0
|
| 396 |
+
G.nodes[1]["community"] = 1
|
| 397 |
+
G.nodes[2]["community"] = 0
|
| 398 |
+
G.nodes[3]["community"] = 0
|
| 399 |
+
assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
|
| 400 |
+
|
| 401 |
+
def test_no_common_neighbor(self):
|
| 402 |
+
G = nx.Graph()
|
| 403 |
+
G.add_nodes_from([0, 1])
|
| 404 |
+
G.nodes[0]["community"] = 0
|
| 405 |
+
G.nodes[1]["community"] = 0
|
| 406 |
+
self.test(G, [(0, 1)], [(0, 1, 0)])
|
| 407 |
+
|
| 408 |
+
def test_equal_nodes(self):
|
| 409 |
+
G = nx.complete_graph(3)
|
| 410 |
+
G.nodes[0]["community"] = 0
|
| 411 |
+
G.nodes[1]["community"] = 0
|
| 412 |
+
G.nodes[2]["community"] = 0
|
| 413 |
+
self.test(G, [(0, 0)], [(0, 0, 1)])
|
| 414 |
+
|
| 415 |
+
def test_different_community(self):
|
| 416 |
+
G = nx.Graph()
|
| 417 |
+
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 418 |
+
G.nodes[0]["community"] = 0
|
| 419 |
+
G.nodes[1]["community"] = 0
|
| 420 |
+
G.nodes[2]["community"] = 0
|
| 421 |
+
G.nodes[3]["community"] = 1
|
| 422 |
+
self.test(G, [(0, 3)], [(0, 3, 0)])
|
| 423 |
+
|
| 424 |
+
def test_no_community_information(self):
|
| 425 |
+
G = nx.complete_graph(5)
|
| 426 |
+
assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 1)]))
|
| 427 |
+
|
| 428 |
+
def test_insufficient_community_information(self):
|
| 429 |
+
G = nx.Graph()
|
| 430 |
+
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 431 |
+
G.nodes[0]["community"] = 0
|
| 432 |
+
G.nodes[1]["community"] = 0
|
| 433 |
+
G.nodes[3]["community"] = 0
|
| 434 |
+
assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 3)]))
|
| 435 |
+
|
| 436 |
+
def test_sufficient_community_information(self):
|
| 437 |
+
G = nx.Graph()
|
| 438 |
+
G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)])
|
| 439 |
+
G.nodes[1]["community"] = 0
|
| 440 |
+
G.nodes[2]["community"] = 0
|
| 441 |
+
G.nodes[3]["community"] = 0
|
| 442 |
+
G.nodes[4]["community"] = 0
|
| 443 |
+
self.test(G, [(1, 4)], [(1, 4, 1)])
|
| 444 |
+
|
| 445 |
+
def test_custom_community_attribute_name(self):
|
| 446 |
+
G = nx.Graph()
|
| 447 |
+
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 448 |
+
G.nodes[0]["cmty"] = 0
|
| 449 |
+
G.nodes[1]["cmty"] = 0
|
| 450 |
+
G.nodes[2]["cmty"] = 0
|
| 451 |
+
G.nodes[3]["cmty"] = 1
|
| 452 |
+
self.test(G, [(0, 3)], [(0, 3, 0)], community="cmty")
|
| 453 |
+
|
| 454 |
+
def test_all_nonexistent_edges(self):
|
| 455 |
+
G = nx.Graph()
|
| 456 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 457 |
+
G.nodes[0]["community"] = 0
|
| 458 |
+
G.nodes[1]["community"] = 1
|
| 459 |
+
G.nodes[2]["community"] = 0
|
| 460 |
+
G.nodes[3]["community"] = 0
|
| 461 |
+
self.test(G, None, [(0, 3, 0.5), (1, 2, 0), (1, 3, 0)])
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
class TestWithinInterCluster:
|
| 465 |
+
@classmethod
|
| 466 |
+
def setup_class(cls):
|
| 467 |
+
cls.delta = 0.001
|
| 468 |
+
cls.func = staticmethod(nx.within_inter_cluster)
|
| 469 |
+
cls.test = partial(
|
| 470 |
+
_test_func, predict_func=cls.func, delta=cls.delta, community="community"
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
def test_K5(self):
|
| 474 |
+
G = nx.complete_graph(5)
|
| 475 |
+
G.nodes[0]["community"] = 0
|
| 476 |
+
G.nodes[1]["community"] = 0
|
| 477 |
+
G.nodes[2]["community"] = 0
|
| 478 |
+
G.nodes[3]["community"] = 0
|
| 479 |
+
G.nodes[4]["community"] = 1
|
| 480 |
+
self.test(G, [(0, 1)], [(0, 1, 2 / (1 + self.delta))])
|
| 481 |
+
|
| 482 |
+
def test_P3(self):
|
| 483 |
+
G = nx.path_graph(3)
|
| 484 |
+
G.nodes[0]["community"] = 0
|
| 485 |
+
G.nodes[1]["community"] = 1
|
| 486 |
+
G.nodes[2]["community"] = 0
|
| 487 |
+
self.test(G, [(0, 2)], [(0, 2, 0)])
|
| 488 |
+
|
| 489 |
+
def test_S4(self):
|
| 490 |
+
G = nx.star_graph(4)
|
| 491 |
+
G.nodes[0]["community"] = 1
|
| 492 |
+
G.nodes[1]["community"] = 1
|
| 493 |
+
G.nodes[2]["community"] = 1
|
| 494 |
+
G.nodes[3]["community"] = 0
|
| 495 |
+
G.nodes[4]["community"] = 0
|
| 496 |
+
self.test(G, [(1, 2)], [(1, 2, 1 / self.delta)])
|
| 497 |
+
|
| 498 |
+
@pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
|
| 499 |
+
def test_notimplemented(self, graph_type):
|
| 500 |
+
G = graph_type([(0, 1), (1, 2)])
|
| 501 |
+
G.add_nodes_from([0, 1, 2], community=0)
|
| 502 |
+
assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
|
| 503 |
+
|
| 504 |
+
def test_node_not_found(self):
|
| 505 |
+
G = nx.Graph()
|
| 506 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 507 |
+
G.nodes[0]["community"] = 0
|
| 508 |
+
G.nodes[1]["community"] = 1
|
| 509 |
+
G.nodes[2]["community"] = 0
|
| 510 |
+
G.nodes[3]["community"] = 0
|
| 511 |
+
assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
|
| 512 |
+
|
| 513 |
+
def test_no_common_neighbor(self):
|
| 514 |
+
G = nx.Graph()
|
| 515 |
+
G.add_nodes_from([0, 1])
|
| 516 |
+
G.nodes[0]["community"] = 0
|
| 517 |
+
G.nodes[1]["community"] = 0
|
| 518 |
+
self.test(G, [(0, 1)], [(0, 1, 0)])
|
| 519 |
+
|
| 520 |
+
def test_equal_nodes(self):
|
| 521 |
+
G = nx.complete_graph(3)
|
| 522 |
+
G.nodes[0]["community"] = 0
|
| 523 |
+
G.nodes[1]["community"] = 0
|
| 524 |
+
G.nodes[2]["community"] = 0
|
| 525 |
+
self.test(G, [(0, 0)], [(0, 0, 2 / self.delta)])
|
| 526 |
+
|
| 527 |
+
def test_different_community(self):
|
| 528 |
+
G = nx.Graph()
|
| 529 |
+
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 530 |
+
G.nodes[0]["community"] = 0
|
| 531 |
+
G.nodes[1]["community"] = 0
|
| 532 |
+
G.nodes[2]["community"] = 0
|
| 533 |
+
G.nodes[3]["community"] = 1
|
| 534 |
+
self.test(G, [(0, 3)], [(0, 3, 0)])
|
| 535 |
+
|
| 536 |
+
def test_no_inter_cluster_common_neighbor(self):
|
| 537 |
+
G = nx.complete_graph(4)
|
| 538 |
+
G.nodes[0]["community"] = 0
|
| 539 |
+
G.nodes[1]["community"] = 0
|
| 540 |
+
G.nodes[2]["community"] = 0
|
| 541 |
+
G.nodes[3]["community"] = 0
|
| 542 |
+
self.test(G, [(0, 3)], [(0, 3, 2 / self.delta)])
|
| 543 |
+
|
| 544 |
+
def test_no_community_information(self):
|
| 545 |
+
G = nx.complete_graph(5)
|
| 546 |
+
assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 1)]))
|
| 547 |
+
|
| 548 |
+
def test_insufficient_community_information(self):
|
| 549 |
+
G = nx.Graph()
|
| 550 |
+
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
| 551 |
+
G.nodes[0]["community"] = 0
|
| 552 |
+
G.nodes[1]["community"] = 0
|
| 553 |
+
G.nodes[3]["community"] = 0
|
| 554 |
+
assert pytest.raises(nx.NetworkXAlgorithmError, list, self.func(G, [(0, 3)]))
|
| 555 |
+
|
| 556 |
+
def test_sufficient_community_information(self):
|
| 557 |
+
G = nx.Graph()
|
| 558 |
+
G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)])
|
| 559 |
+
G.nodes[1]["community"] = 0
|
| 560 |
+
G.nodes[2]["community"] = 0
|
| 561 |
+
G.nodes[3]["community"] = 0
|
| 562 |
+
G.nodes[4]["community"] = 0
|
| 563 |
+
self.test(G, [(1, 4)], [(1, 4, 2 / self.delta)])
|
| 564 |
+
|
| 565 |
+
def test_invalid_delta(self):
|
| 566 |
+
G = nx.complete_graph(3)
|
| 567 |
+
G.add_nodes_from([0, 1, 2], community=0)
|
| 568 |
+
assert pytest.raises(nx.NetworkXAlgorithmError, self.func, G, [(0, 1)], 0)
|
| 569 |
+
assert pytest.raises(nx.NetworkXAlgorithmError, self.func, G, [(0, 1)], -0.5)
|
| 570 |
+
|
| 571 |
+
def test_custom_community_attribute_name(self):
|
| 572 |
+
G = nx.complete_graph(4)
|
| 573 |
+
G.nodes[0]["cmty"] = 0
|
| 574 |
+
G.nodes[1]["cmty"] = 0
|
| 575 |
+
G.nodes[2]["cmty"] = 0
|
| 576 |
+
G.nodes[3]["cmty"] = 0
|
| 577 |
+
self.test(G, [(0, 3)], [(0, 3, 2 / self.delta)], community="cmty")
|
| 578 |
+
|
| 579 |
+
def test_all_nonexistent_edges(self):
|
| 580 |
+
G = nx.Graph()
|
| 581 |
+
G.add_edges_from([(0, 1), (0, 2), (2, 3)])
|
| 582 |
+
G.nodes[0]["community"] = 0
|
| 583 |
+
G.nodes[1]["community"] = 1
|
| 584 |
+
G.nodes[2]["community"] = 0
|
| 585 |
+
G.nodes[3]["community"] = 0
|
| 586 |
+
self.test(G, None, [(0, 3, 1 / self.delta), (1, 2, 0), (1, 3, 0)])
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_lowest_common_ancestors.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from itertools import chain, combinations, product
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
tree_all_pairs_lca = nx.tree_all_pairs_lowest_common_ancestor
|
| 8 |
+
all_pairs_lca = nx.all_pairs_lowest_common_ancestor
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def get_pair(dictionary, n1, n2):
|
| 12 |
+
if (n1, n2) in dictionary:
|
| 13 |
+
return dictionary[n1, n2]
|
| 14 |
+
else:
|
| 15 |
+
return dictionary[n2, n1]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TestTreeLCA:
|
| 19 |
+
@classmethod
|
| 20 |
+
def setup_class(cls):
|
| 21 |
+
cls.DG = nx.DiGraph()
|
| 22 |
+
edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
|
| 23 |
+
cls.DG.add_edges_from(edges)
|
| 24 |
+
cls.ans = dict(tree_all_pairs_lca(cls.DG, 0))
|
| 25 |
+
gold = {(n, n): n for n in cls.DG}
|
| 26 |
+
gold.update({(0, i): 0 for i in range(1, 7)})
|
| 27 |
+
gold.update(
|
| 28 |
+
{
|
| 29 |
+
(1, 2): 0,
|
| 30 |
+
(1, 3): 1,
|
| 31 |
+
(1, 4): 1,
|
| 32 |
+
(1, 5): 0,
|
| 33 |
+
(1, 6): 0,
|
| 34 |
+
(2, 3): 0,
|
| 35 |
+
(2, 4): 0,
|
| 36 |
+
(2, 5): 2,
|
| 37 |
+
(2, 6): 2,
|
| 38 |
+
(3, 4): 1,
|
| 39 |
+
(3, 5): 0,
|
| 40 |
+
(3, 6): 0,
|
| 41 |
+
(4, 5): 0,
|
| 42 |
+
(4, 6): 0,
|
| 43 |
+
(5, 6): 2,
|
| 44 |
+
}
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
cls.gold = gold
|
| 48 |
+
|
| 49 |
+
@staticmethod
|
| 50 |
+
def assert_has_same_pairs(d1, d2):
|
| 51 |
+
for a, b in ((min(pair), max(pair)) for pair in chain(d1, d2)):
|
| 52 |
+
assert get_pair(d1, a, b) == get_pair(d2, a, b)
|
| 53 |
+
|
| 54 |
+
def test_tree_all_pairs_lca_default_root(self):
|
| 55 |
+
assert dict(tree_all_pairs_lca(self.DG)) == self.ans
|
| 56 |
+
|
| 57 |
+
def test_tree_all_pairs_lca_return_subset(self):
|
| 58 |
+
test_pairs = [(0, 1), (0, 1), (1, 0)]
|
| 59 |
+
ans = dict(tree_all_pairs_lca(self.DG, 0, test_pairs))
|
| 60 |
+
assert (0, 1) in ans and (1, 0) in ans
|
| 61 |
+
assert len(ans) == 2
|
| 62 |
+
|
| 63 |
+
def test_tree_all_pairs_lca(self):
|
| 64 |
+
all_pairs = chain(combinations(self.DG, 2), ((node, node) for node in self.DG))
|
| 65 |
+
|
| 66 |
+
ans = dict(tree_all_pairs_lca(self.DG, 0, all_pairs))
|
| 67 |
+
self.assert_has_same_pairs(ans, self.ans)
|
| 68 |
+
|
| 69 |
+
def test_tree_all_pairs_gold_example(self):
|
| 70 |
+
ans = dict(tree_all_pairs_lca(self.DG))
|
| 71 |
+
self.assert_has_same_pairs(self.gold, ans)
|
| 72 |
+
|
| 73 |
+
def test_tree_all_pairs_lca_invalid_input(self):
|
| 74 |
+
empty_digraph = tree_all_pairs_lca(nx.DiGraph())
|
| 75 |
+
pytest.raises(nx.NetworkXPointlessConcept, list, empty_digraph)
|
| 76 |
+
|
| 77 |
+
bad_pairs_digraph = tree_all_pairs_lca(self.DG, pairs=[(-1, -2)])
|
| 78 |
+
pytest.raises(nx.NodeNotFound, list, bad_pairs_digraph)
|
| 79 |
+
|
| 80 |
+
def test_tree_all_pairs_lca_subtrees(self):
|
| 81 |
+
ans = dict(tree_all_pairs_lca(self.DG, 1))
|
| 82 |
+
gold = {
|
| 83 |
+
pair: lca
|
| 84 |
+
for (pair, lca) in self.gold.items()
|
| 85 |
+
if all(n in (1, 3, 4) for n in pair)
|
| 86 |
+
}
|
| 87 |
+
self.assert_has_same_pairs(gold, ans)
|
| 88 |
+
|
| 89 |
+
def test_tree_all_pairs_lca_disconnected_nodes(self):
|
| 90 |
+
G = nx.DiGraph()
|
| 91 |
+
G.add_node(1)
|
| 92 |
+
assert {(1, 1): 1} == dict(tree_all_pairs_lca(G))
|
| 93 |
+
|
| 94 |
+
G.add_node(0)
|
| 95 |
+
assert {(1, 1): 1} == dict(tree_all_pairs_lca(G, 1))
|
| 96 |
+
assert {(0, 0): 0} == dict(tree_all_pairs_lca(G, 0))
|
| 97 |
+
|
| 98 |
+
pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G))
|
| 99 |
+
|
| 100 |
+
def test_tree_all_pairs_lca_error_if_input_not_tree(self):
|
| 101 |
+
# Cycle
|
| 102 |
+
G = nx.DiGraph([(1, 2), (2, 1)])
|
| 103 |
+
pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G))
|
| 104 |
+
# DAG
|
| 105 |
+
G = nx.DiGraph([(0, 2), (1, 2)])
|
| 106 |
+
pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G))
|
| 107 |
+
|
| 108 |
+
def test_tree_all_pairs_lca_generator(self):
|
| 109 |
+
pairs = iter([(0, 1), (0, 1), (1, 0)])
|
| 110 |
+
some_pairs = dict(tree_all_pairs_lca(self.DG, 0, pairs))
|
| 111 |
+
assert (0, 1) in some_pairs and (1, 0) in some_pairs
|
| 112 |
+
assert len(some_pairs) == 2
|
| 113 |
+
|
| 114 |
+
def test_tree_all_pairs_lca_nonexisting_pairs_exception(self):
|
| 115 |
+
lca = tree_all_pairs_lca(self.DG, 0, [(-1, -1)])
|
| 116 |
+
pytest.raises(nx.NodeNotFound, list, lca)
|
| 117 |
+
# check if node is None
|
| 118 |
+
lca = tree_all_pairs_lca(self.DG, None, [(-1, -1)])
|
| 119 |
+
pytest.raises(nx.NodeNotFound, list, lca)
|
| 120 |
+
|
| 121 |
+
def test_tree_all_pairs_lca_routine_bails_on_DAGs(self):
|
| 122 |
+
G = nx.DiGraph([(3, 4), (5, 4)])
|
| 123 |
+
pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G))
|
| 124 |
+
|
| 125 |
+
def test_tree_all_pairs_lca_not_implemented(self):
|
| 126 |
+
NNI = nx.NetworkXNotImplemented
|
| 127 |
+
G = nx.Graph([(0, 1)])
|
| 128 |
+
with pytest.raises(NNI):
|
| 129 |
+
next(tree_all_pairs_lca(G))
|
| 130 |
+
with pytest.raises(NNI):
|
| 131 |
+
next(all_pairs_lca(G))
|
| 132 |
+
pytest.raises(NNI, nx.lowest_common_ancestor, G, 0, 1)
|
| 133 |
+
G = nx.MultiGraph([(0, 1)])
|
| 134 |
+
with pytest.raises(NNI):
|
| 135 |
+
next(tree_all_pairs_lca(G))
|
| 136 |
+
with pytest.raises(NNI):
|
| 137 |
+
next(all_pairs_lca(G))
|
| 138 |
+
pytest.raises(NNI, nx.lowest_common_ancestor, G, 0, 1)
|
| 139 |
+
|
| 140 |
+
def test_tree_all_pairs_lca_trees_without_LCAs(self):
|
| 141 |
+
G = nx.DiGraph()
|
| 142 |
+
G.add_node(3)
|
| 143 |
+
ans = list(tree_all_pairs_lca(G))
|
| 144 |
+
assert ans == [((3, 3), 3)]
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class TestMultiTreeLCA(TestTreeLCA):
|
| 148 |
+
@classmethod
|
| 149 |
+
def setup_class(cls):
|
| 150 |
+
cls.DG = nx.MultiDiGraph()
|
| 151 |
+
edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
|
| 152 |
+
cls.DG.add_edges_from(edges)
|
| 153 |
+
cls.ans = dict(tree_all_pairs_lca(cls.DG, 0))
|
| 154 |
+
# add multiedges
|
| 155 |
+
cls.DG.add_edges_from(edges)
|
| 156 |
+
|
| 157 |
+
gold = {(n, n): n for n in cls.DG}
|
| 158 |
+
gold.update({(0, i): 0 for i in range(1, 7)})
|
| 159 |
+
gold.update(
|
| 160 |
+
{
|
| 161 |
+
(1, 2): 0,
|
| 162 |
+
(1, 3): 1,
|
| 163 |
+
(1, 4): 1,
|
| 164 |
+
(1, 5): 0,
|
| 165 |
+
(1, 6): 0,
|
| 166 |
+
(2, 3): 0,
|
| 167 |
+
(2, 4): 0,
|
| 168 |
+
(2, 5): 2,
|
| 169 |
+
(2, 6): 2,
|
| 170 |
+
(3, 4): 1,
|
| 171 |
+
(3, 5): 0,
|
| 172 |
+
(3, 6): 0,
|
| 173 |
+
(4, 5): 0,
|
| 174 |
+
(4, 6): 0,
|
| 175 |
+
(5, 6): 2,
|
| 176 |
+
}
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
cls.gold = gold
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class TestDAGLCA:
|
| 183 |
+
@classmethod
|
| 184 |
+
def setup_class(cls):
|
| 185 |
+
cls.DG = nx.DiGraph()
|
| 186 |
+
nx.add_path(cls.DG, (0, 1, 2, 3))
|
| 187 |
+
nx.add_path(cls.DG, (0, 4, 3))
|
| 188 |
+
nx.add_path(cls.DG, (0, 5, 6, 8, 3))
|
| 189 |
+
nx.add_path(cls.DG, (5, 7, 8))
|
| 190 |
+
cls.DG.add_edge(6, 2)
|
| 191 |
+
cls.DG.add_edge(7, 2)
|
| 192 |
+
|
| 193 |
+
cls.root_distance = nx.shortest_path_length(cls.DG, source=0)
|
| 194 |
+
|
| 195 |
+
cls.gold = {
|
| 196 |
+
(1, 1): 1,
|
| 197 |
+
(1, 2): 1,
|
| 198 |
+
(1, 3): 1,
|
| 199 |
+
(1, 4): 0,
|
| 200 |
+
(1, 5): 0,
|
| 201 |
+
(1, 6): 0,
|
| 202 |
+
(1, 7): 0,
|
| 203 |
+
(1, 8): 0,
|
| 204 |
+
(2, 2): 2,
|
| 205 |
+
(2, 3): 2,
|
| 206 |
+
(2, 4): 0,
|
| 207 |
+
(2, 5): 5,
|
| 208 |
+
(2, 6): 6,
|
| 209 |
+
(2, 7): 7,
|
| 210 |
+
(2, 8): 7,
|
| 211 |
+
(3, 3): 3,
|
| 212 |
+
(3, 4): 4,
|
| 213 |
+
(3, 5): 5,
|
| 214 |
+
(3, 6): 6,
|
| 215 |
+
(3, 7): 7,
|
| 216 |
+
(3, 8): 8,
|
| 217 |
+
(4, 4): 4,
|
| 218 |
+
(4, 5): 0,
|
| 219 |
+
(4, 6): 0,
|
| 220 |
+
(4, 7): 0,
|
| 221 |
+
(4, 8): 0,
|
| 222 |
+
(5, 5): 5,
|
| 223 |
+
(5, 6): 5,
|
| 224 |
+
(5, 7): 5,
|
| 225 |
+
(5, 8): 5,
|
| 226 |
+
(6, 6): 6,
|
| 227 |
+
(6, 7): 5,
|
| 228 |
+
(6, 8): 6,
|
| 229 |
+
(7, 7): 7,
|
| 230 |
+
(7, 8): 7,
|
| 231 |
+
(8, 8): 8,
|
| 232 |
+
}
|
| 233 |
+
cls.gold.update(((0, n), 0) for n in cls.DG)
|
| 234 |
+
|
| 235 |
+
def assert_lca_dicts_same(self, d1, d2, G=None):
|
| 236 |
+
"""Checks if d1 and d2 contain the same pairs and
|
| 237 |
+
have a node at the same distance from root for each.
|
| 238 |
+
If G is None use self.DG."""
|
| 239 |
+
if G is None:
|
| 240 |
+
G = self.DG
|
| 241 |
+
root_distance = self.root_distance
|
| 242 |
+
else:
|
| 243 |
+
roots = [n for n, deg in G.in_degree if deg == 0]
|
| 244 |
+
assert len(roots) == 1
|
| 245 |
+
root_distance = nx.shortest_path_length(G, source=roots[0])
|
| 246 |
+
|
| 247 |
+
for a, b in ((min(pair), max(pair)) for pair in chain(d1, d2)):
|
| 248 |
+
assert (
|
| 249 |
+
root_distance[get_pair(d1, a, b)] == root_distance[get_pair(d2, a, b)]
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
def test_all_pairs_lca_gold_example(self):
|
| 253 |
+
self.assert_lca_dicts_same(dict(all_pairs_lca(self.DG)), self.gold)
|
| 254 |
+
|
| 255 |
+
def test_all_pairs_lca_all_pairs_given(self):
|
| 256 |
+
all_pairs = list(product(self.DG.nodes(), self.DG.nodes()))
|
| 257 |
+
ans = all_pairs_lca(self.DG, pairs=all_pairs)
|
| 258 |
+
self.assert_lca_dicts_same(dict(ans), self.gold)
|
| 259 |
+
|
| 260 |
+
def test_all_pairs_lca_generator(self):
|
| 261 |
+
all_pairs = product(self.DG.nodes(), self.DG.nodes())
|
| 262 |
+
ans = all_pairs_lca(self.DG, pairs=all_pairs)
|
| 263 |
+
self.assert_lca_dicts_same(dict(ans), self.gold)
|
| 264 |
+
|
| 265 |
+
def test_all_pairs_lca_input_graph_with_two_roots(self):
|
| 266 |
+
G = self.DG.copy()
|
| 267 |
+
G.add_edge(9, 10)
|
| 268 |
+
G.add_edge(9, 4)
|
| 269 |
+
gold = self.gold.copy()
|
| 270 |
+
gold[9, 9] = 9
|
| 271 |
+
gold[9, 10] = 9
|
| 272 |
+
gold[9, 4] = 9
|
| 273 |
+
gold[9, 3] = 9
|
| 274 |
+
gold[10, 4] = 9
|
| 275 |
+
gold[10, 3] = 9
|
| 276 |
+
gold[10, 10] = 10
|
| 277 |
+
|
| 278 |
+
testing = dict(all_pairs_lca(G))
|
| 279 |
+
|
| 280 |
+
G.add_edge(-1, 9)
|
| 281 |
+
G.add_edge(-1, 0)
|
| 282 |
+
self.assert_lca_dicts_same(testing, gold, G)
|
| 283 |
+
|
| 284 |
+
def test_all_pairs_lca_nonexisting_pairs_exception(self):
|
| 285 |
+
pytest.raises(nx.NodeNotFound, all_pairs_lca, self.DG, [(-1, -1)])
|
| 286 |
+
|
| 287 |
+
def test_all_pairs_lca_pairs_without_lca(self):
|
| 288 |
+
G = self.DG.copy()
|
| 289 |
+
G.add_node(-1)
|
| 290 |
+
gen = all_pairs_lca(G, [(-1, -1), (-1, 0)])
|
| 291 |
+
assert dict(gen) == {(-1, -1): -1}
|
| 292 |
+
|
| 293 |
+
def test_all_pairs_lca_null_graph(self):
|
| 294 |
+
pytest.raises(nx.NetworkXPointlessConcept, all_pairs_lca, nx.DiGraph())
|
| 295 |
+
|
| 296 |
+
def test_all_pairs_lca_non_dags(self):
|
| 297 |
+
pytest.raises(nx.NetworkXError, all_pairs_lca, nx.DiGraph([(3, 4), (4, 3)]))
|
| 298 |
+
|
| 299 |
+
def test_all_pairs_lca_nonempty_graph_without_lca(self):
|
| 300 |
+
G = nx.DiGraph()
|
| 301 |
+
G.add_node(3)
|
| 302 |
+
ans = list(all_pairs_lca(G))
|
| 303 |
+
assert ans == [((3, 3), 3)]
|
| 304 |
+
|
| 305 |
+
def test_all_pairs_lca_bug_gh4942(self):
|
| 306 |
+
G = nx.DiGraph([(0, 2), (1, 2), (2, 3)])
|
| 307 |
+
ans = list(all_pairs_lca(G))
|
| 308 |
+
assert len(ans) == 9
|
| 309 |
+
|
| 310 |
+
def test_all_pairs_lca_default_kwarg(self):
|
| 311 |
+
G = nx.DiGraph([(0, 1), (2, 1)])
|
| 312 |
+
sentinel = object()
|
| 313 |
+
assert nx.lowest_common_ancestor(G, 0, 2, default=sentinel) is sentinel
|
| 314 |
+
|
| 315 |
+
def test_all_pairs_lca_identity(self):
|
| 316 |
+
G = nx.DiGraph()
|
| 317 |
+
G.add_node(3)
|
| 318 |
+
assert nx.lowest_common_ancestor(G, 3, 3) == 3
|
| 319 |
+
|
| 320 |
+
def test_all_pairs_lca_issue_4574(self):
|
| 321 |
+
G = nx.DiGraph()
|
| 322 |
+
G.add_nodes_from(range(17))
|
| 323 |
+
G.add_edges_from(
|
| 324 |
+
[
|
| 325 |
+
(2, 0),
|
| 326 |
+
(1, 2),
|
| 327 |
+
(3, 2),
|
| 328 |
+
(5, 2),
|
| 329 |
+
(8, 2),
|
| 330 |
+
(11, 2),
|
| 331 |
+
(4, 5),
|
| 332 |
+
(6, 5),
|
| 333 |
+
(7, 8),
|
| 334 |
+
(10, 8),
|
| 335 |
+
(13, 11),
|
| 336 |
+
(14, 11),
|
| 337 |
+
(15, 11),
|
| 338 |
+
(9, 10),
|
| 339 |
+
(12, 13),
|
| 340 |
+
(16, 15),
|
| 341 |
+
]
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
assert nx.lowest_common_ancestor(G, 7, 9) == None
|
| 345 |
+
|
| 346 |
+
def test_all_pairs_lca_one_pair_gh4942(self):
|
| 347 |
+
G = nx.DiGraph()
|
| 348 |
+
# Note: order edge addition is critical to the test
|
| 349 |
+
G.add_edge(0, 1)
|
| 350 |
+
G.add_edge(2, 0)
|
| 351 |
+
G.add_edge(2, 3)
|
| 352 |
+
G.add_edge(4, 0)
|
| 353 |
+
G.add_edge(5, 2)
|
| 354 |
+
|
| 355 |
+
assert nx.lowest_common_ancestor(G, 1, 3) == 2
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
class TestMultiDiGraph_DAGLCA(TestDAGLCA):
|
| 359 |
+
@classmethod
|
| 360 |
+
def setup_class(cls):
|
| 361 |
+
cls.DG = nx.MultiDiGraph()
|
| 362 |
+
nx.add_path(cls.DG, (0, 1, 2, 3))
|
| 363 |
+
# add multiedges
|
| 364 |
+
nx.add_path(cls.DG, (0, 1, 2, 3))
|
| 365 |
+
nx.add_path(cls.DG, (0, 4, 3))
|
| 366 |
+
nx.add_path(cls.DG, (0, 5, 6, 8, 3))
|
| 367 |
+
nx.add_path(cls.DG, (5, 7, 8))
|
| 368 |
+
cls.DG.add_edge(6, 2)
|
| 369 |
+
cls.DG.add_edge(7, 2)
|
| 370 |
+
|
| 371 |
+
cls.root_distance = nx.shortest_path_length(cls.DG, source=0)
|
| 372 |
+
|
| 373 |
+
cls.gold = {
|
| 374 |
+
(1, 1): 1,
|
| 375 |
+
(1, 2): 1,
|
| 376 |
+
(1, 3): 1,
|
| 377 |
+
(1, 4): 0,
|
| 378 |
+
(1, 5): 0,
|
| 379 |
+
(1, 6): 0,
|
| 380 |
+
(1, 7): 0,
|
| 381 |
+
(1, 8): 0,
|
| 382 |
+
(2, 2): 2,
|
| 383 |
+
(2, 3): 2,
|
| 384 |
+
(2, 4): 0,
|
| 385 |
+
(2, 5): 5,
|
| 386 |
+
(2, 6): 6,
|
| 387 |
+
(2, 7): 7,
|
| 388 |
+
(2, 8): 7,
|
| 389 |
+
(3, 3): 3,
|
| 390 |
+
(3, 4): 4,
|
| 391 |
+
(3, 5): 5,
|
| 392 |
+
(3, 6): 6,
|
| 393 |
+
(3, 7): 7,
|
| 394 |
+
(3, 8): 8,
|
| 395 |
+
(4, 4): 4,
|
| 396 |
+
(4, 5): 0,
|
| 397 |
+
(4, 6): 0,
|
| 398 |
+
(4, 7): 0,
|
| 399 |
+
(4, 8): 0,
|
| 400 |
+
(5, 5): 5,
|
| 401 |
+
(5, 6): 5,
|
| 402 |
+
(5, 7): 5,
|
| 403 |
+
(5, 8): 5,
|
| 404 |
+
(6, 6): 6,
|
| 405 |
+
(6, 7): 5,
|
| 406 |
+
(6, 8): 6,
|
| 407 |
+
(7, 7): 7,
|
| 408 |
+
(7, 8): 7,
|
| 409 |
+
(8, 8): 8,
|
| 410 |
+
}
|
| 411 |
+
cls.gold.update(((0, n), 0) for n in cls.DG)
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def test_all_pairs_lca_self_ancestors():
|
| 415 |
+
"""Self-ancestors should always be the node itself, i.e. lca of (0, 0) is 0.
|
| 416 |
+
See gh-4458."""
|
| 417 |
+
# DAG for test - note order of node/edge addition is relevant
|
| 418 |
+
G = nx.DiGraph()
|
| 419 |
+
G.add_nodes_from(range(5))
|
| 420 |
+
G.add_edges_from([(1, 0), (2, 0), (3, 2), (4, 1), (4, 3)])
|
| 421 |
+
|
| 422 |
+
ap_lca = nx.all_pairs_lowest_common_ancestor
|
| 423 |
+
assert all(u == v == a for (u, v), a in ap_lca(G) if u == v)
|
| 424 |
+
MG = nx.MultiDiGraph(G)
|
| 425 |
+
assert all(u == v == a for (u, v), a in ap_lca(MG) if u == v)
|
| 426 |
+
MG.add_edges_from([(1, 0), (2, 0)])
|
| 427 |
+
assert all(u == v == a for (u, v), a in ap_lca(MG) if u == v)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/test_matching.py
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from itertools import permutations
|
| 3 |
+
|
| 4 |
+
from pytest import raises
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from networkx.algorithms.matching import matching_dict_to_set
|
| 8 |
+
from networkx.utils import edges_equal
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TestMaxWeightMatching:
|
| 12 |
+
"""Unit tests for the
|
| 13 |
+
:func:`~networkx.algorithms.matching.max_weight_matching` function.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def test_trivial1(self):
|
| 18 |
+
"""Empty graph"""
|
| 19 |
+
G = nx.Graph()
|
| 20 |
+
assert nx.max_weight_matching(G) == set()
|
| 21 |
+
assert nx.min_weight_matching(G) == set()
|
| 22 |
+
|
| 23 |
+
def test_selfloop(self):
|
| 24 |
+
G = nx.Graph()
|
| 25 |
+
G.add_edge(0, 0, weight=100)
|
| 26 |
+
assert nx.max_weight_matching(G) == set()
|
| 27 |
+
assert nx.min_weight_matching(G) == set()
|
| 28 |
+
|
| 29 |
+
def test_single_edge(self):
|
| 30 |
+
G = nx.Graph()
|
| 31 |
+
G.add_edge(0, 1)
|
| 32 |
+
assert edges_equal(
|
| 33 |
+
nx.max_weight_matching(G), matching_dict_to_set({0: 1, 1: 0})
|
| 34 |
+
)
|
| 35 |
+
assert edges_equal(
|
| 36 |
+
nx.min_weight_matching(G), matching_dict_to_set({0: 1, 1: 0})
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def test_two_path(self):
|
| 40 |
+
G = nx.Graph()
|
| 41 |
+
G.add_edge("one", "two", weight=10)
|
| 42 |
+
G.add_edge("two", "three", weight=11)
|
| 43 |
+
assert edges_equal(
|
| 44 |
+
nx.max_weight_matching(G),
|
| 45 |
+
matching_dict_to_set({"three": "two", "two": "three"}),
|
| 46 |
+
)
|
| 47 |
+
assert edges_equal(
|
| 48 |
+
nx.min_weight_matching(G),
|
| 49 |
+
matching_dict_to_set({"one": "two", "two": "one"}),
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
def test_path(self):
|
| 53 |
+
G = nx.Graph()
|
| 54 |
+
G.add_edge(1, 2, weight=5)
|
| 55 |
+
G.add_edge(2, 3, weight=11)
|
| 56 |
+
G.add_edge(3, 4, weight=5)
|
| 57 |
+
assert edges_equal(
|
| 58 |
+
nx.max_weight_matching(G), matching_dict_to_set({2: 3, 3: 2})
|
| 59 |
+
)
|
| 60 |
+
assert edges_equal(
|
| 61 |
+
nx.max_weight_matching(G, 1), matching_dict_to_set({1: 2, 2: 1, 3: 4, 4: 3})
|
| 62 |
+
)
|
| 63 |
+
assert edges_equal(
|
| 64 |
+
nx.min_weight_matching(G), matching_dict_to_set({1: 2, 3: 4})
|
| 65 |
+
)
|
| 66 |
+
assert edges_equal(
|
| 67 |
+
nx.min_weight_matching(G, 1), matching_dict_to_set({1: 2, 3: 4})
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def test_square(self):
|
| 71 |
+
G = nx.Graph()
|
| 72 |
+
G.add_edge(1, 4, weight=2)
|
| 73 |
+
G.add_edge(2, 3, weight=2)
|
| 74 |
+
G.add_edge(1, 2, weight=1)
|
| 75 |
+
G.add_edge(3, 4, weight=4)
|
| 76 |
+
assert edges_equal(
|
| 77 |
+
nx.max_weight_matching(G), matching_dict_to_set({1: 2, 3: 4})
|
| 78 |
+
)
|
| 79 |
+
assert edges_equal(
|
| 80 |
+
nx.min_weight_matching(G), matching_dict_to_set({1: 4, 2: 3})
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
def test_edge_attribute_name(self):
|
| 84 |
+
G = nx.Graph()
|
| 85 |
+
G.add_edge("one", "two", weight=10, abcd=11)
|
| 86 |
+
G.add_edge("two", "three", weight=11, abcd=10)
|
| 87 |
+
assert edges_equal(
|
| 88 |
+
nx.max_weight_matching(G, weight="abcd"),
|
| 89 |
+
matching_dict_to_set({"one": "two", "two": "one"}),
|
| 90 |
+
)
|
| 91 |
+
assert edges_equal(
|
| 92 |
+
nx.min_weight_matching(G, weight="abcd"),
|
| 93 |
+
matching_dict_to_set({"three": "two"}),
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
def test_floating_point_weights(self):
|
| 97 |
+
G = nx.Graph()
|
| 98 |
+
G.add_edge(1, 2, weight=math.pi)
|
| 99 |
+
G.add_edge(2, 3, weight=math.exp(1))
|
| 100 |
+
G.add_edge(1, 3, weight=3.0)
|
| 101 |
+
G.add_edge(1, 4, weight=math.sqrt(2.0))
|
| 102 |
+
assert edges_equal(
|
| 103 |
+
nx.max_weight_matching(G), matching_dict_to_set({1: 4, 2: 3, 3: 2, 4: 1})
|
| 104 |
+
)
|
| 105 |
+
assert edges_equal(
|
| 106 |
+
nx.min_weight_matching(G), matching_dict_to_set({1: 4, 2: 3, 3: 2, 4: 1})
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
def test_negative_weights(self):
|
| 110 |
+
G = nx.Graph()
|
| 111 |
+
G.add_edge(1, 2, weight=2)
|
| 112 |
+
G.add_edge(1, 3, weight=-2)
|
| 113 |
+
G.add_edge(2, 3, weight=1)
|
| 114 |
+
G.add_edge(2, 4, weight=-1)
|
| 115 |
+
G.add_edge(3, 4, weight=-6)
|
| 116 |
+
assert edges_equal(
|
| 117 |
+
nx.max_weight_matching(G), matching_dict_to_set({1: 2, 2: 1})
|
| 118 |
+
)
|
| 119 |
+
assert edges_equal(
|
| 120 |
+
nx.max_weight_matching(G, maxcardinality=True),
|
| 121 |
+
matching_dict_to_set({1: 3, 2: 4, 3: 1, 4: 2}),
|
| 122 |
+
)
|
| 123 |
+
assert edges_equal(
|
| 124 |
+
nx.min_weight_matching(G), matching_dict_to_set({1: 2, 3: 4})
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
def test_s_blossom(self):
|
| 128 |
+
"""Create S-blossom and use it for augmentation:"""
|
| 129 |
+
G = nx.Graph()
|
| 130 |
+
G.add_weighted_edges_from([(1, 2, 8), (1, 3, 9), (2, 3, 10), (3, 4, 7)])
|
| 131 |
+
answer = matching_dict_to_set({1: 2, 2: 1, 3: 4, 4: 3})
|
| 132 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 133 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 134 |
+
|
| 135 |
+
G.add_weighted_edges_from([(1, 6, 5), (4, 5, 6)])
|
| 136 |
+
answer = matching_dict_to_set({1: 6, 2: 3, 3: 2, 4: 5, 5: 4, 6: 1})
|
| 137 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 138 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 139 |
+
|
| 140 |
+
def test_s_t_blossom(self):
|
| 141 |
+
"""Create S-blossom, relabel as T-blossom, use for augmentation:"""
|
| 142 |
+
G = nx.Graph()
|
| 143 |
+
G.add_weighted_edges_from(
|
| 144 |
+
[(1, 2, 9), (1, 3, 8), (2, 3, 10), (1, 4, 5), (4, 5, 4), (1, 6, 3)]
|
| 145 |
+
)
|
| 146 |
+
answer = matching_dict_to_set({1: 6, 2: 3, 3: 2, 4: 5, 5: 4, 6: 1})
|
| 147 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 148 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 149 |
+
|
| 150 |
+
G.add_edge(4, 5, weight=3)
|
| 151 |
+
G.add_edge(1, 6, weight=4)
|
| 152 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 153 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 154 |
+
|
| 155 |
+
G.remove_edge(1, 6)
|
| 156 |
+
G.add_edge(3, 6, weight=4)
|
| 157 |
+
answer = matching_dict_to_set({1: 2, 2: 1, 3: 6, 4: 5, 5: 4, 6: 3})
|
| 158 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 159 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 160 |
+
|
| 161 |
+
def test_nested_s_blossom(self):
|
| 162 |
+
"""Create nested S-blossom, use for augmentation:"""
|
| 163 |
+
|
| 164 |
+
G = nx.Graph()
|
| 165 |
+
G.add_weighted_edges_from(
|
| 166 |
+
[
|
| 167 |
+
(1, 2, 9),
|
| 168 |
+
(1, 3, 9),
|
| 169 |
+
(2, 3, 10),
|
| 170 |
+
(2, 4, 8),
|
| 171 |
+
(3, 5, 8),
|
| 172 |
+
(4, 5, 10),
|
| 173 |
+
(5, 6, 6),
|
| 174 |
+
]
|
| 175 |
+
)
|
| 176 |
+
dict_format = {1: 3, 2: 4, 3: 1, 4: 2, 5: 6, 6: 5}
|
| 177 |
+
expected = {frozenset(e) for e in matching_dict_to_set(dict_format)}
|
| 178 |
+
answer = {frozenset(e) for e in nx.max_weight_matching(G)}
|
| 179 |
+
assert answer == expected
|
| 180 |
+
answer = {frozenset(e) for e in nx.min_weight_matching(G)}
|
| 181 |
+
assert answer == expected
|
| 182 |
+
|
| 183 |
+
def test_nested_s_blossom_relabel(self):
|
| 184 |
+
"""Create S-blossom, relabel as S, include in nested S-blossom:"""
|
| 185 |
+
G = nx.Graph()
|
| 186 |
+
G.add_weighted_edges_from(
|
| 187 |
+
[
|
| 188 |
+
(1, 2, 10),
|
| 189 |
+
(1, 7, 10),
|
| 190 |
+
(2, 3, 12),
|
| 191 |
+
(3, 4, 20),
|
| 192 |
+
(3, 5, 20),
|
| 193 |
+
(4, 5, 25),
|
| 194 |
+
(5, 6, 10),
|
| 195 |
+
(6, 7, 10),
|
| 196 |
+
(7, 8, 8),
|
| 197 |
+
]
|
| 198 |
+
)
|
| 199 |
+
answer = matching_dict_to_set({1: 2, 2: 1, 3: 4, 4: 3, 5: 6, 6: 5, 7: 8, 8: 7})
|
| 200 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 201 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 202 |
+
|
| 203 |
+
def test_nested_s_blossom_expand(self):
|
| 204 |
+
"""Create nested S-blossom, augment, expand recursively:"""
|
| 205 |
+
G = nx.Graph()
|
| 206 |
+
G.add_weighted_edges_from(
|
| 207 |
+
[
|
| 208 |
+
(1, 2, 8),
|
| 209 |
+
(1, 3, 8),
|
| 210 |
+
(2, 3, 10),
|
| 211 |
+
(2, 4, 12),
|
| 212 |
+
(3, 5, 12),
|
| 213 |
+
(4, 5, 14),
|
| 214 |
+
(4, 6, 12),
|
| 215 |
+
(5, 7, 12),
|
| 216 |
+
(6, 7, 14),
|
| 217 |
+
(7, 8, 12),
|
| 218 |
+
]
|
| 219 |
+
)
|
| 220 |
+
answer = matching_dict_to_set({1: 2, 2: 1, 3: 5, 4: 6, 5: 3, 6: 4, 7: 8, 8: 7})
|
| 221 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 222 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 223 |
+
|
| 224 |
+
def test_s_blossom_relabel_expand(self):
|
| 225 |
+
"""Create S-blossom, relabel as T, expand:"""
|
| 226 |
+
G = nx.Graph()
|
| 227 |
+
G.add_weighted_edges_from(
|
| 228 |
+
[
|
| 229 |
+
(1, 2, 23),
|
| 230 |
+
(1, 5, 22),
|
| 231 |
+
(1, 6, 15),
|
| 232 |
+
(2, 3, 25),
|
| 233 |
+
(3, 4, 22),
|
| 234 |
+
(4, 5, 25),
|
| 235 |
+
(4, 8, 14),
|
| 236 |
+
(5, 7, 13),
|
| 237 |
+
]
|
| 238 |
+
)
|
| 239 |
+
answer = matching_dict_to_set({1: 6, 2: 3, 3: 2, 4: 8, 5: 7, 6: 1, 7: 5, 8: 4})
|
| 240 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 241 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 242 |
+
|
| 243 |
+
def test_nested_s_blossom_relabel_expand(self):
|
| 244 |
+
"""Create nested S-blossom, relabel as T, expand:"""
|
| 245 |
+
G = nx.Graph()
|
| 246 |
+
G.add_weighted_edges_from(
|
| 247 |
+
[
|
| 248 |
+
(1, 2, 19),
|
| 249 |
+
(1, 3, 20),
|
| 250 |
+
(1, 8, 8),
|
| 251 |
+
(2, 3, 25),
|
| 252 |
+
(2, 4, 18),
|
| 253 |
+
(3, 5, 18),
|
| 254 |
+
(4, 5, 13),
|
| 255 |
+
(4, 7, 7),
|
| 256 |
+
(5, 6, 7),
|
| 257 |
+
]
|
| 258 |
+
)
|
| 259 |
+
answer = matching_dict_to_set({1: 8, 2: 3, 3: 2, 4: 7, 5: 6, 6: 5, 7: 4, 8: 1})
|
| 260 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 261 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 262 |
+
|
| 263 |
+
def test_nasty_blossom1(self):
|
| 264 |
+
"""Create blossom, relabel as T in more than one way, expand,
|
| 265 |
+
augment:
|
| 266 |
+
"""
|
| 267 |
+
G = nx.Graph()
|
| 268 |
+
G.add_weighted_edges_from(
|
| 269 |
+
[
|
| 270 |
+
(1, 2, 45),
|
| 271 |
+
(1, 5, 45),
|
| 272 |
+
(2, 3, 50),
|
| 273 |
+
(3, 4, 45),
|
| 274 |
+
(4, 5, 50),
|
| 275 |
+
(1, 6, 30),
|
| 276 |
+
(3, 9, 35),
|
| 277 |
+
(4, 8, 35),
|
| 278 |
+
(5, 7, 26),
|
| 279 |
+
(9, 10, 5),
|
| 280 |
+
]
|
| 281 |
+
)
|
| 282 |
+
ansdict = {1: 6, 2: 3, 3: 2, 4: 8, 5: 7, 6: 1, 7: 5, 8: 4, 9: 10, 10: 9}
|
| 283 |
+
answer = matching_dict_to_set(ansdict)
|
| 284 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 285 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 286 |
+
|
| 287 |
+
def test_nasty_blossom2(self):
|
| 288 |
+
"""Again but slightly different:"""
|
| 289 |
+
G = nx.Graph()
|
| 290 |
+
G.add_weighted_edges_from(
|
| 291 |
+
[
|
| 292 |
+
(1, 2, 45),
|
| 293 |
+
(1, 5, 45),
|
| 294 |
+
(2, 3, 50),
|
| 295 |
+
(3, 4, 45),
|
| 296 |
+
(4, 5, 50),
|
| 297 |
+
(1, 6, 30),
|
| 298 |
+
(3, 9, 35),
|
| 299 |
+
(4, 8, 26),
|
| 300 |
+
(5, 7, 40),
|
| 301 |
+
(9, 10, 5),
|
| 302 |
+
]
|
| 303 |
+
)
|
| 304 |
+
ans = {1: 6, 2: 3, 3: 2, 4: 8, 5: 7, 6: 1, 7: 5, 8: 4, 9: 10, 10: 9}
|
| 305 |
+
answer = matching_dict_to_set(ans)
|
| 306 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 307 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 308 |
+
|
| 309 |
+
def test_nasty_blossom_least_slack(self):
|
| 310 |
+
"""Create blossom, relabel as T, expand such that a new
|
| 311 |
+
least-slack S-to-free dge is produced, augment:
|
| 312 |
+
"""
|
| 313 |
+
G = nx.Graph()
|
| 314 |
+
G.add_weighted_edges_from(
|
| 315 |
+
[
|
| 316 |
+
(1, 2, 45),
|
| 317 |
+
(1, 5, 45),
|
| 318 |
+
(2, 3, 50),
|
| 319 |
+
(3, 4, 45),
|
| 320 |
+
(4, 5, 50),
|
| 321 |
+
(1, 6, 30),
|
| 322 |
+
(3, 9, 35),
|
| 323 |
+
(4, 8, 28),
|
| 324 |
+
(5, 7, 26),
|
| 325 |
+
(9, 10, 5),
|
| 326 |
+
]
|
| 327 |
+
)
|
| 328 |
+
ans = {1: 6, 2: 3, 3: 2, 4: 8, 5: 7, 6: 1, 7: 5, 8: 4, 9: 10, 10: 9}
|
| 329 |
+
answer = matching_dict_to_set(ans)
|
| 330 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 331 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 332 |
+
|
| 333 |
+
def test_nasty_blossom_augmenting(self):
|
| 334 |
+
"""Create nested blossom, relabel as T in more than one way"""
|
| 335 |
+
# expand outer blossom such that inner blossom ends up on an
|
| 336 |
+
# augmenting path:
|
| 337 |
+
G = nx.Graph()
|
| 338 |
+
G.add_weighted_edges_from(
|
| 339 |
+
[
|
| 340 |
+
(1, 2, 45),
|
| 341 |
+
(1, 7, 45),
|
| 342 |
+
(2, 3, 50),
|
| 343 |
+
(3, 4, 45),
|
| 344 |
+
(4, 5, 95),
|
| 345 |
+
(4, 6, 94),
|
| 346 |
+
(5, 6, 94),
|
| 347 |
+
(6, 7, 50),
|
| 348 |
+
(1, 8, 30),
|
| 349 |
+
(3, 11, 35),
|
| 350 |
+
(5, 9, 36),
|
| 351 |
+
(7, 10, 26),
|
| 352 |
+
(11, 12, 5),
|
| 353 |
+
]
|
| 354 |
+
)
|
| 355 |
+
ans = {
|
| 356 |
+
1: 8,
|
| 357 |
+
2: 3,
|
| 358 |
+
3: 2,
|
| 359 |
+
4: 6,
|
| 360 |
+
5: 9,
|
| 361 |
+
6: 4,
|
| 362 |
+
7: 10,
|
| 363 |
+
8: 1,
|
| 364 |
+
9: 5,
|
| 365 |
+
10: 7,
|
| 366 |
+
11: 12,
|
| 367 |
+
12: 11,
|
| 368 |
+
}
|
| 369 |
+
answer = matching_dict_to_set(ans)
|
| 370 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 371 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 372 |
+
|
| 373 |
+
def test_nasty_blossom_expand_recursively(self):
|
| 374 |
+
"""Create nested S-blossom, relabel as S, expand recursively:"""
|
| 375 |
+
G = nx.Graph()
|
| 376 |
+
G.add_weighted_edges_from(
|
| 377 |
+
[
|
| 378 |
+
(1, 2, 40),
|
| 379 |
+
(1, 3, 40),
|
| 380 |
+
(2, 3, 60),
|
| 381 |
+
(2, 4, 55),
|
| 382 |
+
(3, 5, 55),
|
| 383 |
+
(4, 5, 50),
|
| 384 |
+
(1, 8, 15),
|
| 385 |
+
(5, 7, 30),
|
| 386 |
+
(7, 6, 10),
|
| 387 |
+
(8, 10, 10),
|
| 388 |
+
(4, 9, 30),
|
| 389 |
+
]
|
| 390 |
+
)
|
| 391 |
+
ans = {1: 2, 2: 1, 3: 5, 4: 9, 5: 3, 6: 7, 7: 6, 8: 10, 9: 4, 10: 8}
|
| 392 |
+
answer = matching_dict_to_set(ans)
|
| 393 |
+
assert edges_equal(nx.max_weight_matching(G), answer)
|
| 394 |
+
assert edges_equal(nx.min_weight_matching(G), answer)
|
| 395 |
+
|
| 396 |
+
def test_wrong_graph_type(self):
|
| 397 |
+
error = nx.NetworkXNotImplemented
|
| 398 |
+
raises(error, nx.max_weight_matching, nx.MultiGraph())
|
| 399 |
+
raises(error, nx.max_weight_matching, nx.MultiDiGraph())
|
| 400 |
+
raises(error, nx.max_weight_matching, nx.DiGraph())
|
| 401 |
+
raises(error, nx.min_weight_matching, nx.DiGraph())
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
class TestIsMatching:
|
| 405 |
+
"""Unit tests for the
|
| 406 |
+
:func:`~networkx.algorithms.matching.is_matching` function.
|
| 407 |
+
|
| 408 |
+
"""
|
| 409 |
+
|
| 410 |
+
def test_dict(self):
|
| 411 |
+
G = nx.path_graph(4)
|
| 412 |
+
assert nx.is_matching(G, {0: 1, 1: 0, 2: 3, 3: 2})
|
| 413 |
+
|
| 414 |
+
def test_empty_matching(self):
|
| 415 |
+
G = nx.path_graph(4)
|
| 416 |
+
assert nx.is_matching(G, set())
|
| 417 |
+
|
| 418 |
+
def test_single_edge(self):
|
| 419 |
+
G = nx.path_graph(4)
|
| 420 |
+
assert nx.is_matching(G, {(1, 2)})
|
| 421 |
+
|
| 422 |
+
def test_edge_order(self):
|
| 423 |
+
G = nx.path_graph(4)
|
| 424 |
+
assert nx.is_matching(G, {(0, 1), (2, 3)})
|
| 425 |
+
assert nx.is_matching(G, {(1, 0), (2, 3)})
|
| 426 |
+
assert nx.is_matching(G, {(0, 1), (3, 2)})
|
| 427 |
+
assert nx.is_matching(G, {(1, 0), (3, 2)})
|
| 428 |
+
|
| 429 |
+
def test_valid_matching(self):
|
| 430 |
+
G = nx.path_graph(4)
|
| 431 |
+
assert nx.is_matching(G, {(0, 1), (2, 3)})
|
| 432 |
+
|
| 433 |
+
def test_invalid_input(self):
|
| 434 |
+
error = nx.NetworkXError
|
| 435 |
+
G = nx.path_graph(4)
|
| 436 |
+
# edge to node not in G
|
| 437 |
+
raises(error, nx.is_matching, G, {(0, 5), (2, 3)})
|
| 438 |
+
# edge not a 2-tuple
|
| 439 |
+
raises(error, nx.is_matching, G, {(0, 1, 2), (2, 3)})
|
| 440 |
+
raises(error, nx.is_matching, G, {(0,), (2, 3)})
|
| 441 |
+
|
| 442 |
+
def test_selfloops(self):
|
| 443 |
+
error = nx.NetworkXError
|
| 444 |
+
G = nx.path_graph(4)
|
| 445 |
+
# selfloop for node not in G
|
| 446 |
+
raises(error, nx.is_matching, G, {(5, 5), (2, 3)})
|
| 447 |
+
# selfloop edge not in G
|
| 448 |
+
assert not nx.is_matching(G, {(0, 0), (1, 2), (2, 3)})
|
| 449 |
+
# selfloop edge in G
|
| 450 |
+
G.add_edge(0, 0)
|
| 451 |
+
assert not nx.is_matching(G, {(0, 0), (1, 2)})
|
| 452 |
+
|
| 453 |
+
def test_invalid_matching(self):
|
| 454 |
+
G = nx.path_graph(4)
|
| 455 |
+
assert not nx.is_matching(G, {(0, 1), (1, 2), (2, 3)})
|
| 456 |
+
|
| 457 |
+
def test_invalid_edge(self):
|
| 458 |
+
G = nx.path_graph(4)
|
| 459 |
+
assert not nx.is_matching(G, {(0, 3), (1, 2)})
|
| 460 |
+
raises(nx.NetworkXError, nx.is_matching, G, {(0, 55)})
|
| 461 |
+
|
| 462 |
+
G = nx.DiGraph(G.edges)
|
| 463 |
+
assert nx.is_matching(G, {(0, 1)})
|
| 464 |
+
assert not nx.is_matching(G, {(1, 0)})
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
class TestIsMaximalMatching:
|
| 468 |
+
"""Unit tests for the
|
| 469 |
+
:func:`~networkx.algorithms.matching.is_maximal_matching` function.
|
| 470 |
+
|
| 471 |
+
"""
|
| 472 |
+
|
| 473 |
+
def test_dict(self):
|
| 474 |
+
G = nx.path_graph(4)
|
| 475 |
+
assert nx.is_maximal_matching(G, {0: 1, 1: 0, 2: 3, 3: 2})
|
| 476 |
+
|
| 477 |
+
def test_invalid_input(self):
|
| 478 |
+
error = nx.NetworkXError
|
| 479 |
+
G = nx.path_graph(4)
|
| 480 |
+
# edge to node not in G
|
| 481 |
+
raises(error, nx.is_maximal_matching, G, {(0, 5)})
|
| 482 |
+
raises(error, nx.is_maximal_matching, G, {(5, 0)})
|
| 483 |
+
# edge not a 2-tuple
|
| 484 |
+
raises(error, nx.is_maximal_matching, G, {(0, 1, 2), (2, 3)})
|
| 485 |
+
raises(error, nx.is_maximal_matching, G, {(0,), (2, 3)})
|
| 486 |
+
|
| 487 |
+
def test_valid(self):
|
| 488 |
+
G = nx.path_graph(4)
|
| 489 |
+
assert nx.is_maximal_matching(G, {(0, 1), (2, 3)})
|
| 490 |
+
|
| 491 |
+
def test_not_matching(self):
|
| 492 |
+
G = nx.path_graph(4)
|
| 493 |
+
assert not nx.is_maximal_matching(G, {(0, 1), (1, 2), (2, 3)})
|
| 494 |
+
assert not nx.is_maximal_matching(G, {(0, 3)})
|
| 495 |
+
G.add_edge(0, 0)
|
| 496 |
+
assert not nx.is_maximal_matching(G, {(0, 0)})
|
| 497 |
+
|
| 498 |
+
def test_not_maximal(self):
|
| 499 |
+
G = nx.path_graph(4)
|
| 500 |
+
assert not nx.is_maximal_matching(G, {(0, 1)})
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
class TestIsPerfectMatching:
|
| 504 |
+
"""Unit tests for the
|
| 505 |
+
:func:`~networkx.algorithms.matching.is_perfect_matching` function.
|
| 506 |
+
|
| 507 |
+
"""
|
| 508 |
+
|
| 509 |
+
def test_dict(self):
|
| 510 |
+
G = nx.path_graph(4)
|
| 511 |
+
assert nx.is_perfect_matching(G, {0: 1, 1: 0, 2: 3, 3: 2})
|
| 512 |
+
|
| 513 |
+
def test_valid(self):
|
| 514 |
+
G = nx.path_graph(4)
|
| 515 |
+
assert nx.is_perfect_matching(G, {(0, 1), (2, 3)})
|
| 516 |
+
|
| 517 |
+
def test_valid_not_path(self):
|
| 518 |
+
G = nx.cycle_graph(4)
|
| 519 |
+
G.add_edge(0, 4)
|
| 520 |
+
G.add_edge(1, 4)
|
| 521 |
+
G.add_edge(5, 2)
|
| 522 |
+
|
| 523 |
+
assert nx.is_perfect_matching(G, {(1, 4), (0, 3), (5, 2)})
|
| 524 |
+
|
| 525 |
+
def test_invalid_input(self):
|
| 526 |
+
error = nx.NetworkXError
|
| 527 |
+
G = nx.path_graph(4)
|
| 528 |
+
# edge to node not in G
|
| 529 |
+
raises(error, nx.is_perfect_matching, G, {(0, 5)})
|
| 530 |
+
raises(error, nx.is_perfect_matching, G, {(5, 0)})
|
| 531 |
+
# edge not a 2-tuple
|
| 532 |
+
raises(error, nx.is_perfect_matching, G, {(0, 1, 2), (2, 3)})
|
| 533 |
+
raises(error, nx.is_perfect_matching, G, {(0,), (2, 3)})
|
| 534 |
+
|
| 535 |
+
def test_selfloops(self):
|
| 536 |
+
error = nx.NetworkXError
|
| 537 |
+
G = nx.path_graph(4)
|
| 538 |
+
# selfloop for node not in G
|
| 539 |
+
raises(error, nx.is_perfect_matching, G, {(5, 5), (2, 3)})
|
| 540 |
+
# selfloop edge not in G
|
| 541 |
+
assert not nx.is_perfect_matching(G, {(0, 0), (1, 2), (2, 3)})
|
| 542 |
+
# selfloop edge in G
|
| 543 |
+
G.add_edge(0, 0)
|
| 544 |
+
assert not nx.is_perfect_matching(G, {(0, 0), (1, 2)})
|
| 545 |
+
|
| 546 |
+
def test_not_matching(self):
|
| 547 |
+
G = nx.path_graph(4)
|
| 548 |
+
assert not nx.is_perfect_matching(G, {(0, 3)})
|
| 549 |
+
assert not nx.is_perfect_matching(G, {(0, 1), (1, 2), (2, 3)})
|
| 550 |
+
|
| 551 |
+
def test_maximal_but_not_perfect(self):
|
| 552 |
+
G = nx.cycle_graph(4)
|
| 553 |
+
G.add_edge(0, 4)
|
| 554 |
+
G.add_edge(1, 4)
|
| 555 |
+
|
| 556 |
+
assert not nx.is_perfect_matching(G, {(1, 4), (0, 3)})
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
class TestMaximalMatching:
|
| 560 |
+
"""Unit tests for the
|
| 561 |
+
:func:`~networkx.algorithms.matching.maximal_matching`.
|
| 562 |
+
|
| 563 |
+
"""
|
| 564 |
+
|
| 565 |
+
def test_valid_matching(self):
|
| 566 |
+
edges = [(1, 2), (1, 5), (2, 3), (2, 5), (3, 4), (3, 6), (5, 6)]
|
| 567 |
+
G = nx.Graph(edges)
|
| 568 |
+
matching = nx.maximal_matching(G)
|
| 569 |
+
assert nx.is_maximal_matching(G, matching)
|
| 570 |
+
|
| 571 |
+
def test_single_edge_matching(self):
|
| 572 |
+
# In the star graph, any maximal matching has just one edge.
|
| 573 |
+
G = nx.star_graph(5)
|
| 574 |
+
matching = nx.maximal_matching(G)
|
| 575 |
+
assert 1 == len(matching)
|
| 576 |
+
assert nx.is_maximal_matching(G, matching)
|
| 577 |
+
|
| 578 |
+
def test_self_loops(self):
|
| 579 |
+
# Create the path graph with two self-loops.
|
| 580 |
+
G = nx.path_graph(3)
|
| 581 |
+
G.add_edges_from([(0, 0), (1, 1)])
|
| 582 |
+
matching = nx.maximal_matching(G)
|
| 583 |
+
assert len(matching) == 1
|
| 584 |
+
# The matching should never include self-loops.
|
| 585 |
+
assert not any(u == v for u, v in matching)
|
| 586 |
+
assert nx.is_maximal_matching(G, matching)
|
| 587 |
+
|
| 588 |
+
def test_ordering(self):
|
| 589 |
+
"""Tests that a maximal matching is computed correctly
|
| 590 |
+
regardless of the order in which nodes are added to the graph.
|
| 591 |
+
|
| 592 |
+
"""
|
| 593 |
+
for nodes in permutations(range(3)):
|
| 594 |
+
G = nx.Graph()
|
| 595 |
+
G.add_nodes_from(nodes)
|
| 596 |
+
G.add_edges_from([(0, 1), (0, 2)])
|
| 597 |
+
matching = nx.maximal_matching(G)
|
| 598 |
+
assert len(matching) == 1
|
| 599 |
+
assert nx.is_maximal_matching(G, matching)
|
| 600 |
+
|
| 601 |
+
def test_wrong_graph_type(self):
|
| 602 |
+
error = nx.NetworkXNotImplemented
|
| 603 |
+
raises(error, nx.maximal_matching, nx.MultiGraph())
|
| 604 |
+
raises(error, nx.maximal_matching, nx.MultiDiGraph())
|
| 605 |
+
raises(error, nx.maximal_matching, nx.DiGraph())
|