Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__init__.py +11 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/connectivity.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/cuts.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/disjoint_paths.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/edge_augmentation.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/edge_kcomponents.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/kcomponents.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/kcutsets.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/stoerwagner.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/connectivity.py +811 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/cuts.py +612 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/disjoint_paths.py +408 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/edge_augmentation.py +1270 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/edge_kcomponents.py +592 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/kcomponents.py +223 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/kcutsets.py +235 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/stoerwagner.py +152 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_connectivity.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_cuts.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_disjoint_paths.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_edge_augmentation.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_edge_kcomponents.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_kcutsets.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_connectivity.py +421 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_cuts.py +309 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_disjoint_paths.py +249 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_edge_augmentation.py +502 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_edge_kcomponents.py +488 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_kcomponents.py +296 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_kcutsets.py +273 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_stoer_wagner.py +102 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/utils.py +88 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_boundary.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_bridges.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_broadcasting.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_chains.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_clique.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_cluster.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_communicability.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_core.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_cycles.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_d_separation.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_dag.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_distance_measures.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_distance_regular.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_dominance.cpython-311.pyc +0 -0
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Connectivity and cut algorithms"""
|
| 2 |
+
|
| 3 |
+
from .connectivity import *
|
| 4 |
+
from .cuts import *
|
| 5 |
+
from .edge_augmentation import *
|
| 6 |
+
from .edge_kcomponents import *
|
| 7 |
+
from .disjoint_paths import *
|
| 8 |
+
from .kcomponents import *
|
| 9 |
+
from .kcutsets import *
|
| 10 |
+
from .stoerwagner import *
|
| 11 |
+
from .utils import *
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (556 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/connectivity.cpython-311.pyc
ADDED
|
Binary file (31.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/cuts.cpython-311.pyc
ADDED
|
Binary file (24.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/disjoint_paths.cpython-311.pyc
ADDED
|
Binary file (15.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/edge_augmentation.cpython-311.pyc
ADDED
|
Binary file (52.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/edge_kcomponents.cpython-311.pyc
ADDED
|
Binary file (24.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/kcomponents.cpython-311.pyc
ADDED
|
Binary file (12.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/kcutsets.cpython-311.pyc
ADDED
|
Binary file (10.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/stoerwagner.cpython-311.pyc
ADDED
|
Binary file (6.81 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (4.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/connectivity.py
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Flow based connectivity algorithms
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import itertools
|
| 6 |
+
from operator import itemgetter
|
| 7 |
+
|
| 8 |
+
import networkx as nx
|
| 9 |
+
|
| 10 |
+
# Define the default maximum flow function to use in all flow based
|
| 11 |
+
# connectivity algorithms.
|
| 12 |
+
from networkx.algorithms.flow import (
|
| 13 |
+
boykov_kolmogorov,
|
| 14 |
+
build_residual_network,
|
| 15 |
+
dinitz,
|
| 16 |
+
edmonds_karp,
|
| 17 |
+
preflow_push,
|
| 18 |
+
shortest_augmenting_path,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
default_flow_func = edmonds_karp
|
| 22 |
+
|
| 23 |
+
from .utils import build_auxiliary_edge_connectivity, build_auxiliary_node_connectivity
|
| 24 |
+
|
| 25 |
+
__all__ = [
|
| 26 |
+
"average_node_connectivity",
|
| 27 |
+
"local_node_connectivity",
|
| 28 |
+
"node_connectivity",
|
| 29 |
+
"local_edge_connectivity",
|
| 30 |
+
"edge_connectivity",
|
| 31 |
+
"all_pairs_node_connectivity",
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@nx._dispatchable(graphs={"G": 0, "auxiliary?": 4}, preserve_graph_attrs={"auxiliary"})
|
| 36 |
+
def local_node_connectivity(
|
| 37 |
+
G, s, t, flow_func=None, auxiliary=None, residual=None, cutoff=None
|
| 38 |
+
):
|
| 39 |
+
r"""Computes local node connectivity for nodes s and t.
|
| 40 |
+
|
| 41 |
+
Local node connectivity for two non adjacent nodes s and t is the
|
| 42 |
+
minimum number of nodes that must be removed (along with their incident
|
| 43 |
+
edges) to disconnect them.
|
| 44 |
+
|
| 45 |
+
This is a flow based implementation of node connectivity. We compute the
|
| 46 |
+
maximum flow on an auxiliary digraph build from the original input
|
| 47 |
+
graph (see below for details).
|
| 48 |
+
|
| 49 |
+
Parameters
|
| 50 |
+
----------
|
| 51 |
+
G : NetworkX graph
|
| 52 |
+
Undirected graph
|
| 53 |
+
|
| 54 |
+
s : node
|
| 55 |
+
Source node
|
| 56 |
+
|
| 57 |
+
t : node
|
| 58 |
+
Target node
|
| 59 |
+
|
| 60 |
+
flow_func : function
|
| 61 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 62 |
+
The function has to accept at least three parameters: a Digraph,
|
| 63 |
+
a source node, and a target node. And return a residual network
|
| 64 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 65 |
+
details). If flow_func is None, the default maximum flow function
|
| 66 |
+
(:meth:`edmonds_karp`) is used. See below for details. The choice
|
| 67 |
+
of the default function may change from version to version and
|
| 68 |
+
should not be relied on. Default value: None.
|
| 69 |
+
|
| 70 |
+
auxiliary : NetworkX DiGraph
|
| 71 |
+
Auxiliary digraph to compute flow based node connectivity. It has
|
| 72 |
+
to have a graph attribute called mapping with a dictionary mapping
|
| 73 |
+
node names in G and in the auxiliary digraph. If provided
|
| 74 |
+
it will be reused instead of recreated. Default value: None.
|
| 75 |
+
|
| 76 |
+
residual : NetworkX DiGraph
|
| 77 |
+
Residual network to compute maximum flow. If provided it will be
|
| 78 |
+
reused instead of recreated. Default value: None.
|
| 79 |
+
|
| 80 |
+
cutoff : integer, float, or None (default: None)
|
| 81 |
+
If specified, the maximum flow algorithm will terminate when the
|
| 82 |
+
flow value reaches or exceeds the cutoff. This only works for flows
|
| 83 |
+
that support the cutoff parameter (most do) and is ignored otherwise.
|
| 84 |
+
|
| 85 |
+
Returns
|
| 86 |
+
-------
|
| 87 |
+
K : integer
|
| 88 |
+
local node connectivity for nodes s and t
|
| 89 |
+
|
| 90 |
+
Examples
|
| 91 |
+
--------
|
| 92 |
+
This function is not imported in the base NetworkX namespace, so you
|
| 93 |
+
have to explicitly import it from the connectivity package:
|
| 94 |
+
|
| 95 |
+
>>> from networkx.algorithms.connectivity import local_node_connectivity
|
| 96 |
+
|
| 97 |
+
We use in this example the platonic icosahedral graph, which has node
|
| 98 |
+
connectivity 5.
|
| 99 |
+
|
| 100 |
+
>>> G = nx.icosahedral_graph()
|
| 101 |
+
>>> local_node_connectivity(G, 0, 6)
|
| 102 |
+
5
|
| 103 |
+
|
| 104 |
+
If you need to compute local connectivity on several pairs of
|
| 105 |
+
nodes in the same graph, it is recommended that you reuse the
|
| 106 |
+
data structures that NetworkX uses in the computation: the
|
| 107 |
+
auxiliary digraph for node connectivity, and the residual
|
| 108 |
+
network for the underlying maximum flow computation.
|
| 109 |
+
|
| 110 |
+
Example of how to compute local node connectivity among
|
| 111 |
+
all pairs of nodes of the platonic icosahedral graph reusing
|
| 112 |
+
the data structures.
|
| 113 |
+
|
| 114 |
+
>>> import itertools
|
| 115 |
+
>>> # You also have to explicitly import the function for
|
| 116 |
+
>>> # building the auxiliary digraph from the connectivity package
|
| 117 |
+
>>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity
|
| 118 |
+
>>> H = build_auxiliary_node_connectivity(G)
|
| 119 |
+
>>> # And the function for building the residual network from the
|
| 120 |
+
>>> # flow package
|
| 121 |
+
>>> from networkx.algorithms.flow import build_residual_network
|
| 122 |
+
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
| 123 |
+
>>> R = build_residual_network(H, "capacity")
|
| 124 |
+
>>> result = dict.fromkeys(G, dict())
|
| 125 |
+
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
| 126 |
+
>>> # as parameters
|
| 127 |
+
>>> for u, v in itertools.combinations(G, 2):
|
| 128 |
+
... k = local_node_connectivity(G, u, v, auxiliary=H, residual=R)
|
| 129 |
+
... result[u][v] = k
|
| 130 |
+
>>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
|
| 131 |
+
True
|
| 132 |
+
|
| 133 |
+
You can also use alternative flow algorithms for computing node
|
| 134 |
+
connectivity. For instance, in dense networks the algorithm
|
| 135 |
+
:meth:`shortest_augmenting_path` will usually perform better than
|
| 136 |
+
the default :meth:`edmonds_karp` which is faster for sparse
|
| 137 |
+
networks with highly skewed degree distributions. Alternative flow
|
| 138 |
+
functions have to be explicitly imported from the flow package.
|
| 139 |
+
|
| 140 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 141 |
+
>>> local_node_connectivity(G, 0, 6, flow_func=shortest_augmenting_path)
|
| 142 |
+
5
|
| 143 |
+
|
| 144 |
+
Notes
|
| 145 |
+
-----
|
| 146 |
+
This is a flow based implementation of node connectivity. We compute the
|
| 147 |
+
maximum flow using, by default, the :meth:`edmonds_karp` algorithm (see:
|
| 148 |
+
:meth:`maximum_flow`) on an auxiliary digraph build from the original
|
| 149 |
+
input graph:
|
| 150 |
+
|
| 151 |
+
For an undirected graph G having `n` nodes and `m` edges we derive a
|
| 152 |
+
directed graph H with `2n` nodes and `2m+n` arcs by replacing each
|
| 153 |
+
original node `v` with two nodes `v_A`, `v_B` linked by an (internal)
|
| 154 |
+
arc in H. Then for each edge (`u`, `v`) in G we add two arcs
|
| 155 |
+
(`u_B`, `v_A`) and (`v_B`, `u_A`) in H. Finally we set the attribute
|
| 156 |
+
capacity = 1 for each arc in H [1]_ .
|
| 157 |
+
|
| 158 |
+
For a directed graph G having `n` nodes and `m` arcs we derive a
|
| 159 |
+
directed graph H with `2n` nodes and `m+n` arcs by replacing each
|
| 160 |
+
original node `v` with two nodes `v_A`, `v_B` linked by an (internal)
|
| 161 |
+
arc (`v_A`, `v_B`) in H. Then for each arc (`u`, `v`) in G we add one arc
|
| 162 |
+
(`u_B`, `v_A`) in H. Finally we set the attribute capacity = 1 for
|
| 163 |
+
each arc in H.
|
| 164 |
+
|
| 165 |
+
This is equal to the local node connectivity because the value of
|
| 166 |
+
a maximum s-t-flow is equal to the capacity of a minimum s-t-cut.
|
| 167 |
+
|
| 168 |
+
See also
|
| 169 |
+
--------
|
| 170 |
+
:meth:`local_edge_connectivity`
|
| 171 |
+
:meth:`node_connectivity`
|
| 172 |
+
:meth:`minimum_node_cut`
|
| 173 |
+
:meth:`maximum_flow`
|
| 174 |
+
:meth:`edmonds_karp`
|
| 175 |
+
:meth:`preflow_push`
|
| 176 |
+
:meth:`shortest_augmenting_path`
|
| 177 |
+
|
| 178 |
+
References
|
| 179 |
+
----------
|
| 180 |
+
.. [1] Kammer, Frank and Hanjo Taubig. Graph Connectivity. in Brandes and
|
| 181 |
+
Erlebach, 'Network Analysis: Methodological Foundations', Lecture
|
| 182 |
+
Notes in Computer Science, Volume 3418, Springer-Verlag, 2005.
|
| 183 |
+
http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf
|
| 184 |
+
|
| 185 |
+
"""
|
| 186 |
+
if flow_func is None:
|
| 187 |
+
flow_func = default_flow_func
|
| 188 |
+
|
| 189 |
+
if auxiliary is None:
|
| 190 |
+
H = build_auxiliary_node_connectivity(G)
|
| 191 |
+
else:
|
| 192 |
+
H = auxiliary
|
| 193 |
+
|
| 194 |
+
mapping = H.graph.get("mapping", None)
|
| 195 |
+
if mapping is None:
|
| 196 |
+
raise nx.NetworkXError("Invalid auxiliary digraph.")
|
| 197 |
+
|
| 198 |
+
kwargs = {"flow_func": flow_func, "residual": residual}
|
| 199 |
+
|
| 200 |
+
if flow_func is not preflow_push:
|
| 201 |
+
kwargs["cutoff"] = cutoff
|
| 202 |
+
|
| 203 |
+
if flow_func is shortest_augmenting_path:
|
| 204 |
+
kwargs["two_phase"] = True
|
| 205 |
+
|
| 206 |
+
return nx.maximum_flow_value(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
@nx._dispatchable
|
| 210 |
+
def node_connectivity(G, s=None, t=None, flow_func=None):
|
| 211 |
+
r"""Returns node connectivity for a graph or digraph G.
|
| 212 |
+
|
| 213 |
+
Node connectivity is equal to the minimum number of nodes that
|
| 214 |
+
must be removed to disconnect G or render it trivial. If source
|
| 215 |
+
and target nodes are provided, this function returns the local node
|
| 216 |
+
connectivity: the minimum number of nodes that must be removed to break
|
| 217 |
+
all paths from source to target in G.
|
| 218 |
+
|
| 219 |
+
Parameters
|
| 220 |
+
----------
|
| 221 |
+
G : NetworkX graph
|
| 222 |
+
Undirected graph
|
| 223 |
+
|
| 224 |
+
s : node
|
| 225 |
+
Source node. Optional. Default value: None.
|
| 226 |
+
|
| 227 |
+
t : node
|
| 228 |
+
Target node. Optional. Default value: None.
|
| 229 |
+
|
| 230 |
+
flow_func : function
|
| 231 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 232 |
+
The function has to accept at least three parameters: a Digraph,
|
| 233 |
+
a source node, and a target node. And return a residual network
|
| 234 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 235 |
+
details). If flow_func is None, the default maximum flow function
|
| 236 |
+
(:meth:`edmonds_karp`) is used. See below for details. The
|
| 237 |
+
choice of the default function may change from version
|
| 238 |
+
to version and should not be relied on. Default value: None.
|
| 239 |
+
|
| 240 |
+
Returns
|
| 241 |
+
-------
|
| 242 |
+
K : integer
|
| 243 |
+
Node connectivity of G, or local node connectivity if source
|
| 244 |
+
and target are provided.
|
| 245 |
+
|
| 246 |
+
Examples
|
| 247 |
+
--------
|
| 248 |
+
>>> # Platonic icosahedral graph is 5-node-connected
|
| 249 |
+
>>> G = nx.icosahedral_graph()
|
| 250 |
+
>>> nx.node_connectivity(G)
|
| 251 |
+
5
|
| 252 |
+
|
| 253 |
+
You can use alternative flow algorithms for the underlying maximum
|
| 254 |
+
flow computation. In dense networks the algorithm
|
| 255 |
+
:meth:`shortest_augmenting_path` will usually perform better
|
| 256 |
+
than the default :meth:`edmonds_karp`, which is faster for
|
| 257 |
+
sparse networks with highly skewed degree distributions. Alternative
|
| 258 |
+
flow functions have to be explicitly imported from the flow package.
|
| 259 |
+
|
| 260 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 261 |
+
>>> nx.node_connectivity(G, flow_func=shortest_augmenting_path)
|
| 262 |
+
5
|
| 263 |
+
|
| 264 |
+
If you specify a pair of nodes (source and target) as parameters,
|
| 265 |
+
this function returns the value of local node connectivity.
|
| 266 |
+
|
| 267 |
+
>>> nx.node_connectivity(G, 3, 7)
|
| 268 |
+
5
|
| 269 |
+
|
| 270 |
+
If you need to perform several local computations among different
|
| 271 |
+
pairs of nodes on the same graph, it is recommended that you reuse
|
| 272 |
+
the data structures used in the maximum flow computations. See
|
| 273 |
+
:meth:`local_node_connectivity` for details.
|
| 274 |
+
|
| 275 |
+
Notes
|
| 276 |
+
-----
|
| 277 |
+
This is a flow based implementation of node connectivity. The
|
| 278 |
+
algorithm works by solving $O((n-\delta-1+\delta(\delta-1)/2))$
|
| 279 |
+
maximum flow problems on an auxiliary digraph. Where $\delta$
|
| 280 |
+
is the minimum degree of G. For details about the auxiliary
|
| 281 |
+
digraph and the computation of local node connectivity see
|
| 282 |
+
:meth:`local_node_connectivity`. This implementation is based
|
| 283 |
+
on algorithm 11 in [1]_.
|
| 284 |
+
|
| 285 |
+
See also
|
| 286 |
+
--------
|
| 287 |
+
:meth:`local_node_connectivity`
|
| 288 |
+
:meth:`edge_connectivity`
|
| 289 |
+
:meth:`maximum_flow`
|
| 290 |
+
:meth:`edmonds_karp`
|
| 291 |
+
:meth:`preflow_push`
|
| 292 |
+
:meth:`shortest_augmenting_path`
|
| 293 |
+
|
| 294 |
+
References
|
| 295 |
+
----------
|
| 296 |
+
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
| 297 |
+
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
| 298 |
+
|
| 299 |
+
"""
|
| 300 |
+
if (s is not None and t is None) or (s is None and t is not None):
|
| 301 |
+
raise nx.NetworkXError("Both source and target must be specified.")
|
| 302 |
+
|
| 303 |
+
# Local node connectivity
|
| 304 |
+
if s is not None and t is not None:
|
| 305 |
+
if s not in G:
|
| 306 |
+
raise nx.NetworkXError(f"node {s} not in graph")
|
| 307 |
+
if t not in G:
|
| 308 |
+
raise nx.NetworkXError(f"node {t} not in graph")
|
| 309 |
+
return local_node_connectivity(G, s, t, flow_func=flow_func)
|
| 310 |
+
|
| 311 |
+
# Global node connectivity
|
| 312 |
+
if G.is_directed():
|
| 313 |
+
if not nx.is_weakly_connected(G):
|
| 314 |
+
return 0
|
| 315 |
+
iter_func = itertools.permutations
|
| 316 |
+
# It is necessary to consider both predecessors
|
| 317 |
+
# and successors for directed graphs
|
| 318 |
+
|
| 319 |
+
def neighbors(v):
|
| 320 |
+
return itertools.chain.from_iterable([G.predecessors(v), G.successors(v)])
|
| 321 |
+
|
| 322 |
+
else:
|
| 323 |
+
if not nx.is_connected(G):
|
| 324 |
+
return 0
|
| 325 |
+
iter_func = itertools.combinations
|
| 326 |
+
neighbors = G.neighbors
|
| 327 |
+
|
| 328 |
+
# Reuse the auxiliary digraph and the residual network
|
| 329 |
+
H = build_auxiliary_node_connectivity(G)
|
| 330 |
+
R = build_residual_network(H, "capacity")
|
| 331 |
+
kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R}
|
| 332 |
+
|
| 333 |
+
# Pick a node with minimum degree
|
| 334 |
+
# Node connectivity is bounded by degree.
|
| 335 |
+
v, K = min(G.degree(), key=itemgetter(1))
|
| 336 |
+
# compute local node connectivity with all its non-neighbors nodes
|
| 337 |
+
for w in set(G) - set(neighbors(v)) - {v}:
|
| 338 |
+
kwargs["cutoff"] = K
|
| 339 |
+
K = min(K, local_node_connectivity(G, v, w, **kwargs))
|
| 340 |
+
# Also for non adjacent pairs of neighbors of v
|
| 341 |
+
for x, y in iter_func(neighbors(v), 2):
|
| 342 |
+
if y in G[x]:
|
| 343 |
+
continue
|
| 344 |
+
kwargs["cutoff"] = K
|
| 345 |
+
K = min(K, local_node_connectivity(G, x, y, **kwargs))
|
| 346 |
+
|
| 347 |
+
return K
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
@nx._dispatchable
|
| 351 |
+
def average_node_connectivity(G, flow_func=None):
|
| 352 |
+
r"""Returns the average connectivity of a graph G.
|
| 353 |
+
|
| 354 |
+
The average connectivity `\bar{\kappa}` of a graph G is the average
|
| 355 |
+
of local node connectivity over all pairs of nodes of G [1]_ .
|
| 356 |
+
|
| 357 |
+
.. math::
|
| 358 |
+
|
| 359 |
+
\bar{\kappa}(G) = \frac{\sum_{u,v} \kappa_{G}(u,v)}{{n \choose 2}}
|
| 360 |
+
|
| 361 |
+
Parameters
|
| 362 |
+
----------
|
| 363 |
+
|
| 364 |
+
G : NetworkX graph
|
| 365 |
+
Undirected graph
|
| 366 |
+
|
| 367 |
+
flow_func : function
|
| 368 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 369 |
+
The function has to accept at least three parameters: a Digraph,
|
| 370 |
+
a source node, and a target node. And return a residual network
|
| 371 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 372 |
+
details). If flow_func is None, the default maximum flow function
|
| 373 |
+
(:meth:`edmonds_karp`) is used. See :meth:`local_node_connectivity`
|
| 374 |
+
for details. The choice of the default function may change from
|
| 375 |
+
version to version and should not be relied on. Default value: None.
|
| 376 |
+
|
| 377 |
+
Returns
|
| 378 |
+
-------
|
| 379 |
+
K : float
|
| 380 |
+
Average node connectivity
|
| 381 |
+
|
| 382 |
+
See also
|
| 383 |
+
--------
|
| 384 |
+
:meth:`local_node_connectivity`
|
| 385 |
+
:meth:`node_connectivity`
|
| 386 |
+
:meth:`edge_connectivity`
|
| 387 |
+
:meth:`maximum_flow`
|
| 388 |
+
:meth:`edmonds_karp`
|
| 389 |
+
:meth:`preflow_push`
|
| 390 |
+
:meth:`shortest_augmenting_path`
|
| 391 |
+
|
| 392 |
+
References
|
| 393 |
+
----------
|
| 394 |
+
.. [1] Beineke, L., O. Oellermann, and R. Pippert (2002). The average
|
| 395 |
+
connectivity of a graph. Discrete mathematics 252(1-3), 31-45.
|
| 396 |
+
http://www.sciencedirect.com/science/article/pii/S0012365X01001807
|
| 397 |
+
|
| 398 |
+
"""
|
| 399 |
+
if G.is_directed():
|
| 400 |
+
iter_func = itertools.permutations
|
| 401 |
+
else:
|
| 402 |
+
iter_func = itertools.combinations
|
| 403 |
+
|
| 404 |
+
# Reuse the auxiliary digraph and the residual network
|
| 405 |
+
H = build_auxiliary_node_connectivity(G)
|
| 406 |
+
R = build_residual_network(H, "capacity")
|
| 407 |
+
kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R}
|
| 408 |
+
|
| 409 |
+
num, den = 0, 0
|
| 410 |
+
for u, v in iter_func(G, 2):
|
| 411 |
+
num += local_node_connectivity(G, u, v, **kwargs)
|
| 412 |
+
den += 1
|
| 413 |
+
|
| 414 |
+
if den == 0: # Null Graph
|
| 415 |
+
return 0
|
| 416 |
+
return num / den
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
@nx._dispatchable
|
| 420 |
+
def all_pairs_node_connectivity(G, nbunch=None, flow_func=None):
|
| 421 |
+
"""Compute node connectivity between all pairs of nodes of G.
|
| 422 |
+
|
| 423 |
+
Parameters
|
| 424 |
+
----------
|
| 425 |
+
G : NetworkX graph
|
| 426 |
+
Undirected graph
|
| 427 |
+
|
| 428 |
+
nbunch: container
|
| 429 |
+
Container of nodes. If provided node connectivity will be computed
|
| 430 |
+
only over pairs of nodes in nbunch.
|
| 431 |
+
|
| 432 |
+
flow_func : function
|
| 433 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 434 |
+
The function has to accept at least three parameters: a Digraph,
|
| 435 |
+
a source node, and a target node. And return a residual network
|
| 436 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 437 |
+
details). If flow_func is None, the default maximum flow function
|
| 438 |
+
(:meth:`edmonds_karp`) is used. See below for details. The
|
| 439 |
+
choice of the default function may change from version
|
| 440 |
+
to version and should not be relied on. Default value: None.
|
| 441 |
+
|
| 442 |
+
Returns
|
| 443 |
+
-------
|
| 444 |
+
all_pairs : dict
|
| 445 |
+
A dictionary with node connectivity between all pairs of nodes
|
| 446 |
+
in G, or in nbunch if provided.
|
| 447 |
+
|
| 448 |
+
See also
|
| 449 |
+
--------
|
| 450 |
+
:meth:`local_node_connectivity`
|
| 451 |
+
:meth:`edge_connectivity`
|
| 452 |
+
:meth:`local_edge_connectivity`
|
| 453 |
+
:meth:`maximum_flow`
|
| 454 |
+
:meth:`edmonds_karp`
|
| 455 |
+
:meth:`preflow_push`
|
| 456 |
+
:meth:`shortest_augmenting_path`
|
| 457 |
+
|
| 458 |
+
"""
|
| 459 |
+
if nbunch is None:
|
| 460 |
+
nbunch = G
|
| 461 |
+
else:
|
| 462 |
+
nbunch = set(nbunch)
|
| 463 |
+
|
| 464 |
+
directed = G.is_directed()
|
| 465 |
+
if directed:
|
| 466 |
+
iter_func = itertools.permutations
|
| 467 |
+
else:
|
| 468 |
+
iter_func = itertools.combinations
|
| 469 |
+
|
| 470 |
+
all_pairs = {n: {} for n in nbunch}
|
| 471 |
+
|
| 472 |
+
# Reuse auxiliary digraph and residual network
|
| 473 |
+
H = build_auxiliary_node_connectivity(G)
|
| 474 |
+
mapping = H.graph["mapping"]
|
| 475 |
+
R = build_residual_network(H, "capacity")
|
| 476 |
+
kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R}
|
| 477 |
+
|
| 478 |
+
for u, v in iter_func(nbunch, 2):
|
| 479 |
+
K = local_node_connectivity(G, u, v, **kwargs)
|
| 480 |
+
all_pairs[u][v] = K
|
| 481 |
+
if not directed:
|
| 482 |
+
all_pairs[v][u] = K
|
| 483 |
+
|
| 484 |
+
return all_pairs
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
@nx._dispatchable(graphs={"G": 0, "auxiliary?": 4})
|
| 488 |
+
def local_edge_connectivity(
|
| 489 |
+
G, s, t, flow_func=None, auxiliary=None, residual=None, cutoff=None
|
| 490 |
+
):
|
| 491 |
+
r"""Returns local edge connectivity for nodes s and t in G.
|
| 492 |
+
|
| 493 |
+
Local edge connectivity for two nodes s and t is the minimum number
|
| 494 |
+
of edges that must be removed to disconnect them.
|
| 495 |
+
|
| 496 |
+
This is a flow based implementation of edge connectivity. We compute the
|
| 497 |
+
maximum flow on an auxiliary digraph build from the original
|
| 498 |
+
network (see below for details). This is equal to the local edge
|
| 499 |
+
connectivity because the value of a maximum s-t-flow is equal to the
|
| 500 |
+
capacity of a minimum s-t-cut (Ford and Fulkerson theorem) [1]_ .
|
| 501 |
+
|
| 502 |
+
Parameters
|
| 503 |
+
----------
|
| 504 |
+
G : NetworkX graph
|
| 505 |
+
Undirected or directed graph
|
| 506 |
+
|
| 507 |
+
s : node
|
| 508 |
+
Source node
|
| 509 |
+
|
| 510 |
+
t : node
|
| 511 |
+
Target node
|
| 512 |
+
|
| 513 |
+
flow_func : function
|
| 514 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 515 |
+
The function has to accept at least three parameters: a Digraph,
|
| 516 |
+
a source node, and a target node. And return a residual network
|
| 517 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 518 |
+
details). If flow_func is None, the default maximum flow function
|
| 519 |
+
(:meth:`edmonds_karp`) is used. See below for details. The
|
| 520 |
+
choice of the default function may change from version
|
| 521 |
+
to version and should not be relied on. Default value: None.
|
| 522 |
+
|
| 523 |
+
auxiliary : NetworkX DiGraph
|
| 524 |
+
Auxiliary digraph for computing flow based edge connectivity. If
|
| 525 |
+
provided it will be reused instead of recreated. Default value: None.
|
| 526 |
+
|
| 527 |
+
residual : NetworkX DiGraph
|
| 528 |
+
Residual network to compute maximum flow. If provided it will be
|
| 529 |
+
reused instead of recreated. Default value: None.
|
| 530 |
+
|
| 531 |
+
cutoff : integer, float, or None (default: None)
|
| 532 |
+
If specified, the maximum flow algorithm will terminate when the
|
| 533 |
+
flow value reaches or exceeds the cutoff. This only works for flows
|
| 534 |
+
that support the cutoff parameter (most do) and is ignored otherwise.
|
| 535 |
+
|
| 536 |
+
Returns
|
| 537 |
+
-------
|
| 538 |
+
K : integer
|
| 539 |
+
local edge connectivity for nodes s and t.
|
| 540 |
+
|
| 541 |
+
Examples
|
| 542 |
+
--------
|
| 543 |
+
This function is not imported in the base NetworkX namespace, so you
|
| 544 |
+
have to explicitly import it from the connectivity package:
|
| 545 |
+
|
| 546 |
+
>>> from networkx.algorithms.connectivity import local_edge_connectivity
|
| 547 |
+
|
| 548 |
+
We use in this example the platonic icosahedral graph, which has edge
|
| 549 |
+
connectivity 5.
|
| 550 |
+
|
| 551 |
+
>>> G = nx.icosahedral_graph()
|
| 552 |
+
>>> local_edge_connectivity(G, 0, 6)
|
| 553 |
+
5
|
| 554 |
+
|
| 555 |
+
If you need to compute local connectivity on several pairs of
|
| 556 |
+
nodes in the same graph, it is recommended that you reuse the
|
| 557 |
+
data structures that NetworkX uses in the computation: the
|
| 558 |
+
auxiliary digraph for edge connectivity, and the residual
|
| 559 |
+
network for the underlying maximum flow computation.
|
| 560 |
+
|
| 561 |
+
Example of how to compute local edge connectivity among
|
| 562 |
+
all pairs of nodes of the platonic icosahedral graph reusing
|
| 563 |
+
the data structures.
|
| 564 |
+
|
| 565 |
+
>>> import itertools
|
| 566 |
+
>>> # You also have to explicitly import the function for
|
| 567 |
+
>>> # building the auxiliary digraph from the connectivity package
|
| 568 |
+
>>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity
|
| 569 |
+
>>> H = build_auxiliary_edge_connectivity(G)
|
| 570 |
+
>>> # And the function for building the residual network from the
|
| 571 |
+
>>> # flow package
|
| 572 |
+
>>> from networkx.algorithms.flow import build_residual_network
|
| 573 |
+
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
| 574 |
+
>>> R = build_residual_network(H, "capacity")
|
| 575 |
+
>>> result = dict.fromkeys(G, dict())
|
| 576 |
+
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
| 577 |
+
>>> # as parameters
|
| 578 |
+
>>> for u, v in itertools.combinations(G, 2):
|
| 579 |
+
... k = local_edge_connectivity(G, u, v, auxiliary=H, residual=R)
|
| 580 |
+
... result[u][v] = k
|
| 581 |
+
>>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
|
| 582 |
+
True
|
| 583 |
+
|
| 584 |
+
You can also use alternative flow algorithms for computing edge
|
| 585 |
+
connectivity. For instance, in dense networks the algorithm
|
| 586 |
+
:meth:`shortest_augmenting_path` will usually perform better than
|
| 587 |
+
the default :meth:`edmonds_karp` which is faster for sparse
|
| 588 |
+
networks with highly skewed degree distributions. Alternative flow
|
| 589 |
+
functions have to be explicitly imported from the flow package.
|
| 590 |
+
|
| 591 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 592 |
+
>>> local_edge_connectivity(G, 0, 6, flow_func=shortest_augmenting_path)
|
| 593 |
+
5
|
| 594 |
+
|
| 595 |
+
Notes
|
| 596 |
+
-----
|
| 597 |
+
This is a flow based implementation of edge connectivity. We compute the
|
| 598 |
+
maximum flow using, by default, the :meth:`edmonds_karp` algorithm on an
|
| 599 |
+
auxiliary digraph build from the original input graph:
|
| 600 |
+
|
| 601 |
+
If the input graph is undirected, we replace each edge (`u`,`v`) with
|
| 602 |
+
two reciprocal arcs (`u`, `v`) and (`v`, `u`) and then we set the attribute
|
| 603 |
+
'capacity' for each arc to 1. If the input graph is directed we simply
|
| 604 |
+
add the 'capacity' attribute. This is an implementation of algorithm 1
|
| 605 |
+
in [1]_.
|
| 606 |
+
|
| 607 |
+
The maximum flow in the auxiliary network is equal to the local edge
|
| 608 |
+
connectivity because the value of a maximum s-t-flow is equal to the
|
| 609 |
+
capacity of a minimum s-t-cut (Ford and Fulkerson theorem).
|
| 610 |
+
|
| 611 |
+
See also
|
| 612 |
+
--------
|
| 613 |
+
:meth:`edge_connectivity`
|
| 614 |
+
:meth:`local_node_connectivity`
|
| 615 |
+
:meth:`node_connectivity`
|
| 616 |
+
:meth:`maximum_flow`
|
| 617 |
+
:meth:`edmonds_karp`
|
| 618 |
+
:meth:`preflow_push`
|
| 619 |
+
:meth:`shortest_augmenting_path`
|
| 620 |
+
|
| 621 |
+
References
|
| 622 |
+
----------
|
| 623 |
+
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
| 624 |
+
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
| 625 |
+
|
| 626 |
+
"""
|
| 627 |
+
if flow_func is None:
|
| 628 |
+
flow_func = default_flow_func
|
| 629 |
+
|
| 630 |
+
if auxiliary is None:
|
| 631 |
+
H = build_auxiliary_edge_connectivity(G)
|
| 632 |
+
else:
|
| 633 |
+
H = auxiliary
|
| 634 |
+
|
| 635 |
+
kwargs = {"flow_func": flow_func, "residual": residual}
|
| 636 |
+
|
| 637 |
+
if flow_func is not preflow_push:
|
| 638 |
+
kwargs["cutoff"] = cutoff
|
| 639 |
+
|
| 640 |
+
if flow_func is shortest_augmenting_path:
|
| 641 |
+
kwargs["two_phase"] = True
|
| 642 |
+
|
| 643 |
+
return nx.maximum_flow_value(H, s, t, **kwargs)
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
@nx._dispatchable
|
| 647 |
+
def edge_connectivity(G, s=None, t=None, flow_func=None, cutoff=None):
|
| 648 |
+
r"""Returns the edge connectivity of the graph or digraph G.
|
| 649 |
+
|
| 650 |
+
The edge connectivity is equal to the minimum number of edges that
|
| 651 |
+
must be removed to disconnect G or render it trivial. If source
|
| 652 |
+
and target nodes are provided, this function returns the local edge
|
| 653 |
+
connectivity: the minimum number of edges that must be removed to
|
| 654 |
+
break all paths from source to target in G.
|
| 655 |
+
|
| 656 |
+
Parameters
|
| 657 |
+
----------
|
| 658 |
+
G : NetworkX graph
|
| 659 |
+
Undirected or directed graph
|
| 660 |
+
|
| 661 |
+
s : node
|
| 662 |
+
Source node. Optional. Default value: None.
|
| 663 |
+
|
| 664 |
+
t : node
|
| 665 |
+
Target node. Optional. Default value: None.
|
| 666 |
+
|
| 667 |
+
flow_func : function
|
| 668 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 669 |
+
The function has to accept at least three parameters: a Digraph,
|
| 670 |
+
a source node, and a target node. And return a residual network
|
| 671 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 672 |
+
details). If flow_func is None, the default maximum flow function
|
| 673 |
+
(:meth:`edmonds_karp`) is used. See below for details. The
|
| 674 |
+
choice of the default function may change from version
|
| 675 |
+
to version and should not be relied on. Default value: None.
|
| 676 |
+
|
| 677 |
+
cutoff : integer, float, or None (default: None)
|
| 678 |
+
If specified, the maximum flow algorithm will terminate when the
|
| 679 |
+
flow value reaches or exceeds the cutoff. This only works for flows
|
| 680 |
+
that support the cutoff parameter (most do) and is ignored otherwise.
|
| 681 |
+
|
| 682 |
+
Returns
|
| 683 |
+
-------
|
| 684 |
+
K : integer
|
| 685 |
+
Edge connectivity for G, or local edge connectivity if source
|
| 686 |
+
and target were provided
|
| 687 |
+
|
| 688 |
+
Examples
|
| 689 |
+
--------
|
| 690 |
+
>>> # Platonic icosahedral graph is 5-edge-connected
|
| 691 |
+
>>> G = nx.icosahedral_graph()
|
| 692 |
+
>>> nx.edge_connectivity(G)
|
| 693 |
+
5
|
| 694 |
+
|
| 695 |
+
You can use alternative flow algorithms for the underlying
|
| 696 |
+
maximum flow computation. In dense networks the algorithm
|
| 697 |
+
:meth:`shortest_augmenting_path` will usually perform better
|
| 698 |
+
than the default :meth:`edmonds_karp`, which is faster for
|
| 699 |
+
sparse networks with highly skewed degree distributions.
|
| 700 |
+
Alternative flow functions have to be explicitly imported
|
| 701 |
+
from the flow package.
|
| 702 |
+
|
| 703 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 704 |
+
>>> nx.edge_connectivity(G, flow_func=shortest_augmenting_path)
|
| 705 |
+
5
|
| 706 |
+
|
| 707 |
+
If you specify a pair of nodes (source and target) as parameters,
|
| 708 |
+
this function returns the value of local edge connectivity.
|
| 709 |
+
|
| 710 |
+
>>> nx.edge_connectivity(G, 3, 7)
|
| 711 |
+
5
|
| 712 |
+
|
| 713 |
+
If you need to perform several local computations among different
|
| 714 |
+
pairs of nodes on the same graph, it is recommended that you reuse
|
| 715 |
+
the data structures used in the maximum flow computations. See
|
| 716 |
+
:meth:`local_edge_connectivity` for details.
|
| 717 |
+
|
| 718 |
+
Notes
|
| 719 |
+
-----
|
| 720 |
+
This is a flow based implementation of global edge connectivity.
|
| 721 |
+
For undirected graphs the algorithm works by finding a 'small'
|
| 722 |
+
dominating set of nodes of G (see algorithm 7 in [1]_ ) and
|
| 723 |
+
computing local maximum flow (see :meth:`local_edge_connectivity`)
|
| 724 |
+
between an arbitrary node in the dominating set and the rest of
|
| 725 |
+
nodes in it. This is an implementation of algorithm 6 in [1]_ .
|
| 726 |
+
For directed graphs, the algorithm does n calls to the maximum
|
| 727 |
+
flow function. This is an implementation of algorithm 8 in [1]_ .
|
| 728 |
+
|
| 729 |
+
See also
|
| 730 |
+
--------
|
| 731 |
+
:meth:`local_edge_connectivity`
|
| 732 |
+
:meth:`local_node_connectivity`
|
| 733 |
+
:meth:`node_connectivity`
|
| 734 |
+
:meth:`maximum_flow`
|
| 735 |
+
:meth:`edmonds_karp`
|
| 736 |
+
:meth:`preflow_push`
|
| 737 |
+
:meth:`shortest_augmenting_path`
|
| 738 |
+
:meth:`k_edge_components`
|
| 739 |
+
:meth:`k_edge_subgraphs`
|
| 740 |
+
|
| 741 |
+
References
|
| 742 |
+
----------
|
| 743 |
+
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
| 744 |
+
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
| 745 |
+
|
| 746 |
+
"""
|
| 747 |
+
if (s is not None and t is None) or (s is None and t is not None):
|
| 748 |
+
raise nx.NetworkXError("Both source and target must be specified.")
|
| 749 |
+
|
| 750 |
+
# Local edge connectivity
|
| 751 |
+
if s is not None and t is not None:
|
| 752 |
+
if s not in G:
|
| 753 |
+
raise nx.NetworkXError(f"node {s} not in graph")
|
| 754 |
+
if t not in G:
|
| 755 |
+
raise nx.NetworkXError(f"node {t} not in graph")
|
| 756 |
+
return local_edge_connectivity(G, s, t, flow_func=flow_func, cutoff=cutoff)
|
| 757 |
+
|
| 758 |
+
# Global edge connectivity
|
| 759 |
+
# reuse auxiliary digraph and residual network
|
| 760 |
+
H = build_auxiliary_edge_connectivity(G)
|
| 761 |
+
R = build_residual_network(H, "capacity")
|
| 762 |
+
kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R}
|
| 763 |
+
|
| 764 |
+
if G.is_directed():
|
| 765 |
+
# Algorithm 8 in [1]
|
| 766 |
+
if not nx.is_weakly_connected(G):
|
| 767 |
+
return 0
|
| 768 |
+
|
| 769 |
+
# initial value for \lambda is minimum degree
|
| 770 |
+
L = min(d for n, d in G.degree())
|
| 771 |
+
nodes = list(G)
|
| 772 |
+
n = len(nodes)
|
| 773 |
+
|
| 774 |
+
if cutoff is not None:
|
| 775 |
+
L = min(cutoff, L)
|
| 776 |
+
|
| 777 |
+
for i in range(n):
|
| 778 |
+
kwargs["cutoff"] = L
|
| 779 |
+
try:
|
| 780 |
+
L = min(L, local_edge_connectivity(G, nodes[i], nodes[i + 1], **kwargs))
|
| 781 |
+
except IndexError: # last node!
|
| 782 |
+
L = min(L, local_edge_connectivity(G, nodes[i], nodes[0], **kwargs))
|
| 783 |
+
return L
|
| 784 |
+
else: # undirected
|
| 785 |
+
# Algorithm 6 in [1]
|
| 786 |
+
if not nx.is_connected(G):
|
| 787 |
+
return 0
|
| 788 |
+
|
| 789 |
+
# initial value for \lambda is minimum degree
|
| 790 |
+
L = min(d for n, d in G.degree())
|
| 791 |
+
|
| 792 |
+
if cutoff is not None:
|
| 793 |
+
L = min(cutoff, L)
|
| 794 |
+
|
| 795 |
+
# A dominating set is \lambda-covering
|
| 796 |
+
# We need a dominating set with at least two nodes
|
| 797 |
+
for node in G:
|
| 798 |
+
D = nx.dominating_set(G, start_with=node)
|
| 799 |
+
v = D.pop()
|
| 800 |
+
if D:
|
| 801 |
+
break
|
| 802 |
+
else:
|
| 803 |
+
# in complete graphs the dominating sets will always be of one node
|
| 804 |
+
# thus we return min degree
|
| 805 |
+
return L
|
| 806 |
+
|
| 807 |
+
for w in D:
|
| 808 |
+
kwargs["cutoff"] = L
|
| 809 |
+
L = min(L, local_edge_connectivity(G, v, w, **kwargs))
|
| 810 |
+
|
| 811 |
+
return L
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/cuts.py
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Flow based cut algorithms
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import itertools
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
|
| 9 |
+
# Define the default maximum flow function to use in all flow based
|
| 10 |
+
# cut algorithms.
|
| 11 |
+
from networkx.algorithms.flow import build_residual_network, edmonds_karp
|
| 12 |
+
|
| 13 |
+
default_flow_func = edmonds_karp
|
| 14 |
+
|
| 15 |
+
from .utils import build_auxiliary_edge_connectivity, build_auxiliary_node_connectivity
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
"minimum_st_node_cut",
|
| 19 |
+
"minimum_node_cut",
|
| 20 |
+
"minimum_st_edge_cut",
|
| 21 |
+
"minimum_edge_cut",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@nx._dispatchable(
|
| 26 |
+
graphs={"G": 0, "auxiliary?": 4},
|
| 27 |
+
preserve_edge_attrs={"auxiliary": {"capacity": float("inf")}},
|
| 28 |
+
preserve_graph_attrs={"auxiliary"},
|
| 29 |
+
)
|
| 30 |
+
def minimum_st_edge_cut(G, s, t, flow_func=None, auxiliary=None, residual=None):
|
| 31 |
+
"""Returns the edges of the cut-set of a minimum (s, t)-cut.
|
| 32 |
+
|
| 33 |
+
This function returns the set of edges of minimum cardinality that,
|
| 34 |
+
if removed, would destroy all paths among source and target in G.
|
| 35 |
+
Edge weights are not considered. See :meth:`minimum_cut` for
|
| 36 |
+
computing minimum cuts considering edge weights.
|
| 37 |
+
|
| 38 |
+
Parameters
|
| 39 |
+
----------
|
| 40 |
+
G : NetworkX graph
|
| 41 |
+
|
| 42 |
+
s : node
|
| 43 |
+
Source node for the flow.
|
| 44 |
+
|
| 45 |
+
t : node
|
| 46 |
+
Sink node for the flow.
|
| 47 |
+
|
| 48 |
+
auxiliary : NetworkX DiGraph
|
| 49 |
+
Auxiliary digraph to compute flow based node connectivity. It has
|
| 50 |
+
to have a graph attribute called mapping with a dictionary mapping
|
| 51 |
+
node names in G and in the auxiliary digraph. If provided
|
| 52 |
+
it will be reused instead of recreated. Default value: None.
|
| 53 |
+
|
| 54 |
+
flow_func : function
|
| 55 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 56 |
+
The function has to accept at least three parameters: a Digraph,
|
| 57 |
+
a source node, and a target node. And return a residual network
|
| 58 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 59 |
+
details). If flow_func is None, the default maximum flow function
|
| 60 |
+
(:meth:`edmonds_karp`) is used. See :meth:`node_connectivity` for
|
| 61 |
+
details. The choice of the default function may change from version
|
| 62 |
+
to version and should not be relied on. Default value: None.
|
| 63 |
+
|
| 64 |
+
residual : NetworkX DiGraph
|
| 65 |
+
Residual network to compute maximum flow. If provided it will be
|
| 66 |
+
reused instead of recreated. Default value: None.
|
| 67 |
+
|
| 68 |
+
Returns
|
| 69 |
+
-------
|
| 70 |
+
cutset : set
|
| 71 |
+
Set of edges that, if removed from the graph, will disconnect it.
|
| 72 |
+
|
| 73 |
+
See also
|
| 74 |
+
--------
|
| 75 |
+
:meth:`minimum_cut`
|
| 76 |
+
:meth:`minimum_node_cut`
|
| 77 |
+
:meth:`minimum_edge_cut`
|
| 78 |
+
:meth:`stoer_wagner`
|
| 79 |
+
:meth:`node_connectivity`
|
| 80 |
+
:meth:`edge_connectivity`
|
| 81 |
+
:meth:`maximum_flow`
|
| 82 |
+
:meth:`edmonds_karp`
|
| 83 |
+
:meth:`preflow_push`
|
| 84 |
+
:meth:`shortest_augmenting_path`
|
| 85 |
+
|
| 86 |
+
Examples
|
| 87 |
+
--------
|
| 88 |
+
This function is not imported in the base NetworkX namespace, so you
|
| 89 |
+
have to explicitly import it from the connectivity package:
|
| 90 |
+
|
| 91 |
+
>>> from networkx.algorithms.connectivity import minimum_st_edge_cut
|
| 92 |
+
|
| 93 |
+
We use in this example the platonic icosahedral graph, which has edge
|
| 94 |
+
connectivity 5.
|
| 95 |
+
|
| 96 |
+
>>> G = nx.icosahedral_graph()
|
| 97 |
+
>>> len(minimum_st_edge_cut(G, 0, 6))
|
| 98 |
+
5
|
| 99 |
+
|
| 100 |
+
If you need to compute local edge cuts on several pairs of
|
| 101 |
+
nodes in the same graph, it is recommended that you reuse the
|
| 102 |
+
data structures that NetworkX uses in the computation: the
|
| 103 |
+
auxiliary digraph for edge connectivity, and the residual
|
| 104 |
+
network for the underlying maximum flow computation.
|
| 105 |
+
|
| 106 |
+
Example of how to compute local edge cuts among all pairs of
|
| 107 |
+
nodes of the platonic icosahedral graph reusing the data
|
| 108 |
+
structures.
|
| 109 |
+
|
| 110 |
+
>>> import itertools
|
| 111 |
+
>>> # You also have to explicitly import the function for
|
| 112 |
+
>>> # building the auxiliary digraph from the connectivity package
|
| 113 |
+
>>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity
|
| 114 |
+
>>> H = build_auxiliary_edge_connectivity(G)
|
| 115 |
+
>>> # And the function for building the residual network from the
|
| 116 |
+
>>> # flow package
|
| 117 |
+
>>> from networkx.algorithms.flow import build_residual_network
|
| 118 |
+
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
| 119 |
+
>>> R = build_residual_network(H, "capacity")
|
| 120 |
+
>>> result = dict.fromkeys(G, dict())
|
| 121 |
+
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
| 122 |
+
>>> # as parameters
|
| 123 |
+
>>> for u, v in itertools.combinations(G, 2):
|
| 124 |
+
... k = len(minimum_st_edge_cut(G, u, v, auxiliary=H, residual=R))
|
| 125 |
+
... result[u][v] = k
|
| 126 |
+
>>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
|
| 127 |
+
True
|
| 128 |
+
|
| 129 |
+
You can also use alternative flow algorithms for computing edge
|
| 130 |
+
cuts. For instance, in dense networks the algorithm
|
| 131 |
+
:meth:`shortest_augmenting_path` will usually perform better than
|
| 132 |
+
the default :meth:`edmonds_karp` which is faster for sparse
|
| 133 |
+
networks with highly skewed degree distributions. Alternative flow
|
| 134 |
+
functions have to be explicitly imported from the flow package.
|
| 135 |
+
|
| 136 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 137 |
+
>>> len(minimum_st_edge_cut(G, 0, 6, flow_func=shortest_augmenting_path))
|
| 138 |
+
5
|
| 139 |
+
|
| 140 |
+
"""
|
| 141 |
+
if flow_func is None:
|
| 142 |
+
flow_func = default_flow_func
|
| 143 |
+
|
| 144 |
+
if auxiliary is None:
|
| 145 |
+
H = build_auxiliary_edge_connectivity(G)
|
| 146 |
+
else:
|
| 147 |
+
H = auxiliary
|
| 148 |
+
|
| 149 |
+
kwargs = {"capacity": "capacity", "flow_func": flow_func, "residual": residual}
|
| 150 |
+
|
| 151 |
+
cut_value, partition = nx.minimum_cut(H, s, t, **kwargs)
|
| 152 |
+
reachable, non_reachable = partition
|
| 153 |
+
# Any edge in the original graph linking the two sets in the
|
| 154 |
+
# partition is part of the edge cutset
|
| 155 |
+
cutset = set()
|
| 156 |
+
for u, nbrs in ((n, G[n]) for n in reachable):
|
| 157 |
+
cutset.update((u, v) for v in nbrs if v in non_reachable)
|
| 158 |
+
|
| 159 |
+
return cutset
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@nx._dispatchable(
|
| 163 |
+
graphs={"G": 0, "auxiliary?": 4},
|
| 164 |
+
preserve_node_attrs={"auxiliary": {"id": None}},
|
| 165 |
+
preserve_graph_attrs={"auxiliary"},
|
| 166 |
+
)
|
| 167 |
+
def minimum_st_node_cut(G, s, t, flow_func=None, auxiliary=None, residual=None):
|
| 168 |
+
r"""Returns a set of nodes of minimum cardinality that disconnect source
|
| 169 |
+
from target in G.
|
| 170 |
+
|
| 171 |
+
This function returns the set of nodes of minimum cardinality that,
|
| 172 |
+
if removed, would destroy all paths among source and target in G.
|
| 173 |
+
|
| 174 |
+
Parameters
|
| 175 |
+
----------
|
| 176 |
+
G : NetworkX graph
|
| 177 |
+
|
| 178 |
+
s : node
|
| 179 |
+
Source node.
|
| 180 |
+
|
| 181 |
+
t : node
|
| 182 |
+
Target node.
|
| 183 |
+
|
| 184 |
+
flow_func : function
|
| 185 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 186 |
+
The function has to accept at least three parameters: a Digraph,
|
| 187 |
+
a source node, and a target node. And return a residual network
|
| 188 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 189 |
+
details). If flow_func is None, the default maximum flow function
|
| 190 |
+
(:meth:`edmonds_karp`) is used. See below for details. The choice
|
| 191 |
+
of the default function may change from version to version and
|
| 192 |
+
should not be relied on. Default value: None.
|
| 193 |
+
|
| 194 |
+
auxiliary : NetworkX DiGraph
|
| 195 |
+
Auxiliary digraph to compute flow based node connectivity. It has
|
| 196 |
+
to have a graph attribute called mapping with a dictionary mapping
|
| 197 |
+
node names in G and in the auxiliary digraph. If provided
|
| 198 |
+
it will be reused instead of recreated. Default value: None.
|
| 199 |
+
|
| 200 |
+
residual : NetworkX DiGraph
|
| 201 |
+
Residual network to compute maximum flow. If provided it will be
|
| 202 |
+
reused instead of recreated. Default value: None.
|
| 203 |
+
|
| 204 |
+
Returns
|
| 205 |
+
-------
|
| 206 |
+
cutset : set
|
| 207 |
+
Set of nodes that, if removed, would destroy all paths between
|
| 208 |
+
source and target in G.
|
| 209 |
+
|
| 210 |
+
Examples
|
| 211 |
+
--------
|
| 212 |
+
This function is not imported in the base NetworkX namespace, so you
|
| 213 |
+
have to explicitly import it from the connectivity package:
|
| 214 |
+
|
| 215 |
+
>>> from networkx.algorithms.connectivity import minimum_st_node_cut
|
| 216 |
+
|
| 217 |
+
We use in this example the platonic icosahedral graph, which has node
|
| 218 |
+
connectivity 5.
|
| 219 |
+
|
| 220 |
+
>>> G = nx.icosahedral_graph()
|
| 221 |
+
>>> len(minimum_st_node_cut(G, 0, 6))
|
| 222 |
+
5
|
| 223 |
+
|
| 224 |
+
If you need to compute local st cuts between several pairs of
|
| 225 |
+
nodes in the same graph, it is recommended that you reuse the
|
| 226 |
+
data structures that NetworkX uses in the computation: the
|
| 227 |
+
auxiliary digraph for node connectivity and node cuts, and the
|
| 228 |
+
residual network for the underlying maximum flow computation.
|
| 229 |
+
|
| 230 |
+
Example of how to compute local st node cuts reusing the data
|
| 231 |
+
structures:
|
| 232 |
+
|
| 233 |
+
>>> # You also have to explicitly import the function for
|
| 234 |
+
>>> # building the auxiliary digraph from the connectivity package
|
| 235 |
+
>>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity
|
| 236 |
+
>>> H = build_auxiliary_node_connectivity(G)
|
| 237 |
+
>>> # And the function for building the residual network from the
|
| 238 |
+
>>> # flow package
|
| 239 |
+
>>> from networkx.algorithms.flow import build_residual_network
|
| 240 |
+
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
| 241 |
+
>>> R = build_residual_network(H, "capacity")
|
| 242 |
+
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
| 243 |
+
>>> # as parameters
|
| 244 |
+
>>> len(minimum_st_node_cut(G, 0, 6, auxiliary=H, residual=R))
|
| 245 |
+
5
|
| 246 |
+
|
| 247 |
+
You can also use alternative flow algorithms for computing minimum st
|
| 248 |
+
node cuts. For instance, in dense networks the algorithm
|
| 249 |
+
:meth:`shortest_augmenting_path` will usually perform better than
|
| 250 |
+
the default :meth:`edmonds_karp` which is faster for sparse
|
| 251 |
+
networks with highly skewed degree distributions. Alternative flow
|
| 252 |
+
functions have to be explicitly imported from the flow package.
|
| 253 |
+
|
| 254 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 255 |
+
>>> len(minimum_st_node_cut(G, 0, 6, flow_func=shortest_augmenting_path))
|
| 256 |
+
5
|
| 257 |
+
|
| 258 |
+
Notes
|
| 259 |
+
-----
|
| 260 |
+
This is a flow based implementation of minimum node cut. The algorithm
|
| 261 |
+
is based in solving a number of maximum flow computations to determine
|
| 262 |
+
the capacity of the minimum cut on an auxiliary directed network that
|
| 263 |
+
corresponds to the minimum node cut of G. It handles both directed
|
| 264 |
+
and undirected graphs. This implementation is based on algorithm 11
|
| 265 |
+
in [1]_.
|
| 266 |
+
|
| 267 |
+
See also
|
| 268 |
+
--------
|
| 269 |
+
:meth:`minimum_node_cut`
|
| 270 |
+
:meth:`minimum_edge_cut`
|
| 271 |
+
:meth:`stoer_wagner`
|
| 272 |
+
:meth:`node_connectivity`
|
| 273 |
+
:meth:`edge_connectivity`
|
| 274 |
+
:meth:`maximum_flow`
|
| 275 |
+
:meth:`edmonds_karp`
|
| 276 |
+
:meth:`preflow_push`
|
| 277 |
+
:meth:`shortest_augmenting_path`
|
| 278 |
+
|
| 279 |
+
References
|
| 280 |
+
----------
|
| 281 |
+
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
| 282 |
+
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
| 283 |
+
|
| 284 |
+
"""
|
| 285 |
+
if auxiliary is None:
|
| 286 |
+
H = build_auxiliary_node_connectivity(G)
|
| 287 |
+
else:
|
| 288 |
+
H = auxiliary
|
| 289 |
+
|
| 290 |
+
mapping = H.graph.get("mapping", None)
|
| 291 |
+
if mapping is None:
|
| 292 |
+
raise nx.NetworkXError("Invalid auxiliary digraph.")
|
| 293 |
+
if G.has_edge(s, t) or G.has_edge(t, s):
|
| 294 |
+
return {}
|
| 295 |
+
kwargs = {"flow_func": flow_func, "residual": residual, "auxiliary": H}
|
| 296 |
+
|
| 297 |
+
# The edge cut in the auxiliary digraph corresponds to the node cut in the
|
| 298 |
+
# original graph.
|
| 299 |
+
edge_cut = minimum_st_edge_cut(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs)
|
| 300 |
+
# Each node in the original graph maps to two nodes of the auxiliary graph
|
| 301 |
+
node_cut = {H.nodes[node]["id"] for edge in edge_cut for node in edge}
|
| 302 |
+
return node_cut - {s, t}
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
@nx._dispatchable
|
| 306 |
+
def minimum_node_cut(G, s=None, t=None, flow_func=None):
|
| 307 |
+
r"""Returns a set of nodes of minimum cardinality that disconnects G.
|
| 308 |
+
|
| 309 |
+
If source and target nodes are provided, this function returns the
|
| 310 |
+
set of nodes of minimum cardinality that, if removed, would destroy
|
| 311 |
+
all paths among source and target in G. If not, it returns a set
|
| 312 |
+
of nodes of minimum cardinality that disconnects G.
|
| 313 |
+
|
| 314 |
+
Parameters
|
| 315 |
+
----------
|
| 316 |
+
G : NetworkX graph
|
| 317 |
+
|
| 318 |
+
s : node
|
| 319 |
+
Source node. Optional. Default value: None.
|
| 320 |
+
|
| 321 |
+
t : node
|
| 322 |
+
Target node. Optional. Default value: None.
|
| 323 |
+
|
| 324 |
+
flow_func : function
|
| 325 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 326 |
+
The function has to accept at least three parameters: a Digraph,
|
| 327 |
+
a source node, and a target node. And return a residual network
|
| 328 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 329 |
+
details). If flow_func is None, the default maximum flow function
|
| 330 |
+
(:meth:`edmonds_karp`) is used. See below for details. The
|
| 331 |
+
choice of the default function may change from version
|
| 332 |
+
to version and should not be relied on. Default value: None.
|
| 333 |
+
|
| 334 |
+
Returns
|
| 335 |
+
-------
|
| 336 |
+
cutset : set
|
| 337 |
+
Set of nodes that, if removed, would disconnect G. If source
|
| 338 |
+
and target nodes are provided, the set contains the nodes that
|
| 339 |
+
if removed, would destroy all paths between source and target.
|
| 340 |
+
|
| 341 |
+
Examples
|
| 342 |
+
--------
|
| 343 |
+
>>> # Platonic icosahedral graph has node connectivity 5
|
| 344 |
+
>>> G = nx.icosahedral_graph()
|
| 345 |
+
>>> node_cut = nx.minimum_node_cut(G)
|
| 346 |
+
>>> len(node_cut)
|
| 347 |
+
5
|
| 348 |
+
|
| 349 |
+
You can use alternative flow algorithms for the underlying maximum
|
| 350 |
+
flow computation. In dense networks the algorithm
|
| 351 |
+
:meth:`shortest_augmenting_path` will usually perform better
|
| 352 |
+
than the default :meth:`edmonds_karp`, which is faster for
|
| 353 |
+
sparse networks with highly skewed degree distributions. Alternative
|
| 354 |
+
flow functions have to be explicitly imported from the flow package.
|
| 355 |
+
|
| 356 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 357 |
+
>>> node_cut == nx.minimum_node_cut(G, flow_func=shortest_augmenting_path)
|
| 358 |
+
True
|
| 359 |
+
|
| 360 |
+
If you specify a pair of nodes (source and target) as parameters,
|
| 361 |
+
this function returns a local st node cut.
|
| 362 |
+
|
| 363 |
+
>>> len(nx.minimum_node_cut(G, 3, 7))
|
| 364 |
+
5
|
| 365 |
+
|
| 366 |
+
If you need to perform several local st cuts among different
|
| 367 |
+
pairs of nodes on the same graph, it is recommended that you reuse
|
| 368 |
+
the data structures used in the maximum flow computations. See
|
| 369 |
+
:meth:`minimum_st_node_cut` for details.
|
| 370 |
+
|
| 371 |
+
Notes
|
| 372 |
+
-----
|
| 373 |
+
This is a flow based implementation of minimum node cut. The algorithm
|
| 374 |
+
is based in solving a number of maximum flow computations to determine
|
| 375 |
+
the capacity of the minimum cut on an auxiliary directed network that
|
| 376 |
+
corresponds to the minimum node cut of G. It handles both directed
|
| 377 |
+
and undirected graphs. This implementation is based on algorithm 11
|
| 378 |
+
in [1]_.
|
| 379 |
+
|
| 380 |
+
See also
|
| 381 |
+
--------
|
| 382 |
+
:meth:`minimum_st_node_cut`
|
| 383 |
+
:meth:`minimum_cut`
|
| 384 |
+
:meth:`minimum_edge_cut`
|
| 385 |
+
:meth:`stoer_wagner`
|
| 386 |
+
:meth:`node_connectivity`
|
| 387 |
+
:meth:`edge_connectivity`
|
| 388 |
+
:meth:`maximum_flow`
|
| 389 |
+
:meth:`edmonds_karp`
|
| 390 |
+
:meth:`preflow_push`
|
| 391 |
+
:meth:`shortest_augmenting_path`
|
| 392 |
+
|
| 393 |
+
References
|
| 394 |
+
----------
|
| 395 |
+
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
| 396 |
+
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
| 397 |
+
|
| 398 |
+
"""
|
| 399 |
+
if (s is not None and t is None) or (s is None and t is not None):
|
| 400 |
+
raise nx.NetworkXError("Both source and target must be specified.")
|
| 401 |
+
|
| 402 |
+
# Local minimum node cut.
|
| 403 |
+
if s is not None and t is not None:
|
| 404 |
+
if s not in G:
|
| 405 |
+
raise nx.NetworkXError(f"node {s} not in graph")
|
| 406 |
+
if t not in G:
|
| 407 |
+
raise nx.NetworkXError(f"node {t} not in graph")
|
| 408 |
+
return minimum_st_node_cut(G, s, t, flow_func=flow_func)
|
| 409 |
+
|
| 410 |
+
# Global minimum node cut.
|
| 411 |
+
# Analog to the algorithm 11 for global node connectivity in [1].
|
| 412 |
+
if G.is_directed():
|
| 413 |
+
if not nx.is_weakly_connected(G):
|
| 414 |
+
raise nx.NetworkXError("Input graph is not connected")
|
| 415 |
+
iter_func = itertools.permutations
|
| 416 |
+
|
| 417 |
+
def neighbors(v):
|
| 418 |
+
return itertools.chain.from_iterable([G.predecessors(v), G.successors(v)])
|
| 419 |
+
|
| 420 |
+
else:
|
| 421 |
+
if not nx.is_connected(G):
|
| 422 |
+
raise nx.NetworkXError("Input graph is not connected")
|
| 423 |
+
iter_func = itertools.combinations
|
| 424 |
+
neighbors = G.neighbors
|
| 425 |
+
|
| 426 |
+
# Reuse the auxiliary digraph and the residual network.
|
| 427 |
+
H = build_auxiliary_node_connectivity(G)
|
| 428 |
+
R = build_residual_network(H, "capacity")
|
| 429 |
+
kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R}
|
| 430 |
+
|
| 431 |
+
# Choose a node with minimum degree.
|
| 432 |
+
v = min(G, key=G.degree)
|
| 433 |
+
# Initial node cutset is all neighbors of the node with minimum degree.
|
| 434 |
+
min_cut = set(G[v])
|
| 435 |
+
# Compute st node cuts between v and all its non-neighbors nodes in G.
|
| 436 |
+
for w in set(G) - set(neighbors(v)) - {v}:
|
| 437 |
+
this_cut = minimum_st_node_cut(G, v, w, **kwargs)
|
| 438 |
+
if len(min_cut) >= len(this_cut):
|
| 439 |
+
min_cut = this_cut
|
| 440 |
+
# Also for non adjacent pairs of neighbors of v.
|
| 441 |
+
for x, y in iter_func(neighbors(v), 2):
|
| 442 |
+
if y in G[x]:
|
| 443 |
+
continue
|
| 444 |
+
this_cut = minimum_st_node_cut(G, x, y, **kwargs)
|
| 445 |
+
if len(min_cut) >= len(this_cut):
|
| 446 |
+
min_cut = this_cut
|
| 447 |
+
|
| 448 |
+
return min_cut
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
@nx._dispatchable
|
| 452 |
+
def minimum_edge_cut(G, s=None, t=None, flow_func=None):
|
| 453 |
+
r"""Returns a set of edges of minimum cardinality that disconnects G.
|
| 454 |
+
|
| 455 |
+
If source and target nodes are provided, this function returns the
|
| 456 |
+
set of edges of minimum cardinality that, if removed, would break
|
| 457 |
+
all paths among source and target in G. If not, it returns a set of
|
| 458 |
+
edges of minimum cardinality that disconnects G.
|
| 459 |
+
|
| 460 |
+
Parameters
|
| 461 |
+
----------
|
| 462 |
+
G : NetworkX graph
|
| 463 |
+
|
| 464 |
+
s : node
|
| 465 |
+
Source node. Optional. Default value: None.
|
| 466 |
+
|
| 467 |
+
t : node
|
| 468 |
+
Target node. Optional. Default value: None.
|
| 469 |
+
|
| 470 |
+
flow_func : function
|
| 471 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 472 |
+
The function has to accept at least three parameters: a Digraph,
|
| 473 |
+
a source node, and a target node. And return a residual network
|
| 474 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 475 |
+
details). If flow_func is None, the default maximum flow function
|
| 476 |
+
(:meth:`edmonds_karp`) is used. See below for details. The
|
| 477 |
+
choice of the default function may change from version
|
| 478 |
+
to version and should not be relied on. Default value: None.
|
| 479 |
+
|
| 480 |
+
Returns
|
| 481 |
+
-------
|
| 482 |
+
cutset : set
|
| 483 |
+
Set of edges that, if removed, would disconnect G. If source
|
| 484 |
+
and target nodes are provided, the set contains the edges that
|
| 485 |
+
if removed, would destroy all paths between source and target.
|
| 486 |
+
|
| 487 |
+
Examples
|
| 488 |
+
--------
|
| 489 |
+
>>> # Platonic icosahedral graph has edge connectivity 5
|
| 490 |
+
>>> G = nx.icosahedral_graph()
|
| 491 |
+
>>> len(nx.minimum_edge_cut(G))
|
| 492 |
+
5
|
| 493 |
+
|
| 494 |
+
You can use alternative flow algorithms for the underlying
|
| 495 |
+
maximum flow computation. In dense networks the algorithm
|
| 496 |
+
:meth:`shortest_augmenting_path` will usually perform better
|
| 497 |
+
than the default :meth:`edmonds_karp`, which is faster for
|
| 498 |
+
sparse networks with highly skewed degree distributions.
|
| 499 |
+
Alternative flow functions have to be explicitly imported
|
| 500 |
+
from the flow package.
|
| 501 |
+
|
| 502 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 503 |
+
>>> len(nx.minimum_edge_cut(G, flow_func=shortest_augmenting_path))
|
| 504 |
+
5
|
| 505 |
+
|
| 506 |
+
If you specify a pair of nodes (source and target) as parameters,
|
| 507 |
+
this function returns the value of local edge connectivity.
|
| 508 |
+
|
| 509 |
+
>>> nx.edge_connectivity(G, 3, 7)
|
| 510 |
+
5
|
| 511 |
+
|
| 512 |
+
If you need to perform several local computations among different
|
| 513 |
+
pairs of nodes on the same graph, it is recommended that you reuse
|
| 514 |
+
the data structures used in the maximum flow computations. See
|
| 515 |
+
:meth:`local_edge_connectivity` for details.
|
| 516 |
+
|
| 517 |
+
Notes
|
| 518 |
+
-----
|
| 519 |
+
This is a flow based implementation of minimum edge cut. For
|
| 520 |
+
undirected graphs the algorithm works by finding a 'small' dominating
|
| 521 |
+
set of nodes of G (see algorithm 7 in [1]_) and computing the maximum
|
| 522 |
+
flow between an arbitrary node in the dominating set and the rest of
|
| 523 |
+
nodes in it. This is an implementation of algorithm 6 in [1]_. For
|
| 524 |
+
directed graphs, the algorithm does n calls to the max flow function.
|
| 525 |
+
The function raises an error if the directed graph is not weakly
|
| 526 |
+
connected and returns an empty set if it is weakly connected.
|
| 527 |
+
It is an implementation of algorithm 8 in [1]_.
|
| 528 |
+
|
| 529 |
+
See also
|
| 530 |
+
--------
|
| 531 |
+
:meth:`minimum_st_edge_cut`
|
| 532 |
+
:meth:`minimum_node_cut`
|
| 533 |
+
:meth:`stoer_wagner`
|
| 534 |
+
:meth:`node_connectivity`
|
| 535 |
+
:meth:`edge_connectivity`
|
| 536 |
+
:meth:`maximum_flow`
|
| 537 |
+
:meth:`edmonds_karp`
|
| 538 |
+
:meth:`preflow_push`
|
| 539 |
+
:meth:`shortest_augmenting_path`
|
| 540 |
+
|
| 541 |
+
References
|
| 542 |
+
----------
|
| 543 |
+
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
| 544 |
+
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
| 545 |
+
|
| 546 |
+
"""
|
| 547 |
+
if (s is not None and t is None) or (s is None and t is not None):
|
| 548 |
+
raise nx.NetworkXError("Both source and target must be specified.")
|
| 549 |
+
|
| 550 |
+
# reuse auxiliary digraph and residual network
|
| 551 |
+
H = build_auxiliary_edge_connectivity(G)
|
| 552 |
+
R = build_residual_network(H, "capacity")
|
| 553 |
+
kwargs = {"flow_func": flow_func, "residual": R, "auxiliary": H}
|
| 554 |
+
|
| 555 |
+
# Local minimum edge cut if s and t are not None
|
| 556 |
+
if s is not None and t is not None:
|
| 557 |
+
if s not in G:
|
| 558 |
+
raise nx.NetworkXError(f"node {s} not in graph")
|
| 559 |
+
if t not in G:
|
| 560 |
+
raise nx.NetworkXError(f"node {t} not in graph")
|
| 561 |
+
return minimum_st_edge_cut(H, s, t, **kwargs)
|
| 562 |
+
|
| 563 |
+
# Global minimum edge cut
|
| 564 |
+
# Analog to the algorithm for global edge connectivity
|
| 565 |
+
if G.is_directed():
|
| 566 |
+
# Based on algorithm 8 in [1]
|
| 567 |
+
if not nx.is_weakly_connected(G):
|
| 568 |
+
raise nx.NetworkXError("Input graph is not connected")
|
| 569 |
+
|
| 570 |
+
# Initial cutset is all edges of a node with minimum degree
|
| 571 |
+
node = min(G, key=G.degree)
|
| 572 |
+
min_cut = set(G.edges(node))
|
| 573 |
+
nodes = list(G)
|
| 574 |
+
n = len(nodes)
|
| 575 |
+
for i in range(n):
|
| 576 |
+
try:
|
| 577 |
+
this_cut = minimum_st_edge_cut(H, nodes[i], nodes[i + 1], **kwargs)
|
| 578 |
+
if len(this_cut) <= len(min_cut):
|
| 579 |
+
min_cut = this_cut
|
| 580 |
+
except IndexError: # Last node!
|
| 581 |
+
this_cut = minimum_st_edge_cut(H, nodes[i], nodes[0], **kwargs)
|
| 582 |
+
if len(this_cut) <= len(min_cut):
|
| 583 |
+
min_cut = this_cut
|
| 584 |
+
|
| 585 |
+
return min_cut
|
| 586 |
+
|
| 587 |
+
else: # undirected
|
| 588 |
+
# Based on algorithm 6 in [1]
|
| 589 |
+
if not nx.is_connected(G):
|
| 590 |
+
raise nx.NetworkXError("Input graph is not connected")
|
| 591 |
+
|
| 592 |
+
# Initial cutset is all edges of a node with minimum degree
|
| 593 |
+
node = min(G, key=G.degree)
|
| 594 |
+
min_cut = set(G.edges(node))
|
| 595 |
+
# A dominating set is \lambda-covering
|
| 596 |
+
# We need a dominating set with at least two nodes
|
| 597 |
+
for node in G:
|
| 598 |
+
D = nx.dominating_set(G, start_with=node)
|
| 599 |
+
v = D.pop()
|
| 600 |
+
if D:
|
| 601 |
+
break
|
| 602 |
+
else:
|
| 603 |
+
# in complete graphs the dominating set will always be of one node
|
| 604 |
+
# thus we return min_cut, which now contains the edges of a node
|
| 605 |
+
# with minimum degree
|
| 606 |
+
return min_cut
|
| 607 |
+
for w in D:
|
| 608 |
+
this_cut = minimum_st_edge_cut(H, v, w, **kwargs)
|
| 609 |
+
if len(this_cut) <= len(min_cut):
|
| 610 |
+
min_cut = this_cut
|
| 611 |
+
|
| 612 |
+
return min_cut
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/disjoint_paths.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Flow based node and edge disjoint paths."""
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
|
| 5 |
+
# Define the default maximum flow function to use for the underlying
|
| 6 |
+
# maximum flow computations
|
| 7 |
+
from networkx.algorithms.flow import (
|
| 8 |
+
edmonds_karp,
|
| 9 |
+
preflow_push,
|
| 10 |
+
shortest_augmenting_path,
|
| 11 |
+
)
|
| 12 |
+
from networkx.exception import NetworkXNoPath
|
| 13 |
+
|
| 14 |
+
default_flow_func = edmonds_karp
|
| 15 |
+
from itertools import filterfalse as _filterfalse
|
| 16 |
+
|
| 17 |
+
# Functions to build auxiliary data structures.
|
| 18 |
+
from .utils import build_auxiliary_edge_connectivity, build_auxiliary_node_connectivity
|
| 19 |
+
|
| 20 |
+
__all__ = ["edge_disjoint_paths", "node_disjoint_paths"]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@nx._dispatchable(
|
| 24 |
+
graphs={"G": 0, "auxiliary?": 5},
|
| 25 |
+
preserve_edge_attrs={"auxiliary": {"capacity": float("inf")}},
|
| 26 |
+
)
|
| 27 |
+
def edge_disjoint_paths(
|
| 28 |
+
G, s, t, flow_func=None, cutoff=None, auxiliary=None, residual=None
|
| 29 |
+
):
|
| 30 |
+
"""Returns the edges disjoint paths between source and target.
|
| 31 |
+
|
| 32 |
+
Edge disjoint paths are paths that do not share any edge. The
|
| 33 |
+
number of edge disjoint paths between source and target is equal
|
| 34 |
+
to their edge connectivity.
|
| 35 |
+
|
| 36 |
+
Parameters
|
| 37 |
+
----------
|
| 38 |
+
G : NetworkX graph
|
| 39 |
+
|
| 40 |
+
s : node
|
| 41 |
+
Source node for the flow.
|
| 42 |
+
|
| 43 |
+
t : node
|
| 44 |
+
Sink node for the flow.
|
| 45 |
+
|
| 46 |
+
flow_func : function
|
| 47 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 48 |
+
The function has to accept at least three parameters: a Digraph,
|
| 49 |
+
a source node, and a target node. And return a residual network
|
| 50 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 51 |
+
details). If flow_func is None, the default maximum flow function
|
| 52 |
+
(:meth:`edmonds_karp`) is used. The choice of the default function
|
| 53 |
+
may change from version to version and should not be relied on.
|
| 54 |
+
Default value: None.
|
| 55 |
+
|
| 56 |
+
cutoff : integer or None (default: None)
|
| 57 |
+
Maximum number of paths to yield. If specified, the maximum flow
|
| 58 |
+
algorithm will terminate when the flow value reaches or exceeds the
|
| 59 |
+
cutoff. This only works for flows that support the cutoff parameter
|
| 60 |
+
(most do) and is ignored otherwise.
|
| 61 |
+
|
| 62 |
+
auxiliary : NetworkX DiGraph
|
| 63 |
+
Auxiliary digraph to compute flow based edge connectivity. It has
|
| 64 |
+
to have a graph attribute called mapping with a dictionary mapping
|
| 65 |
+
node names in G and in the auxiliary digraph. If provided
|
| 66 |
+
it will be reused instead of recreated. Default value: None.
|
| 67 |
+
|
| 68 |
+
residual : NetworkX DiGraph
|
| 69 |
+
Residual network to compute maximum flow. If provided it will be
|
| 70 |
+
reused instead of recreated. Default value: None.
|
| 71 |
+
|
| 72 |
+
Returns
|
| 73 |
+
-------
|
| 74 |
+
paths : generator
|
| 75 |
+
A generator of edge independent paths.
|
| 76 |
+
|
| 77 |
+
Raises
|
| 78 |
+
------
|
| 79 |
+
NetworkXNoPath
|
| 80 |
+
If there is no path between source and target.
|
| 81 |
+
|
| 82 |
+
NetworkXError
|
| 83 |
+
If source or target are not in the graph G.
|
| 84 |
+
|
| 85 |
+
See also
|
| 86 |
+
--------
|
| 87 |
+
:meth:`node_disjoint_paths`
|
| 88 |
+
:meth:`edge_connectivity`
|
| 89 |
+
:meth:`maximum_flow`
|
| 90 |
+
:meth:`edmonds_karp`
|
| 91 |
+
:meth:`preflow_push`
|
| 92 |
+
:meth:`shortest_augmenting_path`
|
| 93 |
+
|
| 94 |
+
Examples
|
| 95 |
+
--------
|
| 96 |
+
We use in this example the platonic icosahedral graph, which has node
|
| 97 |
+
edge connectivity 5, thus there are 5 edge disjoint paths between any
|
| 98 |
+
pair of nodes.
|
| 99 |
+
|
| 100 |
+
>>> G = nx.icosahedral_graph()
|
| 101 |
+
>>> len(list(nx.edge_disjoint_paths(G, 0, 6)))
|
| 102 |
+
5
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
If you need to compute edge disjoint paths on several pairs of
|
| 106 |
+
nodes in the same graph, it is recommended that you reuse the
|
| 107 |
+
data structures that NetworkX uses in the computation: the
|
| 108 |
+
auxiliary digraph for edge connectivity, and the residual
|
| 109 |
+
network for the underlying maximum flow computation.
|
| 110 |
+
|
| 111 |
+
Example of how to compute edge disjoint paths among all pairs of
|
| 112 |
+
nodes of the platonic icosahedral graph reusing the data
|
| 113 |
+
structures.
|
| 114 |
+
|
| 115 |
+
>>> import itertools
|
| 116 |
+
>>> # You also have to explicitly import the function for
|
| 117 |
+
>>> # building the auxiliary digraph from the connectivity package
|
| 118 |
+
>>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity
|
| 119 |
+
>>> H = build_auxiliary_edge_connectivity(G)
|
| 120 |
+
>>> # And the function for building the residual network from the
|
| 121 |
+
>>> # flow package
|
| 122 |
+
>>> from networkx.algorithms.flow import build_residual_network
|
| 123 |
+
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
| 124 |
+
>>> R = build_residual_network(H, "capacity")
|
| 125 |
+
>>> result = {n: {} for n in G}
|
| 126 |
+
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
| 127 |
+
>>> # as arguments
|
| 128 |
+
>>> for u, v in itertools.combinations(G, 2):
|
| 129 |
+
... k = len(list(nx.edge_disjoint_paths(G, u, v, auxiliary=H, residual=R)))
|
| 130 |
+
... result[u][v] = k
|
| 131 |
+
>>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
|
| 132 |
+
True
|
| 133 |
+
|
| 134 |
+
You can also use alternative flow algorithms for computing edge disjoint
|
| 135 |
+
paths. For instance, in dense networks the algorithm
|
| 136 |
+
:meth:`shortest_augmenting_path` will usually perform better than
|
| 137 |
+
the default :meth:`edmonds_karp` which is faster for sparse
|
| 138 |
+
networks with highly skewed degree distributions. Alternative flow
|
| 139 |
+
functions have to be explicitly imported from the flow package.
|
| 140 |
+
|
| 141 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 142 |
+
>>> len(list(nx.edge_disjoint_paths(G, 0, 6, flow_func=shortest_augmenting_path)))
|
| 143 |
+
5
|
| 144 |
+
|
| 145 |
+
Notes
|
| 146 |
+
-----
|
| 147 |
+
This is a flow based implementation of edge disjoint paths. We compute
|
| 148 |
+
the maximum flow between source and target on an auxiliary directed
|
| 149 |
+
network. The saturated edges in the residual network after running the
|
| 150 |
+
maximum flow algorithm correspond to edge disjoint paths between source
|
| 151 |
+
and target in the original network. This function handles both directed
|
| 152 |
+
and undirected graphs, and can use all flow algorithms from NetworkX flow
|
| 153 |
+
package.
|
| 154 |
+
|
| 155 |
+
"""
|
| 156 |
+
if s not in G:
|
| 157 |
+
raise nx.NetworkXError(f"node {s} not in graph")
|
| 158 |
+
if t not in G:
|
| 159 |
+
raise nx.NetworkXError(f"node {t} not in graph")
|
| 160 |
+
|
| 161 |
+
if flow_func is None:
|
| 162 |
+
flow_func = default_flow_func
|
| 163 |
+
|
| 164 |
+
if auxiliary is None:
|
| 165 |
+
H = build_auxiliary_edge_connectivity(G)
|
| 166 |
+
else:
|
| 167 |
+
H = auxiliary
|
| 168 |
+
|
| 169 |
+
# Maximum possible edge disjoint paths
|
| 170 |
+
possible = min(H.out_degree(s), H.in_degree(t))
|
| 171 |
+
if not possible:
|
| 172 |
+
raise NetworkXNoPath
|
| 173 |
+
|
| 174 |
+
if cutoff is None:
|
| 175 |
+
cutoff = possible
|
| 176 |
+
else:
|
| 177 |
+
cutoff = min(cutoff, possible)
|
| 178 |
+
|
| 179 |
+
# Compute maximum flow between source and target. Flow functions in
|
| 180 |
+
# NetworkX return a residual network.
|
| 181 |
+
kwargs = {
|
| 182 |
+
"capacity": "capacity",
|
| 183 |
+
"residual": residual,
|
| 184 |
+
"cutoff": cutoff,
|
| 185 |
+
"value_only": True,
|
| 186 |
+
}
|
| 187 |
+
if flow_func is preflow_push:
|
| 188 |
+
del kwargs["cutoff"]
|
| 189 |
+
if flow_func is shortest_augmenting_path:
|
| 190 |
+
kwargs["two_phase"] = True
|
| 191 |
+
R = flow_func(H, s, t, **kwargs)
|
| 192 |
+
|
| 193 |
+
if R.graph["flow_value"] == 0:
|
| 194 |
+
raise NetworkXNoPath
|
| 195 |
+
|
| 196 |
+
# Saturated edges in the residual network form the edge disjoint paths
|
| 197 |
+
# between source and target
|
| 198 |
+
cutset = [
|
| 199 |
+
(u, v)
|
| 200 |
+
for u, v, d in R.edges(data=True)
|
| 201 |
+
if d["capacity"] == d["flow"] and d["flow"] > 0
|
| 202 |
+
]
|
| 203 |
+
# This is equivalent of what flow.utils.build_flow_dict returns, but
|
| 204 |
+
# only for the nodes with saturated edges and without reporting 0 flows.
|
| 205 |
+
flow_dict = {n: {} for edge in cutset for n in edge}
|
| 206 |
+
for u, v in cutset:
|
| 207 |
+
flow_dict[u][v] = 1
|
| 208 |
+
|
| 209 |
+
# Rebuild the edge disjoint paths from the flow dictionary.
|
| 210 |
+
paths_found = 0
|
| 211 |
+
for v in list(flow_dict[s]):
|
| 212 |
+
if paths_found >= cutoff:
|
| 213 |
+
# preflow_push does not support cutoff: we have to
|
| 214 |
+
# keep track of the paths founds and stop at cutoff.
|
| 215 |
+
break
|
| 216 |
+
path = [s]
|
| 217 |
+
if v == t:
|
| 218 |
+
path.append(v)
|
| 219 |
+
yield path
|
| 220 |
+
continue
|
| 221 |
+
u = v
|
| 222 |
+
while u != t:
|
| 223 |
+
path.append(u)
|
| 224 |
+
try:
|
| 225 |
+
u, _ = flow_dict[u].popitem()
|
| 226 |
+
except KeyError:
|
| 227 |
+
break
|
| 228 |
+
else:
|
| 229 |
+
path.append(t)
|
| 230 |
+
yield path
|
| 231 |
+
paths_found += 1
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
@nx._dispatchable(
|
| 235 |
+
graphs={"G": 0, "auxiliary?": 5},
|
| 236 |
+
preserve_node_attrs={"auxiliary": {"id": None}},
|
| 237 |
+
preserve_graph_attrs={"auxiliary"},
|
| 238 |
+
)
|
| 239 |
+
def node_disjoint_paths(
|
| 240 |
+
G, s, t, flow_func=None, cutoff=None, auxiliary=None, residual=None
|
| 241 |
+
):
|
| 242 |
+
r"""Computes node disjoint paths between source and target.
|
| 243 |
+
|
| 244 |
+
Node disjoint paths are paths that only share their first and last
|
| 245 |
+
nodes. The number of node independent paths between two nodes is
|
| 246 |
+
equal to their local node connectivity.
|
| 247 |
+
|
| 248 |
+
Parameters
|
| 249 |
+
----------
|
| 250 |
+
G : NetworkX graph
|
| 251 |
+
|
| 252 |
+
s : node
|
| 253 |
+
Source node.
|
| 254 |
+
|
| 255 |
+
t : node
|
| 256 |
+
Target node.
|
| 257 |
+
|
| 258 |
+
flow_func : function
|
| 259 |
+
A function for computing the maximum flow among a pair of nodes.
|
| 260 |
+
The function has to accept at least three parameters: a Digraph,
|
| 261 |
+
a source node, and a target node. And return a residual network
|
| 262 |
+
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
| 263 |
+
details). If flow_func is None, the default maximum flow function
|
| 264 |
+
(:meth:`edmonds_karp`) is used. See below for details. The choice
|
| 265 |
+
of the default function may change from version to version and
|
| 266 |
+
should not be relied on. Default value: None.
|
| 267 |
+
|
| 268 |
+
cutoff : integer or None (default: None)
|
| 269 |
+
Maximum number of paths to yield. If specified, the maximum flow
|
| 270 |
+
algorithm will terminate when the flow value reaches or exceeds the
|
| 271 |
+
cutoff. This only works for flows that support the cutoff parameter
|
| 272 |
+
(most do) and is ignored otherwise.
|
| 273 |
+
|
| 274 |
+
auxiliary : NetworkX DiGraph
|
| 275 |
+
Auxiliary digraph to compute flow based node connectivity. It has
|
| 276 |
+
to have a graph attribute called mapping with a dictionary mapping
|
| 277 |
+
node names in G and in the auxiliary digraph. If provided
|
| 278 |
+
it will be reused instead of recreated. Default value: None.
|
| 279 |
+
|
| 280 |
+
residual : NetworkX DiGraph
|
| 281 |
+
Residual network to compute maximum flow. If provided it will be
|
| 282 |
+
reused instead of recreated. Default value: None.
|
| 283 |
+
|
| 284 |
+
Returns
|
| 285 |
+
-------
|
| 286 |
+
paths : generator
|
| 287 |
+
Generator of node disjoint paths.
|
| 288 |
+
|
| 289 |
+
Raises
|
| 290 |
+
------
|
| 291 |
+
NetworkXNoPath
|
| 292 |
+
If there is no path between source and target.
|
| 293 |
+
|
| 294 |
+
NetworkXError
|
| 295 |
+
If source or target are not in the graph G.
|
| 296 |
+
|
| 297 |
+
Examples
|
| 298 |
+
--------
|
| 299 |
+
We use in this example the platonic icosahedral graph, which has node
|
| 300 |
+
connectivity 5, thus there are 5 node disjoint paths between any pair
|
| 301 |
+
of non neighbor nodes.
|
| 302 |
+
|
| 303 |
+
>>> G = nx.icosahedral_graph()
|
| 304 |
+
>>> len(list(nx.node_disjoint_paths(G, 0, 6)))
|
| 305 |
+
5
|
| 306 |
+
|
| 307 |
+
If you need to compute node disjoint paths between several pairs of
|
| 308 |
+
nodes in the same graph, it is recommended that you reuse the
|
| 309 |
+
data structures that NetworkX uses in the computation: the
|
| 310 |
+
auxiliary digraph for node connectivity and node cuts, and the
|
| 311 |
+
residual network for the underlying maximum flow computation.
|
| 312 |
+
|
| 313 |
+
Example of how to compute node disjoint paths reusing the data
|
| 314 |
+
structures:
|
| 315 |
+
|
| 316 |
+
>>> # You also have to explicitly import the function for
|
| 317 |
+
>>> # building the auxiliary digraph from the connectivity package
|
| 318 |
+
>>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity
|
| 319 |
+
>>> H = build_auxiliary_node_connectivity(G)
|
| 320 |
+
>>> # And the function for building the residual network from the
|
| 321 |
+
>>> # flow package
|
| 322 |
+
>>> from networkx.algorithms.flow import build_residual_network
|
| 323 |
+
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
| 324 |
+
>>> R = build_residual_network(H, "capacity")
|
| 325 |
+
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
| 326 |
+
>>> # as arguments
|
| 327 |
+
>>> len(list(nx.node_disjoint_paths(G, 0, 6, auxiliary=H, residual=R)))
|
| 328 |
+
5
|
| 329 |
+
|
| 330 |
+
You can also use alternative flow algorithms for computing node disjoint
|
| 331 |
+
paths. For instance, in dense networks the algorithm
|
| 332 |
+
:meth:`shortest_augmenting_path` will usually perform better than
|
| 333 |
+
the default :meth:`edmonds_karp` which is faster for sparse
|
| 334 |
+
networks with highly skewed degree distributions. Alternative flow
|
| 335 |
+
functions have to be explicitly imported from the flow package.
|
| 336 |
+
|
| 337 |
+
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
| 338 |
+
>>> len(list(nx.node_disjoint_paths(G, 0, 6, flow_func=shortest_augmenting_path)))
|
| 339 |
+
5
|
| 340 |
+
|
| 341 |
+
Notes
|
| 342 |
+
-----
|
| 343 |
+
This is a flow based implementation of node disjoint paths. We compute
|
| 344 |
+
the maximum flow between source and target on an auxiliary directed
|
| 345 |
+
network. The saturated edges in the residual network after running the
|
| 346 |
+
maximum flow algorithm correspond to node disjoint paths between source
|
| 347 |
+
and target in the original network. This function handles both directed
|
| 348 |
+
and undirected graphs, and can use all flow algorithms from NetworkX flow
|
| 349 |
+
package.
|
| 350 |
+
|
| 351 |
+
See also
|
| 352 |
+
--------
|
| 353 |
+
:meth:`edge_disjoint_paths`
|
| 354 |
+
:meth:`node_connectivity`
|
| 355 |
+
:meth:`maximum_flow`
|
| 356 |
+
:meth:`edmonds_karp`
|
| 357 |
+
:meth:`preflow_push`
|
| 358 |
+
:meth:`shortest_augmenting_path`
|
| 359 |
+
|
| 360 |
+
"""
|
| 361 |
+
if s not in G:
|
| 362 |
+
raise nx.NetworkXError(f"node {s} not in graph")
|
| 363 |
+
if t not in G:
|
| 364 |
+
raise nx.NetworkXError(f"node {t} not in graph")
|
| 365 |
+
|
| 366 |
+
if auxiliary is None:
|
| 367 |
+
H = build_auxiliary_node_connectivity(G)
|
| 368 |
+
else:
|
| 369 |
+
H = auxiliary
|
| 370 |
+
|
| 371 |
+
mapping = H.graph.get("mapping", None)
|
| 372 |
+
if mapping is None:
|
| 373 |
+
raise nx.NetworkXError("Invalid auxiliary digraph.")
|
| 374 |
+
|
| 375 |
+
# Maximum possible edge disjoint paths
|
| 376 |
+
possible = min(H.out_degree(f"{mapping[s]}B"), H.in_degree(f"{mapping[t]}A"))
|
| 377 |
+
if not possible:
|
| 378 |
+
raise NetworkXNoPath
|
| 379 |
+
|
| 380 |
+
if cutoff is None:
|
| 381 |
+
cutoff = possible
|
| 382 |
+
else:
|
| 383 |
+
cutoff = min(cutoff, possible)
|
| 384 |
+
|
| 385 |
+
kwargs = {
|
| 386 |
+
"flow_func": flow_func,
|
| 387 |
+
"residual": residual,
|
| 388 |
+
"auxiliary": H,
|
| 389 |
+
"cutoff": cutoff,
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
# The edge disjoint paths in the auxiliary digraph correspond to the node
|
| 393 |
+
# disjoint paths in the original graph.
|
| 394 |
+
paths_edges = edge_disjoint_paths(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs)
|
| 395 |
+
for path in paths_edges:
|
| 396 |
+
# Each node in the original graph maps to two nodes in auxiliary graph
|
| 397 |
+
yield list(_unique_everseen(H.nodes[node]["id"] for node in path))
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def _unique_everseen(iterable):
|
| 401 |
+
# Adapted from https://docs.python.org/3/library/itertools.html examples
|
| 402 |
+
"List unique elements, preserving order. Remember all elements ever seen."
|
| 403 |
+
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
| 404 |
+
seen = set()
|
| 405 |
+
seen_add = seen.add
|
| 406 |
+
for element in _filterfalse(seen.__contains__, iterable):
|
| 407 |
+
seen_add(element)
|
| 408 |
+
yield element
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/edge_augmentation.py
ADDED
|
@@ -0,0 +1,1270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Algorithms for finding k-edge-augmentations
|
| 3 |
+
|
| 4 |
+
A k-edge-augmentation is a set of edges, that once added to a graph, ensures
|
| 5 |
+
that the graph is k-edge-connected; i.e. the graph cannot be disconnected
|
| 6 |
+
unless k or more edges are removed. Typically, the goal is to find the
|
| 7 |
+
augmentation with minimum weight. In general, it is not guaranteed that a
|
| 8 |
+
k-edge-augmentation exists.
|
| 9 |
+
|
| 10 |
+
See Also
|
| 11 |
+
--------
|
| 12 |
+
:mod:`edge_kcomponents` : algorithms for finding k-edge-connected components
|
| 13 |
+
:mod:`connectivity` : algorithms for determining edge connectivity.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import itertools as it
|
| 17 |
+
import math
|
| 18 |
+
from collections import defaultdict, namedtuple
|
| 19 |
+
|
| 20 |
+
import networkx as nx
|
| 21 |
+
from networkx.utils import not_implemented_for, py_random_state
|
| 22 |
+
|
| 23 |
+
__all__ = ["k_edge_augmentation", "is_k_edge_connected", "is_locally_k_edge_connected"]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@not_implemented_for("directed")
|
| 27 |
+
@not_implemented_for("multigraph")
|
| 28 |
+
@nx._dispatchable
|
| 29 |
+
def is_k_edge_connected(G, k):
|
| 30 |
+
"""Tests to see if a graph is k-edge-connected.
|
| 31 |
+
|
| 32 |
+
Is it impossible to disconnect the graph by removing fewer than k edges?
|
| 33 |
+
If so, then G is k-edge-connected.
|
| 34 |
+
|
| 35 |
+
Parameters
|
| 36 |
+
----------
|
| 37 |
+
G : NetworkX graph
|
| 38 |
+
An undirected graph.
|
| 39 |
+
|
| 40 |
+
k : integer
|
| 41 |
+
edge connectivity to test for
|
| 42 |
+
|
| 43 |
+
Returns
|
| 44 |
+
-------
|
| 45 |
+
boolean
|
| 46 |
+
True if G is k-edge-connected.
|
| 47 |
+
|
| 48 |
+
See Also
|
| 49 |
+
--------
|
| 50 |
+
:func:`is_locally_k_edge_connected`
|
| 51 |
+
|
| 52 |
+
Examples
|
| 53 |
+
--------
|
| 54 |
+
>>> G = nx.barbell_graph(10, 0)
|
| 55 |
+
>>> nx.is_k_edge_connected(G, k=1)
|
| 56 |
+
True
|
| 57 |
+
>>> nx.is_k_edge_connected(G, k=2)
|
| 58 |
+
False
|
| 59 |
+
"""
|
| 60 |
+
if k < 1:
|
| 61 |
+
raise ValueError(f"k must be positive, not {k}")
|
| 62 |
+
# First try to quickly determine if G is not k-edge-connected
|
| 63 |
+
if G.number_of_nodes() < k + 1:
|
| 64 |
+
return False
|
| 65 |
+
elif any(d < k for n, d in G.degree()):
|
| 66 |
+
return False
|
| 67 |
+
else:
|
| 68 |
+
# Otherwise perform the full check
|
| 69 |
+
if k == 1:
|
| 70 |
+
return nx.is_connected(G)
|
| 71 |
+
elif k == 2:
|
| 72 |
+
return nx.is_connected(G) and not nx.has_bridges(G)
|
| 73 |
+
else:
|
| 74 |
+
return nx.edge_connectivity(G, cutoff=k) >= k
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@not_implemented_for("directed")
|
| 78 |
+
@not_implemented_for("multigraph")
|
| 79 |
+
@nx._dispatchable
|
| 80 |
+
def is_locally_k_edge_connected(G, s, t, k):
|
| 81 |
+
"""Tests to see if an edge in a graph is locally k-edge-connected.
|
| 82 |
+
|
| 83 |
+
Is it impossible to disconnect s and t by removing fewer than k edges?
|
| 84 |
+
If so, then s and t are locally k-edge-connected in G.
|
| 85 |
+
|
| 86 |
+
Parameters
|
| 87 |
+
----------
|
| 88 |
+
G : NetworkX graph
|
| 89 |
+
An undirected graph.
|
| 90 |
+
|
| 91 |
+
s : node
|
| 92 |
+
Source node
|
| 93 |
+
|
| 94 |
+
t : node
|
| 95 |
+
Target node
|
| 96 |
+
|
| 97 |
+
k : integer
|
| 98 |
+
local edge connectivity for nodes s and t
|
| 99 |
+
|
| 100 |
+
Returns
|
| 101 |
+
-------
|
| 102 |
+
boolean
|
| 103 |
+
True if s and t are locally k-edge-connected in G.
|
| 104 |
+
|
| 105 |
+
See Also
|
| 106 |
+
--------
|
| 107 |
+
:func:`is_k_edge_connected`
|
| 108 |
+
|
| 109 |
+
Examples
|
| 110 |
+
--------
|
| 111 |
+
>>> from networkx.algorithms.connectivity import is_locally_k_edge_connected
|
| 112 |
+
>>> G = nx.barbell_graph(10, 0)
|
| 113 |
+
>>> is_locally_k_edge_connected(G, 5, 15, k=1)
|
| 114 |
+
True
|
| 115 |
+
>>> is_locally_k_edge_connected(G, 5, 15, k=2)
|
| 116 |
+
False
|
| 117 |
+
>>> is_locally_k_edge_connected(G, 1, 5, k=2)
|
| 118 |
+
True
|
| 119 |
+
"""
|
| 120 |
+
if k < 1:
|
| 121 |
+
raise ValueError(f"k must be positive, not {k}")
|
| 122 |
+
|
| 123 |
+
# First try to quickly determine s, t is not k-locally-edge-connected in G
|
| 124 |
+
if G.degree(s) < k or G.degree(t) < k:
|
| 125 |
+
return False
|
| 126 |
+
else:
|
| 127 |
+
# Otherwise perform the full check
|
| 128 |
+
if k == 1:
|
| 129 |
+
return nx.has_path(G, s, t)
|
| 130 |
+
else:
|
| 131 |
+
localk = nx.connectivity.local_edge_connectivity(G, s, t, cutoff=k)
|
| 132 |
+
return localk >= k
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@not_implemented_for("directed")
|
| 136 |
+
@not_implemented_for("multigraph")
|
| 137 |
+
@nx._dispatchable
|
| 138 |
+
def k_edge_augmentation(G, k, avail=None, weight=None, partial=False):
|
| 139 |
+
"""Finds set of edges to k-edge-connect G.
|
| 140 |
+
|
| 141 |
+
Adding edges from the augmentation to G make it impossible to disconnect G
|
| 142 |
+
unless k or more edges are removed. This function uses the most efficient
|
| 143 |
+
function available (depending on the value of k and if the problem is
|
| 144 |
+
weighted or unweighted) to search for a minimum weight subset of available
|
| 145 |
+
edges that k-edge-connects G. In general, finding a k-edge-augmentation is
|
| 146 |
+
NP-hard, so solutions are not guaranteed to be minimal. Furthermore, a
|
| 147 |
+
k-edge-augmentation may not exist.
|
| 148 |
+
|
| 149 |
+
Parameters
|
| 150 |
+
----------
|
| 151 |
+
G : NetworkX graph
|
| 152 |
+
An undirected graph.
|
| 153 |
+
|
| 154 |
+
k : integer
|
| 155 |
+
Desired edge connectivity
|
| 156 |
+
|
| 157 |
+
avail : dict or a set of 2 or 3 tuples
|
| 158 |
+
The available edges that can be used in the augmentation.
|
| 159 |
+
|
| 160 |
+
If unspecified, then all edges in the complement of G are available.
|
| 161 |
+
Otherwise, each item is an available edge (with an optional weight).
|
| 162 |
+
|
| 163 |
+
In the unweighted case, each item is an edge ``(u, v)``.
|
| 164 |
+
|
| 165 |
+
In the weighted case, each item is a 3-tuple ``(u, v, d)`` or a dict
|
| 166 |
+
with items ``(u, v): d``. The third item, ``d``, can be a dictionary
|
| 167 |
+
or a real number. If ``d`` is a dictionary ``d[weight]``
|
| 168 |
+
correspondings to the weight.
|
| 169 |
+
|
| 170 |
+
weight : string
|
| 171 |
+
key to use to find weights if ``avail`` is a set of 3-tuples where the
|
| 172 |
+
third item in each tuple is a dictionary.
|
| 173 |
+
|
| 174 |
+
partial : boolean
|
| 175 |
+
If partial is True and no feasible k-edge-augmentation exists, then all
|
| 176 |
+
a partial k-edge-augmentation is generated. Adding the edges in a
|
| 177 |
+
partial augmentation to G, minimizes the number of k-edge-connected
|
| 178 |
+
components and maximizes the edge connectivity between those
|
| 179 |
+
components. For details, see :func:`partial_k_edge_augmentation`.
|
| 180 |
+
|
| 181 |
+
Yields
|
| 182 |
+
------
|
| 183 |
+
edge : tuple
|
| 184 |
+
Edges that, once added to G, would cause G to become k-edge-connected.
|
| 185 |
+
If partial is False, an error is raised if this is not possible.
|
| 186 |
+
Otherwise, generated edges form a partial augmentation, which
|
| 187 |
+
k-edge-connects any part of G where it is possible, and maximally
|
| 188 |
+
connects the remaining parts.
|
| 189 |
+
|
| 190 |
+
Raises
|
| 191 |
+
------
|
| 192 |
+
NetworkXUnfeasible
|
| 193 |
+
If partial is False and no k-edge-augmentation exists.
|
| 194 |
+
|
| 195 |
+
NetworkXNotImplemented
|
| 196 |
+
If the input graph is directed or a multigraph.
|
| 197 |
+
|
| 198 |
+
ValueError:
|
| 199 |
+
If k is less than 1
|
| 200 |
+
|
| 201 |
+
Notes
|
| 202 |
+
-----
|
| 203 |
+
When k=1 this returns an optimal solution.
|
| 204 |
+
|
| 205 |
+
When k=2 and ``avail`` is None, this returns an optimal solution.
|
| 206 |
+
Otherwise when k=2, this returns a 2-approximation of the optimal solution.
|
| 207 |
+
|
| 208 |
+
For k>3, this problem is NP-hard and this uses a randomized algorithm that
|
| 209 |
+
produces a feasible solution, but provides no guarantees on the
|
| 210 |
+
solution weight.
|
| 211 |
+
|
| 212 |
+
Examples
|
| 213 |
+
--------
|
| 214 |
+
>>> # Unweighted cases
|
| 215 |
+
>>> G = nx.path_graph((1, 2, 3, 4))
|
| 216 |
+
>>> G.add_node(5)
|
| 217 |
+
>>> sorted(nx.k_edge_augmentation(G, k=1))
|
| 218 |
+
[(1, 5)]
|
| 219 |
+
>>> sorted(nx.k_edge_augmentation(G, k=2))
|
| 220 |
+
[(1, 5), (5, 4)]
|
| 221 |
+
>>> sorted(nx.k_edge_augmentation(G, k=3))
|
| 222 |
+
[(1, 4), (1, 5), (2, 5), (3, 5), (4, 5)]
|
| 223 |
+
>>> complement = list(nx.k_edge_augmentation(G, k=5, partial=True))
|
| 224 |
+
>>> G.add_edges_from(complement)
|
| 225 |
+
>>> nx.edge_connectivity(G)
|
| 226 |
+
4
|
| 227 |
+
|
| 228 |
+
>>> # Weighted cases
|
| 229 |
+
>>> G = nx.path_graph((1, 2, 3, 4))
|
| 230 |
+
>>> G.add_node(5)
|
| 231 |
+
>>> # avail can be a tuple with a dict
|
| 232 |
+
>>> avail = [(1, 5, {"weight": 11}), (2, 5, {"weight": 10})]
|
| 233 |
+
>>> sorted(nx.k_edge_augmentation(G, k=1, avail=avail, weight="weight"))
|
| 234 |
+
[(2, 5)]
|
| 235 |
+
>>> # or avail can be a 3-tuple with a real number
|
| 236 |
+
>>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 51)]
|
| 237 |
+
>>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail))
|
| 238 |
+
[(1, 5), (2, 5), (4, 5)]
|
| 239 |
+
>>> # or avail can be a dict
|
| 240 |
+
>>> avail = {(1, 5): 11, (2, 5): 10, (4, 3): 1, (4, 5): 51}
|
| 241 |
+
>>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail))
|
| 242 |
+
[(1, 5), (2, 5), (4, 5)]
|
| 243 |
+
>>> # If augmentation is infeasible, then a partial solution can be found
|
| 244 |
+
>>> avail = {(1, 5): 11}
|
| 245 |
+
>>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail, partial=True))
|
| 246 |
+
[(1, 5)]
|
| 247 |
+
"""
|
| 248 |
+
try:
|
| 249 |
+
if k <= 0:
|
| 250 |
+
raise ValueError(f"k must be a positive integer, not {k}")
|
| 251 |
+
elif G.number_of_nodes() < k + 1:
|
| 252 |
+
msg = f"impossible to {k} connect in graph with less than {k + 1} nodes"
|
| 253 |
+
raise nx.NetworkXUnfeasible(msg)
|
| 254 |
+
elif avail is not None and len(avail) == 0:
|
| 255 |
+
if not nx.is_k_edge_connected(G, k):
|
| 256 |
+
raise nx.NetworkXUnfeasible("no available edges")
|
| 257 |
+
aug_edges = []
|
| 258 |
+
elif k == 1:
|
| 259 |
+
aug_edges = one_edge_augmentation(
|
| 260 |
+
G, avail=avail, weight=weight, partial=partial
|
| 261 |
+
)
|
| 262 |
+
elif k == 2:
|
| 263 |
+
aug_edges = bridge_augmentation(G, avail=avail, weight=weight)
|
| 264 |
+
else:
|
| 265 |
+
# raise NotImplementedError(f'not implemented for k>2. k={k}')
|
| 266 |
+
aug_edges = greedy_k_edge_augmentation(
|
| 267 |
+
G, k=k, avail=avail, weight=weight, seed=0
|
| 268 |
+
)
|
| 269 |
+
# Do eager evaluation so we can catch any exceptions
|
| 270 |
+
# Before executing partial code.
|
| 271 |
+
yield from list(aug_edges)
|
| 272 |
+
except nx.NetworkXUnfeasible:
|
| 273 |
+
if partial:
|
| 274 |
+
# Return all available edges
|
| 275 |
+
if avail is None:
|
| 276 |
+
aug_edges = complement_edges(G)
|
| 277 |
+
else:
|
| 278 |
+
# If we can't k-edge-connect the entire graph, try to
|
| 279 |
+
# k-edge-connect as much as possible
|
| 280 |
+
aug_edges = partial_k_edge_augmentation(
|
| 281 |
+
G, k=k, avail=avail, weight=weight
|
| 282 |
+
)
|
| 283 |
+
yield from aug_edges
|
| 284 |
+
else:
|
| 285 |
+
raise
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
@nx._dispatchable
|
| 289 |
+
def partial_k_edge_augmentation(G, k, avail, weight=None):
|
| 290 |
+
"""Finds augmentation that k-edge-connects as much of the graph as possible.
|
| 291 |
+
|
| 292 |
+
When a k-edge-augmentation is not possible, we can still try to find a
|
| 293 |
+
small set of edges that partially k-edge-connects as much of the graph as
|
| 294 |
+
possible. All possible edges are generated between remaining parts.
|
| 295 |
+
This minimizes the number of k-edge-connected subgraphs in the resulting
|
| 296 |
+
graph and maximizes the edge connectivity between those subgraphs.
|
| 297 |
+
|
| 298 |
+
Parameters
|
| 299 |
+
----------
|
| 300 |
+
G : NetworkX graph
|
| 301 |
+
An undirected graph.
|
| 302 |
+
|
| 303 |
+
k : integer
|
| 304 |
+
Desired edge connectivity
|
| 305 |
+
|
| 306 |
+
avail : dict or a set of 2 or 3 tuples
|
| 307 |
+
For more details, see :func:`k_edge_augmentation`.
|
| 308 |
+
|
| 309 |
+
weight : string
|
| 310 |
+
key to use to find weights if ``avail`` is a set of 3-tuples.
|
| 311 |
+
For more details, see :func:`k_edge_augmentation`.
|
| 312 |
+
|
| 313 |
+
Yields
|
| 314 |
+
------
|
| 315 |
+
edge : tuple
|
| 316 |
+
Edges in the partial augmentation of G. These edges k-edge-connect any
|
| 317 |
+
part of G where it is possible, and maximally connects the remaining
|
| 318 |
+
parts. In other words, all edges from avail are generated except for
|
| 319 |
+
those within subgraphs that have already become k-edge-connected.
|
| 320 |
+
|
| 321 |
+
Notes
|
| 322 |
+
-----
|
| 323 |
+
Construct H that augments G with all edges in avail.
|
| 324 |
+
Find the k-edge-subgraphs of H.
|
| 325 |
+
For each k-edge-subgraph, if the number of nodes is more than k, then find
|
| 326 |
+
the k-edge-augmentation of that graph and add it to the solution. Then add
|
| 327 |
+
all edges in avail between k-edge subgraphs to the solution.
|
| 328 |
+
|
| 329 |
+
See Also
|
| 330 |
+
--------
|
| 331 |
+
:func:`k_edge_augmentation`
|
| 332 |
+
|
| 333 |
+
Examples
|
| 334 |
+
--------
|
| 335 |
+
>>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7))
|
| 336 |
+
>>> G.add_node(8)
|
| 337 |
+
>>> avail = [(1, 3), (1, 4), (1, 5), (2, 4), (2, 5), (3, 5), (1, 8)]
|
| 338 |
+
>>> sorted(partial_k_edge_augmentation(G, k=2, avail=avail))
|
| 339 |
+
[(1, 5), (1, 8)]
|
| 340 |
+
"""
|
| 341 |
+
|
| 342 |
+
def _edges_between_disjoint(H, only1, only2):
|
| 343 |
+
"""finds edges between disjoint nodes"""
|
| 344 |
+
only1_adj = {u: set(H.adj[u]) for u in only1}
|
| 345 |
+
for u, neighbs in only1_adj.items():
|
| 346 |
+
# Find the neighbors of u in only1 that are also in only2
|
| 347 |
+
neighbs12 = neighbs.intersection(only2)
|
| 348 |
+
for v in neighbs12:
|
| 349 |
+
yield (u, v)
|
| 350 |
+
|
| 351 |
+
avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G)
|
| 352 |
+
|
| 353 |
+
# Find which parts of the graph can be k-edge-connected
|
| 354 |
+
H = G.copy()
|
| 355 |
+
H.add_edges_from(
|
| 356 |
+
(
|
| 357 |
+
(u, v, {"weight": w, "generator": (u, v)})
|
| 358 |
+
for (u, v), w in zip(avail, avail_w)
|
| 359 |
+
)
|
| 360 |
+
)
|
| 361 |
+
k_edge_subgraphs = list(nx.k_edge_subgraphs(H, k=k))
|
| 362 |
+
|
| 363 |
+
# Generate edges to k-edge-connect internal subgraphs
|
| 364 |
+
for nodes in k_edge_subgraphs:
|
| 365 |
+
if len(nodes) > 1:
|
| 366 |
+
# Get the k-edge-connected subgraph
|
| 367 |
+
C = H.subgraph(nodes).copy()
|
| 368 |
+
# Find the internal edges that were available
|
| 369 |
+
sub_avail = {
|
| 370 |
+
d["generator"]: d["weight"]
|
| 371 |
+
for (u, v, d) in C.edges(data=True)
|
| 372 |
+
if "generator" in d
|
| 373 |
+
}
|
| 374 |
+
# Remove potential augmenting edges
|
| 375 |
+
C.remove_edges_from(sub_avail.keys())
|
| 376 |
+
# Find a subset of these edges that makes the component
|
| 377 |
+
# k-edge-connected and ignore the rest
|
| 378 |
+
yield from nx.k_edge_augmentation(C, k=k, avail=sub_avail)
|
| 379 |
+
|
| 380 |
+
# Generate all edges between CCs that could not be k-edge-connected
|
| 381 |
+
for cc1, cc2 in it.combinations(k_edge_subgraphs, 2):
|
| 382 |
+
for u, v in _edges_between_disjoint(H, cc1, cc2):
|
| 383 |
+
d = H.get_edge_data(u, v)
|
| 384 |
+
edge = d.get("generator", None)
|
| 385 |
+
if edge is not None:
|
| 386 |
+
yield edge
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
@not_implemented_for("multigraph")
|
| 390 |
+
@not_implemented_for("directed")
|
| 391 |
+
@nx._dispatchable
|
| 392 |
+
def one_edge_augmentation(G, avail=None, weight=None, partial=False):
|
| 393 |
+
"""Finds minimum weight set of edges to connect G.
|
| 394 |
+
|
| 395 |
+
Equivalent to :func:`k_edge_augmentation` when k=1. Adding the resulting
|
| 396 |
+
edges to G will make it 1-edge-connected. The solution is optimal for both
|
| 397 |
+
weighted and non-weighted variants.
|
| 398 |
+
|
| 399 |
+
Parameters
|
| 400 |
+
----------
|
| 401 |
+
G : NetworkX graph
|
| 402 |
+
An undirected graph.
|
| 403 |
+
|
| 404 |
+
avail : dict or a set of 2 or 3 tuples
|
| 405 |
+
For more details, see :func:`k_edge_augmentation`.
|
| 406 |
+
|
| 407 |
+
weight : string
|
| 408 |
+
key to use to find weights if ``avail`` is a set of 3-tuples.
|
| 409 |
+
For more details, see :func:`k_edge_augmentation`.
|
| 410 |
+
|
| 411 |
+
partial : boolean
|
| 412 |
+
If partial is True and no feasible k-edge-augmentation exists, then the
|
| 413 |
+
augmenting edges minimize the number of connected components.
|
| 414 |
+
|
| 415 |
+
Yields
|
| 416 |
+
------
|
| 417 |
+
edge : tuple
|
| 418 |
+
Edges in the one-augmentation of G
|
| 419 |
+
|
| 420 |
+
Raises
|
| 421 |
+
------
|
| 422 |
+
NetworkXUnfeasible
|
| 423 |
+
If partial is False and no one-edge-augmentation exists.
|
| 424 |
+
|
| 425 |
+
Notes
|
| 426 |
+
-----
|
| 427 |
+
Uses either :func:`unconstrained_one_edge_augmentation` or
|
| 428 |
+
:func:`weighted_one_edge_augmentation` depending on whether ``avail`` is
|
| 429 |
+
specified. Both algorithms are based on finding a minimum spanning tree.
|
| 430 |
+
As such both algorithms find optimal solutions and run in linear time.
|
| 431 |
+
|
| 432 |
+
See Also
|
| 433 |
+
--------
|
| 434 |
+
:func:`k_edge_augmentation`
|
| 435 |
+
"""
|
| 436 |
+
if avail is None:
|
| 437 |
+
return unconstrained_one_edge_augmentation(G)
|
| 438 |
+
else:
|
| 439 |
+
return weighted_one_edge_augmentation(
|
| 440 |
+
G, avail=avail, weight=weight, partial=partial
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
@not_implemented_for("multigraph")
|
| 445 |
+
@not_implemented_for("directed")
|
| 446 |
+
@nx._dispatchable
|
| 447 |
+
def bridge_augmentation(G, avail=None, weight=None):
|
| 448 |
+
"""Finds the a set of edges that bridge connects G.
|
| 449 |
+
|
| 450 |
+
Equivalent to :func:`k_edge_augmentation` when k=2, and partial=False.
|
| 451 |
+
Adding the resulting edges to G will make it 2-edge-connected. If no
|
| 452 |
+
constraints are specified the returned set of edges is minimum an optimal,
|
| 453 |
+
otherwise the solution is approximated.
|
| 454 |
+
|
| 455 |
+
Parameters
|
| 456 |
+
----------
|
| 457 |
+
G : NetworkX graph
|
| 458 |
+
An undirected graph.
|
| 459 |
+
|
| 460 |
+
avail : dict or a set of 2 or 3 tuples
|
| 461 |
+
For more details, see :func:`k_edge_augmentation`.
|
| 462 |
+
|
| 463 |
+
weight : string
|
| 464 |
+
key to use to find weights if ``avail`` is a set of 3-tuples.
|
| 465 |
+
For more details, see :func:`k_edge_augmentation`.
|
| 466 |
+
|
| 467 |
+
Yields
|
| 468 |
+
------
|
| 469 |
+
edge : tuple
|
| 470 |
+
Edges in the bridge-augmentation of G
|
| 471 |
+
|
| 472 |
+
Raises
|
| 473 |
+
------
|
| 474 |
+
NetworkXUnfeasible
|
| 475 |
+
If no bridge-augmentation exists.
|
| 476 |
+
|
| 477 |
+
Notes
|
| 478 |
+
-----
|
| 479 |
+
If there are no constraints the solution can be computed in linear time
|
| 480 |
+
using :func:`unconstrained_bridge_augmentation`. Otherwise, the problem
|
| 481 |
+
becomes NP-hard and is the solution is approximated by
|
| 482 |
+
:func:`weighted_bridge_augmentation`.
|
| 483 |
+
|
| 484 |
+
See Also
|
| 485 |
+
--------
|
| 486 |
+
:func:`k_edge_augmentation`
|
| 487 |
+
"""
|
| 488 |
+
if G.number_of_nodes() < 3:
|
| 489 |
+
raise nx.NetworkXUnfeasible("impossible to bridge connect less than 3 nodes")
|
| 490 |
+
if avail is None:
|
| 491 |
+
return unconstrained_bridge_augmentation(G)
|
| 492 |
+
else:
|
| 493 |
+
return weighted_bridge_augmentation(G, avail, weight=weight)
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
# --- Algorithms and Helpers ---
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
def _ordered(u, v):
|
| 500 |
+
"""Returns the nodes in an undirected edge in lower-triangular order"""
|
| 501 |
+
return (u, v) if u < v else (v, u)
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
def _unpack_available_edges(avail, weight=None, G=None):
|
| 505 |
+
"""Helper to separate avail into edges and corresponding weights"""
|
| 506 |
+
if weight is None:
|
| 507 |
+
weight = "weight"
|
| 508 |
+
if isinstance(avail, dict):
|
| 509 |
+
avail_uv = list(avail.keys())
|
| 510 |
+
avail_w = list(avail.values())
|
| 511 |
+
else:
|
| 512 |
+
|
| 513 |
+
def _try_getitem(d):
|
| 514 |
+
try:
|
| 515 |
+
return d[weight]
|
| 516 |
+
except TypeError:
|
| 517 |
+
return d
|
| 518 |
+
|
| 519 |
+
avail_uv = [tup[0:2] for tup in avail]
|
| 520 |
+
avail_w = [1 if len(tup) == 2 else _try_getitem(tup[-1]) for tup in avail]
|
| 521 |
+
|
| 522 |
+
if G is not None:
|
| 523 |
+
# Edges already in the graph are filtered
|
| 524 |
+
flags = [not G.has_edge(u, v) for u, v in avail_uv]
|
| 525 |
+
avail_uv = list(it.compress(avail_uv, flags))
|
| 526 |
+
avail_w = list(it.compress(avail_w, flags))
|
| 527 |
+
return avail_uv, avail_w
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
MetaEdge = namedtuple("MetaEdge", ("meta_uv", "uv", "w"))
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
def _lightest_meta_edges(mapping, avail_uv, avail_w):
|
| 534 |
+
"""Maps available edges in the original graph to edges in the metagraph.
|
| 535 |
+
|
| 536 |
+
Parameters
|
| 537 |
+
----------
|
| 538 |
+
mapping : dict
|
| 539 |
+
mapping produced by :func:`collapse`, that maps each node in the
|
| 540 |
+
original graph to a node in the meta graph
|
| 541 |
+
|
| 542 |
+
avail_uv : list
|
| 543 |
+
list of edges
|
| 544 |
+
|
| 545 |
+
avail_w : list
|
| 546 |
+
list of edge weights
|
| 547 |
+
|
| 548 |
+
Notes
|
| 549 |
+
-----
|
| 550 |
+
Each node in the metagraph is a k-edge-connected component in the original
|
| 551 |
+
graph. We don't care about any edge within the same k-edge-connected
|
| 552 |
+
component, so we ignore self edges. We also are only interested in the
|
| 553 |
+
minimum weight edge bridging each k-edge-connected component so, we group
|
| 554 |
+
the edges by meta-edge and take the lightest in each group.
|
| 555 |
+
|
| 556 |
+
Examples
|
| 557 |
+
--------
|
| 558 |
+
>>> # Each group represents a meta-node
|
| 559 |
+
>>> groups = ([1, 2, 3], [4, 5], [6])
|
| 560 |
+
>>> mapping = {n: meta_n for meta_n, ns in enumerate(groups) for n in ns}
|
| 561 |
+
>>> avail_uv = [(1, 2), (3, 6), (1, 4), (5, 2), (6, 1), (2, 6), (3, 1)]
|
| 562 |
+
>>> avail_w = [20, 99, 20, 15, 50, 99, 20]
|
| 563 |
+
>>> sorted(_lightest_meta_edges(mapping, avail_uv, avail_w))
|
| 564 |
+
[MetaEdge(meta_uv=(0, 1), uv=(5, 2), w=15), MetaEdge(meta_uv=(0, 2), uv=(6, 1), w=50)]
|
| 565 |
+
"""
|
| 566 |
+
grouped_wuv = defaultdict(list)
|
| 567 |
+
for w, (u, v) in zip(avail_w, avail_uv):
|
| 568 |
+
# Order the meta-edge so it can be used as a dict key
|
| 569 |
+
meta_uv = _ordered(mapping[u], mapping[v])
|
| 570 |
+
# Group each available edge using the meta-edge as a key
|
| 571 |
+
grouped_wuv[meta_uv].append((w, u, v))
|
| 572 |
+
|
| 573 |
+
# Now that all available edges are grouped, choose one per group
|
| 574 |
+
for (mu, mv), choices_wuv in grouped_wuv.items():
|
| 575 |
+
# Ignore available edges within the same meta-node
|
| 576 |
+
if mu != mv:
|
| 577 |
+
# Choose the lightest available edge belonging to each meta-edge
|
| 578 |
+
w, u, v = min(choices_wuv)
|
| 579 |
+
yield MetaEdge((mu, mv), (u, v), w)
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
@nx._dispatchable
|
| 583 |
+
def unconstrained_one_edge_augmentation(G):
|
| 584 |
+
"""Finds the smallest set of edges to connect G.
|
| 585 |
+
|
| 586 |
+
This is a variant of the unweighted MST problem.
|
| 587 |
+
If G is not empty, a feasible solution always exists.
|
| 588 |
+
|
| 589 |
+
Parameters
|
| 590 |
+
----------
|
| 591 |
+
G : NetworkX graph
|
| 592 |
+
An undirected graph.
|
| 593 |
+
|
| 594 |
+
Yields
|
| 595 |
+
------
|
| 596 |
+
edge : tuple
|
| 597 |
+
Edges in the one-edge-augmentation of G
|
| 598 |
+
|
| 599 |
+
See Also
|
| 600 |
+
--------
|
| 601 |
+
:func:`one_edge_augmentation`
|
| 602 |
+
:func:`k_edge_augmentation`
|
| 603 |
+
|
| 604 |
+
Examples
|
| 605 |
+
--------
|
| 606 |
+
>>> G = nx.Graph([(1, 2), (2, 3), (4, 5)])
|
| 607 |
+
>>> G.add_nodes_from([6, 7, 8])
|
| 608 |
+
>>> sorted(unconstrained_one_edge_augmentation(G))
|
| 609 |
+
[(1, 4), (4, 6), (6, 7), (7, 8)]
|
| 610 |
+
"""
|
| 611 |
+
ccs1 = list(nx.connected_components(G))
|
| 612 |
+
C = collapse(G, ccs1)
|
| 613 |
+
# When we are not constrained, we can just make a meta graph tree.
|
| 614 |
+
meta_nodes = list(C.nodes())
|
| 615 |
+
# build a path in the metagraph
|
| 616 |
+
meta_aug = list(zip(meta_nodes, meta_nodes[1:]))
|
| 617 |
+
# map that path to the original graph
|
| 618 |
+
inverse = defaultdict(list)
|
| 619 |
+
for k, v in C.graph["mapping"].items():
|
| 620 |
+
inverse[v].append(k)
|
| 621 |
+
for mu, mv in meta_aug:
|
| 622 |
+
yield (inverse[mu][0], inverse[mv][0])
|
| 623 |
+
|
| 624 |
+
|
| 625 |
+
@nx._dispatchable
|
| 626 |
+
def weighted_one_edge_augmentation(G, avail, weight=None, partial=False):
|
| 627 |
+
"""Finds the minimum weight set of edges to connect G if one exists.
|
| 628 |
+
|
| 629 |
+
This is a variant of the weighted MST problem.
|
| 630 |
+
|
| 631 |
+
Parameters
|
| 632 |
+
----------
|
| 633 |
+
G : NetworkX graph
|
| 634 |
+
An undirected graph.
|
| 635 |
+
|
| 636 |
+
avail : dict or a set of 2 or 3 tuples
|
| 637 |
+
For more details, see :func:`k_edge_augmentation`.
|
| 638 |
+
|
| 639 |
+
weight : string
|
| 640 |
+
key to use to find weights if ``avail`` is a set of 3-tuples.
|
| 641 |
+
For more details, see :func:`k_edge_augmentation`.
|
| 642 |
+
|
| 643 |
+
partial : boolean
|
| 644 |
+
If partial is True and no feasible k-edge-augmentation exists, then the
|
| 645 |
+
augmenting edges minimize the number of connected components.
|
| 646 |
+
|
| 647 |
+
Yields
|
| 648 |
+
------
|
| 649 |
+
edge : tuple
|
| 650 |
+
Edges in the subset of avail chosen to connect G.
|
| 651 |
+
|
| 652 |
+
See Also
|
| 653 |
+
--------
|
| 654 |
+
:func:`one_edge_augmentation`
|
| 655 |
+
:func:`k_edge_augmentation`
|
| 656 |
+
|
| 657 |
+
Examples
|
| 658 |
+
--------
|
| 659 |
+
>>> G = nx.Graph([(1, 2), (2, 3), (4, 5)])
|
| 660 |
+
>>> G.add_nodes_from([6, 7, 8])
|
| 661 |
+
>>> # any edge not in avail has an implicit weight of infinity
|
| 662 |
+
>>> avail = [(1, 3), (1, 5), (4, 7), (4, 8), (6, 1), (8, 1), (8, 2)]
|
| 663 |
+
>>> sorted(weighted_one_edge_augmentation(G, avail))
|
| 664 |
+
[(1, 5), (4, 7), (6, 1), (8, 1)]
|
| 665 |
+
>>> # find another solution by giving large weights to edges in the
|
| 666 |
+
>>> # previous solution (note some of the old edges must be used)
|
| 667 |
+
>>> avail = [(1, 3), (1, 5, 99), (4, 7, 9), (6, 1, 99), (8, 1, 99), (8, 2)]
|
| 668 |
+
>>> sorted(weighted_one_edge_augmentation(G, avail))
|
| 669 |
+
[(1, 5), (4, 7), (6, 1), (8, 2)]
|
| 670 |
+
"""
|
| 671 |
+
avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G)
|
| 672 |
+
# Collapse CCs in the original graph into nodes in a metagraph
|
| 673 |
+
# Then find an MST of the metagraph instead of the original graph
|
| 674 |
+
C = collapse(G, nx.connected_components(G))
|
| 675 |
+
mapping = C.graph["mapping"]
|
| 676 |
+
# Assign each available edge to an edge in the metagraph
|
| 677 |
+
candidate_mapping = _lightest_meta_edges(mapping, avail_uv, avail_w)
|
| 678 |
+
# nx.set_edge_attributes(C, name='weight', values=0)
|
| 679 |
+
C.add_edges_from(
|
| 680 |
+
(mu, mv, {"weight": w, "generator": uv})
|
| 681 |
+
for (mu, mv), uv, w in candidate_mapping
|
| 682 |
+
)
|
| 683 |
+
# Find MST of the meta graph
|
| 684 |
+
meta_mst = nx.minimum_spanning_tree(C)
|
| 685 |
+
if not partial and not nx.is_connected(meta_mst):
|
| 686 |
+
raise nx.NetworkXUnfeasible("Not possible to connect G with available edges")
|
| 687 |
+
# Yield the edge that generated the meta-edge
|
| 688 |
+
for mu, mv, d in meta_mst.edges(data=True):
|
| 689 |
+
if "generator" in d:
|
| 690 |
+
edge = d["generator"]
|
| 691 |
+
yield edge
|
| 692 |
+
|
| 693 |
+
|
| 694 |
+
@nx._dispatchable
|
| 695 |
+
def unconstrained_bridge_augmentation(G):
|
| 696 |
+
"""Finds an optimal 2-edge-augmentation of G using the fewest edges.
|
| 697 |
+
|
| 698 |
+
This is an implementation of the algorithm detailed in [1]_.
|
| 699 |
+
The basic idea is to construct a meta-graph of bridge-ccs, connect leaf
|
| 700 |
+
nodes of the trees to connect the entire graph, and finally connect the
|
| 701 |
+
leafs of the tree in dfs-preorder to bridge connect the entire graph.
|
| 702 |
+
|
| 703 |
+
Parameters
|
| 704 |
+
----------
|
| 705 |
+
G : NetworkX graph
|
| 706 |
+
An undirected graph.
|
| 707 |
+
|
| 708 |
+
Yields
|
| 709 |
+
------
|
| 710 |
+
edge : tuple
|
| 711 |
+
Edges in the bridge augmentation of G
|
| 712 |
+
|
| 713 |
+
Notes
|
| 714 |
+
-----
|
| 715 |
+
Input: a graph G.
|
| 716 |
+
First find the bridge components of G and collapse each bridge-cc into a
|
| 717 |
+
node of a metagraph graph C, which is guaranteed to be a forest of trees.
|
| 718 |
+
|
| 719 |
+
C contains p "leafs" --- nodes with exactly one incident edge.
|
| 720 |
+
C contains q "isolated nodes" --- nodes with no incident edges.
|
| 721 |
+
|
| 722 |
+
Theorem: If p + q > 1, then at least :math:`ceil(p / 2) + q` edges are
|
| 723 |
+
needed to bridge connect C. This algorithm achieves this min number.
|
| 724 |
+
|
| 725 |
+
The method first adds enough edges to make G into a tree and then pairs
|
| 726 |
+
leafs in a simple fashion.
|
| 727 |
+
|
| 728 |
+
Let n be the number of trees in C. Let v(i) be an isolated vertex in the
|
| 729 |
+
i-th tree if one exists, otherwise it is a pair of distinct leafs nodes
|
| 730 |
+
in the i-th tree. Alternating edges from these sets (i.e. adding edges
|
| 731 |
+
A1 = [(v(i)[0], v(i + 1)[1]), v(i + 1)[0], v(i + 2)[1])...]) connects C
|
| 732 |
+
into a tree T. This tree has p' = p + 2q - 2(n -1) leafs and no isolated
|
| 733 |
+
vertices. A1 has n - 1 edges. The next step finds ceil(p' / 2) edges to
|
| 734 |
+
biconnect any tree with p' leafs.
|
| 735 |
+
|
| 736 |
+
Convert T into an arborescence T' by picking an arbitrary root node with
|
| 737 |
+
degree >= 2 and directing all edges away from the root. Note the
|
| 738 |
+
implementation implicitly constructs T'.
|
| 739 |
+
|
| 740 |
+
The leafs of T are the nodes with no existing edges in T'.
|
| 741 |
+
Order the leafs of T' by DFS preorder. Then break this list in half
|
| 742 |
+
and add the zipped pairs to A2.
|
| 743 |
+
|
| 744 |
+
The set A = A1 + A2 is the minimum augmentation in the metagraph.
|
| 745 |
+
|
| 746 |
+
To convert this to edges in the original graph
|
| 747 |
+
|
| 748 |
+
References
|
| 749 |
+
----------
|
| 750 |
+
.. [1] Eswaran, Kapali P., and R. Endre Tarjan. (1975) Augmentation problems.
|
| 751 |
+
http://epubs.siam.org/doi/abs/10.1137/0205044
|
| 752 |
+
|
| 753 |
+
See Also
|
| 754 |
+
--------
|
| 755 |
+
:func:`bridge_augmentation`
|
| 756 |
+
:func:`k_edge_augmentation`
|
| 757 |
+
|
| 758 |
+
Examples
|
| 759 |
+
--------
|
| 760 |
+
>>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7))
|
| 761 |
+
>>> sorted(unconstrained_bridge_augmentation(G))
|
| 762 |
+
[(1, 7)]
|
| 763 |
+
>>> G = nx.path_graph((1, 2, 3, 2, 4, 5, 6, 7))
|
| 764 |
+
>>> sorted(unconstrained_bridge_augmentation(G))
|
| 765 |
+
[(1, 3), (3, 7)]
|
| 766 |
+
>>> G = nx.Graph([(0, 1), (0, 2), (1, 2)])
|
| 767 |
+
>>> G.add_node(4)
|
| 768 |
+
>>> sorted(unconstrained_bridge_augmentation(G))
|
| 769 |
+
[(1, 4), (4, 0)]
|
| 770 |
+
"""
|
| 771 |
+
# -----
|
| 772 |
+
# Mapping of terms from (Eswaran and Tarjan):
|
| 773 |
+
# G = G_0 - the input graph
|
| 774 |
+
# C = G_0' - the bridge condensation of G. (This is a forest of trees)
|
| 775 |
+
# A1 = A_1 - the edges to connect the forest into a tree
|
| 776 |
+
# leaf = pendant - a node with degree of 1
|
| 777 |
+
|
| 778 |
+
# alpha(v) = maps the node v in G to its meta-node in C
|
| 779 |
+
# beta(x) = maps the meta-node x in C to any node in the bridge
|
| 780 |
+
# component of G corresponding to x.
|
| 781 |
+
|
| 782 |
+
# find the 2-edge-connected components of G
|
| 783 |
+
bridge_ccs = list(nx.connectivity.bridge_components(G))
|
| 784 |
+
# condense G into an forest C
|
| 785 |
+
C = collapse(G, bridge_ccs)
|
| 786 |
+
|
| 787 |
+
# Choose pairs of distinct leaf nodes in each tree. If this is not
|
| 788 |
+
# possible then make a pair using the single isolated node in the tree.
|
| 789 |
+
vset1 = [
|
| 790 |
+
tuple(cc) * 2 # case1: an isolated node
|
| 791 |
+
if len(cc) == 1
|
| 792 |
+
else sorted(cc, key=C.degree)[0:2] # case2: pair of leaf nodes
|
| 793 |
+
for cc in nx.connected_components(C)
|
| 794 |
+
]
|
| 795 |
+
if len(vset1) > 1:
|
| 796 |
+
# Use this set to construct edges that connect C into a tree.
|
| 797 |
+
nodes1 = [vs[0] for vs in vset1]
|
| 798 |
+
nodes2 = [vs[1] for vs in vset1]
|
| 799 |
+
A1 = list(zip(nodes1[1:], nodes2))
|
| 800 |
+
else:
|
| 801 |
+
A1 = []
|
| 802 |
+
# Connect each tree in the forest to construct an arborescence
|
| 803 |
+
T = C.copy()
|
| 804 |
+
T.add_edges_from(A1)
|
| 805 |
+
|
| 806 |
+
# If there are only two leaf nodes, we simply connect them.
|
| 807 |
+
leafs = [n for n, d in T.degree() if d == 1]
|
| 808 |
+
if len(leafs) == 1:
|
| 809 |
+
A2 = []
|
| 810 |
+
if len(leafs) == 2:
|
| 811 |
+
A2 = [tuple(leafs)]
|
| 812 |
+
else:
|
| 813 |
+
# Choose an arbitrary non-leaf root
|
| 814 |
+
try:
|
| 815 |
+
root = next(n for n, d in T.degree() if d > 1)
|
| 816 |
+
except StopIteration: # no nodes found with degree > 1
|
| 817 |
+
return
|
| 818 |
+
# order the leaves of C by (induced directed) preorder
|
| 819 |
+
v2 = [n for n in nx.dfs_preorder_nodes(T, root) if T.degree(n) == 1]
|
| 820 |
+
# connecting first half of the leafs in pre-order to the second
|
| 821 |
+
# half will bridge connect the tree with the fewest edges.
|
| 822 |
+
half = math.ceil(len(v2) / 2)
|
| 823 |
+
A2 = list(zip(v2[:half], v2[-half:]))
|
| 824 |
+
|
| 825 |
+
# collect the edges used to augment the original forest
|
| 826 |
+
aug_tree_edges = A1 + A2
|
| 827 |
+
|
| 828 |
+
# Construct the mapping (beta) from meta-nodes to regular nodes
|
| 829 |
+
inverse = defaultdict(list)
|
| 830 |
+
for k, v in C.graph["mapping"].items():
|
| 831 |
+
inverse[v].append(k)
|
| 832 |
+
# sort so we choose minimum degree nodes first
|
| 833 |
+
inverse = {
|
| 834 |
+
mu: sorted(mapped, key=lambda u: (G.degree(u), u))
|
| 835 |
+
for mu, mapped in inverse.items()
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
# For each meta-edge, map back to an arbitrary pair in the original graph
|
| 839 |
+
G2 = G.copy()
|
| 840 |
+
for mu, mv in aug_tree_edges:
|
| 841 |
+
# Find the first available edge that doesn't exist and return it
|
| 842 |
+
for u, v in it.product(inverse[mu], inverse[mv]):
|
| 843 |
+
if not G2.has_edge(u, v):
|
| 844 |
+
G2.add_edge(u, v)
|
| 845 |
+
yield u, v
|
| 846 |
+
break
|
| 847 |
+
|
| 848 |
+
|
| 849 |
+
@nx._dispatchable
|
| 850 |
+
def weighted_bridge_augmentation(G, avail, weight=None):
|
| 851 |
+
"""Finds an approximate min-weight 2-edge-augmentation of G.
|
| 852 |
+
|
| 853 |
+
This is an implementation of the approximation algorithm detailed in [1]_.
|
| 854 |
+
It chooses a set of edges from avail to add to G that renders it
|
| 855 |
+
2-edge-connected if such a subset exists. This is done by finding a
|
| 856 |
+
minimum spanning arborescence of a specially constructed metagraph.
|
| 857 |
+
|
| 858 |
+
Parameters
|
| 859 |
+
----------
|
| 860 |
+
G : NetworkX graph
|
| 861 |
+
An undirected graph.
|
| 862 |
+
|
| 863 |
+
avail : set of 2 or 3 tuples.
|
| 864 |
+
candidate edges (with optional weights) to choose from
|
| 865 |
+
|
| 866 |
+
weight : string
|
| 867 |
+
key to use to find weights if avail is a set of 3-tuples where the
|
| 868 |
+
third item in each tuple is a dictionary.
|
| 869 |
+
|
| 870 |
+
Yields
|
| 871 |
+
------
|
| 872 |
+
edge : tuple
|
| 873 |
+
Edges in the subset of avail chosen to bridge augment G.
|
| 874 |
+
|
| 875 |
+
Notes
|
| 876 |
+
-----
|
| 877 |
+
Finding a weighted 2-edge-augmentation is NP-hard.
|
| 878 |
+
Any edge not in ``avail`` is considered to have a weight of infinity.
|
| 879 |
+
The approximation factor is 2 if ``G`` is connected and 3 if it is not.
|
| 880 |
+
Runs in :math:`O(m + n log(n))` time
|
| 881 |
+
|
| 882 |
+
References
|
| 883 |
+
----------
|
| 884 |
+
.. [1] Khuller, Samir, and Ramakrishna Thurimella. (1993) Approximation
|
| 885 |
+
algorithms for graph augmentation.
|
| 886 |
+
http://www.sciencedirect.com/science/article/pii/S0196677483710102
|
| 887 |
+
|
| 888 |
+
See Also
|
| 889 |
+
--------
|
| 890 |
+
:func:`bridge_augmentation`
|
| 891 |
+
:func:`k_edge_augmentation`
|
| 892 |
+
|
| 893 |
+
Examples
|
| 894 |
+
--------
|
| 895 |
+
>>> G = nx.path_graph((1, 2, 3, 4))
|
| 896 |
+
>>> # When the weights are equal, (1, 4) is the best
|
| 897 |
+
>>> avail = [(1, 4, 1), (1, 3, 1), (2, 4, 1)]
|
| 898 |
+
>>> sorted(weighted_bridge_augmentation(G, avail))
|
| 899 |
+
[(1, 4)]
|
| 900 |
+
>>> # Giving (1, 4) a high weight makes the two edge solution the best.
|
| 901 |
+
>>> avail = [(1, 4, 1000), (1, 3, 1), (2, 4, 1)]
|
| 902 |
+
>>> sorted(weighted_bridge_augmentation(G, avail))
|
| 903 |
+
[(1, 3), (2, 4)]
|
| 904 |
+
>>> # ------
|
| 905 |
+
>>> G = nx.path_graph((1, 2, 3, 4))
|
| 906 |
+
>>> G.add_node(5)
|
| 907 |
+
>>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 1)]
|
| 908 |
+
>>> sorted(weighted_bridge_augmentation(G, avail=avail))
|
| 909 |
+
[(1, 5), (4, 5)]
|
| 910 |
+
>>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 51)]
|
| 911 |
+
>>> sorted(weighted_bridge_augmentation(G, avail=avail))
|
| 912 |
+
[(1, 5), (2, 5), (4, 5)]
|
| 913 |
+
"""
|
| 914 |
+
|
| 915 |
+
if weight is None:
|
| 916 |
+
weight = "weight"
|
| 917 |
+
|
| 918 |
+
# If input G is not connected the approximation factor increases to 3
|
| 919 |
+
if not nx.is_connected(G):
|
| 920 |
+
H = G.copy()
|
| 921 |
+
connectors = list(one_edge_augmentation(H, avail=avail, weight=weight))
|
| 922 |
+
H.add_edges_from(connectors)
|
| 923 |
+
|
| 924 |
+
yield from connectors
|
| 925 |
+
else:
|
| 926 |
+
connectors = []
|
| 927 |
+
H = G
|
| 928 |
+
|
| 929 |
+
if len(avail) == 0:
|
| 930 |
+
if nx.has_bridges(H):
|
| 931 |
+
raise nx.NetworkXUnfeasible("no augmentation possible")
|
| 932 |
+
|
| 933 |
+
avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=H)
|
| 934 |
+
|
| 935 |
+
# Collapse input into a metagraph. Meta nodes are bridge-ccs
|
| 936 |
+
bridge_ccs = nx.connectivity.bridge_components(H)
|
| 937 |
+
C = collapse(H, bridge_ccs)
|
| 938 |
+
|
| 939 |
+
# Use the meta graph to shrink avail to a small feasible subset
|
| 940 |
+
mapping = C.graph["mapping"]
|
| 941 |
+
# Choose the minimum weight feasible edge in each group
|
| 942 |
+
meta_to_wuv = {
|
| 943 |
+
(mu, mv): (w, uv)
|
| 944 |
+
for (mu, mv), uv, w in _lightest_meta_edges(mapping, avail_uv, avail_w)
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
# Mapping of terms from (Khuller and Thurimella):
|
| 948 |
+
# C : G_0 = (V, E^0)
|
| 949 |
+
# This is the metagraph where each node is a 2-edge-cc in G.
|
| 950 |
+
# The edges in C represent bridges in the original graph.
|
| 951 |
+
# (mu, mv) : E - E^0 # they group both avail and given edges in E
|
| 952 |
+
# T : \Gamma
|
| 953 |
+
# D : G^D = (V, E_D)
|
| 954 |
+
|
| 955 |
+
# The paper uses ancestor because children point to parents, which is
|
| 956 |
+
# contrary to networkx standards. So, we actually need to run
|
| 957 |
+
# nx.least_common_ancestor on the reversed Tree.
|
| 958 |
+
|
| 959 |
+
# Pick an arbitrary leaf from C as the root
|
| 960 |
+
try:
|
| 961 |
+
root = next(n for n, d in C.degree() if d == 1)
|
| 962 |
+
except StopIteration: # no nodes found with degree == 1
|
| 963 |
+
return
|
| 964 |
+
# Root C into a tree TR by directing all edges away from the root
|
| 965 |
+
# Note in their paper T directs edges towards the root
|
| 966 |
+
TR = nx.dfs_tree(C, root)
|
| 967 |
+
|
| 968 |
+
# Add to D the directed edges of T and set their weight to zero
|
| 969 |
+
# This indicates that it costs nothing to use edges that were given.
|
| 970 |
+
D = nx.reverse(TR).copy()
|
| 971 |
+
|
| 972 |
+
nx.set_edge_attributes(D, name="weight", values=0)
|
| 973 |
+
|
| 974 |
+
# The LCA of mu and mv in T is the shared ancestor of mu and mv that is
|
| 975 |
+
# located farthest from the root.
|
| 976 |
+
lca_gen = nx.tree_all_pairs_lowest_common_ancestor(
|
| 977 |
+
TR, root=root, pairs=meta_to_wuv.keys()
|
| 978 |
+
)
|
| 979 |
+
|
| 980 |
+
for (mu, mv), lca in lca_gen:
|
| 981 |
+
w, uv = meta_to_wuv[(mu, mv)]
|
| 982 |
+
if lca == mu:
|
| 983 |
+
# If u is an ancestor of v in TR, then add edge u->v to D
|
| 984 |
+
D.add_edge(lca, mv, weight=w, generator=uv)
|
| 985 |
+
elif lca == mv:
|
| 986 |
+
# If v is an ancestor of u in TR, then add edge v->u to D
|
| 987 |
+
D.add_edge(lca, mu, weight=w, generator=uv)
|
| 988 |
+
else:
|
| 989 |
+
# If neither u nor v is a ancestor of the other in TR
|
| 990 |
+
# let t = lca(TR, u, v) and add edges t->u and t->v
|
| 991 |
+
# Track the original edge that GENERATED these edges.
|
| 992 |
+
D.add_edge(lca, mu, weight=w, generator=uv)
|
| 993 |
+
D.add_edge(lca, mv, weight=w, generator=uv)
|
| 994 |
+
|
| 995 |
+
# Then compute a minimum rooted branching
|
| 996 |
+
try:
|
| 997 |
+
# Note the original edges must be directed towards to root for the
|
| 998 |
+
# branching to give us a bridge-augmentation.
|
| 999 |
+
A = _minimum_rooted_branching(D, root)
|
| 1000 |
+
except nx.NetworkXException as err:
|
| 1001 |
+
# If there is no branching then augmentation is not possible
|
| 1002 |
+
raise nx.NetworkXUnfeasible("no 2-edge-augmentation possible") from err
|
| 1003 |
+
|
| 1004 |
+
# For each edge e, in the branching that did not belong to the directed
|
| 1005 |
+
# tree T, add the corresponding edge that **GENERATED** it (this is not
|
| 1006 |
+
# necessarily e itself!)
|
| 1007 |
+
|
| 1008 |
+
# ensure the third case does not generate edges twice
|
| 1009 |
+
bridge_connectors = set()
|
| 1010 |
+
for mu, mv in A.edges():
|
| 1011 |
+
data = D.get_edge_data(mu, mv)
|
| 1012 |
+
if "generator" in data:
|
| 1013 |
+
# Add the avail edge that generated the branching edge.
|
| 1014 |
+
edge = data["generator"]
|
| 1015 |
+
bridge_connectors.add(edge)
|
| 1016 |
+
|
| 1017 |
+
yield from bridge_connectors
|
| 1018 |
+
|
| 1019 |
+
|
| 1020 |
+
def _minimum_rooted_branching(D, root):
|
| 1021 |
+
"""Helper function to compute a minimum rooted branching (aka rooted
|
| 1022 |
+
arborescence)
|
| 1023 |
+
|
| 1024 |
+
Before the branching can be computed, the directed graph must be rooted by
|
| 1025 |
+
removing the predecessors of root.
|
| 1026 |
+
|
| 1027 |
+
A branching / arborescence of rooted graph G is a subgraph that contains a
|
| 1028 |
+
directed path from the root to every other vertex. It is the directed
|
| 1029 |
+
analog of the minimum spanning tree problem.
|
| 1030 |
+
|
| 1031 |
+
References
|
| 1032 |
+
----------
|
| 1033 |
+
[1] Khuller, Samir (2002) Advanced Algorithms Lecture 24 Notes.
|
| 1034 |
+
https://web.archive.org/web/20121030033722/https://www.cs.umd.edu/class/spring2011/cmsc651/lec07.pdf
|
| 1035 |
+
"""
|
| 1036 |
+
rooted = D.copy()
|
| 1037 |
+
# root the graph by removing all predecessors to `root`.
|
| 1038 |
+
rooted.remove_edges_from([(u, root) for u in D.predecessors(root)])
|
| 1039 |
+
# Then compute the branching / arborescence.
|
| 1040 |
+
A = nx.minimum_spanning_arborescence(rooted)
|
| 1041 |
+
return A
|
| 1042 |
+
|
| 1043 |
+
|
| 1044 |
+
@nx._dispatchable(returns_graph=True)
|
| 1045 |
+
def collapse(G, grouped_nodes):
|
| 1046 |
+
"""Collapses each group of nodes into a single node.
|
| 1047 |
+
|
| 1048 |
+
This is similar to condensation, but works on undirected graphs.
|
| 1049 |
+
|
| 1050 |
+
Parameters
|
| 1051 |
+
----------
|
| 1052 |
+
G : NetworkX Graph
|
| 1053 |
+
|
| 1054 |
+
grouped_nodes: list or generator
|
| 1055 |
+
Grouping of nodes to collapse. The grouping must be disjoint.
|
| 1056 |
+
If grouped_nodes are strongly_connected_components then this is
|
| 1057 |
+
equivalent to :func:`condensation`.
|
| 1058 |
+
|
| 1059 |
+
Returns
|
| 1060 |
+
-------
|
| 1061 |
+
C : NetworkX Graph
|
| 1062 |
+
The collapsed graph C of G with respect to the node grouping. The node
|
| 1063 |
+
labels are integers corresponding to the index of the component in the
|
| 1064 |
+
list of grouped_nodes. C has a graph attribute named 'mapping' with a
|
| 1065 |
+
dictionary mapping the original nodes to the nodes in C to which they
|
| 1066 |
+
belong. Each node in C also has a node attribute 'members' with the set
|
| 1067 |
+
of original nodes in G that form the group that the node in C
|
| 1068 |
+
represents.
|
| 1069 |
+
|
| 1070 |
+
Examples
|
| 1071 |
+
--------
|
| 1072 |
+
>>> # Collapses a graph using disjoint groups, but not necessarily connected
|
| 1073 |
+
>>> G = nx.Graph([(1, 0), (2, 3), (3, 1), (3, 4), (4, 5), (5, 6), (5, 7)])
|
| 1074 |
+
>>> G.add_node("A")
|
| 1075 |
+
>>> grouped_nodes = [{0, 1, 2, 3}, {5, 6, 7}]
|
| 1076 |
+
>>> C = collapse(G, grouped_nodes)
|
| 1077 |
+
>>> members = nx.get_node_attributes(C, "members")
|
| 1078 |
+
>>> sorted(members.keys())
|
| 1079 |
+
[0, 1, 2, 3]
|
| 1080 |
+
>>> member_values = set(map(frozenset, members.values()))
|
| 1081 |
+
>>> assert {0, 1, 2, 3} in member_values
|
| 1082 |
+
>>> assert {4} in member_values
|
| 1083 |
+
>>> assert {5, 6, 7} in member_values
|
| 1084 |
+
>>> assert {"A"} in member_values
|
| 1085 |
+
"""
|
| 1086 |
+
mapping = {}
|
| 1087 |
+
members = {}
|
| 1088 |
+
C = G.__class__()
|
| 1089 |
+
i = 0 # required if G is empty
|
| 1090 |
+
remaining = set(G.nodes())
|
| 1091 |
+
for i, group in enumerate(grouped_nodes):
|
| 1092 |
+
group = set(group)
|
| 1093 |
+
assert remaining.issuperset(
|
| 1094 |
+
group
|
| 1095 |
+
), "grouped nodes must exist in G and be disjoint"
|
| 1096 |
+
remaining.difference_update(group)
|
| 1097 |
+
members[i] = group
|
| 1098 |
+
mapping.update((n, i) for n in group)
|
| 1099 |
+
# remaining nodes are in their own group
|
| 1100 |
+
for i, node in enumerate(remaining, start=i + 1):
|
| 1101 |
+
group = {node}
|
| 1102 |
+
members[i] = group
|
| 1103 |
+
mapping.update((n, i) for n in group)
|
| 1104 |
+
number_of_groups = i + 1
|
| 1105 |
+
C.add_nodes_from(range(number_of_groups))
|
| 1106 |
+
C.add_edges_from(
|
| 1107 |
+
(mapping[u], mapping[v]) for u, v in G.edges() if mapping[u] != mapping[v]
|
| 1108 |
+
)
|
| 1109 |
+
# Add a list of members (ie original nodes) to each node (ie scc) in C.
|
| 1110 |
+
nx.set_node_attributes(C, name="members", values=members)
|
| 1111 |
+
# Add mapping dict as graph attribute
|
| 1112 |
+
C.graph["mapping"] = mapping
|
| 1113 |
+
return C
|
| 1114 |
+
|
| 1115 |
+
|
| 1116 |
+
@nx._dispatchable
|
| 1117 |
+
def complement_edges(G):
|
| 1118 |
+
"""Returns only the edges in the complement of G
|
| 1119 |
+
|
| 1120 |
+
Parameters
|
| 1121 |
+
----------
|
| 1122 |
+
G : NetworkX Graph
|
| 1123 |
+
|
| 1124 |
+
Yields
|
| 1125 |
+
------
|
| 1126 |
+
edge : tuple
|
| 1127 |
+
Edges in the complement of G
|
| 1128 |
+
|
| 1129 |
+
Examples
|
| 1130 |
+
--------
|
| 1131 |
+
>>> G = nx.path_graph((1, 2, 3, 4))
|
| 1132 |
+
>>> sorted(complement_edges(G))
|
| 1133 |
+
[(1, 3), (1, 4), (2, 4)]
|
| 1134 |
+
>>> G = nx.path_graph((1, 2, 3, 4), nx.DiGraph())
|
| 1135 |
+
>>> sorted(complement_edges(G))
|
| 1136 |
+
[(1, 3), (1, 4), (2, 1), (2, 4), (3, 1), (3, 2), (4, 1), (4, 2), (4, 3)]
|
| 1137 |
+
>>> G = nx.complete_graph(1000)
|
| 1138 |
+
>>> sorted(complement_edges(G))
|
| 1139 |
+
[]
|
| 1140 |
+
"""
|
| 1141 |
+
G_adj = G._adj # Store as a variable to eliminate attribute lookup
|
| 1142 |
+
if G.is_directed():
|
| 1143 |
+
for u, v in it.combinations(G.nodes(), 2):
|
| 1144 |
+
if v not in G_adj[u]:
|
| 1145 |
+
yield (u, v)
|
| 1146 |
+
if u not in G_adj[v]:
|
| 1147 |
+
yield (v, u)
|
| 1148 |
+
else:
|
| 1149 |
+
for u, v in it.combinations(G.nodes(), 2):
|
| 1150 |
+
if v not in G_adj[u]:
|
| 1151 |
+
yield (u, v)
|
| 1152 |
+
|
| 1153 |
+
|
| 1154 |
+
def _compat_shuffle(rng, input):
|
| 1155 |
+
"""wrapper around rng.shuffle for python 2 compatibility reasons"""
|
| 1156 |
+
rng.shuffle(input)
|
| 1157 |
+
|
| 1158 |
+
|
| 1159 |
+
@not_implemented_for("multigraph")
|
| 1160 |
+
@not_implemented_for("directed")
|
| 1161 |
+
@py_random_state(4)
|
| 1162 |
+
@nx._dispatchable
|
| 1163 |
+
def greedy_k_edge_augmentation(G, k, avail=None, weight=None, seed=None):
|
| 1164 |
+
"""Greedy algorithm for finding a k-edge-augmentation
|
| 1165 |
+
|
| 1166 |
+
Parameters
|
| 1167 |
+
----------
|
| 1168 |
+
G : NetworkX graph
|
| 1169 |
+
An undirected graph.
|
| 1170 |
+
|
| 1171 |
+
k : integer
|
| 1172 |
+
Desired edge connectivity
|
| 1173 |
+
|
| 1174 |
+
avail : dict or a set of 2 or 3 tuples
|
| 1175 |
+
For more details, see :func:`k_edge_augmentation`.
|
| 1176 |
+
|
| 1177 |
+
weight : string
|
| 1178 |
+
key to use to find weights if ``avail`` is a set of 3-tuples.
|
| 1179 |
+
For more details, see :func:`k_edge_augmentation`.
|
| 1180 |
+
|
| 1181 |
+
seed : integer, random_state, or None (default)
|
| 1182 |
+
Indicator of random number generation state.
|
| 1183 |
+
See :ref:`Randomness<randomness>`.
|
| 1184 |
+
|
| 1185 |
+
Yields
|
| 1186 |
+
------
|
| 1187 |
+
edge : tuple
|
| 1188 |
+
Edges in the greedy augmentation of G
|
| 1189 |
+
|
| 1190 |
+
Notes
|
| 1191 |
+
-----
|
| 1192 |
+
The algorithm is simple. Edges are incrementally added between parts of the
|
| 1193 |
+
graph that are not yet locally k-edge-connected. Then edges are from the
|
| 1194 |
+
augmenting set are pruned as long as local-edge-connectivity is not broken.
|
| 1195 |
+
|
| 1196 |
+
This algorithm is greedy and does not provide optimality guarantees. It
|
| 1197 |
+
exists only to provide :func:`k_edge_augmentation` with the ability to
|
| 1198 |
+
generate a feasible solution for arbitrary k.
|
| 1199 |
+
|
| 1200 |
+
See Also
|
| 1201 |
+
--------
|
| 1202 |
+
:func:`k_edge_augmentation`
|
| 1203 |
+
|
| 1204 |
+
Examples
|
| 1205 |
+
--------
|
| 1206 |
+
>>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7))
|
| 1207 |
+
>>> sorted(greedy_k_edge_augmentation(G, k=2))
|
| 1208 |
+
[(1, 7)]
|
| 1209 |
+
>>> sorted(greedy_k_edge_augmentation(G, k=1, avail=[]))
|
| 1210 |
+
[]
|
| 1211 |
+
>>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7))
|
| 1212 |
+
>>> avail = {(u, v): 1 for (u, v) in complement_edges(G)}
|
| 1213 |
+
>>> # randomized pruning process can produce different solutions
|
| 1214 |
+
>>> sorted(greedy_k_edge_augmentation(G, k=4, avail=avail, seed=2))
|
| 1215 |
+
[(1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (2, 4), (2, 6), (3, 7), (5, 7)]
|
| 1216 |
+
>>> sorted(greedy_k_edge_augmentation(G, k=4, avail=avail, seed=3))
|
| 1217 |
+
[(1, 3), (1, 5), (1, 6), (2, 4), (2, 6), (3, 7), (4, 7), (5, 7)]
|
| 1218 |
+
"""
|
| 1219 |
+
# Result set
|
| 1220 |
+
aug_edges = []
|
| 1221 |
+
|
| 1222 |
+
done = is_k_edge_connected(G, k)
|
| 1223 |
+
if done:
|
| 1224 |
+
return
|
| 1225 |
+
if avail is None:
|
| 1226 |
+
# all edges are available
|
| 1227 |
+
avail_uv = list(complement_edges(G))
|
| 1228 |
+
avail_w = [1] * len(avail_uv)
|
| 1229 |
+
else:
|
| 1230 |
+
# Get the unique set of unweighted edges
|
| 1231 |
+
avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G)
|
| 1232 |
+
|
| 1233 |
+
# Greedy: order lightest edges. Use degree sum to tie-break
|
| 1234 |
+
tiebreaker = [sum(map(G.degree, uv)) for uv in avail_uv]
|
| 1235 |
+
avail_wduv = sorted(zip(avail_w, tiebreaker, avail_uv))
|
| 1236 |
+
avail_uv = [uv for w, d, uv in avail_wduv]
|
| 1237 |
+
|
| 1238 |
+
# Incrementally add edges in until we are k-connected
|
| 1239 |
+
H = G.copy()
|
| 1240 |
+
for u, v in avail_uv:
|
| 1241 |
+
done = False
|
| 1242 |
+
if not is_locally_k_edge_connected(H, u, v, k=k):
|
| 1243 |
+
# Only add edges in parts that are not yet locally k-edge-connected
|
| 1244 |
+
aug_edges.append((u, v))
|
| 1245 |
+
H.add_edge(u, v)
|
| 1246 |
+
# Did adding this edge help?
|
| 1247 |
+
if H.degree(u) >= k and H.degree(v) >= k:
|
| 1248 |
+
done = is_k_edge_connected(H, k)
|
| 1249 |
+
if done:
|
| 1250 |
+
break
|
| 1251 |
+
|
| 1252 |
+
# Check for feasibility
|
| 1253 |
+
if not done:
|
| 1254 |
+
raise nx.NetworkXUnfeasible("not able to k-edge-connect with available edges")
|
| 1255 |
+
|
| 1256 |
+
# Randomized attempt to reduce the size of the solution
|
| 1257 |
+
_compat_shuffle(seed, aug_edges)
|
| 1258 |
+
for u, v in list(aug_edges):
|
| 1259 |
+
# Don't remove if we know it would break connectivity
|
| 1260 |
+
if H.degree(u) <= k or H.degree(v) <= k:
|
| 1261 |
+
continue
|
| 1262 |
+
H.remove_edge(u, v)
|
| 1263 |
+
aug_edges.remove((u, v))
|
| 1264 |
+
if not is_k_edge_connected(H, k=k):
|
| 1265 |
+
# If removing this edge breaks feasibility, undo
|
| 1266 |
+
H.add_edge(u, v)
|
| 1267 |
+
aug_edges.append((u, v))
|
| 1268 |
+
|
| 1269 |
+
# Generate results
|
| 1270 |
+
yield from aug_edges
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/edge_kcomponents.py
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Algorithms for finding k-edge-connected components and subgraphs.
|
| 3 |
+
|
| 4 |
+
A k-edge-connected component (k-edge-cc) is a maximal set of nodes in G, such
|
| 5 |
+
that all pairs of node have an edge-connectivity of at least k.
|
| 6 |
+
|
| 7 |
+
A k-edge-connected subgraph (k-edge-subgraph) is a maximal set of nodes in G,
|
| 8 |
+
such that the subgraph of G defined by the nodes has an edge-connectivity at
|
| 9 |
+
least k.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import itertools as it
|
| 13 |
+
from functools import partial
|
| 14 |
+
|
| 15 |
+
import networkx as nx
|
| 16 |
+
from networkx.utils import arbitrary_element, not_implemented_for
|
| 17 |
+
|
| 18 |
+
__all__ = [
|
| 19 |
+
"k_edge_components",
|
| 20 |
+
"k_edge_subgraphs",
|
| 21 |
+
"bridge_components",
|
| 22 |
+
"EdgeComponentAuxGraph",
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@not_implemented_for("multigraph")
|
| 27 |
+
@nx._dispatchable
|
| 28 |
+
def k_edge_components(G, k):
|
| 29 |
+
"""Generates nodes in each maximal k-edge-connected component in G.
|
| 30 |
+
|
| 31 |
+
Parameters
|
| 32 |
+
----------
|
| 33 |
+
G : NetworkX graph
|
| 34 |
+
|
| 35 |
+
k : Integer
|
| 36 |
+
Desired edge connectivity
|
| 37 |
+
|
| 38 |
+
Returns
|
| 39 |
+
-------
|
| 40 |
+
k_edge_components : a generator of k-edge-ccs. Each set of returned nodes
|
| 41 |
+
will have k-edge-connectivity in the graph G.
|
| 42 |
+
|
| 43 |
+
See Also
|
| 44 |
+
--------
|
| 45 |
+
:func:`local_edge_connectivity`
|
| 46 |
+
:func:`k_edge_subgraphs` : similar to this function, but the subgraph
|
| 47 |
+
defined by the nodes must also have k-edge-connectivity.
|
| 48 |
+
:func:`k_components` : similar to this function, but uses node-connectivity
|
| 49 |
+
instead of edge-connectivity
|
| 50 |
+
|
| 51 |
+
Raises
|
| 52 |
+
------
|
| 53 |
+
NetworkXNotImplemented
|
| 54 |
+
If the input graph is a multigraph.
|
| 55 |
+
|
| 56 |
+
ValueError:
|
| 57 |
+
If k is less than 1
|
| 58 |
+
|
| 59 |
+
Notes
|
| 60 |
+
-----
|
| 61 |
+
Attempts to use the most efficient implementation available based on k.
|
| 62 |
+
If k=1, this is simply connected components for directed graphs and
|
| 63 |
+
connected components for undirected graphs.
|
| 64 |
+
If k=2 on an efficient bridge connected component algorithm from _[1] is
|
| 65 |
+
run based on the chain decomposition.
|
| 66 |
+
Otherwise, the algorithm from _[2] is used.
|
| 67 |
+
|
| 68 |
+
Examples
|
| 69 |
+
--------
|
| 70 |
+
>>> import itertools as it
|
| 71 |
+
>>> from networkx.utils import pairwise
|
| 72 |
+
>>> paths = [
|
| 73 |
+
... (1, 2, 4, 3, 1, 4),
|
| 74 |
+
... (5, 6, 7, 8, 5, 7, 8, 6),
|
| 75 |
+
... ]
|
| 76 |
+
>>> G = nx.Graph()
|
| 77 |
+
>>> G.add_nodes_from(it.chain(*paths))
|
| 78 |
+
>>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
|
| 79 |
+
>>> # note this returns {1, 4} unlike k_edge_subgraphs
|
| 80 |
+
>>> sorted(map(sorted, nx.k_edge_components(G, k=3)))
|
| 81 |
+
[[1, 4], [2], [3], [5, 6, 7, 8]]
|
| 82 |
+
|
| 83 |
+
References
|
| 84 |
+
----------
|
| 85 |
+
.. [1] https://en.wikipedia.org/wiki/Bridge_%28graph_theory%29
|
| 86 |
+
.. [2] Wang, Tianhao, et al. (2015) A simple algorithm for finding all
|
| 87 |
+
k-edge-connected components.
|
| 88 |
+
http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
|
| 89 |
+
"""
|
| 90 |
+
# Compute k-edge-ccs using the most efficient algorithms available.
|
| 91 |
+
if k < 1:
|
| 92 |
+
raise ValueError("k cannot be less than 1")
|
| 93 |
+
if G.is_directed():
|
| 94 |
+
if k == 1:
|
| 95 |
+
return nx.strongly_connected_components(G)
|
| 96 |
+
else:
|
| 97 |
+
# TODO: investigate https://arxiv.org/abs/1412.6466 for k=2
|
| 98 |
+
aux_graph = EdgeComponentAuxGraph.construct(G)
|
| 99 |
+
return aux_graph.k_edge_components(k)
|
| 100 |
+
else:
|
| 101 |
+
if k == 1:
|
| 102 |
+
return nx.connected_components(G)
|
| 103 |
+
elif k == 2:
|
| 104 |
+
return bridge_components(G)
|
| 105 |
+
else:
|
| 106 |
+
aux_graph = EdgeComponentAuxGraph.construct(G)
|
| 107 |
+
return aux_graph.k_edge_components(k)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@not_implemented_for("multigraph")
|
| 111 |
+
@nx._dispatchable
|
| 112 |
+
def k_edge_subgraphs(G, k):
|
| 113 |
+
"""Generates nodes in each maximal k-edge-connected subgraph in G.
|
| 114 |
+
|
| 115 |
+
Parameters
|
| 116 |
+
----------
|
| 117 |
+
G : NetworkX graph
|
| 118 |
+
|
| 119 |
+
k : Integer
|
| 120 |
+
Desired edge connectivity
|
| 121 |
+
|
| 122 |
+
Returns
|
| 123 |
+
-------
|
| 124 |
+
k_edge_subgraphs : a generator of k-edge-subgraphs
|
| 125 |
+
Each k-edge-subgraph is a maximal set of nodes that defines a subgraph
|
| 126 |
+
of G that is k-edge-connected.
|
| 127 |
+
|
| 128 |
+
See Also
|
| 129 |
+
--------
|
| 130 |
+
:func:`edge_connectivity`
|
| 131 |
+
:func:`k_edge_components` : similar to this function, but nodes only
|
| 132 |
+
need to have k-edge-connectivity within the graph G and the subgraphs
|
| 133 |
+
might not be k-edge-connected.
|
| 134 |
+
|
| 135 |
+
Raises
|
| 136 |
+
------
|
| 137 |
+
NetworkXNotImplemented
|
| 138 |
+
If the input graph is a multigraph.
|
| 139 |
+
|
| 140 |
+
ValueError:
|
| 141 |
+
If k is less than 1
|
| 142 |
+
|
| 143 |
+
Notes
|
| 144 |
+
-----
|
| 145 |
+
Attempts to use the most efficient implementation available based on k.
|
| 146 |
+
If k=1, or k=2 and the graph is undirected, then this simply calls
|
| 147 |
+
`k_edge_components`. Otherwise the algorithm from _[1] is used.
|
| 148 |
+
|
| 149 |
+
Examples
|
| 150 |
+
--------
|
| 151 |
+
>>> import itertools as it
|
| 152 |
+
>>> from networkx.utils import pairwise
|
| 153 |
+
>>> paths = [
|
| 154 |
+
... (1, 2, 4, 3, 1, 4),
|
| 155 |
+
... (5, 6, 7, 8, 5, 7, 8, 6),
|
| 156 |
+
... ]
|
| 157 |
+
>>> G = nx.Graph()
|
| 158 |
+
>>> G.add_nodes_from(it.chain(*paths))
|
| 159 |
+
>>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
|
| 160 |
+
>>> # note this does not return {1, 4} unlike k_edge_components
|
| 161 |
+
>>> sorted(map(sorted, nx.k_edge_subgraphs(G, k=3)))
|
| 162 |
+
[[1], [2], [3], [4], [5, 6, 7, 8]]
|
| 163 |
+
|
| 164 |
+
References
|
| 165 |
+
----------
|
| 166 |
+
.. [1] Zhou, Liu, et al. (2012) Finding maximal k-edge-connected subgraphs
|
| 167 |
+
from a large graph. ACM International Conference on Extending Database
|
| 168 |
+
Technology 2012 480-–491.
|
| 169 |
+
https://openproceedings.org/2012/conf/edbt/ZhouLYLCL12.pdf
|
| 170 |
+
"""
|
| 171 |
+
if k < 1:
|
| 172 |
+
raise ValueError("k cannot be less than 1")
|
| 173 |
+
if G.is_directed():
|
| 174 |
+
if k <= 1:
|
| 175 |
+
# For directed graphs ,
|
| 176 |
+
# When k == 1, k-edge-ccs and k-edge-subgraphs are the same
|
| 177 |
+
return k_edge_components(G, k)
|
| 178 |
+
else:
|
| 179 |
+
return _k_edge_subgraphs_nodes(G, k)
|
| 180 |
+
else:
|
| 181 |
+
if k <= 2:
|
| 182 |
+
# For undirected graphs,
|
| 183 |
+
# when k <= 2, k-edge-ccs and k-edge-subgraphs are the same
|
| 184 |
+
return k_edge_components(G, k)
|
| 185 |
+
else:
|
| 186 |
+
return _k_edge_subgraphs_nodes(G, k)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _k_edge_subgraphs_nodes(G, k):
|
| 190 |
+
"""Helper to get the nodes from the subgraphs.
|
| 191 |
+
|
| 192 |
+
This allows k_edge_subgraphs to return a generator.
|
| 193 |
+
"""
|
| 194 |
+
for C in general_k_edge_subgraphs(G, k):
|
| 195 |
+
yield set(C.nodes())
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@not_implemented_for("directed")
|
| 199 |
+
@not_implemented_for("multigraph")
|
| 200 |
+
@nx._dispatchable
|
| 201 |
+
def bridge_components(G):
|
| 202 |
+
"""Finds all bridge-connected components G.
|
| 203 |
+
|
| 204 |
+
Parameters
|
| 205 |
+
----------
|
| 206 |
+
G : NetworkX undirected graph
|
| 207 |
+
|
| 208 |
+
Returns
|
| 209 |
+
-------
|
| 210 |
+
bridge_components : a generator of 2-edge-connected components
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
See Also
|
| 214 |
+
--------
|
| 215 |
+
:func:`k_edge_subgraphs` : this function is a special case for an
|
| 216 |
+
undirected graph where k=2.
|
| 217 |
+
:func:`biconnected_components` : similar to this function, but is defined
|
| 218 |
+
using 2-node-connectivity instead of 2-edge-connectivity.
|
| 219 |
+
|
| 220 |
+
Raises
|
| 221 |
+
------
|
| 222 |
+
NetworkXNotImplemented
|
| 223 |
+
If the input graph is directed or a multigraph.
|
| 224 |
+
|
| 225 |
+
Notes
|
| 226 |
+
-----
|
| 227 |
+
Bridge-connected components are also known as 2-edge-connected components.
|
| 228 |
+
|
| 229 |
+
Examples
|
| 230 |
+
--------
|
| 231 |
+
>>> # The barbell graph with parameter zero has a single bridge
|
| 232 |
+
>>> G = nx.barbell_graph(5, 0)
|
| 233 |
+
>>> from networkx.algorithms.connectivity.edge_kcomponents import bridge_components
|
| 234 |
+
>>> sorted(map(sorted, bridge_components(G)))
|
| 235 |
+
[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
|
| 236 |
+
"""
|
| 237 |
+
H = G.copy()
|
| 238 |
+
H.remove_edges_from(nx.bridges(G))
|
| 239 |
+
yield from nx.connected_components(H)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
class EdgeComponentAuxGraph:
|
| 243 |
+
r"""A simple algorithm to find all k-edge-connected components in a graph.
|
| 244 |
+
|
| 245 |
+
Constructing the auxiliary graph (which may take some time) allows for the
|
| 246 |
+
k-edge-ccs to be found in linear time for arbitrary k.
|
| 247 |
+
|
| 248 |
+
Notes
|
| 249 |
+
-----
|
| 250 |
+
This implementation is based on [1]_. The idea is to construct an auxiliary
|
| 251 |
+
graph from which the k-edge-ccs can be extracted in linear time. The
|
| 252 |
+
auxiliary graph is constructed in $O(|V|\cdot F)$ operations, where F is the
|
| 253 |
+
complexity of max flow. Querying the components takes an additional $O(|V|)$
|
| 254 |
+
operations. This algorithm can be slow for large graphs, but it handles an
|
| 255 |
+
arbitrary k and works for both directed and undirected inputs.
|
| 256 |
+
|
| 257 |
+
The undirected case for k=1 is exactly connected components.
|
| 258 |
+
The undirected case for k=2 is exactly bridge connected components.
|
| 259 |
+
The directed case for k=1 is exactly strongly connected components.
|
| 260 |
+
|
| 261 |
+
References
|
| 262 |
+
----------
|
| 263 |
+
.. [1] Wang, Tianhao, et al. (2015) A simple algorithm for finding all
|
| 264 |
+
k-edge-connected components.
|
| 265 |
+
http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
|
| 266 |
+
|
| 267 |
+
Examples
|
| 268 |
+
--------
|
| 269 |
+
>>> import itertools as it
|
| 270 |
+
>>> from networkx.utils import pairwise
|
| 271 |
+
>>> from networkx.algorithms.connectivity import EdgeComponentAuxGraph
|
| 272 |
+
>>> # Build an interesting graph with multiple levels of k-edge-ccs
|
| 273 |
+
>>> paths = [
|
| 274 |
+
... (1, 2, 3, 4, 1, 3, 4, 2), # a 3-edge-cc (a 4 clique)
|
| 275 |
+
... (5, 6, 7, 5), # a 2-edge-cc (a 3 clique)
|
| 276 |
+
... (1, 5), # combine first two ccs into a 1-edge-cc
|
| 277 |
+
... (0,), # add an additional disconnected 1-edge-cc
|
| 278 |
+
... ]
|
| 279 |
+
>>> G = nx.Graph()
|
| 280 |
+
>>> G.add_nodes_from(it.chain(*paths))
|
| 281 |
+
>>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
|
| 282 |
+
>>> # Constructing the AuxGraph takes about O(n ** 4)
|
| 283 |
+
>>> aux_graph = EdgeComponentAuxGraph.construct(G)
|
| 284 |
+
>>> # Once constructed, querying takes O(n)
|
| 285 |
+
>>> sorted(map(sorted, aux_graph.k_edge_components(k=1)))
|
| 286 |
+
[[0], [1, 2, 3, 4, 5, 6, 7]]
|
| 287 |
+
>>> sorted(map(sorted, aux_graph.k_edge_components(k=2)))
|
| 288 |
+
[[0], [1, 2, 3, 4], [5, 6, 7]]
|
| 289 |
+
>>> sorted(map(sorted, aux_graph.k_edge_components(k=3)))
|
| 290 |
+
[[0], [1, 2, 3, 4], [5], [6], [7]]
|
| 291 |
+
>>> sorted(map(sorted, aux_graph.k_edge_components(k=4)))
|
| 292 |
+
[[0], [1], [2], [3], [4], [5], [6], [7]]
|
| 293 |
+
|
| 294 |
+
The auxiliary graph is primarily used for k-edge-ccs but it
|
| 295 |
+
can also speed up the queries of k-edge-subgraphs by refining the
|
| 296 |
+
search space.
|
| 297 |
+
|
| 298 |
+
>>> import itertools as it
|
| 299 |
+
>>> from networkx.utils import pairwise
|
| 300 |
+
>>> from networkx.algorithms.connectivity import EdgeComponentAuxGraph
|
| 301 |
+
>>> paths = [
|
| 302 |
+
... (1, 2, 4, 3, 1, 4),
|
| 303 |
+
... ]
|
| 304 |
+
>>> G = nx.Graph()
|
| 305 |
+
>>> G.add_nodes_from(it.chain(*paths))
|
| 306 |
+
>>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
|
| 307 |
+
>>> aux_graph = EdgeComponentAuxGraph.construct(G)
|
| 308 |
+
>>> sorted(map(sorted, aux_graph.k_edge_subgraphs(k=3)))
|
| 309 |
+
[[1], [2], [3], [4]]
|
| 310 |
+
>>> sorted(map(sorted, aux_graph.k_edge_components(k=3)))
|
| 311 |
+
[[1, 4], [2], [3]]
|
| 312 |
+
"""
|
| 313 |
+
|
| 314 |
+
# @not_implemented_for('multigraph') # TODO: fix decor for classmethods
|
| 315 |
+
@classmethod
|
| 316 |
+
def construct(EdgeComponentAuxGraph, G):
|
| 317 |
+
"""Builds an auxiliary graph encoding edge-connectivity between nodes.
|
| 318 |
+
|
| 319 |
+
Notes
|
| 320 |
+
-----
|
| 321 |
+
Given G=(V, E), initialize an empty auxiliary graph A.
|
| 322 |
+
Choose an arbitrary source node s. Initialize a set N of available
|
| 323 |
+
nodes (that can be used as the sink). The algorithm picks an
|
| 324 |
+
arbitrary node t from N - {s}, and then computes the minimum st-cut
|
| 325 |
+
(S, T) with value w. If G is directed the minimum of the st-cut or
|
| 326 |
+
the ts-cut is used instead. Then, the edge (s, t) is added to the
|
| 327 |
+
auxiliary graph with weight w. The algorithm is called recursively
|
| 328 |
+
first using S as the available nodes and s as the source, and then
|
| 329 |
+
using T and t. Recursion stops when the source is the only available
|
| 330 |
+
node.
|
| 331 |
+
|
| 332 |
+
Parameters
|
| 333 |
+
----------
|
| 334 |
+
G : NetworkX graph
|
| 335 |
+
"""
|
| 336 |
+
# workaround for classmethod decorator
|
| 337 |
+
not_implemented_for("multigraph")(lambda G: G)(G)
|
| 338 |
+
|
| 339 |
+
def _recursive_build(H, A, source, avail):
|
| 340 |
+
# Terminate once the flow has been compute to every node.
|
| 341 |
+
if {source} == avail:
|
| 342 |
+
return
|
| 343 |
+
# pick an arbitrary node as the sink
|
| 344 |
+
sink = arbitrary_element(avail - {source})
|
| 345 |
+
# find the minimum cut and its weight
|
| 346 |
+
value, (S, T) = nx.minimum_cut(H, source, sink)
|
| 347 |
+
if H.is_directed():
|
| 348 |
+
# check if the reverse direction has a smaller cut
|
| 349 |
+
value_, (T_, S_) = nx.minimum_cut(H, sink, source)
|
| 350 |
+
if value_ < value:
|
| 351 |
+
value, S, T = value_, S_, T_
|
| 352 |
+
# add edge with weight of cut to the aux graph
|
| 353 |
+
A.add_edge(source, sink, weight=value)
|
| 354 |
+
# recursively call until all but one node is used
|
| 355 |
+
_recursive_build(H, A, source, avail.intersection(S))
|
| 356 |
+
_recursive_build(H, A, sink, avail.intersection(T))
|
| 357 |
+
|
| 358 |
+
# Copy input to ensure all edges have unit capacity
|
| 359 |
+
H = G.__class__()
|
| 360 |
+
H.add_nodes_from(G.nodes())
|
| 361 |
+
H.add_edges_from(G.edges(), capacity=1)
|
| 362 |
+
|
| 363 |
+
# A is the auxiliary graph to be constructed
|
| 364 |
+
# It is a weighted undirected tree
|
| 365 |
+
A = nx.Graph()
|
| 366 |
+
|
| 367 |
+
# Pick an arbitrary node as the source
|
| 368 |
+
if H.number_of_nodes() > 0:
|
| 369 |
+
source = arbitrary_element(H.nodes())
|
| 370 |
+
# Initialize a set of elements that can be chosen as the sink
|
| 371 |
+
avail = set(H.nodes())
|
| 372 |
+
|
| 373 |
+
# This constructs A
|
| 374 |
+
_recursive_build(H, A, source, avail)
|
| 375 |
+
|
| 376 |
+
# This class is a container the holds the auxiliary graph A and
|
| 377 |
+
# provides access the k_edge_components function.
|
| 378 |
+
self = EdgeComponentAuxGraph()
|
| 379 |
+
self.A = A
|
| 380 |
+
self.H = H
|
| 381 |
+
return self
|
| 382 |
+
|
| 383 |
+
def k_edge_components(self, k):
|
| 384 |
+
"""Queries the auxiliary graph for k-edge-connected components.
|
| 385 |
+
|
| 386 |
+
Parameters
|
| 387 |
+
----------
|
| 388 |
+
k : Integer
|
| 389 |
+
Desired edge connectivity
|
| 390 |
+
|
| 391 |
+
Returns
|
| 392 |
+
-------
|
| 393 |
+
k_edge_components : a generator of k-edge-ccs
|
| 394 |
+
|
| 395 |
+
Notes
|
| 396 |
+
-----
|
| 397 |
+
Given the auxiliary graph, the k-edge-connected components can be
|
| 398 |
+
determined in linear time by removing all edges with weights less than
|
| 399 |
+
k from the auxiliary graph. The resulting connected components are the
|
| 400 |
+
k-edge-ccs in the original graph.
|
| 401 |
+
"""
|
| 402 |
+
if k < 1:
|
| 403 |
+
raise ValueError("k cannot be less than 1")
|
| 404 |
+
A = self.A
|
| 405 |
+
# "traverse the auxiliary graph A and delete all edges with weights less
|
| 406 |
+
# than k"
|
| 407 |
+
aux_weights = nx.get_edge_attributes(A, "weight")
|
| 408 |
+
# Create a relevant graph with the auxiliary edges with weights >= k
|
| 409 |
+
R = nx.Graph()
|
| 410 |
+
R.add_nodes_from(A.nodes())
|
| 411 |
+
R.add_edges_from(e for e, w in aux_weights.items() if w >= k)
|
| 412 |
+
|
| 413 |
+
# Return the nodes that are k-edge-connected in the original graph
|
| 414 |
+
yield from nx.connected_components(R)
|
| 415 |
+
|
| 416 |
+
def k_edge_subgraphs(self, k):
|
| 417 |
+
"""Queries the auxiliary graph for k-edge-connected subgraphs.
|
| 418 |
+
|
| 419 |
+
Parameters
|
| 420 |
+
----------
|
| 421 |
+
k : Integer
|
| 422 |
+
Desired edge connectivity
|
| 423 |
+
|
| 424 |
+
Returns
|
| 425 |
+
-------
|
| 426 |
+
k_edge_subgraphs : a generator of k-edge-subgraphs
|
| 427 |
+
|
| 428 |
+
Notes
|
| 429 |
+
-----
|
| 430 |
+
Refines the k-edge-ccs into k-edge-subgraphs. The running time is more
|
| 431 |
+
than $O(|V|)$.
|
| 432 |
+
|
| 433 |
+
For single values of k it is faster to use `nx.k_edge_subgraphs`.
|
| 434 |
+
But for multiple values of k, it can be faster to build AuxGraph and
|
| 435 |
+
then use this method.
|
| 436 |
+
"""
|
| 437 |
+
if k < 1:
|
| 438 |
+
raise ValueError("k cannot be less than 1")
|
| 439 |
+
H = self.H
|
| 440 |
+
A = self.A
|
| 441 |
+
# "traverse the auxiliary graph A and delete all edges with weights less
|
| 442 |
+
# than k"
|
| 443 |
+
aux_weights = nx.get_edge_attributes(A, "weight")
|
| 444 |
+
# Create a relevant graph with the auxiliary edges with weights >= k
|
| 445 |
+
R = nx.Graph()
|
| 446 |
+
R.add_nodes_from(A.nodes())
|
| 447 |
+
R.add_edges_from(e for e, w in aux_weights.items() if w >= k)
|
| 448 |
+
|
| 449 |
+
# Return the components whose subgraphs are k-edge-connected
|
| 450 |
+
for cc in nx.connected_components(R):
|
| 451 |
+
if len(cc) < k:
|
| 452 |
+
# Early return optimization
|
| 453 |
+
for node in cc:
|
| 454 |
+
yield {node}
|
| 455 |
+
else:
|
| 456 |
+
# Call subgraph solution to refine the results
|
| 457 |
+
C = H.subgraph(cc)
|
| 458 |
+
yield from k_edge_subgraphs(C, k)
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def _low_degree_nodes(G, k, nbunch=None):
|
| 462 |
+
"""Helper for finding nodes with degree less than k."""
|
| 463 |
+
# Nodes with degree less than k cannot be k-edge-connected.
|
| 464 |
+
if G.is_directed():
|
| 465 |
+
# Consider both in and out degree in the directed case
|
| 466 |
+
seen = set()
|
| 467 |
+
for node, degree in G.out_degree(nbunch):
|
| 468 |
+
if degree < k:
|
| 469 |
+
seen.add(node)
|
| 470 |
+
yield node
|
| 471 |
+
for node, degree in G.in_degree(nbunch):
|
| 472 |
+
if node not in seen and degree < k:
|
| 473 |
+
seen.add(node)
|
| 474 |
+
yield node
|
| 475 |
+
else:
|
| 476 |
+
# Only the degree matters in the undirected case
|
| 477 |
+
for node, degree in G.degree(nbunch):
|
| 478 |
+
if degree < k:
|
| 479 |
+
yield node
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def _high_degree_components(G, k):
|
| 483 |
+
"""Helper for filtering components that can't be k-edge-connected.
|
| 484 |
+
|
| 485 |
+
Removes and generates each node with degree less than k. Then generates
|
| 486 |
+
remaining components where all nodes have degree at least k.
|
| 487 |
+
"""
|
| 488 |
+
# Iteratively remove parts of the graph that are not k-edge-connected
|
| 489 |
+
H = G.copy()
|
| 490 |
+
singletons = set(_low_degree_nodes(H, k))
|
| 491 |
+
while singletons:
|
| 492 |
+
# Only search neighbors of removed nodes
|
| 493 |
+
nbunch = set(it.chain.from_iterable(map(H.neighbors, singletons)))
|
| 494 |
+
nbunch.difference_update(singletons)
|
| 495 |
+
H.remove_nodes_from(singletons)
|
| 496 |
+
for node in singletons:
|
| 497 |
+
yield {node}
|
| 498 |
+
singletons = set(_low_degree_nodes(H, k, nbunch))
|
| 499 |
+
|
| 500 |
+
# Note: remaining connected components may not be k-edge-connected
|
| 501 |
+
if G.is_directed():
|
| 502 |
+
yield from nx.strongly_connected_components(H)
|
| 503 |
+
else:
|
| 504 |
+
yield from nx.connected_components(H)
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
@nx._dispatchable(returns_graph=True)
|
| 508 |
+
def general_k_edge_subgraphs(G, k):
|
| 509 |
+
"""General algorithm to find all maximal k-edge-connected subgraphs in `G`.
|
| 510 |
+
|
| 511 |
+
Parameters
|
| 512 |
+
----------
|
| 513 |
+
G : nx.Graph
|
| 514 |
+
Graph in which all maximal k-edge-connected subgraphs will be found.
|
| 515 |
+
|
| 516 |
+
k : int
|
| 517 |
+
|
| 518 |
+
Yields
|
| 519 |
+
------
|
| 520 |
+
k_edge_subgraphs : Graph instances that are k-edge-subgraphs
|
| 521 |
+
Each k-edge-subgraph contains a maximal set of nodes that defines a
|
| 522 |
+
subgraph of `G` that is k-edge-connected.
|
| 523 |
+
|
| 524 |
+
Notes
|
| 525 |
+
-----
|
| 526 |
+
Implementation of the basic algorithm from [1]_. The basic idea is to find
|
| 527 |
+
a global minimum cut of the graph. If the cut value is at least k, then the
|
| 528 |
+
graph is a k-edge-connected subgraph and can be added to the results.
|
| 529 |
+
Otherwise, the cut is used to split the graph in two and the procedure is
|
| 530 |
+
applied recursively. If the graph is just a single node, then it is also
|
| 531 |
+
added to the results. At the end, each result is either guaranteed to be
|
| 532 |
+
a single node or a subgraph of G that is k-edge-connected.
|
| 533 |
+
|
| 534 |
+
This implementation contains optimizations for reducing the number of calls
|
| 535 |
+
to max-flow, but there are other optimizations in [1]_ that could be
|
| 536 |
+
implemented.
|
| 537 |
+
|
| 538 |
+
References
|
| 539 |
+
----------
|
| 540 |
+
.. [1] Zhou, Liu, et al. (2012) Finding maximal k-edge-connected subgraphs
|
| 541 |
+
from a large graph. ACM International Conference on Extending Database
|
| 542 |
+
Technology 2012 480-–491.
|
| 543 |
+
https://openproceedings.org/2012/conf/edbt/ZhouLYLCL12.pdf
|
| 544 |
+
|
| 545 |
+
Examples
|
| 546 |
+
--------
|
| 547 |
+
>>> from networkx.utils import pairwise
|
| 548 |
+
>>> paths = [
|
| 549 |
+
... (11, 12, 13, 14, 11, 13, 14, 12), # a 4-clique
|
| 550 |
+
... (21, 22, 23, 24, 21, 23, 24, 22), # another 4-clique
|
| 551 |
+
... # connect the cliques with high degree but low connectivity
|
| 552 |
+
... (50, 13),
|
| 553 |
+
... (12, 50, 22),
|
| 554 |
+
... (13, 102, 23),
|
| 555 |
+
... (14, 101, 24),
|
| 556 |
+
... ]
|
| 557 |
+
>>> G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
|
| 558 |
+
>>> sorted(len(k_sg) for k_sg in k_edge_subgraphs(G, k=3))
|
| 559 |
+
[1, 1, 1, 4, 4]
|
| 560 |
+
"""
|
| 561 |
+
if k < 1:
|
| 562 |
+
raise ValueError("k cannot be less than 1")
|
| 563 |
+
|
| 564 |
+
# Node pruning optimization (incorporates early return)
|
| 565 |
+
# find_ccs is either connected_components/strongly_connected_components
|
| 566 |
+
find_ccs = partial(_high_degree_components, k=k)
|
| 567 |
+
|
| 568 |
+
# Quick return optimization
|
| 569 |
+
if G.number_of_nodes() < k:
|
| 570 |
+
for node in G.nodes():
|
| 571 |
+
yield G.subgraph([node]).copy()
|
| 572 |
+
return
|
| 573 |
+
|
| 574 |
+
# Intermediate results
|
| 575 |
+
R0 = {G.subgraph(cc).copy() for cc in find_ccs(G)}
|
| 576 |
+
# Subdivide CCs in the intermediate results until they are k-conn
|
| 577 |
+
while R0:
|
| 578 |
+
G1 = R0.pop()
|
| 579 |
+
if G1.number_of_nodes() == 1:
|
| 580 |
+
yield G1
|
| 581 |
+
else:
|
| 582 |
+
# Find a global minimum cut
|
| 583 |
+
cut_edges = nx.minimum_edge_cut(G1)
|
| 584 |
+
cut_value = len(cut_edges)
|
| 585 |
+
if cut_value < k:
|
| 586 |
+
# G1 is not k-edge-connected, so subdivide it
|
| 587 |
+
G1.remove_edges_from(cut_edges)
|
| 588 |
+
for cc in find_ccs(G1):
|
| 589 |
+
R0.add(G1.subgraph(cc).copy())
|
| 590 |
+
else:
|
| 591 |
+
# Otherwise we found a k-edge-connected subgraph
|
| 592 |
+
yield G1
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/kcomponents.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Moody and White algorithm for k-components
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
from itertools import combinations
|
| 7 |
+
from operator import itemgetter
|
| 8 |
+
|
| 9 |
+
import networkx as nx
|
| 10 |
+
|
| 11 |
+
# Define the default maximum flow function.
|
| 12 |
+
from networkx.algorithms.flow import edmonds_karp
|
| 13 |
+
from networkx.utils import not_implemented_for
|
| 14 |
+
|
| 15 |
+
default_flow_func = edmonds_karp
|
| 16 |
+
|
| 17 |
+
__all__ = ["k_components"]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@not_implemented_for("directed")
|
| 21 |
+
@nx._dispatchable
|
| 22 |
+
def k_components(G, flow_func=None):
|
| 23 |
+
r"""Returns the k-component structure of a graph G.
|
| 24 |
+
|
| 25 |
+
A `k`-component is a maximal subgraph of a graph G that has, at least,
|
| 26 |
+
node connectivity `k`: we need to remove at least `k` nodes to break it
|
| 27 |
+
into more components. `k`-components have an inherent hierarchical
|
| 28 |
+
structure because they are nested in terms of connectivity: a connected
|
| 29 |
+
graph can contain several 2-components, each of which can contain
|
| 30 |
+
one or more 3-components, and so forth.
|
| 31 |
+
|
| 32 |
+
Parameters
|
| 33 |
+
----------
|
| 34 |
+
G : NetworkX graph
|
| 35 |
+
|
| 36 |
+
flow_func : function
|
| 37 |
+
Function to perform the underlying flow computations. Default value
|
| 38 |
+
:meth:`edmonds_karp`. This function performs better in sparse graphs with
|
| 39 |
+
right tailed degree distributions. :meth:`shortest_augmenting_path` will
|
| 40 |
+
perform better in denser graphs.
|
| 41 |
+
|
| 42 |
+
Returns
|
| 43 |
+
-------
|
| 44 |
+
k_components : dict
|
| 45 |
+
Dictionary with all connectivity levels `k` in the input Graph as keys
|
| 46 |
+
and a list of sets of nodes that form a k-component of level `k` as
|
| 47 |
+
values.
|
| 48 |
+
|
| 49 |
+
Raises
|
| 50 |
+
------
|
| 51 |
+
NetworkXNotImplemented
|
| 52 |
+
If the input graph is directed.
|
| 53 |
+
|
| 54 |
+
Examples
|
| 55 |
+
--------
|
| 56 |
+
>>> # Petersen graph has 10 nodes and it is triconnected, thus all
|
| 57 |
+
>>> # nodes are in a single component on all three connectivity levels
|
| 58 |
+
>>> G = nx.petersen_graph()
|
| 59 |
+
>>> k_components = nx.k_components(G)
|
| 60 |
+
|
| 61 |
+
Notes
|
| 62 |
+
-----
|
| 63 |
+
Moody and White [1]_ (appendix A) provide an algorithm for identifying
|
| 64 |
+
k-components in a graph, which is based on Kanevsky's algorithm [2]_
|
| 65 |
+
for finding all minimum-size node cut-sets of a graph (implemented in
|
| 66 |
+
:meth:`all_node_cuts` function):
|
| 67 |
+
|
| 68 |
+
1. Compute node connectivity, k, of the input graph G.
|
| 69 |
+
|
| 70 |
+
2. Identify all k-cutsets at the current level of connectivity using
|
| 71 |
+
Kanevsky's algorithm.
|
| 72 |
+
|
| 73 |
+
3. Generate new graph components based on the removal of
|
| 74 |
+
these cutsets. Nodes in a cutset belong to both sides
|
| 75 |
+
of the induced cut.
|
| 76 |
+
|
| 77 |
+
4. If the graph is neither complete nor trivial, return to 1;
|
| 78 |
+
else end.
|
| 79 |
+
|
| 80 |
+
This implementation also uses some heuristics (see [3]_ for details)
|
| 81 |
+
to speed up the computation.
|
| 82 |
+
|
| 83 |
+
See also
|
| 84 |
+
--------
|
| 85 |
+
node_connectivity
|
| 86 |
+
all_node_cuts
|
| 87 |
+
biconnected_components : special case of this function when k=2
|
| 88 |
+
k_edge_components : similar to this function, but uses edge-connectivity
|
| 89 |
+
instead of node-connectivity
|
| 90 |
+
|
| 91 |
+
References
|
| 92 |
+
----------
|
| 93 |
+
.. [1] Moody, J. and D. White (2003). Social cohesion and embeddedness:
|
| 94 |
+
A hierarchical conception of social groups.
|
| 95 |
+
American Sociological Review 68(1), 103--28.
|
| 96 |
+
http://www2.asanet.org/journals/ASRFeb03MoodyWhite.pdf
|
| 97 |
+
|
| 98 |
+
.. [2] Kanevsky, A. (1993). Finding all minimum-size separating vertex
|
| 99 |
+
sets in a graph. Networks 23(6), 533--541.
|
| 100 |
+
http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract
|
| 101 |
+
|
| 102 |
+
.. [3] Torrents, J. and F. Ferraro (2015). Structural Cohesion:
|
| 103 |
+
Visualization and Heuristics for Fast Computation.
|
| 104 |
+
https://arxiv.org/pdf/1503.04476v1
|
| 105 |
+
|
| 106 |
+
"""
|
| 107 |
+
# Dictionary with connectivity level (k) as keys and a list of
|
| 108 |
+
# sets of nodes that form a k-component as values. Note that
|
| 109 |
+
# k-components can overlap (but only k - 1 nodes).
|
| 110 |
+
k_components = defaultdict(list)
|
| 111 |
+
# Define default flow function
|
| 112 |
+
if flow_func is None:
|
| 113 |
+
flow_func = default_flow_func
|
| 114 |
+
# Bicomponents as a base to check for higher order k-components
|
| 115 |
+
for component in nx.connected_components(G):
|
| 116 |
+
# isolated nodes have connectivity 0
|
| 117 |
+
comp = set(component)
|
| 118 |
+
if len(comp) > 1:
|
| 119 |
+
k_components[1].append(comp)
|
| 120 |
+
bicomponents = [G.subgraph(c) for c in nx.biconnected_components(G)]
|
| 121 |
+
for bicomponent in bicomponents:
|
| 122 |
+
bicomp = set(bicomponent)
|
| 123 |
+
# avoid considering dyads as bicomponents
|
| 124 |
+
if len(bicomp) > 2:
|
| 125 |
+
k_components[2].append(bicomp)
|
| 126 |
+
for B in bicomponents:
|
| 127 |
+
if len(B) <= 2:
|
| 128 |
+
continue
|
| 129 |
+
k = nx.node_connectivity(B, flow_func=flow_func)
|
| 130 |
+
if k > 2:
|
| 131 |
+
k_components[k].append(set(B))
|
| 132 |
+
# Perform cuts in a DFS like order.
|
| 133 |
+
cuts = list(nx.all_node_cuts(B, k=k, flow_func=flow_func))
|
| 134 |
+
stack = [(k, _generate_partition(B, cuts, k))]
|
| 135 |
+
while stack:
|
| 136 |
+
(parent_k, partition) = stack[-1]
|
| 137 |
+
try:
|
| 138 |
+
nodes = next(partition)
|
| 139 |
+
C = B.subgraph(nodes)
|
| 140 |
+
this_k = nx.node_connectivity(C, flow_func=flow_func)
|
| 141 |
+
if this_k > parent_k and this_k > 2:
|
| 142 |
+
k_components[this_k].append(set(C))
|
| 143 |
+
cuts = list(nx.all_node_cuts(C, k=this_k, flow_func=flow_func))
|
| 144 |
+
if cuts:
|
| 145 |
+
stack.append((this_k, _generate_partition(C, cuts, this_k)))
|
| 146 |
+
except StopIteration:
|
| 147 |
+
stack.pop()
|
| 148 |
+
|
| 149 |
+
# This is necessary because k-components may only be reported at their
|
| 150 |
+
# maximum k level. But we want to return a dictionary in which keys are
|
| 151 |
+
# connectivity levels and values list of sets of components, without
|
| 152 |
+
# skipping any connectivity level. Also, it's possible that subsets of
|
| 153 |
+
# an already detected k-component appear at a level k. Checking for this
|
| 154 |
+
# in the while loop above penalizes the common case. Thus we also have to
|
| 155 |
+
# _consolidate all connectivity levels in _reconstruct_k_components.
|
| 156 |
+
return _reconstruct_k_components(k_components)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _consolidate(sets, k):
|
| 160 |
+
"""Merge sets that share k or more elements.
|
| 161 |
+
|
| 162 |
+
See: http://rosettacode.org/wiki/Set_consolidation
|
| 163 |
+
|
| 164 |
+
The iterative python implementation posted there is
|
| 165 |
+
faster than this because of the overhead of building a
|
| 166 |
+
Graph and calling nx.connected_components, but it's not
|
| 167 |
+
clear for us if we can use it in NetworkX because there
|
| 168 |
+
is no licence for the code.
|
| 169 |
+
|
| 170 |
+
"""
|
| 171 |
+
G = nx.Graph()
|
| 172 |
+
nodes = dict(enumerate(sets))
|
| 173 |
+
G.add_nodes_from(nodes)
|
| 174 |
+
G.add_edges_from(
|
| 175 |
+
(u, v) for u, v in combinations(nodes, 2) if len(nodes[u] & nodes[v]) >= k
|
| 176 |
+
)
|
| 177 |
+
for component in nx.connected_components(G):
|
| 178 |
+
yield set.union(*[nodes[n] for n in component])
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _generate_partition(G, cuts, k):
|
| 182 |
+
def has_nbrs_in_partition(G, node, partition):
|
| 183 |
+
return any(n in partition for n in G[node])
|
| 184 |
+
|
| 185 |
+
components = []
|
| 186 |
+
nodes = {n for n, d in G.degree() if d > k} - {n for cut in cuts for n in cut}
|
| 187 |
+
H = G.subgraph(nodes)
|
| 188 |
+
for cc in nx.connected_components(H):
|
| 189 |
+
component = set(cc)
|
| 190 |
+
for cut in cuts:
|
| 191 |
+
for node in cut:
|
| 192 |
+
if has_nbrs_in_partition(G, node, cc):
|
| 193 |
+
component.add(node)
|
| 194 |
+
if len(component) < G.order():
|
| 195 |
+
components.append(component)
|
| 196 |
+
yield from _consolidate(components, k + 1)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def _reconstruct_k_components(k_comps):
|
| 200 |
+
result = {}
|
| 201 |
+
max_k = max(k_comps)
|
| 202 |
+
for k in reversed(range(1, max_k + 1)):
|
| 203 |
+
if k == max_k:
|
| 204 |
+
result[k] = list(_consolidate(k_comps[k], k))
|
| 205 |
+
elif k not in k_comps:
|
| 206 |
+
result[k] = list(_consolidate(result[k + 1], k))
|
| 207 |
+
else:
|
| 208 |
+
nodes_at_k = set.union(*k_comps[k])
|
| 209 |
+
to_add = [c for c in result[k + 1] if any(n not in nodes_at_k for n in c)]
|
| 210 |
+
if to_add:
|
| 211 |
+
result[k] = list(_consolidate(k_comps[k] + to_add, k))
|
| 212 |
+
else:
|
| 213 |
+
result[k] = list(_consolidate(k_comps[k], k))
|
| 214 |
+
return result
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def build_k_number_dict(kcomps):
|
| 218 |
+
result = {}
|
| 219 |
+
for k, comps in sorted(kcomps.items(), key=itemgetter(0)):
|
| 220 |
+
for comp in comps:
|
| 221 |
+
for node in comp:
|
| 222 |
+
result[node] = k
|
| 223 |
+
return result
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/kcutsets.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kanevsky all minimum node k cutsets algorithm.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import copy
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
from itertools import combinations
|
| 8 |
+
from operator import itemgetter
|
| 9 |
+
|
| 10 |
+
import networkx as nx
|
| 11 |
+
from networkx.algorithms.flow import (
|
| 12 |
+
build_residual_network,
|
| 13 |
+
edmonds_karp,
|
| 14 |
+
shortest_augmenting_path,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
from .utils import build_auxiliary_node_connectivity
|
| 18 |
+
|
| 19 |
+
default_flow_func = edmonds_karp
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
__all__ = ["all_node_cuts"]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@nx._dispatchable
|
| 26 |
+
def all_node_cuts(G, k=None, flow_func=None):
|
| 27 |
+
r"""Returns all minimum k cutsets of an undirected graph G.
|
| 28 |
+
|
| 29 |
+
This implementation is based on Kanevsky's algorithm [1]_ for finding all
|
| 30 |
+
minimum-size node cut-sets of an undirected graph G; ie the set (or sets)
|
| 31 |
+
of nodes of cardinality equal to the node connectivity of G. Thus if
|
| 32 |
+
removed, would break G into two or more connected components.
|
| 33 |
+
|
| 34 |
+
Parameters
|
| 35 |
+
----------
|
| 36 |
+
G : NetworkX graph
|
| 37 |
+
Undirected graph
|
| 38 |
+
|
| 39 |
+
k : Integer
|
| 40 |
+
Node connectivity of the input graph. If k is None, then it is
|
| 41 |
+
computed. Default value: None.
|
| 42 |
+
|
| 43 |
+
flow_func : function
|
| 44 |
+
Function to perform the underlying flow computations. Default value is
|
| 45 |
+
:func:`~networkx.algorithms.flow.edmonds_karp`. This function performs
|
| 46 |
+
better in sparse graphs with right tailed degree distributions.
|
| 47 |
+
:func:`~networkx.algorithms.flow.shortest_augmenting_path` will
|
| 48 |
+
perform better in denser graphs.
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
Returns
|
| 52 |
+
-------
|
| 53 |
+
cuts : a generator of node cutsets
|
| 54 |
+
Each node cutset has cardinality equal to the node connectivity of
|
| 55 |
+
the input graph.
|
| 56 |
+
|
| 57 |
+
Examples
|
| 58 |
+
--------
|
| 59 |
+
>>> # A two-dimensional grid graph has 4 cutsets of cardinality 2
|
| 60 |
+
>>> G = nx.grid_2d_graph(5, 5)
|
| 61 |
+
>>> cutsets = list(nx.all_node_cuts(G))
|
| 62 |
+
>>> len(cutsets)
|
| 63 |
+
4
|
| 64 |
+
>>> all(2 == len(cutset) for cutset in cutsets)
|
| 65 |
+
True
|
| 66 |
+
>>> nx.node_connectivity(G)
|
| 67 |
+
2
|
| 68 |
+
|
| 69 |
+
Notes
|
| 70 |
+
-----
|
| 71 |
+
This implementation is based on the sequential algorithm for finding all
|
| 72 |
+
minimum-size separating vertex sets in a graph [1]_. The main idea is to
|
| 73 |
+
compute minimum cuts using local maximum flow computations among a set
|
| 74 |
+
of nodes of highest degree and all other non-adjacent nodes in the Graph.
|
| 75 |
+
Once we find a minimum cut, we add an edge between the high degree
|
| 76 |
+
node and the target node of the local maximum flow computation to make
|
| 77 |
+
sure that we will not find that minimum cut again.
|
| 78 |
+
|
| 79 |
+
See also
|
| 80 |
+
--------
|
| 81 |
+
node_connectivity
|
| 82 |
+
edmonds_karp
|
| 83 |
+
shortest_augmenting_path
|
| 84 |
+
|
| 85 |
+
References
|
| 86 |
+
----------
|
| 87 |
+
.. [1] Kanevsky, A. (1993). Finding all minimum-size separating vertex
|
| 88 |
+
sets in a graph. Networks 23(6), 533--541.
|
| 89 |
+
http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract
|
| 90 |
+
|
| 91 |
+
"""
|
| 92 |
+
if not nx.is_connected(G):
|
| 93 |
+
raise nx.NetworkXError("Input graph is disconnected.")
|
| 94 |
+
|
| 95 |
+
# Address some corner cases first.
|
| 96 |
+
# For complete Graphs
|
| 97 |
+
|
| 98 |
+
if nx.density(G) == 1:
|
| 99 |
+
yield from ()
|
| 100 |
+
return
|
| 101 |
+
|
| 102 |
+
# Initialize data structures.
|
| 103 |
+
# Keep track of the cuts already computed so we do not repeat them.
|
| 104 |
+
seen = []
|
| 105 |
+
# Even-Tarjan reduction is what we call auxiliary digraph
|
| 106 |
+
# for node connectivity.
|
| 107 |
+
H = build_auxiliary_node_connectivity(G)
|
| 108 |
+
H_nodes = H.nodes # for speed
|
| 109 |
+
mapping = H.graph["mapping"]
|
| 110 |
+
# Keep a copy of original predecessors, H will be modified later.
|
| 111 |
+
# Shallow copy is enough.
|
| 112 |
+
original_H_pred = copy.copy(H._pred)
|
| 113 |
+
R = build_residual_network(H, "capacity")
|
| 114 |
+
kwargs = {"capacity": "capacity", "residual": R}
|
| 115 |
+
# Define default flow function
|
| 116 |
+
if flow_func is None:
|
| 117 |
+
flow_func = default_flow_func
|
| 118 |
+
if flow_func is shortest_augmenting_path:
|
| 119 |
+
kwargs["two_phase"] = True
|
| 120 |
+
# Begin the actual algorithm
|
| 121 |
+
# step 1: Find node connectivity k of G
|
| 122 |
+
if k is None:
|
| 123 |
+
k = nx.node_connectivity(G, flow_func=flow_func)
|
| 124 |
+
# step 2:
|
| 125 |
+
# Find k nodes with top degree, call it X:
|
| 126 |
+
X = {n for n, d in sorted(G.degree(), key=itemgetter(1), reverse=True)[:k]}
|
| 127 |
+
# Check if X is a k-node-cutset
|
| 128 |
+
if _is_separating_set(G, X):
|
| 129 |
+
seen.append(X)
|
| 130 |
+
yield X
|
| 131 |
+
|
| 132 |
+
for x in X:
|
| 133 |
+
# step 3: Compute local connectivity flow of x with all other
|
| 134 |
+
# non adjacent nodes in G
|
| 135 |
+
non_adjacent = set(G) - {x} - set(G[x])
|
| 136 |
+
for v in non_adjacent:
|
| 137 |
+
# step 4: compute maximum flow in an Even-Tarjan reduction H of G
|
| 138 |
+
# and step 5: build the associated residual network R
|
| 139 |
+
R = flow_func(H, f"{mapping[x]}B", f"{mapping[v]}A", **kwargs)
|
| 140 |
+
flow_value = R.graph["flow_value"]
|
| 141 |
+
|
| 142 |
+
if flow_value == k:
|
| 143 |
+
# Find the nodes incident to the flow.
|
| 144 |
+
E1 = flowed_edges = [
|
| 145 |
+
(u, w) for (u, w, d) in R.edges(data=True) if d["flow"] != 0
|
| 146 |
+
]
|
| 147 |
+
VE1 = incident_nodes = {n for edge in E1 for n in edge}
|
| 148 |
+
# Remove saturated edges form the residual network.
|
| 149 |
+
# Note that reversed edges are introduced with capacity 0
|
| 150 |
+
# in the residual graph and they need to be removed too.
|
| 151 |
+
saturated_edges = [
|
| 152 |
+
(u, w, d)
|
| 153 |
+
for (u, w, d) in R.edges(data=True)
|
| 154 |
+
if d["capacity"] == d["flow"] or d["capacity"] == 0
|
| 155 |
+
]
|
| 156 |
+
R.remove_edges_from(saturated_edges)
|
| 157 |
+
R_closure = nx.transitive_closure(R)
|
| 158 |
+
# step 6: shrink the strongly connected components of
|
| 159 |
+
# residual flow network R and call it L.
|
| 160 |
+
L = nx.condensation(R)
|
| 161 |
+
cmap = L.graph["mapping"]
|
| 162 |
+
inv_cmap = defaultdict(list)
|
| 163 |
+
for n, scc in cmap.items():
|
| 164 |
+
inv_cmap[scc].append(n)
|
| 165 |
+
# Find the incident nodes in the condensed graph.
|
| 166 |
+
VE1 = {cmap[n] for n in VE1}
|
| 167 |
+
# step 7: Compute all antichains of L;
|
| 168 |
+
# they map to closed sets in H.
|
| 169 |
+
# Any edge in H that links a closed set is part of a cutset.
|
| 170 |
+
for antichain in nx.antichains(L):
|
| 171 |
+
# Only antichains that are subsets of incident nodes counts.
|
| 172 |
+
# Lemma 8 in reference.
|
| 173 |
+
if not set(antichain).issubset(VE1):
|
| 174 |
+
continue
|
| 175 |
+
# Nodes in an antichain of the condensation graph of
|
| 176 |
+
# the residual network map to a closed set of nodes that
|
| 177 |
+
# define a node partition of the auxiliary digraph H
|
| 178 |
+
# through taking all of antichain's predecessors in the
|
| 179 |
+
# transitive closure.
|
| 180 |
+
S = set()
|
| 181 |
+
for scc in antichain:
|
| 182 |
+
S.update(inv_cmap[scc])
|
| 183 |
+
S_ancestors = set()
|
| 184 |
+
for n in S:
|
| 185 |
+
S_ancestors.update(R_closure._pred[n])
|
| 186 |
+
S.update(S_ancestors)
|
| 187 |
+
if f"{mapping[x]}B" not in S or f"{mapping[v]}A" in S:
|
| 188 |
+
continue
|
| 189 |
+
# Find the cutset that links the node partition (S,~S) in H
|
| 190 |
+
cutset = set()
|
| 191 |
+
for u in S:
|
| 192 |
+
cutset.update((u, w) for w in original_H_pred[u] if w not in S)
|
| 193 |
+
# The edges in H that form the cutset are internal edges
|
| 194 |
+
# (ie edges that represent a node of the original graph G)
|
| 195 |
+
if any(H_nodes[u]["id"] != H_nodes[w]["id"] for u, w in cutset):
|
| 196 |
+
continue
|
| 197 |
+
node_cut = {H_nodes[u]["id"] for u, _ in cutset}
|
| 198 |
+
|
| 199 |
+
if len(node_cut) == k:
|
| 200 |
+
# The cut is invalid if it includes internal edges of
|
| 201 |
+
# end nodes. The other half of Lemma 8 in ref.
|
| 202 |
+
if x in node_cut or v in node_cut:
|
| 203 |
+
continue
|
| 204 |
+
if node_cut not in seen:
|
| 205 |
+
yield node_cut
|
| 206 |
+
seen.append(node_cut)
|
| 207 |
+
|
| 208 |
+
# Add an edge (x, v) to make sure that we do not
|
| 209 |
+
# find this cutset again. This is equivalent
|
| 210 |
+
# of adding the edge in the input graph
|
| 211 |
+
# G.add_edge(x, v) and then regenerate H and R:
|
| 212 |
+
# Add edges to the auxiliary digraph.
|
| 213 |
+
# See build_residual_network for convention we used
|
| 214 |
+
# in residual graphs.
|
| 215 |
+
H.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1)
|
| 216 |
+
H.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1)
|
| 217 |
+
# Add edges to the residual network.
|
| 218 |
+
R.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1)
|
| 219 |
+
R.add_edge(f"{mapping[v]}A", f"{mapping[x]}B", capacity=0)
|
| 220 |
+
R.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1)
|
| 221 |
+
R.add_edge(f"{mapping[x]}A", f"{mapping[v]}B", capacity=0)
|
| 222 |
+
|
| 223 |
+
# Add again the saturated edges to reuse the residual network
|
| 224 |
+
R.add_edges_from(saturated_edges)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def _is_separating_set(G, cut):
|
| 228 |
+
"""Assumes that the input graph is connected"""
|
| 229 |
+
if len(cut) == len(G) - 1:
|
| 230 |
+
return True
|
| 231 |
+
|
| 232 |
+
H = nx.restricted_view(G, cut, [])
|
| 233 |
+
if nx.is_connected(H):
|
| 234 |
+
return False
|
| 235 |
+
return True
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/stoerwagner.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stoer-Wagner minimum cut algorithm.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from itertools import islice
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
|
| 9 |
+
from ...utils import BinaryHeap, arbitrary_element, not_implemented_for
|
| 10 |
+
|
| 11 |
+
__all__ = ["stoer_wagner"]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@not_implemented_for("directed")
|
| 15 |
+
@not_implemented_for("multigraph")
|
| 16 |
+
@nx._dispatchable(edge_attrs="weight")
|
| 17 |
+
def stoer_wagner(G, weight="weight", heap=BinaryHeap):
|
| 18 |
+
r"""Returns the weighted minimum edge cut using the Stoer-Wagner algorithm.
|
| 19 |
+
|
| 20 |
+
Determine the minimum edge cut of a connected graph using the
|
| 21 |
+
Stoer-Wagner algorithm. In weighted cases, all weights must be
|
| 22 |
+
nonnegative.
|
| 23 |
+
|
| 24 |
+
The running time of the algorithm depends on the type of heaps used:
|
| 25 |
+
|
| 26 |
+
============== =============================================
|
| 27 |
+
Type of heap Running time
|
| 28 |
+
============== =============================================
|
| 29 |
+
Binary heap $O(n (m + n) \log n)$
|
| 30 |
+
Fibonacci heap $O(nm + n^2 \log n)$
|
| 31 |
+
Pairing heap $O(2^{2 \sqrt{\log \log n}} nm + n^2 \log n)$
|
| 32 |
+
============== =============================================
|
| 33 |
+
|
| 34 |
+
Parameters
|
| 35 |
+
----------
|
| 36 |
+
G : NetworkX graph
|
| 37 |
+
Edges of the graph are expected to have an attribute named by the
|
| 38 |
+
weight parameter below. If this attribute is not present, the edge is
|
| 39 |
+
considered to have unit weight.
|
| 40 |
+
|
| 41 |
+
weight : string
|
| 42 |
+
Name of the weight attribute of the edges. If the attribute is not
|
| 43 |
+
present, unit weight is assumed. Default value: 'weight'.
|
| 44 |
+
|
| 45 |
+
heap : class
|
| 46 |
+
Type of heap to be used in the algorithm. It should be a subclass of
|
| 47 |
+
:class:`MinHeap` or implement a compatible interface.
|
| 48 |
+
|
| 49 |
+
If a stock heap implementation is to be used, :class:`BinaryHeap` is
|
| 50 |
+
recommended over :class:`PairingHeap` for Python implementations without
|
| 51 |
+
optimized attribute accesses (e.g., CPython) despite a slower
|
| 52 |
+
asymptotic running time. For Python implementations with optimized
|
| 53 |
+
attribute accesses (e.g., PyPy), :class:`PairingHeap` provides better
|
| 54 |
+
performance. Default value: :class:`BinaryHeap`.
|
| 55 |
+
|
| 56 |
+
Returns
|
| 57 |
+
-------
|
| 58 |
+
cut_value : integer or float
|
| 59 |
+
The sum of weights of edges in a minimum cut.
|
| 60 |
+
|
| 61 |
+
partition : pair of node lists
|
| 62 |
+
A partitioning of the nodes that defines a minimum cut.
|
| 63 |
+
|
| 64 |
+
Raises
|
| 65 |
+
------
|
| 66 |
+
NetworkXNotImplemented
|
| 67 |
+
If the graph is directed or a multigraph.
|
| 68 |
+
|
| 69 |
+
NetworkXError
|
| 70 |
+
If the graph has less than two nodes, is not connected or has a
|
| 71 |
+
negative-weighted edge.
|
| 72 |
+
|
| 73 |
+
Examples
|
| 74 |
+
--------
|
| 75 |
+
>>> G = nx.Graph()
|
| 76 |
+
>>> G.add_edge("x", "a", weight=3)
|
| 77 |
+
>>> G.add_edge("x", "b", weight=1)
|
| 78 |
+
>>> G.add_edge("a", "c", weight=3)
|
| 79 |
+
>>> G.add_edge("b", "c", weight=5)
|
| 80 |
+
>>> G.add_edge("b", "d", weight=4)
|
| 81 |
+
>>> G.add_edge("d", "e", weight=2)
|
| 82 |
+
>>> G.add_edge("c", "y", weight=2)
|
| 83 |
+
>>> G.add_edge("e", "y", weight=3)
|
| 84 |
+
>>> cut_value, partition = nx.stoer_wagner(G)
|
| 85 |
+
>>> cut_value
|
| 86 |
+
4
|
| 87 |
+
"""
|
| 88 |
+
n = len(G)
|
| 89 |
+
if n < 2:
|
| 90 |
+
raise nx.NetworkXError("graph has less than two nodes.")
|
| 91 |
+
if not nx.is_connected(G):
|
| 92 |
+
raise nx.NetworkXError("graph is not connected.")
|
| 93 |
+
|
| 94 |
+
# Make a copy of the graph for internal use.
|
| 95 |
+
G = nx.Graph(
|
| 96 |
+
(u, v, {"weight": e.get(weight, 1)}) for u, v, e in G.edges(data=True) if u != v
|
| 97 |
+
)
|
| 98 |
+
G.__networkx_cache__ = None # Disable caching
|
| 99 |
+
|
| 100 |
+
for u, v, e in G.edges(data=True):
|
| 101 |
+
if e["weight"] < 0:
|
| 102 |
+
raise nx.NetworkXError("graph has a negative-weighted edge.")
|
| 103 |
+
|
| 104 |
+
cut_value = float("inf")
|
| 105 |
+
nodes = set(G)
|
| 106 |
+
contractions = [] # contracted node pairs
|
| 107 |
+
|
| 108 |
+
# Repeatedly pick a pair of nodes to contract until only one node is left.
|
| 109 |
+
for i in range(n - 1):
|
| 110 |
+
# Pick an arbitrary node u and create a set A = {u}.
|
| 111 |
+
u = arbitrary_element(G)
|
| 112 |
+
A = {u}
|
| 113 |
+
# Repeatedly pick the node "most tightly connected" to A and add it to
|
| 114 |
+
# A. The tightness of connectivity of a node not in A is defined by the
|
| 115 |
+
# of edges connecting it to nodes in A.
|
| 116 |
+
h = heap() # min-heap emulating a max-heap
|
| 117 |
+
for v, e in G[u].items():
|
| 118 |
+
h.insert(v, -e["weight"])
|
| 119 |
+
# Repeat until all but one node has been added to A.
|
| 120 |
+
for j in range(n - i - 2):
|
| 121 |
+
u = h.pop()[0]
|
| 122 |
+
A.add(u)
|
| 123 |
+
for v, e in G[u].items():
|
| 124 |
+
if v not in A:
|
| 125 |
+
h.insert(v, h.get(v, 0) - e["weight"])
|
| 126 |
+
# A and the remaining node v define a "cut of the phase". There is a
|
| 127 |
+
# minimum cut of the original graph that is also a cut of the phase.
|
| 128 |
+
# Due to contractions in earlier phases, v may in fact represent
|
| 129 |
+
# multiple nodes in the original graph.
|
| 130 |
+
v, w = h.min()
|
| 131 |
+
w = -w
|
| 132 |
+
if w < cut_value:
|
| 133 |
+
cut_value = w
|
| 134 |
+
best_phase = i
|
| 135 |
+
# Contract v and the last node added to A.
|
| 136 |
+
contractions.append((u, v))
|
| 137 |
+
for w, e in G[v].items():
|
| 138 |
+
if w != u:
|
| 139 |
+
if w not in G[u]:
|
| 140 |
+
G.add_edge(u, w, weight=e["weight"])
|
| 141 |
+
else:
|
| 142 |
+
G[u][w]["weight"] += e["weight"]
|
| 143 |
+
G.remove_node(v)
|
| 144 |
+
|
| 145 |
+
# Recover the optimal partitioning from the contractions.
|
| 146 |
+
G = nx.Graph(islice(contractions, best_phase))
|
| 147 |
+
v = contractions[best_phase][1]
|
| 148 |
+
G.add_node(v)
|
| 149 |
+
reachable = set(nx.single_source_shortest_path_length(G, v))
|
| 150 |
+
partition = (list(reachable), list(nodes - reachable))
|
| 151 |
+
|
| 152 |
+
return cut_value, partition
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_connectivity.cpython-311.pyc
ADDED
|
Binary file (29.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_cuts.cpython-311.pyc
ADDED
|
Binary file (18.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_disjoint_paths.cpython-311.pyc
ADDED
|
Binary file (16.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_edge_augmentation.cpython-311.pyc
ADDED
|
Binary file (25.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_edge_kcomponents.cpython-311.pyc
ADDED
|
Binary file (26 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/__pycache__/test_kcutsets.cpython-311.pyc
ADDED
|
Binary file (15.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_connectivity.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import itertools
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.algorithms import flow
|
| 7 |
+
from networkx.algorithms.connectivity import (
|
| 8 |
+
local_edge_connectivity,
|
| 9 |
+
local_node_connectivity,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
flow_funcs = [
|
| 13 |
+
flow.boykov_kolmogorov,
|
| 14 |
+
flow.dinitz,
|
| 15 |
+
flow.edmonds_karp,
|
| 16 |
+
flow.preflow_push,
|
| 17 |
+
flow.shortest_augmenting_path,
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# helper functions for tests
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _generate_no_biconnected(max_attempts=50):
|
| 25 |
+
attempts = 0
|
| 26 |
+
while True:
|
| 27 |
+
G = nx.fast_gnp_random_graph(100, 0.0575, seed=42)
|
| 28 |
+
if nx.is_connected(G) and not nx.is_biconnected(G):
|
| 29 |
+
attempts = 0
|
| 30 |
+
yield G
|
| 31 |
+
else:
|
| 32 |
+
if attempts >= max_attempts:
|
| 33 |
+
msg = f"Tried {max_attempts} times: no suitable Graph."
|
| 34 |
+
raise Exception(msg)
|
| 35 |
+
else:
|
| 36 |
+
attempts += 1
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_average_connectivity():
|
| 40 |
+
# figure 1 from:
|
| 41 |
+
# Beineke, L., O. Oellermann, and R. Pippert (2002). The average
|
| 42 |
+
# connectivity of a graph. Discrete mathematics 252(1-3), 31-45
|
| 43 |
+
# http://www.sciencedirect.com/science/article/pii/S0012365X01001807
|
| 44 |
+
G1 = nx.path_graph(3)
|
| 45 |
+
G1.add_edges_from([(1, 3), (1, 4)])
|
| 46 |
+
G2 = nx.path_graph(3)
|
| 47 |
+
G2.add_edges_from([(1, 3), (1, 4), (0, 3), (0, 4), (3, 4)])
|
| 48 |
+
G3 = nx.Graph()
|
| 49 |
+
for flow_func in flow_funcs:
|
| 50 |
+
kwargs = {"flow_func": flow_func}
|
| 51 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 52 |
+
assert nx.average_node_connectivity(G1, **kwargs) == 1, errmsg
|
| 53 |
+
assert nx.average_node_connectivity(G2, **kwargs) == 2.2, errmsg
|
| 54 |
+
assert nx.average_node_connectivity(G3, **kwargs) == 0, errmsg
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_average_connectivity_directed():
|
| 58 |
+
G = nx.DiGraph([(1, 3), (1, 4), (1, 5)])
|
| 59 |
+
for flow_func in flow_funcs:
|
| 60 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 61 |
+
assert nx.average_node_connectivity(G) == 0.25, errmsg
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def test_articulation_points():
|
| 65 |
+
Ggen = _generate_no_biconnected()
|
| 66 |
+
for flow_func in flow_funcs:
|
| 67 |
+
for i in range(3):
|
| 68 |
+
G = next(Ggen)
|
| 69 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 70 |
+
assert nx.node_connectivity(G, flow_func=flow_func) == 1, errmsg
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_brandes_erlebach():
|
| 74 |
+
# Figure 1 chapter 7: Connectivity
|
| 75 |
+
# http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf
|
| 76 |
+
G = nx.Graph()
|
| 77 |
+
G.add_edges_from(
|
| 78 |
+
[
|
| 79 |
+
(1, 2),
|
| 80 |
+
(1, 3),
|
| 81 |
+
(1, 4),
|
| 82 |
+
(1, 5),
|
| 83 |
+
(2, 3),
|
| 84 |
+
(2, 6),
|
| 85 |
+
(3, 4),
|
| 86 |
+
(3, 6),
|
| 87 |
+
(4, 6),
|
| 88 |
+
(4, 7),
|
| 89 |
+
(5, 7),
|
| 90 |
+
(6, 8),
|
| 91 |
+
(6, 9),
|
| 92 |
+
(7, 8),
|
| 93 |
+
(7, 10),
|
| 94 |
+
(8, 11),
|
| 95 |
+
(9, 10),
|
| 96 |
+
(9, 11),
|
| 97 |
+
(10, 11),
|
| 98 |
+
]
|
| 99 |
+
)
|
| 100 |
+
for flow_func in flow_funcs:
|
| 101 |
+
kwargs = {"flow_func": flow_func}
|
| 102 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 103 |
+
assert 3 == local_edge_connectivity(G, 1, 11, **kwargs), errmsg
|
| 104 |
+
assert 3 == nx.edge_connectivity(G, 1, 11, **kwargs), errmsg
|
| 105 |
+
assert 2 == local_node_connectivity(G, 1, 11, **kwargs), errmsg
|
| 106 |
+
assert 2 == nx.node_connectivity(G, 1, 11, **kwargs), errmsg
|
| 107 |
+
assert 2 == nx.edge_connectivity(G, **kwargs), errmsg
|
| 108 |
+
assert 2 == nx.node_connectivity(G, **kwargs), errmsg
|
| 109 |
+
if flow_func is flow.preflow_push:
|
| 110 |
+
assert 3 == nx.edge_connectivity(G, 1, 11, cutoff=2, **kwargs), errmsg
|
| 111 |
+
else:
|
| 112 |
+
assert 2 == nx.edge_connectivity(G, 1, 11, cutoff=2, **kwargs), errmsg
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def test_white_harary_1():
|
| 116 |
+
# Figure 1b white and harary (2001)
|
| 117 |
+
# https://doi.org/10.1111/0081-1750.00098
|
| 118 |
+
# A graph with high adhesion (edge connectivity) and low cohesion
|
| 119 |
+
# (vertex connectivity)
|
| 120 |
+
G = nx.disjoint_union(nx.complete_graph(4), nx.complete_graph(4))
|
| 121 |
+
G.remove_node(7)
|
| 122 |
+
for i in range(4, 7):
|
| 123 |
+
G.add_edge(0, i)
|
| 124 |
+
G = nx.disjoint_union(G, nx.complete_graph(4))
|
| 125 |
+
G.remove_node(G.order() - 1)
|
| 126 |
+
for i in range(7, 10):
|
| 127 |
+
G.add_edge(0, i)
|
| 128 |
+
for flow_func in flow_funcs:
|
| 129 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 130 |
+
assert 1 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
| 131 |
+
assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def test_white_harary_2():
|
| 135 |
+
# Figure 8 white and harary (2001)
|
| 136 |
+
# https://doi.org/10.1111/0081-1750.00098
|
| 137 |
+
G = nx.disjoint_union(nx.complete_graph(4), nx.complete_graph(4))
|
| 138 |
+
G.add_edge(0, 4)
|
| 139 |
+
# kappa <= lambda <= delta
|
| 140 |
+
assert 3 == min(nx.core_number(G).values())
|
| 141 |
+
for flow_func in flow_funcs:
|
| 142 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 143 |
+
assert 1 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
| 144 |
+
assert 1 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def test_complete_graphs():
|
| 148 |
+
for n in range(5, 20, 5):
|
| 149 |
+
for flow_func in flow_funcs:
|
| 150 |
+
G = nx.complete_graph(n)
|
| 151 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 152 |
+
assert n - 1 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
| 153 |
+
assert n - 1 == nx.node_connectivity(
|
| 154 |
+
G.to_directed(), flow_func=flow_func
|
| 155 |
+
), errmsg
|
| 156 |
+
assert n - 1 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
| 157 |
+
assert n - 1 == nx.edge_connectivity(
|
| 158 |
+
G.to_directed(), flow_func=flow_func
|
| 159 |
+
), errmsg
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def test_empty_graphs():
|
| 163 |
+
for k in range(5, 25, 5):
|
| 164 |
+
G = nx.empty_graph(k)
|
| 165 |
+
for flow_func in flow_funcs:
|
| 166 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 167 |
+
assert 0 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
| 168 |
+
assert 0 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def test_petersen():
|
| 172 |
+
G = nx.petersen_graph()
|
| 173 |
+
for flow_func in flow_funcs:
|
| 174 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 175 |
+
assert 3 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
| 176 |
+
assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def test_tutte():
|
| 180 |
+
G = nx.tutte_graph()
|
| 181 |
+
for flow_func in flow_funcs:
|
| 182 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 183 |
+
assert 3 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
| 184 |
+
assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def test_dodecahedral():
|
| 188 |
+
G = nx.dodecahedral_graph()
|
| 189 |
+
for flow_func in flow_funcs:
|
| 190 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 191 |
+
assert 3 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
| 192 |
+
assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def test_octahedral():
|
| 196 |
+
G = nx.octahedral_graph()
|
| 197 |
+
for flow_func in flow_funcs:
|
| 198 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 199 |
+
assert 4 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
| 200 |
+
assert 4 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def test_icosahedral():
|
| 204 |
+
G = nx.icosahedral_graph()
|
| 205 |
+
for flow_func in flow_funcs:
|
| 206 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 207 |
+
assert 5 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
| 208 |
+
assert 5 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def test_missing_source():
|
| 212 |
+
G = nx.path_graph(4)
|
| 213 |
+
for flow_func in flow_funcs:
|
| 214 |
+
pytest.raises(
|
| 215 |
+
nx.NetworkXError, nx.node_connectivity, G, 10, 1, flow_func=flow_func
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def test_missing_target():
|
| 220 |
+
G = nx.path_graph(4)
|
| 221 |
+
for flow_func in flow_funcs:
|
| 222 |
+
pytest.raises(
|
| 223 |
+
nx.NetworkXError, nx.node_connectivity, G, 1, 10, flow_func=flow_func
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def test_edge_missing_source():
|
| 228 |
+
G = nx.path_graph(4)
|
| 229 |
+
for flow_func in flow_funcs:
|
| 230 |
+
pytest.raises(
|
| 231 |
+
nx.NetworkXError, nx.edge_connectivity, G, 10, 1, flow_func=flow_func
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def test_edge_missing_target():
|
| 236 |
+
G = nx.path_graph(4)
|
| 237 |
+
for flow_func in flow_funcs:
|
| 238 |
+
pytest.raises(
|
| 239 |
+
nx.NetworkXError, nx.edge_connectivity, G, 1, 10, flow_func=flow_func
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def test_not_weakly_connected():
|
| 244 |
+
G = nx.DiGraph()
|
| 245 |
+
nx.add_path(G, [1, 2, 3])
|
| 246 |
+
nx.add_path(G, [4, 5])
|
| 247 |
+
for flow_func in flow_funcs:
|
| 248 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 249 |
+
assert nx.node_connectivity(G) == 0, errmsg
|
| 250 |
+
assert nx.edge_connectivity(G) == 0, errmsg
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def test_not_connected():
|
| 254 |
+
G = nx.Graph()
|
| 255 |
+
nx.add_path(G, [1, 2, 3])
|
| 256 |
+
nx.add_path(G, [4, 5])
|
| 257 |
+
for flow_func in flow_funcs:
|
| 258 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 259 |
+
assert nx.node_connectivity(G) == 0, errmsg
|
| 260 |
+
assert nx.edge_connectivity(G) == 0, errmsg
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def test_directed_edge_connectivity():
|
| 264 |
+
G = nx.cycle_graph(10, create_using=nx.DiGraph()) # only one direction
|
| 265 |
+
D = nx.cycle_graph(10).to_directed() # 2 reciprocal edges
|
| 266 |
+
for flow_func in flow_funcs:
|
| 267 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 268 |
+
assert 1 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
| 269 |
+
assert 1 == local_edge_connectivity(G, 1, 4, flow_func=flow_func), errmsg
|
| 270 |
+
assert 1 == nx.edge_connectivity(G, 1, 4, flow_func=flow_func), errmsg
|
| 271 |
+
assert 2 == nx.edge_connectivity(D, flow_func=flow_func), errmsg
|
| 272 |
+
assert 2 == local_edge_connectivity(D, 1, 4, flow_func=flow_func), errmsg
|
| 273 |
+
assert 2 == nx.edge_connectivity(D, 1, 4, flow_func=flow_func), errmsg
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def test_cutoff():
|
| 277 |
+
G = nx.complete_graph(5)
|
| 278 |
+
for local_func in [local_edge_connectivity, local_node_connectivity]:
|
| 279 |
+
for flow_func in flow_funcs:
|
| 280 |
+
if flow_func is flow.preflow_push:
|
| 281 |
+
# cutoff is not supported by preflow_push
|
| 282 |
+
continue
|
| 283 |
+
for cutoff in [3, 2, 1]:
|
| 284 |
+
result = local_func(G, 0, 4, flow_func=flow_func, cutoff=cutoff)
|
| 285 |
+
assert cutoff == result, f"cutoff error in {flow_func.__name__}"
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def test_invalid_auxiliary():
|
| 289 |
+
G = nx.complete_graph(5)
|
| 290 |
+
pytest.raises(nx.NetworkXError, local_node_connectivity, G, 0, 3, auxiliary=G)
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def test_interface_only_source():
|
| 294 |
+
G = nx.complete_graph(5)
|
| 295 |
+
for interface_func in [nx.node_connectivity, nx.edge_connectivity]:
|
| 296 |
+
pytest.raises(nx.NetworkXError, interface_func, G, s=0)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def test_interface_only_target():
|
| 300 |
+
G = nx.complete_graph(5)
|
| 301 |
+
for interface_func in [nx.node_connectivity, nx.edge_connectivity]:
|
| 302 |
+
pytest.raises(nx.NetworkXError, interface_func, G, t=3)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def test_edge_connectivity_flow_vs_stoer_wagner():
|
| 306 |
+
graph_funcs = [nx.icosahedral_graph, nx.octahedral_graph, nx.dodecahedral_graph]
|
| 307 |
+
for graph_func in graph_funcs:
|
| 308 |
+
G = graph_func()
|
| 309 |
+
assert nx.stoer_wagner(G)[0] == nx.edge_connectivity(G)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
class TestAllPairsNodeConnectivity:
|
| 313 |
+
@classmethod
|
| 314 |
+
def setup_class(cls):
|
| 315 |
+
cls.path = nx.path_graph(7)
|
| 316 |
+
cls.directed_path = nx.path_graph(7, create_using=nx.DiGraph())
|
| 317 |
+
cls.cycle = nx.cycle_graph(7)
|
| 318 |
+
cls.directed_cycle = nx.cycle_graph(7, create_using=nx.DiGraph())
|
| 319 |
+
cls.gnp = nx.gnp_random_graph(30, 0.1, seed=42)
|
| 320 |
+
cls.directed_gnp = nx.gnp_random_graph(30, 0.1, directed=True, seed=42)
|
| 321 |
+
cls.K20 = nx.complete_graph(20)
|
| 322 |
+
cls.K10 = nx.complete_graph(10)
|
| 323 |
+
cls.K5 = nx.complete_graph(5)
|
| 324 |
+
cls.G_list = [
|
| 325 |
+
cls.path,
|
| 326 |
+
cls.directed_path,
|
| 327 |
+
cls.cycle,
|
| 328 |
+
cls.directed_cycle,
|
| 329 |
+
cls.gnp,
|
| 330 |
+
cls.directed_gnp,
|
| 331 |
+
cls.K10,
|
| 332 |
+
cls.K5,
|
| 333 |
+
cls.K20,
|
| 334 |
+
]
|
| 335 |
+
|
| 336 |
+
def test_cycles(self):
|
| 337 |
+
K_undir = nx.all_pairs_node_connectivity(self.cycle)
|
| 338 |
+
for source in K_undir:
|
| 339 |
+
for target, k in K_undir[source].items():
|
| 340 |
+
assert k == 2
|
| 341 |
+
K_dir = nx.all_pairs_node_connectivity(self.directed_cycle)
|
| 342 |
+
for source in K_dir:
|
| 343 |
+
for target, k in K_dir[source].items():
|
| 344 |
+
assert k == 1
|
| 345 |
+
|
| 346 |
+
def test_complete(self):
|
| 347 |
+
for G in [self.K10, self.K5, self.K20]:
|
| 348 |
+
K = nx.all_pairs_node_connectivity(G)
|
| 349 |
+
for source in K:
|
| 350 |
+
for target, k in K[source].items():
|
| 351 |
+
assert k == len(G) - 1
|
| 352 |
+
|
| 353 |
+
def test_paths(self):
|
| 354 |
+
K_undir = nx.all_pairs_node_connectivity(self.path)
|
| 355 |
+
for source in K_undir:
|
| 356 |
+
for target, k in K_undir[source].items():
|
| 357 |
+
assert k == 1
|
| 358 |
+
K_dir = nx.all_pairs_node_connectivity(self.directed_path)
|
| 359 |
+
for source in K_dir:
|
| 360 |
+
for target, k in K_dir[source].items():
|
| 361 |
+
if source < target:
|
| 362 |
+
assert k == 1
|
| 363 |
+
else:
|
| 364 |
+
assert k == 0
|
| 365 |
+
|
| 366 |
+
def test_all_pairs_connectivity_nbunch(self):
|
| 367 |
+
G = nx.complete_graph(5)
|
| 368 |
+
nbunch = [0, 2, 3]
|
| 369 |
+
C = nx.all_pairs_node_connectivity(G, nbunch=nbunch)
|
| 370 |
+
assert len(C) == len(nbunch)
|
| 371 |
+
|
| 372 |
+
def test_all_pairs_connectivity_icosahedral(self):
|
| 373 |
+
G = nx.icosahedral_graph()
|
| 374 |
+
C = nx.all_pairs_node_connectivity(G)
|
| 375 |
+
assert all(5 == C[u][v] for u, v in itertools.combinations(G, 2))
|
| 376 |
+
|
| 377 |
+
def test_all_pairs_connectivity(self):
|
| 378 |
+
G = nx.Graph()
|
| 379 |
+
nodes = [0, 1, 2, 3]
|
| 380 |
+
nx.add_path(G, nodes)
|
| 381 |
+
A = {n: {} for n in G}
|
| 382 |
+
for u, v in itertools.combinations(nodes, 2):
|
| 383 |
+
A[u][v] = A[v][u] = nx.node_connectivity(G, u, v)
|
| 384 |
+
C = nx.all_pairs_node_connectivity(G)
|
| 385 |
+
assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
|
| 386 |
+
(k, sorted(v)) for k, v in C.items()
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
def test_all_pairs_connectivity_directed(self):
|
| 390 |
+
G = nx.DiGraph()
|
| 391 |
+
nodes = [0, 1, 2, 3]
|
| 392 |
+
nx.add_path(G, nodes)
|
| 393 |
+
A = {n: {} for n in G}
|
| 394 |
+
for u, v in itertools.permutations(nodes, 2):
|
| 395 |
+
A[u][v] = nx.node_connectivity(G, u, v)
|
| 396 |
+
C = nx.all_pairs_node_connectivity(G)
|
| 397 |
+
assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
|
| 398 |
+
(k, sorted(v)) for k, v in C.items()
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
def test_all_pairs_connectivity_nbunch_combinations(self):
|
| 402 |
+
G = nx.complete_graph(5)
|
| 403 |
+
nbunch = [0, 2, 3]
|
| 404 |
+
A = {n: {} for n in nbunch}
|
| 405 |
+
for u, v in itertools.combinations(nbunch, 2):
|
| 406 |
+
A[u][v] = A[v][u] = nx.node_connectivity(G, u, v)
|
| 407 |
+
C = nx.all_pairs_node_connectivity(G, nbunch=nbunch)
|
| 408 |
+
assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
|
| 409 |
+
(k, sorted(v)) for k, v in C.items()
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
def test_all_pairs_connectivity_nbunch_iter(self):
|
| 413 |
+
G = nx.complete_graph(5)
|
| 414 |
+
nbunch = [0, 2, 3]
|
| 415 |
+
A = {n: {} for n in nbunch}
|
| 416 |
+
for u, v in itertools.combinations(nbunch, 2):
|
| 417 |
+
A[u][v] = A[v][u] = nx.node_connectivity(G, u, v)
|
| 418 |
+
C = nx.all_pairs_node_connectivity(G, nbunch=iter(nbunch))
|
| 419 |
+
assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
|
| 420 |
+
(k, sorted(v)) for k, v in C.items()
|
| 421 |
+
)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_cuts.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from networkx.algorithms import flow
|
| 5 |
+
from networkx.algorithms.connectivity import minimum_st_edge_cut, minimum_st_node_cut
|
| 6 |
+
from networkx.utils import arbitrary_element
|
| 7 |
+
|
| 8 |
+
flow_funcs = [
|
| 9 |
+
flow.boykov_kolmogorov,
|
| 10 |
+
flow.dinitz,
|
| 11 |
+
flow.edmonds_karp,
|
| 12 |
+
flow.preflow_push,
|
| 13 |
+
flow.shortest_augmenting_path,
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
# Tests for node and edge cutsets
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _generate_no_biconnected(max_attempts=50):
|
| 20 |
+
attempts = 0
|
| 21 |
+
while True:
|
| 22 |
+
G = nx.fast_gnp_random_graph(100, 0.0575, seed=42)
|
| 23 |
+
if nx.is_connected(G) and not nx.is_biconnected(G):
|
| 24 |
+
attempts = 0
|
| 25 |
+
yield G
|
| 26 |
+
else:
|
| 27 |
+
if attempts >= max_attempts:
|
| 28 |
+
msg = f"Tried {attempts} times: no suitable Graph."
|
| 29 |
+
raise Exception(msg)
|
| 30 |
+
else:
|
| 31 |
+
attempts += 1
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_articulation_points():
|
| 35 |
+
Ggen = _generate_no_biconnected()
|
| 36 |
+
for flow_func in flow_funcs:
|
| 37 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 38 |
+
for i in range(1): # change 1 to 3 or more for more realizations.
|
| 39 |
+
G = next(Ggen)
|
| 40 |
+
cut = nx.minimum_node_cut(G, flow_func=flow_func)
|
| 41 |
+
assert len(cut) == 1, errmsg
|
| 42 |
+
assert cut.pop() in set(nx.articulation_points(G)), errmsg
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_brandes_erlebach_book():
|
| 46 |
+
# Figure 1 chapter 7: Connectivity
|
| 47 |
+
# http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf
|
| 48 |
+
G = nx.Graph()
|
| 49 |
+
G.add_edges_from(
|
| 50 |
+
[
|
| 51 |
+
(1, 2),
|
| 52 |
+
(1, 3),
|
| 53 |
+
(1, 4),
|
| 54 |
+
(1, 5),
|
| 55 |
+
(2, 3),
|
| 56 |
+
(2, 6),
|
| 57 |
+
(3, 4),
|
| 58 |
+
(3, 6),
|
| 59 |
+
(4, 6),
|
| 60 |
+
(4, 7),
|
| 61 |
+
(5, 7),
|
| 62 |
+
(6, 8),
|
| 63 |
+
(6, 9),
|
| 64 |
+
(7, 8),
|
| 65 |
+
(7, 10),
|
| 66 |
+
(8, 11),
|
| 67 |
+
(9, 10),
|
| 68 |
+
(9, 11),
|
| 69 |
+
(10, 11),
|
| 70 |
+
]
|
| 71 |
+
)
|
| 72 |
+
for flow_func in flow_funcs:
|
| 73 |
+
kwargs = {"flow_func": flow_func}
|
| 74 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 75 |
+
# edge cutsets
|
| 76 |
+
assert 3 == len(nx.minimum_edge_cut(G, 1, 11, **kwargs)), errmsg
|
| 77 |
+
edge_cut = nx.minimum_edge_cut(G, **kwargs)
|
| 78 |
+
# Node 5 has only two edges
|
| 79 |
+
assert 2 == len(edge_cut), errmsg
|
| 80 |
+
H = G.copy()
|
| 81 |
+
H.remove_edges_from(edge_cut)
|
| 82 |
+
assert not nx.is_connected(H), errmsg
|
| 83 |
+
# node cuts
|
| 84 |
+
assert {6, 7} == minimum_st_node_cut(G, 1, 11, **kwargs), errmsg
|
| 85 |
+
assert {6, 7} == nx.minimum_node_cut(G, 1, 11, **kwargs), errmsg
|
| 86 |
+
node_cut = nx.minimum_node_cut(G, **kwargs)
|
| 87 |
+
assert 2 == len(node_cut), errmsg
|
| 88 |
+
H = G.copy()
|
| 89 |
+
H.remove_nodes_from(node_cut)
|
| 90 |
+
assert not nx.is_connected(H), errmsg
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_white_harary_paper():
|
| 94 |
+
# Figure 1b white and harary (2001)
|
| 95 |
+
# https://doi.org/10.1111/0081-1750.00098
|
| 96 |
+
# A graph with high adhesion (edge connectivity) and low cohesion
|
| 97 |
+
# (node connectivity)
|
| 98 |
+
G = nx.disjoint_union(nx.complete_graph(4), nx.complete_graph(4))
|
| 99 |
+
G.remove_node(7)
|
| 100 |
+
for i in range(4, 7):
|
| 101 |
+
G.add_edge(0, i)
|
| 102 |
+
G = nx.disjoint_union(G, nx.complete_graph(4))
|
| 103 |
+
G.remove_node(G.order() - 1)
|
| 104 |
+
for i in range(7, 10):
|
| 105 |
+
G.add_edge(0, i)
|
| 106 |
+
for flow_func in flow_funcs:
|
| 107 |
+
kwargs = {"flow_func": flow_func}
|
| 108 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 109 |
+
# edge cuts
|
| 110 |
+
edge_cut = nx.minimum_edge_cut(G, **kwargs)
|
| 111 |
+
assert 3 == len(edge_cut), errmsg
|
| 112 |
+
H = G.copy()
|
| 113 |
+
H.remove_edges_from(edge_cut)
|
| 114 |
+
assert not nx.is_connected(H), errmsg
|
| 115 |
+
# node cuts
|
| 116 |
+
node_cut = nx.minimum_node_cut(G, **kwargs)
|
| 117 |
+
assert {0} == node_cut, errmsg
|
| 118 |
+
H = G.copy()
|
| 119 |
+
H.remove_nodes_from(node_cut)
|
| 120 |
+
assert not nx.is_connected(H), errmsg
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def test_petersen_cutset():
|
| 124 |
+
G = nx.petersen_graph()
|
| 125 |
+
for flow_func in flow_funcs:
|
| 126 |
+
kwargs = {"flow_func": flow_func}
|
| 127 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 128 |
+
# edge cuts
|
| 129 |
+
edge_cut = nx.minimum_edge_cut(G, **kwargs)
|
| 130 |
+
assert 3 == len(edge_cut), errmsg
|
| 131 |
+
H = G.copy()
|
| 132 |
+
H.remove_edges_from(edge_cut)
|
| 133 |
+
assert not nx.is_connected(H), errmsg
|
| 134 |
+
# node cuts
|
| 135 |
+
node_cut = nx.minimum_node_cut(G, **kwargs)
|
| 136 |
+
assert 3 == len(node_cut), errmsg
|
| 137 |
+
H = G.copy()
|
| 138 |
+
H.remove_nodes_from(node_cut)
|
| 139 |
+
assert not nx.is_connected(H), errmsg
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def test_octahedral_cutset():
|
| 143 |
+
G = nx.octahedral_graph()
|
| 144 |
+
for flow_func in flow_funcs:
|
| 145 |
+
kwargs = {"flow_func": flow_func}
|
| 146 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 147 |
+
# edge cuts
|
| 148 |
+
edge_cut = nx.minimum_edge_cut(G, **kwargs)
|
| 149 |
+
assert 4 == len(edge_cut), errmsg
|
| 150 |
+
H = G.copy()
|
| 151 |
+
H.remove_edges_from(edge_cut)
|
| 152 |
+
assert not nx.is_connected(H), errmsg
|
| 153 |
+
# node cuts
|
| 154 |
+
node_cut = nx.minimum_node_cut(G, **kwargs)
|
| 155 |
+
assert 4 == len(node_cut), errmsg
|
| 156 |
+
H = G.copy()
|
| 157 |
+
H.remove_nodes_from(node_cut)
|
| 158 |
+
assert not nx.is_connected(H), errmsg
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def test_icosahedral_cutset():
|
| 162 |
+
G = nx.icosahedral_graph()
|
| 163 |
+
for flow_func in flow_funcs:
|
| 164 |
+
kwargs = {"flow_func": flow_func}
|
| 165 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 166 |
+
# edge cuts
|
| 167 |
+
edge_cut = nx.minimum_edge_cut(G, **kwargs)
|
| 168 |
+
assert 5 == len(edge_cut), errmsg
|
| 169 |
+
H = G.copy()
|
| 170 |
+
H.remove_edges_from(edge_cut)
|
| 171 |
+
assert not nx.is_connected(H), errmsg
|
| 172 |
+
# node cuts
|
| 173 |
+
node_cut = nx.minimum_node_cut(G, **kwargs)
|
| 174 |
+
assert 5 == len(node_cut), errmsg
|
| 175 |
+
H = G.copy()
|
| 176 |
+
H.remove_nodes_from(node_cut)
|
| 177 |
+
assert not nx.is_connected(H), errmsg
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def test_node_cutset_exception():
|
| 181 |
+
G = nx.Graph()
|
| 182 |
+
G.add_edges_from([(1, 2), (3, 4)])
|
| 183 |
+
for flow_func in flow_funcs:
|
| 184 |
+
pytest.raises(nx.NetworkXError, nx.minimum_node_cut, G, flow_func=flow_func)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def test_node_cutset_random_graphs():
|
| 188 |
+
for flow_func in flow_funcs:
|
| 189 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 190 |
+
for i in range(3):
|
| 191 |
+
G = nx.fast_gnp_random_graph(50, 0.25, seed=42)
|
| 192 |
+
if not nx.is_connected(G):
|
| 193 |
+
ccs = iter(nx.connected_components(G))
|
| 194 |
+
start = arbitrary_element(next(ccs))
|
| 195 |
+
G.add_edges_from((start, arbitrary_element(c)) for c in ccs)
|
| 196 |
+
cutset = nx.minimum_node_cut(G, flow_func=flow_func)
|
| 197 |
+
assert nx.node_connectivity(G) == len(cutset), errmsg
|
| 198 |
+
G.remove_nodes_from(cutset)
|
| 199 |
+
assert not nx.is_connected(G), errmsg
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def test_edge_cutset_random_graphs():
|
| 203 |
+
for flow_func in flow_funcs:
|
| 204 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 205 |
+
for i in range(3):
|
| 206 |
+
G = nx.fast_gnp_random_graph(50, 0.25, seed=42)
|
| 207 |
+
if not nx.is_connected(G):
|
| 208 |
+
ccs = iter(nx.connected_components(G))
|
| 209 |
+
start = arbitrary_element(next(ccs))
|
| 210 |
+
G.add_edges_from((start, arbitrary_element(c)) for c in ccs)
|
| 211 |
+
cutset = nx.minimum_edge_cut(G, flow_func=flow_func)
|
| 212 |
+
assert nx.edge_connectivity(G) == len(cutset), errmsg
|
| 213 |
+
G.remove_edges_from(cutset)
|
| 214 |
+
assert not nx.is_connected(G), errmsg
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def test_empty_graphs():
|
| 218 |
+
G = nx.Graph()
|
| 219 |
+
D = nx.DiGraph()
|
| 220 |
+
for interface_func in [nx.minimum_node_cut, nx.minimum_edge_cut]:
|
| 221 |
+
for flow_func in flow_funcs:
|
| 222 |
+
pytest.raises(
|
| 223 |
+
nx.NetworkXPointlessConcept, interface_func, G, flow_func=flow_func
|
| 224 |
+
)
|
| 225 |
+
pytest.raises(
|
| 226 |
+
nx.NetworkXPointlessConcept, interface_func, D, flow_func=flow_func
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def test_unbounded():
|
| 231 |
+
G = nx.complete_graph(5)
|
| 232 |
+
for flow_func in flow_funcs:
|
| 233 |
+
assert 4 == len(minimum_st_edge_cut(G, 1, 4, flow_func=flow_func))
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def test_missing_source():
|
| 237 |
+
G = nx.path_graph(4)
|
| 238 |
+
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
| 239 |
+
for flow_func in flow_funcs:
|
| 240 |
+
pytest.raises(
|
| 241 |
+
nx.NetworkXError, interface_func, G, 10, 1, flow_func=flow_func
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def test_missing_target():
|
| 246 |
+
G = nx.path_graph(4)
|
| 247 |
+
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
| 248 |
+
for flow_func in flow_funcs:
|
| 249 |
+
pytest.raises(
|
| 250 |
+
nx.NetworkXError, interface_func, G, 1, 10, flow_func=flow_func
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def test_not_weakly_connected():
|
| 255 |
+
G = nx.DiGraph()
|
| 256 |
+
nx.add_path(G, [1, 2, 3])
|
| 257 |
+
nx.add_path(G, [4, 5])
|
| 258 |
+
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
| 259 |
+
for flow_func in flow_funcs:
|
| 260 |
+
pytest.raises(nx.NetworkXError, interface_func, G, flow_func=flow_func)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def test_not_connected():
|
| 264 |
+
G = nx.Graph()
|
| 265 |
+
nx.add_path(G, [1, 2, 3])
|
| 266 |
+
nx.add_path(G, [4, 5])
|
| 267 |
+
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
| 268 |
+
for flow_func in flow_funcs:
|
| 269 |
+
pytest.raises(nx.NetworkXError, interface_func, G, flow_func=flow_func)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def tests_min_cut_complete():
|
| 273 |
+
G = nx.complete_graph(5)
|
| 274 |
+
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
| 275 |
+
for flow_func in flow_funcs:
|
| 276 |
+
assert 4 == len(interface_func(G, flow_func=flow_func))
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def tests_min_cut_complete_directed():
|
| 280 |
+
G = nx.complete_graph(5)
|
| 281 |
+
G = G.to_directed()
|
| 282 |
+
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
| 283 |
+
for flow_func in flow_funcs:
|
| 284 |
+
assert 4 == len(interface_func(G, flow_func=flow_func))
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def tests_minimum_st_node_cut():
|
| 288 |
+
G = nx.Graph()
|
| 289 |
+
G.add_nodes_from([0, 1, 2, 3, 7, 8, 11, 12])
|
| 290 |
+
G.add_edges_from([(7, 11), (1, 11), (1, 12), (12, 8), (0, 1)])
|
| 291 |
+
nodelist = minimum_st_node_cut(G, 7, 11)
|
| 292 |
+
assert nodelist == {}
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def test_invalid_auxiliary():
|
| 296 |
+
G = nx.complete_graph(5)
|
| 297 |
+
pytest.raises(nx.NetworkXError, minimum_st_node_cut, G, 0, 3, auxiliary=G)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def test_interface_only_source():
|
| 301 |
+
G = nx.complete_graph(5)
|
| 302 |
+
for interface_func in [nx.minimum_node_cut, nx.minimum_edge_cut]:
|
| 303 |
+
pytest.raises(nx.NetworkXError, interface_func, G, s=0)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def test_interface_only_target():
|
| 307 |
+
G = nx.complete_graph(5)
|
| 308 |
+
for interface_func in [nx.minimum_node_cut, nx.minimum_edge_cut]:
|
| 309 |
+
pytest.raises(nx.NetworkXError, interface_func, G, t=3)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_disjoint_paths.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from networkx.algorithms import flow
|
| 5 |
+
from networkx.utils import pairwise
|
| 6 |
+
|
| 7 |
+
flow_funcs = [
|
| 8 |
+
flow.boykov_kolmogorov,
|
| 9 |
+
flow.edmonds_karp,
|
| 10 |
+
flow.dinitz,
|
| 11 |
+
flow.preflow_push,
|
| 12 |
+
flow.shortest_augmenting_path,
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def is_path(G, path):
|
| 17 |
+
return all(v in G[u] for u, v in pairwise(path))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def are_edge_disjoint_paths(G, paths):
|
| 21 |
+
if not paths:
|
| 22 |
+
return False
|
| 23 |
+
for path in paths:
|
| 24 |
+
assert is_path(G, path)
|
| 25 |
+
paths_edges = [list(pairwise(p)) for p in paths]
|
| 26 |
+
num_of_edges = sum(len(e) for e in paths_edges)
|
| 27 |
+
num_unique_edges = len(set.union(*[set(es) for es in paths_edges]))
|
| 28 |
+
if num_of_edges == num_unique_edges:
|
| 29 |
+
return True
|
| 30 |
+
return False
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def are_node_disjoint_paths(G, paths):
|
| 34 |
+
if not paths:
|
| 35 |
+
return False
|
| 36 |
+
for path in paths:
|
| 37 |
+
assert is_path(G, path)
|
| 38 |
+
# first and last nodes are source and target
|
| 39 |
+
st = {paths[0][0], paths[0][-1]}
|
| 40 |
+
num_of_nodes = len([n for path in paths for n in path if n not in st])
|
| 41 |
+
num_unique_nodes = len({n for path in paths for n in path if n not in st})
|
| 42 |
+
if num_of_nodes == num_unique_nodes:
|
| 43 |
+
return True
|
| 44 |
+
return False
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_graph_from_pr_2053():
|
| 48 |
+
G = nx.Graph()
|
| 49 |
+
G.add_edges_from(
|
| 50 |
+
[
|
| 51 |
+
("A", "B"),
|
| 52 |
+
("A", "D"),
|
| 53 |
+
("A", "F"),
|
| 54 |
+
("A", "G"),
|
| 55 |
+
("B", "C"),
|
| 56 |
+
("B", "D"),
|
| 57 |
+
("B", "G"),
|
| 58 |
+
("C", "D"),
|
| 59 |
+
("C", "E"),
|
| 60 |
+
("C", "Z"),
|
| 61 |
+
("D", "E"),
|
| 62 |
+
("D", "F"),
|
| 63 |
+
("E", "F"),
|
| 64 |
+
("E", "Z"),
|
| 65 |
+
("F", "Z"),
|
| 66 |
+
("G", "Z"),
|
| 67 |
+
]
|
| 68 |
+
)
|
| 69 |
+
for flow_func in flow_funcs:
|
| 70 |
+
kwargs = {"flow_func": flow_func}
|
| 71 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 72 |
+
# edge disjoint paths
|
| 73 |
+
edge_paths = list(nx.edge_disjoint_paths(G, "A", "Z", **kwargs))
|
| 74 |
+
assert are_edge_disjoint_paths(G, edge_paths), errmsg
|
| 75 |
+
assert nx.edge_connectivity(G, "A", "Z") == len(edge_paths), errmsg
|
| 76 |
+
# node disjoint paths
|
| 77 |
+
node_paths = list(nx.node_disjoint_paths(G, "A", "Z", **kwargs))
|
| 78 |
+
assert are_node_disjoint_paths(G, node_paths), errmsg
|
| 79 |
+
assert nx.node_connectivity(G, "A", "Z") == len(node_paths), errmsg
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_florentine_families():
|
| 83 |
+
G = nx.florentine_families_graph()
|
| 84 |
+
for flow_func in flow_funcs:
|
| 85 |
+
kwargs = {"flow_func": flow_func}
|
| 86 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 87 |
+
# edge disjoint paths
|
| 88 |
+
edge_dpaths = list(nx.edge_disjoint_paths(G, "Medici", "Strozzi", **kwargs))
|
| 89 |
+
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
| 90 |
+
assert nx.edge_connectivity(G, "Medici", "Strozzi") == len(edge_dpaths), errmsg
|
| 91 |
+
# node disjoint paths
|
| 92 |
+
node_dpaths = list(nx.node_disjoint_paths(G, "Medici", "Strozzi", **kwargs))
|
| 93 |
+
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
| 94 |
+
assert nx.node_connectivity(G, "Medici", "Strozzi") == len(node_dpaths), errmsg
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def test_karate():
|
| 98 |
+
G = nx.karate_club_graph()
|
| 99 |
+
for flow_func in flow_funcs:
|
| 100 |
+
kwargs = {"flow_func": flow_func}
|
| 101 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 102 |
+
# edge disjoint paths
|
| 103 |
+
edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 33, **kwargs))
|
| 104 |
+
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
| 105 |
+
assert nx.edge_connectivity(G, 0, 33) == len(edge_dpaths), errmsg
|
| 106 |
+
# node disjoint paths
|
| 107 |
+
node_dpaths = list(nx.node_disjoint_paths(G, 0, 33, **kwargs))
|
| 108 |
+
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
| 109 |
+
assert nx.node_connectivity(G, 0, 33) == len(node_dpaths), errmsg
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def test_petersen_disjoint_paths():
|
| 113 |
+
G = nx.petersen_graph()
|
| 114 |
+
for flow_func in flow_funcs:
|
| 115 |
+
kwargs = {"flow_func": flow_func}
|
| 116 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 117 |
+
# edge disjoint paths
|
| 118 |
+
edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 6, **kwargs))
|
| 119 |
+
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
| 120 |
+
assert 3 == len(edge_dpaths), errmsg
|
| 121 |
+
# node disjoint paths
|
| 122 |
+
node_dpaths = list(nx.node_disjoint_paths(G, 0, 6, **kwargs))
|
| 123 |
+
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
| 124 |
+
assert 3 == len(node_dpaths), errmsg
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def test_octahedral_disjoint_paths():
|
| 128 |
+
G = nx.octahedral_graph()
|
| 129 |
+
for flow_func in flow_funcs:
|
| 130 |
+
kwargs = {"flow_func": flow_func}
|
| 131 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 132 |
+
# edge disjoint paths
|
| 133 |
+
edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 5, **kwargs))
|
| 134 |
+
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
| 135 |
+
assert 4 == len(edge_dpaths), errmsg
|
| 136 |
+
# node disjoint paths
|
| 137 |
+
node_dpaths = list(nx.node_disjoint_paths(G, 0, 5, **kwargs))
|
| 138 |
+
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
| 139 |
+
assert 4 == len(node_dpaths), errmsg
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def test_icosahedral_disjoint_paths():
|
| 143 |
+
G = nx.icosahedral_graph()
|
| 144 |
+
for flow_func in flow_funcs:
|
| 145 |
+
kwargs = {"flow_func": flow_func}
|
| 146 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 147 |
+
# edge disjoint paths
|
| 148 |
+
edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 6, **kwargs))
|
| 149 |
+
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
| 150 |
+
assert 5 == len(edge_dpaths), errmsg
|
| 151 |
+
# node disjoint paths
|
| 152 |
+
node_dpaths = list(nx.node_disjoint_paths(G, 0, 6, **kwargs))
|
| 153 |
+
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
| 154 |
+
assert 5 == len(node_dpaths), errmsg
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def test_cutoff_disjoint_paths():
|
| 158 |
+
G = nx.icosahedral_graph()
|
| 159 |
+
for flow_func in flow_funcs:
|
| 160 |
+
kwargs = {"flow_func": flow_func}
|
| 161 |
+
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
| 162 |
+
for cutoff in [2, 4]:
|
| 163 |
+
kwargs["cutoff"] = cutoff
|
| 164 |
+
# edge disjoint paths
|
| 165 |
+
edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 6, **kwargs))
|
| 166 |
+
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
| 167 |
+
assert cutoff == len(edge_dpaths), errmsg
|
| 168 |
+
# node disjoint paths
|
| 169 |
+
node_dpaths = list(nx.node_disjoint_paths(G, 0, 6, **kwargs))
|
| 170 |
+
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
| 171 |
+
assert cutoff == len(node_dpaths), errmsg
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def test_missing_source_edge_paths():
|
| 175 |
+
with pytest.raises(nx.NetworkXError):
|
| 176 |
+
G = nx.path_graph(4)
|
| 177 |
+
list(nx.edge_disjoint_paths(G, 10, 1))
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def test_missing_source_node_paths():
|
| 181 |
+
with pytest.raises(nx.NetworkXError):
|
| 182 |
+
G = nx.path_graph(4)
|
| 183 |
+
list(nx.node_disjoint_paths(G, 10, 1))
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def test_missing_target_edge_paths():
|
| 187 |
+
with pytest.raises(nx.NetworkXError):
|
| 188 |
+
G = nx.path_graph(4)
|
| 189 |
+
list(nx.edge_disjoint_paths(G, 1, 10))
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def test_missing_target_node_paths():
|
| 193 |
+
with pytest.raises(nx.NetworkXError):
|
| 194 |
+
G = nx.path_graph(4)
|
| 195 |
+
list(nx.node_disjoint_paths(G, 1, 10))
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def test_not_weakly_connected_edges():
|
| 199 |
+
with pytest.raises(nx.NetworkXNoPath):
|
| 200 |
+
G = nx.DiGraph()
|
| 201 |
+
nx.add_path(G, [1, 2, 3])
|
| 202 |
+
nx.add_path(G, [4, 5])
|
| 203 |
+
list(nx.edge_disjoint_paths(G, 1, 5))
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def test_not_weakly_connected_nodes():
|
| 207 |
+
with pytest.raises(nx.NetworkXNoPath):
|
| 208 |
+
G = nx.DiGraph()
|
| 209 |
+
nx.add_path(G, [1, 2, 3])
|
| 210 |
+
nx.add_path(G, [4, 5])
|
| 211 |
+
list(nx.node_disjoint_paths(G, 1, 5))
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def test_not_connected_edges():
|
| 215 |
+
with pytest.raises(nx.NetworkXNoPath):
|
| 216 |
+
G = nx.Graph()
|
| 217 |
+
nx.add_path(G, [1, 2, 3])
|
| 218 |
+
nx.add_path(G, [4, 5])
|
| 219 |
+
list(nx.edge_disjoint_paths(G, 1, 5))
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def test_not_connected_nodes():
|
| 223 |
+
with pytest.raises(nx.NetworkXNoPath):
|
| 224 |
+
G = nx.Graph()
|
| 225 |
+
nx.add_path(G, [1, 2, 3])
|
| 226 |
+
nx.add_path(G, [4, 5])
|
| 227 |
+
list(nx.node_disjoint_paths(G, 1, 5))
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def test_isolated_edges():
|
| 231 |
+
with pytest.raises(nx.NetworkXNoPath):
|
| 232 |
+
G = nx.Graph()
|
| 233 |
+
G.add_node(1)
|
| 234 |
+
nx.add_path(G, [4, 5])
|
| 235 |
+
list(nx.edge_disjoint_paths(G, 1, 5))
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def test_isolated_nodes():
|
| 239 |
+
with pytest.raises(nx.NetworkXNoPath):
|
| 240 |
+
G = nx.Graph()
|
| 241 |
+
G.add_node(1)
|
| 242 |
+
nx.add_path(G, [4, 5])
|
| 243 |
+
list(nx.node_disjoint_paths(G, 1, 5))
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def test_invalid_auxiliary():
|
| 247 |
+
with pytest.raises(nx.NetworkXError):
|
| 248 |
+
G = nx.complete_graph(5)
|
| 249 |
+
list(nx.node_disjoint_paths(G, 0, 3, auxiliary=G))
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_edge_augmentation.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import itertools as it
|
| 2 |
+
import random
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from networkx.algorithms.connectivity import k_edge_augmentation
|
| 8 |
+
from networkx.algorithms.connectivity.edge_augmentation import (
|
| 9 |
+
_unpack_available_edges,
|
| 10 |
+
collapse,
|
| 11 |
+
complement_edges,
|
| 12 |
+
is_k_edge_connected,
|
| 13 |
+
is_locally_k_edge_connected,
|
| 14 |
+
)
|
| 15 |
+
from networkx.utils import pairwise
|
| 16 |
+
|
| 17 |
+
# This should be set to the largest k for which an efficient algorithm is
|
| 18 |
+
# explicitly defined.
|
| 19 |
+
MAX_EFFICIENT_K = 2
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def tarjan_bridge_graph():
|
| 23 |
+
# graph from tarjan paper
|
| 24 |
+
# RE Tarjan - "A note on finding the bridges of a graph"
|
| 25 |
+
# Information Processing Letters, 1974 - Elsevier
|
| 26 |
+
# doi:10.1016/0020-0190(74)90003-9.
|
| 27 |
+
# define 2-connected components and bridges
|
| 28 |
+
ccs = [
|
| 29 |
+
(1, 2, 4, 3, 1, 4),
|
| 30 |
+
(5, 6, 7, 5),
|
| 31 |
+
(8, 9, 10, 8),
|
| 32 |
+
(17, 18, 16, 15, 17),
|
| 33 |
+
(11, 12, 14, 13, 11, 14),
|
| 34 |
+
]
|
| 35 |
+
bridges = [(4, 8), (3, 5), (3, 17)]
|
| 36 |
+
G = nx.Graph(it.chain(*(pairwise(path) for path in ccs + bridges)))
|
| 37 |
+
return G
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_weight_key():
|
| 41 |
+
G = nx.Graph()
|
| 42 |
+
G.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8, 9])
|
| 43 |
+
G.add_edges_from([(3, 8), (1, 2), (2, 3)])
|
| 44 |
+
impossible = {(3, 6), (3, 9)}
|
| 45 |
+
rng = random.Random(0)
|
| 46 |
+
avail_uv = list(set(complement_edges(G)) - impossible)
|
| 47 |
+
avail = [(u, v, {"cost": rng.random()}) for u, v in avail_uv]
|
| 48 |
+
|
| 49 |
+
_augment_and_check(G, k=1)
|
| 50 |
+
_augment_and_check(G, k=1, avail=avail_uv)
|
| 51 |
+
_augment_and_check(G, k=1, avail=avail, weight="cost")
|
| 52 |
+
|
| 53 |
+
_check_augmentations(G, avail, weight="cost")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def test_is_locally_k_edge_connected_exceptions():
|
| 57 |
+
pytest.raises(nx.NetworkXNotImplemented, is_k_edge_connected, nx.DiGraph(), k=0)
|
| 58 |
+
pytest.raises(nx.NetworkXNotImplemented, is_k_edge_connected, nx.MultiGraph(), k=0)
|
| 59 |
+
pytest.raises(ValueError, is_k_edge_connected, nx.Graph(), k=0)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def test_is_k_edge_connected():
|
| 63 |
+
G = nx.barbell_graph(10, 0)
|
| 64 |
+
assert is_k_edge_connected(G, k=1)
|
| 65 |
+
assert not is_k_edge_connected(G, k=2)
|
| 66 |
+
|
| 67 |
+
G = nx.Graph()
|
| 68 |
+
G.add_nodes_from([5, 15])
|
| 69 |
+
assert not is_k_edge_connected(G, k=1)
|
| 70 |
+
assert not is_k_edge_connected(G, k=2)
|
| 71 |
+
|
| 72 |
+
G = nx.complete_graph(5)
|
| 73 |
+
assert is_k_edge_connected(G, k=1)
|
| 74 |
+
assert is_k_edge_connected(G, k=2)
|
| 75 |
+
assert is_k_edge_connected(G, k=3)
|
| 76 |
+
assert is_k_edge_connected(G, k=4)
|
| 77 |
+
|
| 78 |
+
G = nx.compose(nx.complete_graph([0, 1, 2]), nx.complete_graph([3, 4, 5]))
|
| 79 |
+
assert not is_k_edge_connected(G, k=1)
|
| 80 |
+
assert not is_k_edge_connected(G, k=2)
|
| 81 |
+
assert not is_k_edge_connected(G, k=3)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def test_is_k_edge_connected_exceptions():
|
| 85 |
+
pytest.raises(
|
| 86 |
+
nx.NetworkXNotImplemented, is_locally_k_edge_connected, nx.DiGraph(), 1, 2, k=0
|
| 87 |
+
)
|
| 88 |
+
pytest.raises(
|
| 89 |
+
nx.NetworkXNotImplemented,
|
| 90 |
+
is_locally_k_edge_connected,
|
| 91 |
+
nx.MultiGraph(),
|
| 92 |
+
1,
|
| 93 |
+
2,
|
| 94 |
+
k=0,
|
| 95 |
+
)
|
| 96 |
+
pytest.raises(ValueError, is_locally_k_edge_connected, nx.Graph(), 1, 2, k=0)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def test_is_locally_k_edge_connected():
|
| 100 |
+
G = nx.barbell_graph(10, 0)
|
| 101 |
+
assert is_locally_k_edge_connected(G, 5, 15, k=1)
|
| 102 |
+
assert not is_locally_k_edge_connected(G, 5, 15, k=2)
|
| 103 |
+
|
| 104 |
+
G = nx.Graph()
|
| 105 |
+
G.add_nodes_from([5, 15])
|
| 106 |
+
assert not is_locally_k_edge_connected(G, 5, 15, k=2)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_null_graph():
|
| 110 |
+
G = nx.Graph()
|
| 111 |
+
_check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def test_cliques():
|
| 115 |
+
for n in range(1, 10):
|
| 116 |
+
G = nx.complete_graph(n)
|
| 117 |
+
_check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def test_clique_and_node():
|
| 121 |
+
for n in range(1, 10):
|
| 122 |
+
G = nx.complete_graph(n)
|
| 123 |
+
G.add_node(n + 1)
|
| 124 |
+
_check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def test_point_graph():
|
| 128 |
+
G = nx.Graph()
|
| 129 |
+
G.add_node(1)
|
| 130 |
+
_check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def test_edgeless_graph():
|
| 134 |
+
G = nx.Graph()
|
| 135 |
+
G.add_nodes_from([1, 2, 3, 4])
|
| 136 |
+
_check_augmentations(G)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def test_invalid_k():
|
| 140 |
+
G = nx.Graph()
|
| 141 |
+
pytest.raises(ValueError, list, k_edge_augmentation(G, k=-1))
|
| 142 |
+
pytest.raises(ValueError, list, k_edge_augmentation(G, k=0))
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_unfeasible():
|
| 146 |
+
G = tarjan_bridge_graph()
|
| 147 |
+
pytest.raises(nx.NetworkXUnfeasible, list, k_edge_augmentation(G, k=1, avail=[]))
|
| 148 |
+
|
| 149 |
+
pytest.raises(nx.NetworkXUnfeasible, list, k_edge_augmentation(G, k=2, avail=[]))
|
| 150 |
+
|
| 151 |
+
pytest.raises(
|
| 152 |
+
nx.NetworkXUnfeasible, list, k_edge_augmentation(G, k=2, avail=[(7, 9)])
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# partial solutions should not error if real solutions are infeasible
|
| 156 |
+
aug_edges = list(k_edge_augmentation(G, k=2, avail=[(7, 9)], partial=True))
|
| 157 |
+
assert aug_edges == [(7, 9)]
|
| 158 |
+
|
| 159 |
+
_check_augmentations(G, avail=[], max_k=MAX_EFFICIENT_K + 2)
|
| 160 |
+
|
| 161 |
+
_check_augmentations(G, avail=[(7, 9)], max_k=MAX_EFFICIENT_K + 2)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def test_tarjan():
|
| 165 |
+
G = tarjan_bridge_graph()
|
| 166 |
+
|
| 167 |
+
aug_edges = set(_augment_and_check(G, k=2)[0])
|
| 168 |
+
print(f"aug_edges = {aug_edges!r}")
|
| 169 |
+
# can't assert edge exactly equality due to non-determinant edge order
|
| 170 |
+
# but we do know the size of the solution must be 3
|
| 171 |
+
assert len(aug_edges) == 3
|
| 172 |
+
|
| 173 |
+
avail = [
|
| 174 |
+
(9, 7),
|
| 175 |
+
(8, 5),
|
| 176 |
+
(2, 10),
|
| 177 |
+
(6, 13),
|
| 178 |
+
(11, 18),
|
| 179 |
+
(1, 17),
|
| 180 |
+
(2, 3),
|
| 181 |
+
(16, 17),
|
| 182 |
+
(18, 14),
|
| 183 |
+
(15, 14),
|
| 184 |
+
]
|
| 185 |
+
aug_edges = set(_augment_and_check(G, avail=avail, k=2)[0])
|
| 186 |
+
|
| 187 |
+
# Can't assert exact length since approximation depends on the order of a
|
| 188 |
+
# dict traversal.
|
| 189 |
+
assert len(aug_edges) <= 3 * 2
|
| 190 |
+
|
| 191 |
+
_check_augmentations(G, avail)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def test_configuration():
|
| 195 |
+
# seeds = [2718183590, 2470619828, 1694705158, 3001036531, 2401251497]
|
| 196 |
+
seeds = [1001, 1002, 1003, 1004]
|
| 197 |
+
for seed in seeds:
|
| 198 |
+
deg_seq = nx.random_powerlaw_tree_sequence(20, seed=seed, tries=5000)
|
| 199 |
+
G = nx.Graph(nx.configuration_model(deg_seq, seed=seed))
|
| 200 |
+
G.remove_edges_from(nx.selfloop_edges(G))
|
| 201 |
+
_check_augmentations(G)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def test_shell():
|
| 205 |
+
# seeds = [2057382236, 3331169846, 1840105863, 476020778, 2247498425]
|
| 206 |
+
seeds = [18]
|
| 207 |
+
for seed in seeds:
|
| 208 |
+
constructor = [(12, 70, 0.8), (15, 40, 0.6)]
|
| 209 |
+
G = nx.random_shell_graph(constructor, seed=seed)
|
| 210 |
+
_check_augmentations(G)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def test_karate():
|
| 214 |
+
G = nx.karate_club_graph()
|
| 215 |
+
_check_augmentations(G)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def test_star():
|
| 219 |
+
G = nx.star_graph(3)
|
| 220 |
+
_check_augmentations(G)
|
| 221 |
+
|
| 222 |
+
G = nx.star_graph(5)
|
| 223 |
+
_check_augmentations(G)
|
| 224 |
+
|
| 225 |
+
G = nx.star_graph(10)
|
| 226 |
+
_check_augmentations(G)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def test_barbell():
|
| 230 |
+
G = nx.barbell_graph(5, 0)
|
| 231 |
+
_check_augmentations(G)
|
| 232 |
+
|
| 233 |
+
G = nx.barbell_graph(5, 2)
|
| 234 |
+
_check_augmentations(G)
|
| 235 |
+
|
| 236 |
+
G = nx.barbell_graph(5, 3)
|
| 237 |
+
_check_augmentations(G)
|
| 238 |
+
|
| 239 |
+
G = nx.barbell_graph(5, 4)
|
| 240 |
+
_check_augmentations(G)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def test_bridge():
|
| 244 |
+
G = nx.Graph([(2393, 2257), (2393, 2685), (2685, 2257), (1758, 2257)])
|
| 245 |
+
_check_augmentations(G)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def test_gnp_augmentation():
|
| 249 |
+
rng = random.Random(0)
|
| 250 |
+
G = nx.gnp_random_graph(30, 0.005, seed=0)
|
| 251 |
+
# Randomly make edges available
|
| 252 |
+
avail = {
|
| 253 |
+
(u, v): 1 + rng.random() for u, v in complement_edges(G) if rng.random() < 0.25
|
| 254 |
+
}
|
| 255 |
+
_check_augmentations(G, avail)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def _assert_solution_properties(G, aug_edges, avail_dict=None):
|
| 259 |
+
"""Checks that aug_edges are consistently formatted"""
|
| 260 |
+
if avail_dict is not None:
|
| 261 |
+
assert all(
|
| 262 |
+
e in avail_dict for e in aug_edges
|
| 263 |
+
), "when avail is specified aug-edges should be in avail"
|
| 264 |
+
|
| 265 |
+
unique_aug = set(map(tuple, map(sorted, aug_edges)))
|
| 266 |
+
unique_aug = list(map(tuple, map(sorted, aug_edges)))
|
| 267 |
+
assert len(aug_edges) == len(unique_aug), "edges should be unique"
|
| 268 |
+
|
| 269 |
+
assert not any(u == v for u, v in unique_aug), "should be no self-edges"
|
| 270 |
+
|
| 271 |
+
assert not any(
|
| 272 |
+
G.has_edge(u, v) for u, v in unique_aug
|
| 273 |
+
), "aug edges and G.edges should be disjoint"
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def _augment_and_check(
|
| 277 |
+
G, k, avail=None, weight=None, verbose=False, orig_k=None, max_aug_k=None
|
| 278 |
+
):
|
| 279 |
+
"""
|
| 280 |
+
Does one specific augmentation and checks for properties of the result
|
| 281 |
+
"""
|
| 282 |
+
if orig_k is None:
|
| 283 |
+
try:
|
| 284 |
+
orig_k = nx.edge_connectivity(G)
|
| 285 |
+
except nx.NetworkXPointlessConcept:
|
| 286 |
+
orig_k = 0
|
| 287 |
+
info = {}
|
| 288 |
+
try:
|
| 289 |
+
if avail is not None:
|
| 290 |
+
# ensure avail is in dict form
|
| 291 |
+
avail_dict = dict(zip(*_unpack_available_edges(avail, weight=weight)))
|
| 292 |
+
else:
|
| 293 |
+
avail_dict = None
|
| 294 |
+
try:
|
| 295 |
+
# Find the augmentation if possible
|
| 296 |
+
generator = nx.k_edge_augmentation(G, k=k, weight=weight, avail=avail)
|
| 297 |
+
assert not isinstance(generator, list), "should always return an iter"
|
| 298 |
+
aug_edges = []
|
| 299 |
+
for edge in generator:
|
| 300 |
+
aug_edges.append(edge)
|
| 301 |
+
except nx.NetworkXUnfeasible:
|
| 302 |
+
infeasible = True
|
| 303 |
+
info["infeasible"] = True
|
| 304 |
+
assert len(aug_edges) == 0, "should not generate anything if unfeasible"
|
| 305 |
+
|
| 306 |
+
if avail is None:
|
| 307 |
+
n_nodes = G.number_of_nodes()
|
| 308 |
+
assert n_nodes <= k, (
|
| 309 |
+
"unconstrained cases are only unfeasible if |V| <= k. "
|
| 310 |
+
f"Got |V|={n_nodes} and k={k}"
|
| 311 |
+
)
|
| 312 |
+
else:
|
| 313 |
+
if max_aug_k is None:
|
| 314 |
+
G_aug_all = G.copy()
|
| 315 |
+
G_aug_all.add_edges_from(avail_dict.keys())
|
| 316 |
+
try:
|
| 317 |
+
max_aug_k = nx.edge_connectivity(G_aug_all)
|
| 318 |
+
except nx.NetworkXPointlessConcept:
|
| 319 |
+
max_aug_k = 0
|
| 320 |
+
|
| 321 |
+
assert max_aug_k < k, (
|
| 322 |
+
"avail should only be unfeasible if using all edges "
|
| 323 |
+
"does not achieve k-edge-connectivity"
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
# Test for a partial solution
|
| 327 |
+
partial_edges = list(
|
| 328 |
+
nx.k_edge_augmentation(G, k=k, weight=weight, partial=True, avail=avail)
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
info["n_partial_edges"] = len(partial_edges)
|
| 332 |
+
|
| 333 |
+
if avail_dict is None:
|
| 334 |
+
assert set(partial_edges) == set(
|
| 335 |
+
complement_edges(G)
|
| 336 |
+
), "unweighted partial solutions should be the complement"
|
| 337 |
+
elif len(avail_dict) > 0:
|
| 338 |
+
H = G.copy()
|
| 339 |
+
|
| 340 |
+
# Find the partial / full augmented connectivity
|
| 341 |
+
H.add_edges_from(partial_edges)
|
| 342 |
+
partial_conn = nx.edge_connectivity(H)
|
| 343 |
+
|
| 344 |
+
H.add_edges_from(set(avail_dict.keys()))
|
| 345 |
+
full_conn = nx.edge_connectivity(H)
|
| 346 |
+
|
| 347 |
+
# Full connectivity should be no better than our partial
|
| 348 |
+
# solution.
|
| 349 |
+
assert (
|
| 350 |
+
partial_conn == full_conn
|
| 351 |
+
), "adding more edges should not increase k-conn"
|
| 352 |
+
|
| 353 |
+
# Find the new edge-connectivity after adding the augmenting edges
|
| 354 |
+
aug_edges = partial_edges
|
| 355 |
+
else:
|
| 356 |
+
infeasible = False
|
| 357 |
+
|
| 358 |
+
# Find the weight of the augmentation
|
| 359 |
+
num_edges = len(aug_edges)
|
| 360 |
+
if avail is not None:
|
| 361 |
+
total_weight = sum(avail_dict[e] for e in aug_edges)
|
| 362 |
+
else:
|
| 363 |
+
total_weight = num_edges
|
| 364 |
+
|
| 365 |
+
info["total_weight"] = total_weight
|
| 366 |
+
info["num_edges"] = num_edges
|
| 367 |
+
|
| 368 |
+
# Find the new edge-connectivity after adding the augmenting edges
|
| 369 |
+
G_aug = G.copy()
|
| 370 |
+
G_aug.add_edges_from(aug_edges)
|
| 371 |
+
try:
|
| 372 |
+
aug_k = nx.edge_connectivity(G_aug)
|
| 373 |
+
except nx.NetworkXPointlessConcept:
|
| 374 |
+
aug_k = 0
|
| 375 |
+
info["aug_k"] = aug_k
|
| 376 |
+
|
| 377 |
+
# Do checks
|
| 378 |
+
if not infeasible and orig_k < k:
|
| 379 |
+
assert info["aug_k"] >= k, f"connectivity should increase to k={k} or more"
|
| 380 |
+
|
| 381 |
+
assert info["aug_k"] >= orig_k, "augmenting should never reduce connectivity"
|
| 382 |
+
|
| 383 |
+
_assert_solution_properties(G, aug_edges, avail_dict)
|
| 384 |
+
|
| 385 |
+
except Exception:
|
| 386 |
+
info["failed"] = True
|
| 387 |
+
print(f"edges = {list(G.edges())}")
|
| 388 |
+
print(f"nodes = {list(G.nodes())}")
|
| 389 |
+
print(f"aug_edges = {list(aug_edges)}")
|
| 390 |
+
print(f"info = {info}")
|
| 391 |
+
raise
|
| 392 |
+
else:
|
| 393 |
+
if verbose:
|
| 394 |
+
print(f"info = {info}")
|
| 395 |
+
|
| 396 |
+
if infeasible:
|
| 397 |
+
aug_edges = None
|
| 398 |
+
return aug_edges, info
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def _check_augmentations(G, avail=None, max_k=None, weight=None, verbose=False):
|
| 402 |
+
"""Helper to check weighted/unweighted cases with multiple values of k"""
|
| 403 |
+
# Using all available edges, find the maximum edge-connectivity
|
| 404 |
+
try:
|
| 405 |
+
orig_k = nx.edge_connectivity(G)
|
| 406 |
+
except nx.NetworkXPointlessConcept:
|
| 407 |
+
orig_k = 0
|
| 408 |
+
|
| 409 |
+
if avail is not None:
|
| 410 |
+
all_aug_edges = _unpack_available_edges(avail, weight=weight)[0]
|
| 411 |
+
G_aug_all = G.copy()
|
| 412 |
+
G_aug_all.add_edges_from(all_aug_edges)
|
| 413 |
+
try:
|
| 414 |
+
max_aug_k = nx.edge_connectivity(G_aug_all)
|
| 415 |
+
except nx.NetworkXPointlessConcept:
|
| 416 |
+
max_aug_k = 0
|
| 417 |
+
else:
|
| 418 |
+
max_aug_k = G.number_of_nodes() - 1
|
| 419 |
+
|
| 420 |
+
if max_k is None:
|
| 421 |
+
max_k = min(4, max_aug_k)
|
| 422 |
+
|
| 423 |
+
avail_uniform = {e: 1 for e in complement_edges(G)}
|
| 424 |
+
|
| 425 |
+
if verbose:
|
| 426 |
+
print("\n=== CHECK_AUGMENTATION ===")
|
| 427 |
+
print(f"G.number_of_nodes = {G.number_of_nodes()!r}")
|
| 428 |
+
print(f"G.number_of_edges = {G.number_of_edges()!r}")
|
| 429 |
+
print(f"max_k = {max_k!r}")
|
| 430 |
+
print(f"max_aug_k = {max_aug_k!r}")
|
| 431 |
+
print(f"orig_k = {orig_k!r}")
|
| 432 |
+
|
| 433 |
+
# check augmentation for multiple values of k
|
| 434 |
+
for k in range(1, max_k + 1):
|
| 435 |
+
if verbose:
|
| 436 |
+
print("---------------")
|
| 437 |
+
print(f"Checking k = {k}")
|
| 438 |
+
|
| 439 |
+
# Check the unweighted version
|
| 440 |
+
if verbose:
|
| 441 |
+
print("unweighted case")
|
| 442 |
+
aug_edges1, info1 = _augment_and_check(G, k=k, verbose=verbose, orig_k=orig_k)
|
| 443 |
+
|
| 444 |
+
# Check that the weighted version with all available edges and uniform
|
| 445 |
+
# weights gives a similar solution to the unweighted case.
|
| 446 |
+
if verbose:
|
| 447 |
+
print("weighted uniform case")
|
| 448 |
+
aug_edges2, info2 = _augment_and_check(
|
| 449 |
+
G,
|
| 450 |
+
k=k,
|
| 451 |
+
avail=avail_uniform,
|
| 452 |
+
verbose=verbose,
|
| 453 |
+
orig_k=orig_k,
|
| 454 |
+
max_aug_k=G.number_of_nodes() - 1,
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
# Check the weighted version
|
| 458 |
+
if avail is not None:
|
| 459 |
+
if verbose:
|
| 460 |
+
print("weighted case")
|
| 461 |
+
aug_edges3, info3 = _augment_and_check(
|
| 462 |
+
G,
|
| 463 |
+
k=k,
|
| 464 |
+
avail=avail,
|
| 465 |
+
weight=weight,
|
| 466 |
+
verbose=verbose,
|
| 467 |
+
max_aug_k=max_aug_k,
|
| 468 |
+
orig_k=orig_k,
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
if aug_edges1 is not None:
|
| 472 |
+
# Check approximation ratios
|
| 473 |
+
if k == 1:
|
| 474 |
+
# when k=1, both solutions should be optimal
|
| 475 |
+
assert info2["total_weight"] == info1["total_weight"]
|
| 476 |
+
if k == 2:
|
| 477 |
+
# when k=2, the weighted version is an approximation
|
| 478 |
+
if orig_k == 0:
|
| 479 |
+
# the approximation ratio is 3 if G is not connected
|
| 480 |
+
assert info2["total_weight"] <= info1["total_weight"] * 3
|
| 481 |
+
else:
|
| 482 |
+
# the approximation ratio is 2 if G is was connected
|
| 483 |
+
assert info2["total_weight"] <= info1["total_weight"] * 2
|
| 484 |
+
_check_unconstrained_bridge_property(G, info1)
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def _check_unconstrained_bridge_property(G, info1):
|
| 488 |
+
# Check Theorem 5 from Eswaran and Tarjan. (1975) Augmentation problems
|
| 489 |
+
import math
|
| 490 |
+
|
| 491 |
+
bridge_ccs = list(nx.connectivity.bridge_components(G))
|
| 492 |
+
# condense G into an forest C
|
| 493 |
+
C = collapse(G, bridge_ccs)
|
| 494 |
+
|
| 495 |
+
p = len([n for n, d in C.degree() if d == 1]) # leafs
|
| 496 |
+
q = len([n for n, d in C.degree() if d == 0]) # isolated
|
| 497 |
+
if p + q > 1:
|
| 498 |
+
size_target = math.ceil(p / 2) + q
|
| 499 |
+
size_aug = info1["num_edges"]
|
| 500 |
+
assert (
|
| 501 |
+
size_aug == size_target
|
| 502 |
+
), "augmentation size is different from what theory predicts"
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_edge_kcomponents.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import itertools as it
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
from networkx.algorithms.connectivity import EdgeComponentAuxGraph, bridge_components
|
| 7 |
+
from networkx.algorithms.connectivity.edge_kcomponents import general_k_edge_subgraphs
|
| 8 |
+
from networkx.utils import pairwise
|
| 9 |
+
|
| 10 |
+
# ----------------
|
| 11 |
+
# Helper functions
|
| 12 |
+
# ----------------
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def fset(list_of_sets):
|
| 16 |
+
"""allows == to be used for list of sets"""
|
| 17 |
+
return set(map(frozenset, list_of_sets))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _assert_subgraph_edge_connectivity(G, ccs_subgraph, k):
|
| 21 |
+
"""
|
| 22 |
+
tests properties of k-edge-connected subgraphs
|
| 23 |
+
|
| 24 |
+
the actual edge connectivity should be no less than k unless the cc is a
|
| 25 |
+
single node.
|
| 26 |
+
"""
|
| 27 |
+
for cc in ccs_subgraph:
|
| 28 |
+
C = G.subgraph(cc)
|
| 29 |
+
if len(cc) > 1:
|
| 30 |
+
connectivity = nx.edge_connectivity(C)
|
| 31 |
+
assert connectivity >= k
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _memo_connectivity(G, u, v, memo):
|
| 35 |
+
edge = (u, v)
|
| 36 |
+
if edge in memo:
|
| 37 |
+
return memo[edge]
|
| 38 |
+
if not G.is_directed():
|
| 39 |
+
redge = (v, u)
|
| 40 |
+
if redge in memo:
|
| 41 |
+
return memo[redge]
|
| 42 |
+
memo[edge] = nx.edge_connectivity(G, *edge)
|
| 43 |
+
return memo[edge]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _all_pairs_connectivity(G, cc, k, memo):
|
| 47 |
+
# Brute force check
|
| 48 |
+
for u, v in it.combinations(cc, 2):
|
| 49 |
+
# Use a memoization dict to save on computation
|
| 50 |
+
connectivity = _memo_connectivity(G, u, v, memo)
|
| 51 |
+
if G.is_directed():
|
| 52 |
+
connectivity = min(connectivity, _memo_connectivity(G, v, u, memo))
|
| 53 |
+
assert connectivity >= k
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _assert_local_cc_edge_connectivity(G, ccs_local, k, memo):
|
| 57 |
+
"""
|
| 58 |
+
tests properties of k-edge-connected components
|
| 59 |
+
|
| 60 |
+
the local edge connectivity between each pair of nodes in the original
|
| 61 |
+
graph should be no less than k unless the cc is a single node.
|
| 62 |
+
"""
|
| 63 |
+
for cc in ccs_local:
|
| 64 |
+
if len(cc) > 1:
|
| 65 |
+
# Strategy for testing a bit faster: If the subgraph has high edge
|
| 66 |
+
# connectivity then it must have local connectivity
|
| 67 |
+
C = G.subgraph(cc)
|
| 68 |
+
connectivity = nx.edge_connectivity(C)
|
| 69 |
+
if connectivity < k:
|
| 70 |
+
# Otherwise do the brute force (with memoization) check
|
| 71 |
+
_all_pairs_connectivity(G, cc, k, memo)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# Helper function
|
| 75 |
+
def _check_edge_connectivity(G):
|
| 76 |
+
"""
|
| 77 |
+
Helper - generates all k-edge-components using the aux graph. Checks the
|
| 78 |
+
both local and subgraph edge connectivity of each cc. Also checks that
|
| 79 |
+
alternate methods of computing the k-edge-ccs generate the same result.
|
| 80 |
+
"""
|
| 81 |
+
# Construct the auxiliary graph that can be used to make each k-cc or k-sub
|
| 82 |
+
aux_graph = EdgeComponentAuxGraph.construct(G)
|
| 83 |
+
|
| 84 |
+
# memoize the local connectivity in this graph
|
| 85 |
+
memo = {}
|
| 86 |
+
|
| 87 |
+
for k in it.count(1):
|
| 88 |
+
# Test "local" k-edge-components and k-edge-subgraphs
|
| 89 |
+
ccs_local = fset(aux_graph.k_edge_components(k))
|
| 90 |
+
ccs_subgraph = fset(aux_graph.k_edge_subgraphs(k))
|
| 91 |
+
|
| 92 |
+
# Check connectivity properties that should be guaranteed by the
|
| 93 |
+
# algorithms.
|
| 94 |
+
_assert_local_cc_edge_connectivity(G, ccs_local, k, memo)
|
| 95 |
+
_assert_subgraph_edge_connectivity(G, ccs_subgraph, k)
|
| 96 |
+
|
| 97 |
+
if k == 1 or k == 2 and not G.is_directed():
|
| 98 |
+
assert (
|
| 99 |
+
ccs_local == ccs_subgraph
|
| 100 |
+
), "Subgraphs and components should be the same when k == 1 or (k == 2 and not G.directed())"
|
| 101 |
+
|
| 102 |
+
if G.is_directed():
|
| 103 |
+
# Test special case methods are the same as the aux graph
|
| 104 |
+
if k == 1:
|
| 105 |
+
alt_sccs = fset(nx.strongly_connected_components(G))
|
| 106 |
+
assert alt_sccs == ccs_local, "k=1 failed alt"
|
| 107 |
+
assert alt_sccs == ccs_subgraph, "k=1 failed alt"
|
| 108 |
+
else:
|
| 109 |
+
# Test special case methods are the same as the aux graph
|
| 110 |
+
if k == 1:
|
| 111 |
+
alt_ccs = fset(nx.connected_components(G))
|
| 112 |
+
assert alt_ccs == ccs_local, "k=1 failed alt"
|
| 113 |
+
assert alt_ccs == ccs_subgraph, "k=1 failed alt"
|
| 114 |
+
elif k == 2:
|
| 115 |
+
alt_bridge_ccs = fset(bridge_components(G))
|
| 116 |
+
assert alt_bridge_ccs == ccs_local, "k=2 failed alt"
|
| 117 |
+
assert alt_bridge_ccs == ccs_subgraph, "k=2 failed alt"
|
| 118 |
+
# if new methods for k == 3 or k == 4 are implemented add them here
|
| 119 |
+
|
| 120 |
+
# Check the general subgraph method works by itself
|
| 121 |
+
alt_subgraph_ccs = fset(
|
| 122 |
+
[set(C.nodes()) for C in general_k_edge_subgraphs(G, k=k)]
|
| 123 |
+
)
|
| 124 |
+
assert alt_subgraph_ccs == ccs_subgraph, "alt subgraph method failed"
|
| 125 |
+
|
| 126 |
+
# Stop once k is larger than all special case methods
|
| 127 |
+
# and we cannot break down ccs any further.
|
| 128 |
+
if k > 2 and all(len(cc) == 1 for cc in ccs_local):
|
| 129 |
+
break
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# ----------------
|
| 133 |
+
# Misc tests
|
| 134 |
+
# ----------------
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def test_zero_k_exception():
|
| 138 |
+
G = nx.Graph()
|
| 139 |
+
# functions that return generators error immediately
|
| 140 |
+
pytest.raises(ValueError, nx.k_edge_components, G, k=0)
|
| 141 |
+
pytest.raises(ValueError, nx.k_edge_subgraphs, G, k=0)
|
| 142 |
+
|
| 143 |
+
# actual generators only error when you get the first item
|
| 144 |
+
aux_graph = EdgeComponentAuxGraph.construct(G)
|
| 145 |
+
pytest.raises(ValueError, list, aux_graph.k_edge_components(k=0))
|
| 146 |
+
pytest.raises(ValueError, list, aux_graph.k_edge_subgraphs(k=0))
|
| 147 |
+
|
| 148 |
+
pytest.raises(ValueError, list, general_k_edge_subgraphs(G, k=0))
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def test_empty_input():
|
| 152 |
+
G = nx.Graph()
|
| 153 |
+
assert [] == list(nx.k_edge_components(G, k=5))
|
| 154 |
+
assert [] == list(nx.k_edge_subgraphs(G, k=5))
|
| 155 |
+
|
| 156 |
+
G = nx.DiGraph()
|
| 157 |
+
assert [] == list(nx.k_edge_components(G, k=5))
|
| 158 |
+
assert [] == list(nx.k_edge_subgraphs(G, k=5))
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def test_not_implemented():
|
| 162 |
+
G = nx.MultiGraph()
|
| 163 |
+
pytest.raises(nx.NetworkXNotImplemented, EdgeComponentAuxGraph.construct, G)
|
| 164 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.k_edge_components, G, k=2)
|
| 165 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.k_edge_subgraphs, G, k=2)
|
| 166 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 167 |
+
next(bridge_components(G))
|
| 168 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 169 |
+
next(bridge_components(nx.DiGraph()))
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def test_general_k_edge_subgraph_quick_return():
|
| 173 |
+
# tests quick return optimization
|
| 174 |
+
G = nx.Graph()
|
| 175 |
+
G.add_node(0)
|
| 176 |
+
subgraphs = list(general_k_edge_subgraphs(G, k=1))
|
| 177 |
+
assert len(subgraphs) == 1
|
| 178 |
+
for subgraph in subgraphs:
|
| 179 |
+
assert subgraph.number_of_nodes() == 1
|
| 180 |
+
|
| 181 |
+
G.add_node(1)
|
| 182 |
+
subgraphs = list(general_k_edge_subgraphs(G, k=1))
|
| 183 |
+
assert len(subgraphs) == 2
|
| 184 |
+
for subgraph in subgraphs:
|
| 185 |
+
assert subgraph.number_of_nodes() == 1
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
# ----------------
|
| 189 |
+
# Undirected tests
|
| 190 |
+
# ----------------
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def test_random_gnp():
|
| 194 |
+
# seeds = [1550709854, 1309423156, 4208992358, 2785630813, 1915069929]
|
| 195 |
+
seeds = [12, 13]
|
| 196 |
+
|
| 197 |
+
for seed in seeds:
|
| 198 |
+
G = nx.gnp_random_graph(20, 0.2, seed=seed)
|
| 199 |
+
_check_edge_connectivity(G)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def test_configuration():
|
| 203 |
+
# seeds = [2718183590, 2470619828, 1694705158, 3001036531, 2401251497]
|
| 204 |
+
seeds = [14, 15]
|
| 205 |
+
for seed in seeds:
|
| 206 |
+
deg_seq = nx.random_powerlaw_tree_sequence(20, seed=seed, tries=5000)
|
| 207 |
+
G = nx.Graph(nx.configuration_model(deg_seq, seed=seed))
|
| 208 |
+
G.remove_edges_from(nx.selfloop_edges(G))
|
| 209 |
+
_check_edge_connectivity(G)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def test_shell():
|
| 213 |
+
# seeds = [2057382236, 3331169846, 1840105863, 476020778, 2247498425]
|
| 214 |
+
seeds = [20]
|
| 215 |
+
for seed in seeds:
|
| 216 |
+
constructor = [(12, 70, 0.8), (15, 40, 0.6)]
|
| 217 |
+
G = nx.random_shell_graph(constructor, seed=seed)
|
| 218 |
+
_check_edge_connectivity(G)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def test_karate():
|
| 222 |
+
G = nx.karate_club_graph()
|
| 223 |
+
_check_edge_connectivity(G)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def test_tarjan_bridge():
|
| 227 |
+
# graph from tarjan paper
|
| 228 |
+
# RE Tarjan - "A note on finding the bridges of a graph"
|
| 229 |
+
# Information Processing Letters, 1974 - Elsevier
|
| 230 |
+
# doi:10.1016/0020-0190(74)90003-9.
|
| 231 |
+
# define 2-connected components and bridges
|
| 232 |
+
ccs = [
|
| 233 |
+
(1, 2, 4, 3, 1, 4),
|
| 234 |
+
(5, 6, 7, 5),
|
| 235 |
+
(8, 9, 10, 8),
|
| 236 |
+
(17, 18, 16, 15, 17),
|
| 237 |
+
(11, 12, 14, 13, 11, 14),
|
| 238 |
+
]
|
| 239 |
+
bridges = [(4, 8), (3, 5), (3, 17)]
|
| 240 |
+
G = nx.Graph(it.chain(*(pairwise(path) for path in ccs + bridges)))
|
| 241 |
+
_check_edge_connectivity(G)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def test_bridge_cc():
|
| 245 |
+
# define 2-connected components and bridges
|
| 246 |
+
cc2 = [(1, 2, 4, 3, 1, 4), (8, 9, 10, 8), (11, 12, 13, 11)]
|
| 247 |
+
bridges = [(4, 8), (3, 5), (20, 21), (22, 23, 24)]
|
| 248 |
+
G = nx.Graph(it.chain(*(pairwise(path) for path in cc2 + bridges)))
|
| 249 |
+
bridge_ccs = fset(bridge_components(G))
|
| 250 |
+
target_ccs = fset(
|
| 251 |
+
[{1, 2, 3, 4}, {5}, {8, 9, 10}, {11, 12, 13}, {20}, {21}, {22}, {23}, {24}]
|
| 252 |
+
)
|
| 253 |
+
assert bridge_ccs == target_ccs
|
| 254 |
+
_check_edge_connectivity(G)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def test_undirected_aux_graph():
|
| 258 |
+
# Graph similar to the one in
|
| 259 |
+
# http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
|
| 260 |
+
a, b, c, d, e, f, g, h, i = "abcdefghi"
|
| 261 |
+
paths = [
|
| 262 |
+
(a, d, b, f, c),
|
| 263 |
+
(a, e, b),
|
| 264 |
+
(a, e, b, c, g, b, a),
|
| 265 |
+
(c, b),
|
| 266 |
+
(f, g, f),
|
| 267 |
+
(h, i),
|
| 268 |
+
]
|
| 269 |
+
G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
|
| 270 |
+
aux_graph = EdgeComponentAuxGraph.construct(G)
|
| 271 |
+
|
| 272 |
+
components_1 = fset(aux_graph.k_edge_subgraphs(k=1))
|
| 273 |
+
target_1 = fset([{a, b, c, d, e, f, g}, {h, i}])
|
| 274 |
+
assert target_1 == components_1
|
| 275 |
+
|
| 276 |
+
# Check that the undirected case for k=1 agrees with CCs
|
| 277 |
+
alt_1 = fset(nx.k_edge_subgraphs(G, k=1))
|
| 278 |
+
assert alt_1 == components_1
|
| 279 |
+
|
| 280 |
+
components_2 = fset(aux_graph.k_edge_subgraphs(k=2))
|
| 281 |
+
target_2 = fset([{a, b, c, d, e, f, g}, {h}, {i}])
|
| 282 |
+
assert target_2 == components_2
|
| 283 |
+
|
| 284 |
+
# Check that the undirected case for k=2 agrees with bridge components
|
| 285 |
+
alt_2 = fset(nx.k_edge_subgraphs(G, k=2))
|
| 286 |
+
assert alt_2 == components_2
|
| 287 |
+
|
| 288 |
+
components_3 = fset(aux_graph.k_edge_subgraphs(k=3))
|
| 289 |
+
target_3 = fset([{a}, {b, c, f, g}, {d}, {e}, {h}, {i}])
|
| 290 |
+
assert target_3 == components_3
|
| 291 |
+
|
| 292 |
+
components_4 = fset(aux_graph.k_edge_subgraphs(k=4))
|
| 293 |
+
target_4 = fset([{a}, {b}, {c}, {d}, {e}, {f}, {g}, {h}, {i}])
|
| 294 |
+
assert target_4 == components_4
|
| 295 |
+
|
| 296 |
+
_check_edge_connectivity(G)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def test_local_subgraph_difference():
|
| 300 |
+
paths = [
|
| 301 |
+
(11, 12, 13, 14, 11, 13, 14, 12), # first 4-clique
|
| 302 |
+
(21, 22, 23, 24, 21, 23, 24, 22), # second 4-clique
|
| 303 |
+
# paths connecting each node of the 4 cliques
|
| 304 |
+
(11, 101, 21),
|
| 305 |
+
(12, 102, 22),
|
| 306 |
+
(13, 103, 23),
|
| 307 |
+
(14, 104, 24),
|
| 308 |
+
]
|
| 309 |
+
G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
|
| 310 |
+
aux_graph = EdgeComponentAuxGraph.construct(G)
|
| 311 |
+
|
| 312 |
+
# Each clique is returned separately in k-edge-subgraphs
|
| 313 |
+
subgraph_ccs = fset(aux_graph.k_edge_subgraphs(3))
|
| 314 |
+
subgraph_target = fset(
|
| 315 |
+
[{101}, {102}, {103}, {104}, {21, 22, 23, 24}, {11, 12, 13, 14}]
|
| 316 |
+
)
|
| 317 |
+
assert subgraph_ccs == subgraph_target
|
| 318 |
+
|
| 319 |
+
# But in k-edge-ccs they are returned together
|
| 320 |
+
# because they are locally 3-edge-connected
|
| 321 |
+
local_ccs = fset(aux_graph.k_edge_components(3))
|
| 322 |
+
local_target = fset([{101}, {102}, {103}, {104}, {11, 12, 13, 14, 21, 22, 23, 24}])
|
| 323 |
+
assert local_ccs == local_target
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def test_local_subgraph_difference_directed():
|
| 327 |
+
dipaths = [(1, 2, 3, 4, 1), (1, 3, 1)]
|
| 328 |
+
G = nx.DiGraph(it.chain(*[pairwise(path) for path in dipaths]))
|
| 329 |
+
|
| 330 |
+
assert fset(nx.k_edge_components(G, k=1)) == fset(nx.k_edge_subgraphs(G, k=1))
|
| 331 |
+
|
| 332 |
+
# Unlike undirected graphs, when k=2, for directed graphs there is a case
|
| 333 |
+
# where the k-edge-ccs are not the same as the k-edge-subgraphs.
|
| 334 |
+
# (in directed graphs ccs and subgraphs are the same when k=2)
|
| 335 |
+
assert fset(nx.k_edge_components(G, k=2)) != fset(nx.k_edge_subgraphs(G, k=2))
|
| 336 |
+
|
| 337 |
+
assert fset(nx.k_edge_components(G, k=3)) == fset(nx.k_edge_subgraphs(G, k=3))
|
| 338 |
+
|
| 339 |
+
_check_edge_connectivity(G)
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def test_triangles():
|
| 343 |
+
paths = [
|
| 344 |
+
(11, 12, 13, 11), # first 3-clique
|
| 345 |
+
(21, 22, 23, 21), # second 3-clique
|
| 346 |
+
(11, 21), # connected by an edge
|
| 347 |
+
]
|
| 348 |
+
G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
|
| 349 |
+
|
| 350 |
+
# subgraph and ccs are the same in all cases here
|
| 351 |
+
assert fset(nx.k_edge_components(G, k=1)) == fset(nx.k_edge_subgraphs(G, k=1))
|
| 352 |
+
|
| 353 |
+
assert fset(nx.k_edge_components(G, k=2)) == fset(nx.k_edge_subgraphs(G, k=2))
|
| 354 |
+
|
| 355 |
+
assert fset(nx.k_edge_components(G, k=3)) == fset(nx.k_edge_subgraphs(G, k=3))
|
| 356 |
+
|
| 357 |
+
_check_edge_connectivity(G)
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def test_four_clique():
|
| 361 |
+
paths = [
|
| 362 |
+
(11, 12, 13, 14, 11, 13, 14, 12), # first 4-clique
|
| 363 |
+
(21, 22, 23, 24, 21, 23, 24, 22), # second 4-clique
|
| 364 |
+
# paths connecting the 4 cliques such that they are
|
| 365 |
+
# 3-connected in G, but not in the subgraph.
|
| 366 |
+
# Case where the nodes bridging them do not have degree less than 3.
|
| 367 |
+
(100, 13),
|
| 368 |
+
(12, 100, 22),
|
| 369 |
+
(13, 200, 23),
|
| 370 |
+
(14, 300, 24),
|
| 371 |
+
]
|
| 372 |
+
G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
|
| 373 |
+
|
| 374 |
+
# The subgraphs and ccs are different for k=3
|
| 375 |
+
local_ccs = fset(nx.k_edge_components(G, k=3))
|
| 376 |
+
subgraphs = fset(nx.k_edge_subgraphs(G, k=3))
|
| 377 |
+
assert local_ccs != subgraphs
|
| 378 |
+
|
| 379 |
+
# The cliques ares in the same cc
|
| 380 |
+
clique1 = frozenset(paths[0])
|
| 381 |
+
clique2 = frozenset(paths[1])
|
| 382 |
+
assert clique1.union(clique2).union({100}) in local_ccs
|
| 383 |
+
|
| 384 |
+
# but different subgraphs
|
| 385 |
+
assert clique1 in subgraphs
|
| 386 |
+
assert clique2 in subgraphs
|
| 387 |
+
|
| 388 |
+
assert G.degree(100) == 3
|
| 389 |
+
|
| 390 |
+
_check_edge_connectivity(G)
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def test_five_clique():
|
| 394 |
+
# Make a graph that can be disconnected less than 4 edges, but no node has
|
| 395 |
+
# degree less than 4.
|
| 396 |
+
G = nx.disjoint_union(nx.complete_graph(5), nx.complete_graph(5))
|
| 397 |
+
paths = [
|
| 398 |
+
# add aux-connections
|
| 399 |
+
(1, 100, 6),
|
| 400 |
+
(2, 100, 7),
|
| 401 |
+
(3, 200, 8),
|
| 402 |
+
(4, 200, 100),
|
| 403 |
+
]
|
| 404 |
+
G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
|
| 405 |
+
assert min(dict(nx.degree(G)).values()) == 4
|
| 406 |
+
|
| 407 |
+
# For k=3 they are the same
|
| 408 |
+
assert fset(nx.k_edge_components(G, k=3)) == fset(nx.k_edge_subgraphs(G, k=3))
|
| 409 |
+
|
| 410 |
+
# For k=4 they are the different
|
| 411 |
+
# the aux nodes are in the same CC as clique 1 but no the same subgraph
|
| 412 |
+
assert fset(nx.k_edge_components(G, k=4)) != fset(nx.k_edge_subgraphs(G, k=4))
|
| 413 |
+
|
| 414 |
+
# For k=5 they are not the same
|
| 415 |
+
assert fset(nx.k_edge_components(G, k=5)) != fset(nx.k_edge_subgraphs(G, k=5))
|
| 416 |
+
|
| 417 |
+
# For k=6 they are the same
|
| 418 |
+
assert fset(nx.k_edge_components(G, k=6)) == fset(nx.k_edge_subgraphs(G, k=6))
|
| 419 |
+
_check_edge_connectivity(G)
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
# ----------------
|
| 423 |
+
# Undirected tests
|
| 424 |
+
# ----------------
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
def test_directed_aux_graph():
|
| 428 |
+
# Graph similar to the one in
|
| 429 |
+
# http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
|
| 430 |
+
a, b, c, d, e, f, g, h, i = "abcdefghi"
|
| 431 |
+
dipaths = [
|
| 432 |
+
(a, d, b, f, c),
|
| 433 |
+
(a, e, b),
|
| 434 |
+
(a, e, b, c, g, b, a),
|
| 435 |
+
(c, b),
|
| 436 |
+
(f, g, f),
|
| 437 |
+
(h, i),
|
| 438 |
+
]
|
| 439 |
+
G = nx.DiGraph(it.chain(*[pairwise(path) for path in dipaths]))
|
| 440 |
+
aux_graph = EdgeComponentAuxGraph.construct(G)
|
| 441 |
+
|
| 442 |
+
components_1 = fset(aux_graph.k_edge_subgraphs(k=1))
|
| 443 |
+
target_1 = fset([{a, b, c, d, e, f, g}, {h}, {i}])
|
| 444 |
+
assert target_1 == components_1
|
| 445 |
+
|
| 446 |
+
# Check that the directed case for k=1 agrees with SCCs
|
| 447 |
+
alt_1 = fset(nx.strongly_connected_components(G))
|
| 448 |
+
assert alt_1 == components_1
|
| 449 |
+
|
| 450 |
+
components_2 = fset(aux_graph.k_edge_subgraphs(k=2))
|
| 451 |
+
target_2 = fset([{i}, {e}, {d}, {b, c, f, g}, {h}, {a}])
|
| 452 |
+
assert target_2 == components_2
|
| 453 |
+
|
| 454 |
+
components_3 = fset(aux_graph.k_edge_subgraphs(k=3))
|
| 455 |
+
target_3 = fset([{a}, {b}, {c}, {d}, {e}, {f}, {g}, {h}, {i}])
|
| 456 |
+
assert target_3 == components_3
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def test_random_gnp_directed():
|
| 460 |
+
# seeds = [3894723670, 500186844, 267231174, 2181982262, 1116750056]
|
| 461 |
+
seeds = [21]
|
| 462 |
+
for seed in seeds:
|
| 463 |
+
G = nx.gnp_random_graph(20, 0.2, directed=True, seed=seed)
|
| 464 |
+
_check_edge_connectivity(G)
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
def test_configuration_directed():
|
| 468 |
+
# seeds = [671221681, 2403749451, 124433910, 672335939, 1193127215]
|
| 469 |
+
seeds = [67]
|
| 470 |
+
for seed in seeds:
|
| 471 |
+
deg_seq = nx.random_powerlaw_tree_sequence(20, seed=seed, tries=5000)
|
| 472 |
+
G = nx.DiGraph(nx.configuration_model(deg_seq, seed=seed))
|
| 473 |
+
G.remove_edges_from(nx.selfloop_edges(G))
|
| 474 |
+
_check_edge_connectivity(G)
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def test_shell_directed():
|
| 478 |
+
# seeds = [3134027055, 4079264063, 1350769518, 1405643020, 530038094]
|
| 479 |
+
seeds = [31]
|
| 480 |
+
for seed in seeds:
|
| 481 |
+
constructor = [(12, 70, 0.8), (15, 40, 0.6)]
|
| 482 |
+
G = nx.random_shell_graph(constructor, seed=seed).to_directed()
|
| 483 |
+
_check_edge_connectivity(G)
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def test_karate_directed():
|
| 487 |
+
G = nx.karate_club_graph().to_directed()
|
| 488 |
+
_check_edge_connectivity(G)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_kcomponents.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Test for Moody and White k-components algorithm
|
| 2 |
+
import pytest
|
| 3 |
+
|
| 4 |
+
import networkx as nx
|
| 5 |
+
from networkx.algorithms.connectivity.kcomponents import (
|
| 6 |
+
_consolidate,
|
| 7 |
+
build_k_number_dict,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
##
|
| 11 |
+
# A nice synthetic graph
|
| 12 |
+
##
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def torrents_and_ferraro_graph():
|
| 16 |
+
# Graph from https://arxiv.org/pdf/1503.04476v1 p.26
|
| 17 |
+
G = nx.convert_node_labels_to_integers(
|
| 18 |
+
nx.grid_graph([5, 5]), label_attribute="labels"
|
| 19 |
+
)
|
| 20 |
+
rlabels = nx.get_node_attributes(G, "labels")
|
| 21 |
+
labels = {v: k for k, v in rlabels.items()}
|
| 22 |
+
|
| 23 |
+
for nodes in [(labels[(0, 4)], labels[(1, 4)]), (labels[(3, 4)], labels[(4, 4)])]:
|
| 24 |
+
new_node = G.order() + 1
|
| 25 |
+
# Petersen graph is triconnected
|
| 26 |
+
P = nx.petersen_graph()
|
| 27 |
+
G = nx.disjoint_union(G, P)
|
| 28 |
+
# Add two edges between the grid and P
|
| 29 |
+
G.add_edge(new_node + 1, nodes[0])
|
| 30 |
+
G.add_edge(new_node, nodes[1])
|
| 31 |
+
# K5 is 4-connected
|
| 32 |
+
K = nx.complete_graph(5)
|
| 33 |
+
G = nx.disjoint_union(G, K)
|
| 34 |
+
# Add three edges between P and K5
|
| 35 |
+
G.add_edge(new_node + 2, new_node + 11)
|
| 36 |
+
G.add_edge(new_node + 3, new_node + 12)
|
| 37 |
+
G.add_edge(new_node + 4, new_node + 13)
|
| 38 |
+
# Add another K5 sharing a node
|
| 39 |
+
G = nx.disjoint_union(G, K)
|
| 40 |
+
nbrs = G[new_node + 10]
|
| 41 |
+
G.remove_node(new_node + 10)
|
| 42 |
+
for nbr in nbrs:
|
| 43 |
+
G.add_edge(new_node + 17, nbr)
|
| 44 |
+
# This edge makes the graph biconnected; it's
|
| 45 |
+
# needed because K5s share only one node.
|
| 46 |
+
G.add_edge(new_node + 16, new_node + 8)
|
| 47 |
+
|
| 48 |
+
for nodes in [(labels[(0, 0)], labels[(1, 0)]), (labels[(3, 0)], labels[(4, 0)])]:
|
| 49 |
+
new_node = G.order() + 1
|
| 50 |
+
# Petersen graph is triconnected
|
| 51 |
+
P = nx.petersen_graph()
|
| 52 |
+
G = nx.disjoint_union(G, P)
|
| 53 |
+
# Add two edges between the grid and P
|
| 54 |
+
G.add_edge(new_node + 1, nodes[0])
|
| 55 |
+
G.add_edge(new_node, nodes[1])
|
| 56 |
+
# K5 is 4-connected
|
| 57 |
+
K = nx.complete_graph(5)
|
| 58 |
+
G = nx.disjoint_union(G, K)
|
| 59 |
+
# Add three edges between P and K5
|
| 60 |
+
G.add_edge(new_node + 2, new_node + 11)
|
| 61 |
+
G.add_edge(new_node + 3, new_node + 12)
|
| 62 |
+
G.add_edge(new_node + 4, new_node + 13)
|
| 63 |
+
# Add another K5 sharing two nodes
|
| 64 |
+
G = nx.disjoint_union(G, K)
|
| 65 |
+
nbrs = G[new_node + 10]
|
| 66 |
+
G.remove_node(new_node + 10)
|
| 67 |
+
for nbr in nbrs:
|
| 68 |
+
G.add_edge(new_node + 17, nbr)
|
| 69 |
+
nbrs2 = G[new_node + 9]
|
| 70 |
+
G.remove_node(new_node + 9)
|
| 71 |
+
for nbr in nbrs2:
|
| 72 |
+
G.add_edge(new_node + 18, nbr)
|
| 73 |
+
return G
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_directed():
|
| 77 |
+
with pytest.raises(nx.NetworkXNotImplemented):
|
| 78 |
+
G = nx.gnp_random_graph(10, 0.2, directed=True, seed=42)
|
| 79 |
+
nx.k_components(G)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# Helper function
|
| 83 |
+
def _check_connectivity(G, k_components):
|
| 84 |
+
for k, components in k_components.items():
|
| 85 |
+
if k < 3:
|
| 86 |
+
continue
|
| 87 |
+
# check that k-components have node connectivity >= k.
|
| 88 |
+
for component in components:
|
| 89 |
+
C = G.subgraph(component)
|
| 90 |
+
K = nx.node_connectivity(C)
|
| 91 |
+
assert K >= k
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@pytest.mark.slow
|
| 95 |
+
def test_torrents_and_ferraro_graph():
|
| 96 |
+
G = torrents_and_ferraro_graph()
|
| 97 |
+
result = nx.k_components(G)
|
| 98 |
+
_check_connectivity(G, result)
|
| 99 |
+
|
| 100 |
+
# In this example graph there are 8 3-components, 4 with 15 nodes
|
| 101 |
+
# and 4 with 5 nodes.
|
| 102 |
+
assert len(result[3]) == 8
|
| 103 |
+
assert len([c for c in result[3] if len(c) == 15]) == 4
|
| 104 |
+
assert len([c for c in result[3] if len(c) == 5]) == 4
|
| 105 |
+
# There are also 8 4-components all with 5 nodes.
|
| 106 |
+
assert len(result[4]) == 8
|
| 107 |
+
assert all(len(c) == 5 for c in result[4])
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@pytest.mark.slow
|
| 111 |
+
def test_random_gnp():
|
| 112 |
+
G = nx.gnp_random_graph(50, 0.2, seed=42)
|
| 113 |
+
result = nx.k_components(G)
|
| 114 |
+
_check_connectivity(G, result)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
@pytest.mark.slow
|
| 118 |
+
def test_shell():
|
| 119 |
+
constructor = [(20, 80, 0.8), (80, 180, 0.6)]
|
| 120 |
+
G = nx.random_shell_graph(constructor, seed=42)
|
| 121 |
+
result = nx.k_components(G)
|
| 122 |
+
_check_connectivity(G, result)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def test_configuration():
|
| 126 |
+
deg_seq = nx.random_powerlaw_tree_sequence(100, tries=5, seed=72)
|
| 127 |
+
G = nx.Graph(nx.configuration_model(deg_seq))
|
| 128 |
+
G.remove_edges_from(nx.selfloop_edges(G))
|
| 129 |
+
result = nx.k_components(G)
|
| 130 |
+
_check_connectivity(G, result)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def test_karate():
|
| 134 |
+
G = nx.karate_club_graph()
|
| 135 |
+
result = nx.k_components(G)
|
| 136 |
+
_check_connectivity(G, result)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def test_karate_component_number():
|
| 140 |
+
karate_k_num = {
|
| 141 |
+
0: 4,
|
| 142 |
+
1: 4,
|
| 143 |
+
2: 4,
|
| 144 |
+
3: 4,
|
| 145 |
+
4: 3,
|
| 146 |
+
5: 3,
|
| 147 |
+
6: 3,
|
| 148 |
+
7: 4,
|
| 149 |
+
8: 4,
|
| 150 |
+
9: 2,
|
| 151 |
+
10: 3,
|
| 152 |
+
11: 1,
|
| 153 |
+
12: 2,
|
| 154 |
+
13: 4,
|
| 155 |
+
14: 2,
|
| 156 |
+
15: 2,
|
| 157 |
+
16: 2,
|
| 158 |
+
17: 2,
|
| 159 |
+
18: 2,
|
| 160 |
+
19: 3,
|
| 161 |
+
20: 2,
|
| 162 |
+
21: 2,
|
| 163 |
+
22: 2,
|
| 164 |
+
23: 3,
|
| 165 |
+
24: 3,
|
| 166 |
+
25: 3,
|
| 167 |
+
26: 2,
|
| 168 |
+
27: 3,
|
| 169 |
+
28: 3,
|
| 170 |
+
29: 3,
|
| 171 |
+
30: 4,
|
| 172 |
+
31: 3,
|
| 173 |
+
32: 4,
|
| 174 |
+
33: 4,
|
| 175 |
+
}
|
| 176 |
+
G = nx.karate_club_graph()
|
| 177 |
+
k_components = nx.k_components(G)
|
| 178 |
+
k_num = build_k_number_dict(k_components)
|
| 179 |
+
assert karate_k_num == k_num
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def test_davis_southern_women():
|
| 183 |
+
G = nx.davis_southern_women_graph()
|
| 184 |
+
result = nx.k_components(G)
|
| 185 |
+
_check_connectivity(G, result)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def test_davis_southern_women_detail_3_and_4():
|
| 189 |
+
solution = {
|
| 190 |
+
3: [
|
| 191 |
+
{
|
| 192 |
+
"Nora Fayette",
|
| 193 |
+
"E10",
|
| 194 |
+
"Myra Liddel",
|
| 195 |
+
"E12",
|
| 196 |
+
"E14",
|
| 197 |
+
"Frances Anderson",
|
| 198 |
+
"Evelyn Jefferson",
|
| 199 |
+
"Ruth DeSand",
|
| 200 |
+
"Helen Lloyd",
|
| 201 |
+
"Eleanor Nye",
|
| 202 |
+
"E9",
|
| 203 |
+
"E8",
|
| 204 |
+
"E5",
|
| 205 |
+
"E4",
|
| 206 |
+
"E7",
|
| 207 |
+
"E6",
|
| 208 |
+
"E1",
|
| 209 |
+
"Verne Sanderson",
|
| 210 |
+
"E3",
|
| 211 |
+
"E2",
|
| 212 |
+
"Theresa Anderson",
|
| 213 |
+
"Pearl Oglethorpe",
|
| 214 |
+
"Katherina Rogers",
|
| 215 |
+
"Brenda Rogers",
|
| 216 |
+
"E13",
|
| 217 |
+
"Charlotte McDowd",
|
| 218 |
+
"Sylvia Avondale",
|
| 219 |
+
"Laura Mandeville",
|
| 220 |
+
}
|
| 221 |
+
],
|
| 222 |
+
4: [
|
| 223 |
+
{
|
| 224 |
+
"Nora Fayette",
|
| 225 |
+
"E10",
|
| 226 |
+
"Verne Sanderson",
|
| 227 |
+
"E12",
|
| 228 |
+
"Frances Anderson",
|
| 229 |
+
"Evelyn Jefferson",
|
| 230 |
+
"Ruth DeSand",
|
| 231 |
+
"Helen Lloyd",
|
| 232 |
+
"Eleanor Nye",
|
| 233 |
+
"E9",
|
| 234 |
+
"E8",
|
| 235 |
+
"E5",
|
| 236 |
+
"E4",
|
| 237 |
+
"E7",
|
| 238 |
+
"E6",
|
| 239 |
+
"Myra Liddel",
|
| 240 |
+
"E3",
|
| 241 |
+
"Theresa Anderson",
|
| 242 |
+
"Katherina Rogers",
|
| 243 |
+
"Brenda Rogers",
|
| 244 |
+
"Charlotte McDowd",
|
| 245 |
+
"Sylvia Avondale",
|
| 246 |
+
"Laura Mandeville",
|
| 247 |
+
}
|
| 248 |
+
],
|
| 249 |
+
}
|
| 250 |
+
G = nx.davis_southern_women_graph()
|
| 251 |
+
result = nx.k_components(G)
|
| 252 |
+
for k, components in result.items():
|
| 253 |
+
if k < 3:
|
| 254 |
+
continue
|
| 255 |
+
assert len(components) == len(solution[k])
|
| 256 |
+
for component in components:
|
| 257 |
+
assert component in solution[k]
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def test_set_consolidation_rosettacode():
|
| 261 |
+
# Tests from http://rosettacode.org/wiki/Set_consolidation
|
| 262 |
+
def list_of_sets_equal(result, solution):
|
| 263 |
+
assert {frozenset(s) for s in result} == {frozenset(s) for s in solution}
|
| 264 |
+
|
| 265 |
+
question = [{"A", "B"}, {"C", "D"}]
|
| 266 |
+
solution = [{"A", "B"}, {"C", "D"}]
|
| 267 |
+
list_of_sets_equal(_consolidate(question, 1), solution)
|
| 268 |
+
question = [{"A", "B"}, {"B", "C"}]
|
| 269 |
+
solution = [{"A", "B", "C"}]
|
| 270 |
+
list_of_sets_equal(_consolidate(question, 1), solution)
|
| 271 |
+
question = [{"A", "B"}, {"C", "D"}, {"D", "B"}]
|
| 272 |
+
solution = [{"A", "C", "B", "D"}]
|
| 273 |
+
list_of_sets_equal(_consolidate(question, 1), solution)
|
| 274 |
+
question = [{"H", "I", "K"}, {"A", "B"}, {"C", "D"}, {"D", "B"}, {"F", "G", "H"}]
|
| 275 |
+
solution = [{"A", "C", "B", "D"}, {"G", "F", "I", "H", "K"}]
|
| 276 |
+
list_of_sets_equal(_consolidate(question, 1), solution)
|
| 277 |
+
question = [
|
| 278 |
+
{"A", "H"},
|
| 279 |
+
{"H", "I", "K"},
|
| 280 |
+
{"A", "B"},
|
| 281 |
+
{"C", "D"},
|
| 282 |
+
{"D", "B"},
|
| 283 |
+
{"F", "G", "H"},
|
| 284 |
+
]
|
| 285 |
+
solution = [{"A", "C", "B", "D", "G", "F", "I", "H", "K"}]
|
| 286 |
+
list_of_sets_equal(_consolidate(question, 1), solution)
|
| 287 |
+
question = [
|
| 288 |
+
{"H", "I", "K"},
|
| 289 |
+
{"A", "B"},
|
| 290 |
+
{"C", "D"},
|
| 291 |
+
{"D", "B"},
|
| 292 |
+
{"F", "G", "H"},
|
| 293 |
+
{"A", "H"},
|
| 294 |
+
]
|
| 295 |
+
solution = [{"A", "C", "B", "D", "G", "F", "I", "H", "K"}]
|
| 296 |
+
list_of_sets_equal(_consolidate(question, 1), solution)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_kcutsets.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Jordi Torrents
|
| 2 |
+
# Test for k-cutsets
|
| 3 |
+
import itertools
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
import networkx as nx
|
| 8 |
+
from networkx.algorithms import flow
|
| 9 |
+
from networkx.algorithms.connectivity.kcutsets import _is_separating_set
|
| 10 |
+
|
| 11 |
+
MAX_CUTSETS_TO_TEST = 4 # originally 100. cut to decrease testing time
|
| 12 |
+
|
| 13 |
+
flow_funcs = [
|
| 14 |
+
flow.boykov_kolmogorov,
|
| 15 |
+
flow.dinitz,
|
| 16 |
+
flow.edmonds_karp,
|
| 17 |
+
flow.preflow_push,
|
| 18 |
+
flow.shortest_augmenting_path,
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
##
|
| 23 |
+
# Some nice synthetic graphs
|
| 24 |
+
##
|
| 25 |
+
def graph_example_1():
|
| 26 |
+
G = nx.convert_node_labels_to_integers(
|
| 27 |
+
nx.grid_graph([5, 5]), label_attribute="labels"
|
| 28 |
+
)
|
| 29 |
+
rlabels = nx.get_node_attributes(G, "labels")
|
| 30 |
+
labels = {v: k for k, v in rlabels.items()}
|
| 31 |
+
|
| 32 |
+
for nodes in [
|
| 33 |
+
(labels[(0, 0)], labels[(1, 0)]),
|
| 34 |
+
(labels[(0, 4)], labels[(1, 4)]),
|
| 35 |
+
(labels[(3, 0)], labels[(4, 0)]),
|
| 36 |
+
(labels[(3, 4)], labels[(4, 4)]),
|
| 37 |
+
]:
|
| 38 |
+
new_node = G.order() + 1
|
| 39 |
+
# Petersen graph is triconnected
|
| 40 |
+
P = nx.petersen_graph()
|
| 41 |
+
G = nx.disjoint_union(G, P)
|
| 42 |
+
# Add two edges between the grid and P
|
| 43 |
+
G.add_edge(new_node + 1, nodes[0])
|
| 44 |
+
G.add_edge(new_node, nodes[1])
|
| 45 |
+
# K5 is 4-connected
|
| 46 |
+
K = nx.complete_graph(5)
|
| 47 |
+
G = nx.disjoint_union(G, K)
|
| 48 |
+
# Add three edges between P and K5
|
| 49 |
+
G.add_edge(new_node + 2, new_node + 11)
|
| 50 |
+
G.add_edge(new_node + 3, new_node + 12)
|
| 51 |
+
G.add_edge(new_node + 4, new_node + 13)
|
| 52 |
+
# Add another K5 sharing a node
|
| 53 |
+
G = nx.disjoint_union(G, K)
|
| 54 |
+
nbrs = G[new_node + 10]
|
| 55 |
+
G.remove_node(new_node + 10)
|
| 56 |
+
for nbr in nbrs:
|
| 57 |
+
G.add_edge(new_node + 17, nbr)
|
| 58 |
+
G.add_edge(new_node + 16, new_node + 5)
|
| 59 |
+
return G
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def torrents_and_ferraro_graph():
|
| 63 |
+
G = nx.convert_node_labels_to_integers(
|
| 64 |
+
nx.grid_graph([5, 5]), label_attribute="labels"
|
| 65 |
+
)
|
| 66 |
+
rlabels = nx.get_node_attributes(G, "labels")
|
| 67 |
+
labels = {v: k for k, v in rlabels.items()}
|
| 68 |
+
|
| 69 |
+
for nodes in [(labels[(0, 4)], labels[(1, 4)]), (labels[(3, 4)], labels[(4, 4)])]:
|
| 70 |
+
new_node = G.order() + 1
|
| 71 |
+
# Petersen graph is triconnected
|
| 72 |
+
P = nx.petersen_graph()
|
| 73 |
+
G = nx.disjoint_union(G, P)
|
| 74 |
+
# Add two edges between the grid and P
|
| 75 |
+
G.add_edge(new_node + 1, nodes[0])
|
| 76 |
+
G.add_edge(new_node, nodes[1])
|
| 77 |
+
# K5 is 4-connected
|
| 78 |
+
K = nx.complete_graph(5)
|
| 79 |
+
G = nx.disjoint_union(G, K)
|
| 80 |
+
# Add three edges between P and K5
|
| 81 |
+
G.add_edge(new_node + 2, new_node + 11)
|
| 82 |
+
G.add_edge(new_node + 3, new_node + 12)
|
| 83 |
+
G.add_edge(new_node + 4, new_node + 13)
|
| 84 |
+
# Add another K5 sharing a node
|
| 85 |
+
G = nx.disjoint_union(G, K)
|
| 86 |
+
nbrs = G[new_node + 10]
|
| 87 |
+
G.remove_node(new_node + 10)
|
| 88 |
+
for nbr in nbrs:
|
| 89 |
+
G.add_edge(new_node + 17, nbr)
|
| 90 |
+
# Commenting this makes the graph not biconnected !!
|
| 91 |
+
# This stupid mistake make one reviewer very angry :P
|
| 92 |
+
G.add_edge(new_node + 16, new_node + 8)
|
| 93 |
+
|
| 94 |
+
for nodes in [(labels[(0, 0)], labels[(1, 0)]), (labels[(3, 0)], labels[(4, 0)])]:
|
| 95 |
+
new_node = G.order() + 1
|
| 96 |
+
# Petersen graph is triconnected
|
| 97 |
+
P = nx.petersen_graph()
|
| 98 |
+
G = nx.disjoint_union(G, P)
|
| 99 |
+
# Add two edges between the grid and P
|
| 100 |
+
G.add_edge(new_node + 1, nodes[0])
|
| 101 |
+
G.add_edge(new_node, nodes[1])
|
| 102 |
+
# K5 is 4-connected
|
| 103 |
+
K = nx.complete_graph(5)
|
| 104 |
+
G = nx.disjoint_union(G, K)
|
| 105 |
+
# Add three edges between P and K5
|
| 106 |
+
G.add_edge(new_node + 2, new_node + 11)
|
| 107 |
+
G.add_edge(new_node + 3, new_node + 12)
|
| 108 |
+
G.add_edge(new_node + 4, new_node + 13)
|
| 109 |
+
# Add another K5 sharing two nodes
|
| 110 |
+
G = nx.disjoint_union(G, K)
|
| 111 |
+
nbrs = G[new_node + 10]
|
| 112 |
+
G.remove_node(new_node + 10)
|
| 113 |
+
for nbr in nbrs:
|
| 114 |
+
G.add_edge(new_node + 17, nbr)
|
| 115 |
+
nbrs2 = G[new_node + 9]
|
| 116 |
+
G.remove_node(new_node + 9)
|
| 117 |
+
for nbr in nbrs2:
|
| 118 |
+
G.add_edge(new_node + 18, nbr)
|
| 119 |
+
return G
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# Helper function
|
| 123 |
+
def _check_separating_sets(G):
|
| 124 |
+
for cc in nx.connected_components(G):
|
| 125 |
+
if len(cc) < 3:
|
| 126 |
+
continue
|
| 127 |
+
Gc = G.subgraph(cc)
|
| 128 |
+
node_conn = nx.node_connectivity(Gc)
|
| 129 |
+
all_cuts = nx.all_node_cuts(Gc)
|
| 130 |
+
# Only test a limited number of cut sets to reduce test time.
|
| 131 |
+
for cut in itertools.islice(all_cuts, MAX_CUTSETS_TO_TEST):
|
| 132 |
+
assert node_conn == len(cut)
|
| 133 |
+
assert not nx.is_connected(nx.restricted_view(G, cut, []))
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@pytest.mark.slow
|
| 137 |
+
def test_torrents_and_ferraro_graph():
|
| 138 |
+
G = torrents_and_ferraro_graph()
|
| 139 |
+
_check_separating_sets(G)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def test_example_1():
|
| 143 |
+
G = graph_example_1()
|
| 144 |
+
_check_separating_sets(G)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def test_random_gnp():
|
| 148 |
+
G = nx.gnp_random_graph(100, 0.1, seed=42)
|
| 149 |
+
_check_separating_sets(G)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def test_shell():
|
| 153 |
+
constructor = [(20, 80, 0.8), (80, 180, 0.6)]
|
| 154 |
+
G = nx.random_shell_graph(constructor, seed=42)
|
| 155 |
+
_check_separating_sets(G)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def test_configuration():
|
| 159 |
+
deg_seq = nx.random_powerlaw_tree_sequence(100, tries=5, seed=72)
|
| 160 |
+
G = nx.Graph(nx.configuration_model(deg_seq))
|
| 161 |
+
G.remove_edges_from(nx.selfloop_edges(G))
|
| 162 |
+
_check_separating_sets(G)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def test_karate():
|
| 166 |
+
G = nx.karate_club_graph()
|
| 167 |
+
_check_separating_sets(G)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _generate_no_biconnected(max_attempts=50):
|
| 171 |
+
attempts = 0
|
| 172 |
+
while True:
|
| 173 |
+
G = nx.fast_gnp_random_graph(100, 0.0575, seed=42)
|
| 174 |
+
if nx.is_connected(G) and not nx.is_biconnected(G):
|
| 175 |
+
attempts = 0
|
| 176 |
+
yield G
|
| 177 |
+
else:
|
| 178 |
+
if attempts >= max_attempts:
|
| 179 |
+
msg = f"Tried {attempts} times: no suitable Graph."
|
| 180 |
+
raise Exception(msg)
|
| 181 |
+
else:
|
| 182 |
+
attempts += 1
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def test_articulation_points():
|
| 186 |
+
Ggen = _generate_no_biconnected()
|
| 187 |
+
for i in range(1): # change 1 to 3 or more for more realizations.
|
| 188 |
+
G = next(Ggen)
|
| 189 |
+
articulation_points = [{a} for a in nx.articulation_points(G)]
|
| 190 |
+
for cut in nx.all_node_cuts(G):
|
| 191 |
+
assert cut in articulation_points
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def test_grid_2d_graph():
|
| 195 |
+
# All minimum node cuts of a 2d grid
|
| 196 |
+
# are the four pairs of nodes that are
|
| 197 |
+
# neighbors of the four corner nodes.
|
| 198 |
+
G = nx.grid_2d_graph(5, 5)
|
| 199 |
+
solution = [{(0, 1), (1, 0)}, {(3, 0), (4, 1)}, {(3, 4), (4, 3)}, {(0, 3), (1, 4)}]
|
| 200 |
+
for cut in nx.all_node_cuts(G):
|
| 201 |
+
assert cut in solution
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def test_disconnected_graph():
|
| 205 |
+
G = nx.fast_gnp_random_graph(100, 0.01, seed=42)
|
| 206 |
+
cuts = nx.all_node_cuts(G)
|
| 207 |
+
pytest.raises(nx.NetworkXError, next, cuts)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
@pytest.mark.slow
|
| 211 |
+
def test_alternative_flow_functions():
|
| 212 |
+
graphs = [nx.grid_2d_graph(4, 4), nx.cycle_graph(5)]
|
| 213 |
+
for G in graphs:
|
| 214 |
+
node_conn = nx.node_connectivity(G)
|
| 215 |
+
for flow_func in flow_funcs:
|
| 216 |
+
all_cuts = nx.all_node_cuts(G, flow_func=flow_func)
|
| 217 |
+
# Only test a limited number of cut sets to reduce test time.
|
| 218 |
+
for cut in itertools.islice(all_cuts, MAX_CUTSETS_TO_TEST):
|
| 219 |
+
assert node_conn == len(cut)
|
| 220 |
+
assert not nx.is_connected(nx.restricted_view(G, cut, []))
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def test_is_separating_set_complete_graph():
|
| 224 |
+
G = nx.complete_graph(5)
|
| 225 |
+
assert _is_separating_set(G, {0, 1, 2, 3})
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def test_is_separating_set():
|
| 229 |
+
for i in [5, 10, 15]:
|
| 230 |
+
G = nx.star_graph(i)
|
| 231 |
+
max_degree_node = max(G, key=G.degree)
|
| 232 |
+
assert _is_separating_set(G, {max_degree_node})
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def test_non_repeated_cuts():
|
| 236 |
+
# The algorithm was repeating the cut {0, 1} for the giant biconnected
|
| 237 |
+
# component of the Karate club graph.
|
| 238 |
+
K = nx.karate_club_graph()
|
| 239 |
+
bcc = max(list(nx.biconnected_components(K)), key=len)
|
| 240 |
+
G = K.subgraph(bcc)
|
| 241 |
+
solution = [{32, 33}, {2, 33}, {0, 3}, {0, 1}, {29, 33}]
|
| 242 |
+
cuts = list(nx.all_node_cuts(G))
|
| 243 |
+
if len(solution) != len(cuts):
|
| 244 |
+
print(f"Solution: {solution}")
|
| 245 |
+
print(f"Result: {cuts}")
|
| 246 |
+
assert len(solution) == len(cuts)
|
| 247 |
+
for cut in cuts:
|
| 248 |
+
assert cut in solution
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def test_cycle_graph():
|
| 252 |
+
G = nx.cycle_graph(5)
|
| 253 |
+
solution = [{0, 2}, {0, 3}, {1, 3}, {1, 4}, {2, 4}]
|
| 254 |
+
cuts = list(nx.all_node_cuts(G))
|
| 255 |
+
assert len(solution) == len(cuts)
|
| 256 |
+
for cut in cuts:
|
| 257 |
+
assert cut in solution
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def test_complete_graph():
|
| 261 |
+
G = nx.complete_graph(5)
|
| 262 |
+
assert nx.node_connectivity(G) == 4
|
| 263 |
+
assert list(nx.all_node_cuts(G)) == []
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def test_all_node_cuts_simple_case():
|
| 267 |
+
G = nx.complete_graph(5)
|
| 268 |
+
G.remove_edges_from([(0, 1), (3, 4)])
|
| 269 |
+
expected = [{0, 1, 2}, {2, 3, 4}]
|
| 270 |
+
actual = list(nx.all_node_cuts(G))
|
| 271 |
+
assert len(actual) == len(expected)
|
| 272 |
+
for cut in actual:
|
| 273 |
+
assert cut in expected
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/tests/test_stoer_wagner.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from itertools import chain
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _check_partition(G, cut_value, partition, weight):
|
| 9 |
+
assert isinstance(partition, tuple)
|
| 10 |
+
assert len(partition) == 2
|
| 11 |
+
assert isinstance(partition[0], list)
|
| 12 |
+
assert isinstance(partition[1], list)
|
| 13 |
+
assert len(partition[0]) > 0
|
| 14 |
+
assert len(partition[1]) > 0
|
| 15 |
+
assert sum(map(len, partition)) == len(G)
|
| 16 |
+
assert set(chain.from_iterable(partition)) == set(G)
|
| 17 |
+
partition = tuple(map(set, partition))
|
| 18 |
+
w = 0
|
| 19 |
+
for u, v, e in G.edges(data=True):
|
| 20 |
+
if (u in partition[0]) == (v in partition[1]):
|
| 21 |
+
w += e.get(weight, 1)
|
| 22 |
+
assert w == cut_value
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _test_stoer_wagner(G, answer, weight="weight"):
|
| 26 |
+
cut_value, partition = nx.stoer_wagner(G, weight, heap=nx.utils.PairingHeap)
|
| 27 |
+
assert cut_value == answer
|
| 28 |
+
_check_partition(G, cut_value, partition, weight)
|
| 29 |
+
cut_value, partition = nx.stoer_wagner(G, weight, heap=nx.utils.BinaryHeap)
|
| 30 |
+
assert cut_value == answer
|
| 31 |
+
_check_partition(G, cut_value, partition, weight)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_graph1():
|
| 35 |
+
G = nx.Graph()
|
| 36 |
+
G.add_edge("x", "a", weight=3)
|
| 37 |
+
G.add_edge("x", "b", weight=1)
|
| 38 |
+
G.add_edge("a", "c", weight=3)
|
| 39 |
+
G.add_edge("b", "c", weight=5)
|
| 40 |
+
G.add_edge("b", "d", weight=4)
|
| 41 |
+
G.add_edge("d", "e", weight=2)
|
| 42 |
+
G.add_edge("c", "y", weight=2)
|
| 43 |
+
G.add_edge("e", "y", weight=3)
|
| 44 |
+
_test_stoer_wagner(G, 4)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_graph2():
|
| 48 |
+
G = nx.Graph()
|
| 49 |
+
G.add_edge("x", "a")
|
| 50 |
+
G.add_edge("x", "b")
|
| 51 |
+
G.add_edge("a", "c")
|
| 52 |
+
G.add_edge("b", "c")
|
| 53 |
+
G.add_edge("b", "d")
|
| 54 |
+
G.add_edge("d", "e")
|
| 55 |
+
G.add_edge("c", "y")
|
| 56 |
+
G.add_edge("e", "y")
|
| 57 |
+
_test_stoer_wagner(G, 2)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_graph3():
|
| 61 |
+
# Source:
|
| 62 |
+
# Stoer, M. and Wagner, F. (1997). "A simple min-cut algorithm". Journal of
|
| 63 |
+
# the ACM 44 (4), 585-591.
|
| 64 |
+
G = nx.Graph()
|
| 65 |
+
G.add_edge(1, 2, weight=2)
|
| 66 |
+
G.add_edge(1, 5, weight=3)
|
| 67 |
+
G.add_edge(2, 3, weight=3)
|
| 68 |
+
G.add_edge(2, 5, weight=2)
|
| 69 |
+
G.add_edge(2, 6, weight=2)
|
| 70 |
+
G.add_edge(3, 4, weight=4)
|
| 71 |
+
G.add_edge(3, 7, weight=2)
|
| 72 |
+
G.add_edge(4, 7, weight=2)
|
| 73 |
+
G.add_edge(4, 8, weight=2)
|
| 74 |
+
G.add_edge(5, 6, weight=3)
|
| 75 |
+
G.add_edge(6, 7, weight=1)
|
| 76 |
+
G.add_edge(7, 8, weight=3)
|
| 77 |
+
_test_stoer_wagner(G, 4)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_weight_name():
|
| 81 |
+
G = nx.Graph()
|
| 82 |
+
G.add_edge(1, 2, weight=1, cost=8)
|
| 83 |
+
G.add_edge(1, 3, cost=2)
|
| 84 |
+
G.add_edge(2, 3, cost=4)
|
| 85 |
+
_test_stoer_wagner(G, 6, weight="cost")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def test_exceptions():
|
| 89 |
+
G = nx.Graph()
|
| 90 |
+
pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
|
| 91 |
+
G.add_node(1)
|
| 92 |
+
pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
|
| 93 |
+
G.add_node(2)
|
| 94 |
+
pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
|
| 95 |
+
G.add_edge(1, 2, weight=-2)
|
| 96 |
+
pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
|
| 97 |
+
G = nx.DiGraph()
|
| 98 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.stoer_wagner, G)
|
| 99 |
+
G = nx.MultiGraph()
|
| 100 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.stoer_wagner, G)
|
| 101 |
+
G = nx.MultiDiGraph()
|
| 102 |
+
pytest.raises(nx.NetworkXNotImplemented, nx.stoer_wagner, G)
|
.venv/lib/python3.11/site-packages/networkx/algorithms/connectivity/utils.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utilities for connectivity package
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
|
| 7 |
+
__all__ = ["build_auxiliary_node_connectivity", "build_auxiliary_edge_connectivity"]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@nx._dispatchable(returns_graph=True)
|
| 11 |
+
def build_auxiliary_node_connectivity(G):
|
| 12 |
+
r"""Creates a directed graph D from an undirected graph G to compute flow
|
| 13 |
+
based node connectivity.
|
| 14 |
+
|
| 15 |
+
For an undirected graph G having `n` nodes and `m` edges we derive a
|
| 16 |
+
directed graph D with `2n` nodes and `2m+n` arcs by replacing each
|
| 17 |
+
original node `v` with two nodes `vA`, `vB` linked by an (internal)
|
| 18 |
+
arc in D. Then for each edge (`u`, `v`) in G we add two arcs (`uB`, `vA`)
|
| 19 |
+
and (`vB`, `uA`) in D. Finally we set the attribute capacity = 1 for each
|
| 20 |
+
arc in D [1]_.
|
| 21 |
+
|
| 22 |
+
For a directed graph having `n` nodes and `m` arcs we derive a
|
| 23 |
+
directed graph D with `2n` nodes and `m+n` arcs by replacing each
|
| 24 |
+
original node `v` with two nodes `vA`, `vB` linked by an (internal)
|
| 25 |
+
arc (`vA`, `vB`) in D. Then for each arc (`u`, `v`) in G we add one
|
| 26 |
+
arc (`uB`, `vA`) in D. Finally we set the attribute capacity = 1 for
|
| 27 |
+
each arc in D.
|
| 28 |
+
|
| 29 |
+
A dictionary with a mapping between nodes in the original graph and the
|
| 30 |
+
auxiliary digraph is stored as a graph attribute: D.graph['mapping'].
|
| 31 |
+
|
| 32 |
+
References
|
| 33 |
+
----------
|
| 34 |
+
.. [1] Kammer, Frank and Hanjo Taubig. Graph Connectivity. in Brandes and
|
| 35 |
+
Erlebach, 'Network Analysis: Methodological Foundations', Lecture
|
| 36 |
+
Notes in Computer Science, Volume 3418, Springer-Verlag, 2005.
|
| 37 |
+
https://doi.org/10.1007/978-3-540-31955-9_7
|
| 38 |
+
|
| 39 |
+
"""
|
| 40 |
+
directed = G.is_directed()
|
| 41 |
+
|
| 42 |
+
mapping = {}
|
| 43 |
+
H = nx.DiGraph()
|
| 44 |
+
|
| 45 |
+
for i, node in enumerate(G):
|
| 46 |
+
mapping[node] = i
|
| 47 |
+
H.add_node(f"{i}A", id=node)
|
| 48 |
+
H.add_node(f"{i}B", id=node)
|
| 49 |
+
H.add_edge(f"{i}A", f"{i}B", capacity=1)
|
| 50 |
+
|
| 51 |
+
edges = []
|
| 52 |
+
for source, target in G.edges():
|
| 53 |
+
edges.append((f"{mapping[source]}B", f"{mapping[target]}A"))
|
| 54 |
+
if not directed:
|
| 55 |
+
edges.append((f"{mapping[target]}B", f"{mapping[source]}A"))
|
| 56 |
+
H.add_edges_from(edges, capacity=1)
|
| 57 |
+
|
| 58 |
+
# Store mapping as graph attribute
|
| 59 |
+
H.graph["mapping"] = mapping
|
| 60 |
+
return H
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@nx._dispatchable(returns_graph=True)
|
| 64 |
+
def build_auxiliary_edge_connectivity(G):
|
| 65 |
+
"""Auxiliary digraph for computing flow based edge connectivity
|
| 66 |
+
|
| 67 |
+
If the input graph is undirected, we replace each edge (`u`,`v`) with
|
| 68 |
+
two reciprocal arcs (`u`, `v`) and (`v`, `u`) and then we set the attribute
|
| 69 |
+
'capacity' for each arc to 1. If the input graph is directed we simply
|
| 70 |
+
add the 'capacity' attribute. Part of algorithm 1 in [1]_ .
|
| 71 |
+
|
| 72 |
+
References
|
| 73 |
+
----------
|
| 74 |
+
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms. (this is a
|
| 75 |
+
chapter, look for the reference of the book).
|
| 76 |
+
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
| 77 |
+
"""
|
| 78 |
+
if G.is_directed():
|
| 79 |
+
H = nx.DiGraph()
|
| 80 |
+
H.add_nodes_from(G.nodes())
|
| 81 |
+
H.add_edges_from(G.edges(), capacity=1)
|
| 82 |
+
return H
|
| 83 |
+
else:
|
| 84 |
+
H = nx.DiGraph()
|
| 85 |
+
H.add_nodes_from(G.nodes())
|
| 86 |
+
for source, target in G.edges():
|
| 87 |
+
H.add_edges_from([(source, target), (target, source)], capacity=1)
|
| 88 |
+
return H
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (198 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_boundary.cpython-311.pyc
ADDED
|
Binary file (13.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_bridges.cpython-311.pyc
ADDED
|
Binary file (8.31 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_broadcasting.cpython-311.pyc
ADDED
|
Binary file (3.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_chains.cpython-311.pyc
ADDED
|
Binary file (7.46 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_clique.cpython-311.pyc
ADDED
|
Binary file (18 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_cluster.cpython-311.pyc
ADDED
|
Binary file (33.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_communicability.cpython-311.pyc
ADDED
|
Binary file (3.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_core.cpython-311.pyc
ADDED
|
Binary file (21.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_cycles.cpython-311.pyc
ADDED
|
Binary file (72.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_d_separation.cpython-311.pyc
ADDED
|
Binary file (20.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_dag.cpython-311.pyc
ADDED
|
Binary file (60.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_distance_measures.cpython-311.pyc
ADDED
|
Binary file (57.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_distance_regular.cpython-311.pyc
ADDED
|
Binary file (7.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/algorithms/tests/__pycache__/test_dominance.cpython-311.pyc
ADDED
|
Binary file (16.2 kB). View file
|
|
|