Fixed database typo and removed unnecessary class identifier.
This commit is contained in:
parent
00ad49a143
commit
45fb349a7d
5098 changed files with 952558 additions and 85 deletions
|
@ -0,0 +1,26 @@
|
|||
"""Connectivity and cut algorithms
|
||||
"""
|
||||
from .connectivity import *
|
||||
from .cuts import *
|
||||
from .edge_augmentation import *
|
||||
from .edge_kcomponents import *
|
||||
from .disjoint_paths import *
|
||||
from .kcomponents import *
|
||||
from .kcutsets import *
|
||||
from .stoerwagner import *
|
||||
from .utils import *
|
||||
|
||||
__all__ = sum(
|
||||
[
|
||||
connectivity.__all__,
|
||||
cuts.__all__,
|
||||
edge_augmentation.__all__,
|
||||
edge_kcomponents.__all__,
|
||||
disjoint_paths.__all__,
|
||||
kcomponents.__all__,
|
||||
kcutsets.__all__,
|
||||
stoerwagner.__all__,
|
||||
utils.__all__,
|
||||
],
|
||||
[],
|
||||
)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,816 @@
|
|||
"""
|
||||
Flow based connectivity algorithms
|
||||
"""
|
||||
|
||||
import itertools
|
||||
from operator import itemgetter
|
||||
|
||||
import networkx as nx
|
||||
|
||||
# Define the default maximum flow function to use in all flow based
|
||||
# connectivity algorithms.
|
||||
from networkx.algorithms.flow import boykov_kolmogorov
|
||||
from networkx.algorithms.flow import dinitz
|
||||
from networkx.algorithms.flow import edmonds_karp
|
||||
from networkx.algorithms.flow import shortest_augmenting_path
|
||||
from networkx.algorithms.flow import build_residual_network
|
||||
|
||||
default_flow_func = edmonds_karp
|
||||
|
||||
from .utils import build_auxiliary_node_connectivity, build_auxiliary_edge_connectivity
|
||||
|
||||
__all__ = [
|
||||
"average_node_connectivity",
|
||||
"local_node_connectivity",
|
||||
"node_connectivity",
|
||||
"local_edge_connectivity",
|
||||
"edge_connectivity",
|
||||
"all_pairs_node_connectivity",
|
||||
]
|
||||
|
||||
|
||||
def local_node_connectivity(
|
||||
G, s, t, flow_func=None, auxiliary=None, residual=None, cutoff=None
|
||||
):
|
||||
r"""Computes local node connectivity for nodes s and t.
|
||||
|
||||
Local node connectivity for two non adjacent nodes s and t is the
|
||||
minimum number of nodes that must be removed (along with their incident
|
||||
edges) to disconnect them.
|
||||
|
||||
This is a flow based implementation of node connectivity. We compute the
|
||||
maximum flow on an auxiliary digraph build from the original input
|
||||
graph (see below for details).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
s : node
|
||||
Source node
|
||||
|
||||
t : node
|
||||
Target node
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See below for details. The choice
|
||||
of the default function may change from version to version and
|
||||
should not be relied on. Default value: None.
|
||||
|
||||
auxiliary : NetworkX DiGraph
|
||||
Auxiliary digraph to compute flow based node connectivity. It has
|
||||
to have a graph attribute called mapping with a dictionary mapping
|
||||
node names in G and in the auxiliary digraph. If provided
|
||||
it will be reused instead of recreated. Default value: None.
|
||||
|
||||
residual : NetworkX DiGraph
|
||||
Residual network to compute maximum flow. If provided it will be
|
||||
reused instead of recreated. Default value: None.
|
||||
|
||||
cutoff : integer, float
|
||||
If specified, the maximum flow algorithm will terminate when the
|
||||
flow value reaches or exceeds the cutoff. This is only for the
|
||||
algorithms that support the cutoff parameter: :meth:`edmonds_karp`
|
||||
and :meth:`shortest_augmenting_path`. Other algorithms will ignore
|
||||
this parameter. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
K : integer
|
||||
local node connectivity for nodes s and t
|
||||
|
||||
Examples
|
||||
--------
|
||||
This function is not imported in the base NetworkX namespace, so you
|
||||
have to explicitly import it from the connectivity package:
|
||||
|
||||
>>> from networkx.algorithms.connectivity import local_node_connectivity
|
||||
|
||||
We use in this example the platonic icosahedral graph, which has node
|
||||
connectivity 5.
|
||||
|
||||
>>> G = nx.icosahedral_graph()
|
||||
>>> local_node_connectivity(G, 0, 6)
|
||||
5
|
||||
|
||||
If you need to compute local connectivity on several pairs of
|
||||
nodes in the same graph, it is recommended that you reuse the
|
||||
data structures that NetworkX uses in the computation: the
|
||||
auxiliary digraph for node connectivity, and the residual
|
||||
network for the underlying maximum flow computation.
|
||||
|
||||
Example of how to compute local node connectivity among
|
||||
all pairs of nodes of the platonic icosahedral graph reusing
|
||||
the data structures.
|
||||
|
||||
>>> import itertools
|
||||
>>> # You also have to explicitly import the function for
|
||||
>>> # building the auxiliary digraph from the connectivity package
|
||||
>>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity
|
||||
...
|
||||
>>> H = build_auxiliary_node_connectivity(G)
|
||||
>>> # And the function for building the residual network from the
|
||||
>>> # flow package
|
||||
>>> from networkx.algorithms.flow import build_residual_network
|
||||
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
||||
>>> R = build_residual_network(H, "capacity")
|
||||
>>> result = dict.fromkeys(G, dict())
|
||||
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
||||
>>> # as parameters
|
||||
>>> for u, v in itertools.combinations(G, 2):
|
||||
... k = local_node_connectivity(G, u, v, auxiliary=H, residual=R)
|
||||
... result[u][v] = k
|
||||
...
|
||||
>>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
|
||||
True
|
||||
|
||||
You can also use alternative flow algorithms for computing node
|
||||
connectivity. For instance, in dense networks the algorithm
|
||||
:meth:`shortest_augmenting_path` will usually perform better than
|
||||
the default :meth:`edmonds_karp` which is faster for sparse
|
||||
networks with highly skewed degree distributions. Alternative flow
|
||||
functions have to be explicitly imported from the flow package.
|
||||
|
||||
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
||||
>>> local_node_connectivity(G, 0, 6, flow_func=shortest_augmenting_path)
|
||||
5
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a flow based implementation of node connectivity. We compute the
|
||||
maximum flow using, by default, the :meth:`edmonds_karp` algorithm (see:
|
||||
:meth:`maximum_flow`) on an auxiliary digraph build from the original
|
||||
input graph:
|
||||
|
||||
For an undirected graph G having `n` nodes and `m` edges we derive a
|
||||
directed graph H with `2n` nodes and `2m+n` arcs by replacing each
|
||||
original node `v` with two nodes `v_A`, `v_B` linked by an (internal)
|
||||
arc in H. Then for each edge (`u`, `v`) in G we add two arcs
|
||||
(`u_B`, `v_A`) and (`v_B`, `u_A`) in H. Finally we set the attribute
|
||||
capacity = 1 for each arc in H [1]_ .
|
||||
|
||||
For a directed graph G having `n` nodes and `m` arcs we derive a
|
||||
directed graph H with `2n` nodes and `m+n` arcs by replacing each
|
||||
original node `v` with two nodes `v_A`, `v_B` linked by an (internal)
|
||||
arc (`v_A`, `v_B`) in H. Then for each arc (`u`, `v`) in G we add one arc
|
||||
(`u_B`, `v_A`) in H. Finally we set the attribute capacity = 1 for
|
||||
each arc in H.
|
||||
|
||||
This is equal to the local node connectivity because the value of
|
||||
a maximum s-t-flow is equal to the capacity of a minimum s-t-cut.
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`local_edge_connectivity`
|
||||
:meth:`node_connectivity`
|
||||
:meth:`minimum_node_cut`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Kammer, Frank and Hanjo Taubig. Graph Connectivity. in Brandes and
|
||||
Erlebach, 'Network Analysis: Methodological Foundations', Lecture
|
||||
Notes in Computer Science, Volume 3418, Springer-Verlag, 2005.
|
||||
http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf
|
||||
|
||||
"""
|
||||
if flow_func is None:
|
||||
flow_func = default_flow_func
|
||||
|
||||
if auxiliary is None:
|
||||
H = build_auxiliary_node_connectivity(G)
|
||||
else:
|
||||
H = auxiliary
|
||||
|
||||
mapping = H.graph.get("mapping", None)
|
||||
if mapping is None:
|
||||
raise nx.NetworkXError("Invalid auxiliary digraph.")
|
||||
|
||||
kwargs = dict(flow_func=flow_func, residual=residual)
|
||||
if flow_func is shortest_augmenting_path:
|
||||
kwargs["cutoff"] = cutoff
|
||||
kwargs["two_phase"] = True
|
||||
elif flow_func is edmonds_karp:
|
||||
kwargs["cutoff"] = cutoff
|
||||
elif flow_func is dinitz:
|
||||
kwargs["cutoff"] = cutoff
|
||||
elif flow_func is boykov_kolmogorov:
|
||||
kwargs["cutoff"] = cutoff
|
||||
|
||||
return nx.maximum_flow_value(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs)
|
||||
|
||||
|
||||
def node_connectivity(G, s=None, t=None, flow_func=None):
|
||||
r"""Returns node connectivity for a graph or digraph G.
|
||||
|
||||
Node connectivity is equal to the minimum number of nodes that
|
||||
must be removed to disconnect G or render it trivial. If source
|
||||
and target nodes are provided, this function returns the local node
|
||||
connectivity: the minimum number of nodes that must be removed to break
|
||||
all paths from source to target in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
s : node
|
||||
Source node. Optional. Default value: None.
|
||||
|
||||
t : node
|
||||
Target node. Optional. Default value: None.
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See below for details. The
|
||||
choice of the default function may change from version
|
||||
to version and should not be relied on. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
K : integer
|
||||
Node connectivity of G, or local node connectivity if source
|
||||
and target are provided.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # Platonic icosahedral graph is 5-node-connected
|
||||
>>> G = nx.icosahedral_graph()
|
||||
>>> nx.node_connectivity(G)
|
||||
5
|
||||
|
||||
You can use alternative flow algorithms for the underlying maximum
|
||||
flow computation. In dense networks the algorithm
|
||||
:meth:`shortest_augmenting_path` will usually perform better
|
||||
than the default :meth:`edmonds_karp`, which is faster for
|
||||
sparse networks with highly skewed degree distributions. Alternative
|
||||
flow functions have to be explicitly imported from the flow package.
|
||||
|
||||
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
||||
>>> nx.node_connectivity(G, flow_func=shortest_augmenting_path)
|
||||
5
|
||||
|
||||
If you specify a pair of nodes (source and target) as parameters,
|
||||
this function returns the value of local node connectivity.
|
||||
|
||||
>>> nx.node_connectivity(G, 3, 7)
|
||||
5
|
||||
|
||||
If you need to perform several local computations among different
|
||||
pairs of nodes on the same graph, it is recommended that you reuse
|
||||
the data structures used in the maximum flow computations. See
|
||||
:meth:`local_node_connectivity` for details.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a flow based implementation of node connectivity. The
|
||||
algorithm works by solving $O((n-\delta-1+\delta(\delta-1)/2))$
|
||||
maximum flow problems on an auxiliary digraph. Where $\delta$
|
||||
is the minimum degree of G. For details about the auxiliary
|
||||
digraph and the computation of local node connectivity see
|
||||
:meth:`local_node_connectivity`. This implementation is based
|
||||
on algorithm 11 in [1]_.
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`local_node_connectivity`
|
||||
:meth:`edge_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
||||
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
||||
|
||||
"""
|
||||
if (s is not None and t is None) or (s is None and t is not None):
|
||||
raise nx.NetworkXError("Both source and target must be specified.")
|
||||
|
||||
# Local node connectivity
|
||||
if s is not None and t is not None:
|
||||
if s not in G:
|
||||
raise nx.NetworkXError(f"node {s} not in graph")
|
||||
if t not in G:
|
||||
raise nx.NetworkXError(f"node {t} not in graph")
|
||||
return local_node_connectivity(G, s, t, flow_func=flow_func)
|
||||
|
||||
# Global node connectivity
|
||||
if G.is_directed():
|
||||
if not nx.is_weakly_connected(G):
|
||||
return 0
|
||||
iter_func = itertools.permutations
|
||||
# It is necessary to consider both predecessors
|
||||
# and successors for directed graphs
|
||||
|
||||
def neighbors(v):
|
||||
return itertools.chain.from_iterable([G.predecessors(v), G.successors(v)])
|
||||
|
||||
else:
|
||||
if not nx.is_connected(G):
|
||||
return 0
|
||||
iter_func = itertools.combinations
|
||||
neighbors = G.neighbors
|
||||
|
||||
# Reuse the auxiliary digraph and the residual network
|
||||
H = build_auxiliary_node_connectivity(G)
|
||||
R = build_residual_network(H, "capacity")
|
||||
kwargs = dict(flow_func=flow_func, auxiliary=H, residual=R)
|
||||
|
||||
# Pick a node with minimum degree
|
||||
# Node connectivity is bounded by degree.
|
||||
v, K = min(G.degree(), key=itemgetter(1))
|
||||
# compute local node connectivity with all its non-neighbors nodes
|
||||
for w in set(G) - set(neighbors(v)) - {v}:
|
||||
kwargs["cutoff"] = K
|
||||
K = min(K, local_node_connectivity(G, v, w, **kwargs))
|
||||
# Also for non adjacent pairs of neighbors of v
|
||||
for x, y in iter_func(neighbors(v), 2):
|
||||
if y in G[x]:
|
||||
continue
|
||||
kwargs["cutoff"] = K
|
||||
K = min(K, local_node_connectivity(G, x, y, **kwargs))
|
||||
|
||||
return K
|
||||
|
||||
|
||||
def average_node_connectivity(G, flow_func=None):
|
||||
r"""Returns the average connectivity of a graph G.
|
||||
|
||||
The average connectivity `\bar{\kappa}` of a graph G is the average
|
||||
of local node connectivity over all pairs of nodes of G [1]_ .
|
||||
|
||||
.. math::
|
||||
|
||||
\bar{\kappa}(G) = \frac{\sum_{u,v} \kappa_{G}(u,v)}{{n \choose 2}}
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See :meth:`local_node_connectivity`
|
||||
for details. The choice of the default function may change from
|
||||
version to version and should not be relied on. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
K : float
|
||||
Average node connectivity
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`local_node_connectivity`
|
||||
:meth:`node_connectivity`
|
||||
:meth:`edge_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Beineke, L., O. Oellermann, and R. Pippert (2002). The average
|
||||
connectivity of a graph. Discrete mathematics 252(1-3), 31-45.
|
||||
http://www.sciencedirect.com/science/article/pii/S0012365X01001807
|
||||
|
||||
"""
|
||||
if G.is_directed():
|
||||
iter_func = itertools.permutations
|
||||
else:
|
||||
iter_func = itertools.combinations
|
||||
|
||||
# Reuse the auxiliary digraph and the residual network
|
||||
H = build_auxiliary_node_connectivity(G)
|
||||
R = build_residual_network(H, "capacity")
|
||||
kwargs = dict(flow_func=flow_func, auxiliary=H, residual=R)
|
||||
|
||||
num, den = 0, 0
|
||||
for u, v in iter_func(G, 2):
|
||||
num += local_node_connectivity(G, u, v, **kwargs)
|
||||
den += 1
|
||||
|
||||
if den == 0: # Null Graph
|
||||
return 0
|
||||
return num / den
|
||||
|
||||
|
||||
def all_pairs_node_connectivity(G, nbunch=None, flow_func=None):
|
||||
"""Compute node connectivity between all pairs of nodes of G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
nbunch: container
|
||||
Container of nodes. If provided node connectivity will be computed
|
||||
only over pairs of nodes in nbunch.
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See below for details. The
|
||||
choice of the default function may change from version
|
||||
to version and should not be relied on. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
all_pairs : dict
|
||||
A dictionary with node connectivity between all pairs of nodes
|
||||
in G, or in nbunch if provided.
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`local_node_connectivity`
|
||||
:meth:`edge_connectivity`
|
||||
:meth:`local_edge_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
"""
|
||||
if nbunch is None:
|
||||
nbunch = G
|
||||
else:
|
||||
nbunch = set(nbunch)
|
||||
|
||||
directed = G.is_directed()
|
||||
if directed:
|
||||
iter_func = itertools.permutations
|
||||
else:
|
||||
iter_func = itertools.combinations
|
||||
|
||||
all_pairs = {n: {} for n in nbunch}
|
||||
|
||||
# Reuse auxiliary digraph and residual network
|
||||
H = build_auxiliary_node_connectivity(G)
|
||||
mapping = H.graph["mapping"]
|
||||
R = build_residual_network(H, "capacity")
|
||||
kwargs = dict(flow_func=flow_func, auxiliary=H, residual=R)
|
||||
|
||||
for u, v in iter_func(nbunch, 2):
|
||||
K = local_node_connectivity(G, u, v, **kwargs)
|
||||
all_pairs[u][v] = K
|
||||
if not directed:
|
||||
all_pairs[v][u] = K
|
||||
|
||||
return all_pairs
|
||||
|
||||
|
||||
def local_edge_connectivity(
|
||||
G, s, t, flow_func=None, auxiliary=None, residual=None, cutoff=None
|
||||
):
|
||||
r"""Returns local edge connectivity for nodes s and t in G.
|
||||
|
||||
Local edge connectivity for two nodes s and t is the minimum number
|
||||
of edges that must be removed to disconnect them.
|
||||
|
||||
This is a flow based implementation of edge connectivity. We compute the
|
||||
maximum flow on an auxiliary digraph build from the original
|
||||
network (see below for details). This is equal to the local edge
|
||||
connectivity because the value of a maximum s-t-flow is equal to the
|
||||
capacity of a minimum s-t-cut (Ford and Fulkerson theorem) [1]_ .
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected or directed graph
|
||||
|
||||
s : node
|
||||
Source node
|
||||
|
||||
t : node
|
||||
Target node
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See below for details. The
|
||||
choice of the default function may change from version
|
||||
to version and should not be relied on. Default value: None.
|
||||
|
||||
auxiliary : NetworkX DiGraph
|
||||
Auxiliary digraph for computing flow based edge connectivity. If
|
||||
provided it will be reused instead of recreated. Default value: None.
|
||||
|
||||
residual : NetworkX DiGraph
|
||||
Residual network to compute maximum flow. If provided it will be
|
||||
reused instead of recreated. Default value: None.
|
||||
|
||||
cutoff : integer, float
|
||||
If specified, the maximum flow algorithm will terminate when the
|
||||
flow value reaches or exceeds the cutoff. This is only for the
|
||||
algorithms that support the cutoff parameter: :meth:`edmonds_karp`
|
||||
and :meth:`shortest_augmenting_path`. Other algorithms will ignore
|
||||
this parameter. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
K : integer
|
||||
local edge connectivity for nodes s and t.
|
||||
|
||||
Examples
|
||||
--------
|
||||
This function is not imported in the base NetworkX namespace, so you
|
||||
have to explicitly import it from the connectivity package:
|
||||
|
||||
>>> from networkx.algorithms.connectivity import local_edge_connectivity
|
||||
|
||||
We use in this example the platonic icosahedral graph, which has edge
|
||||
connectivity 5.
|
||||
|
||||
>>> G = nx.icosahedral_graph()
|
||||
>>> local_edge_connectivity(G, 0, 6)
|
||||
5
|
||||
|
||||
If you need to compute local connectivity on several pairs of
|
||||
nodes in the same graph, it is recommended that you reuse the
|
||||
data structures that NetworkX uses in the computation: the
|
||||
auxiliary digraph for edge connectivity, and the residual
|
||||
network for the underlying maximum flow computation.
|
||||
|
||||
Example of how to compute local edge connectivity among
|
||||
all pairs of nodes of the platonic icosahedral graph reusing
|
||||
the data structures.
|
||||
|
||||
>>> import itertools
|
||||
>>> # You also have to explicitly import the function for
|
||||
>>> # building the auxiliary digraph from the connectivity package
|
||||
>>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity
|
||||
>>> H = build_auxiliary_edge_connectivity(G)
|
||||
>>> # And the function for building the residual network from the
|
||||
>>> # flow package
|
||||
>>> from networkx.algorithms.flow import build_residual_network
|
||||
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
||||
>>> R = build_residual_network(H, "capacity")
|
||||
>>> result = dict.fromkeys(G, dict())
|
||||
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
||||
>>> # as parameters
|
||||
>>> for u, v in itertools.combinations(G, 2):
|
||||
... k = local_edge_connectivity(G, u, v, auxiliary=H, residual=R)
|
||||
... result[u][v] = k
|
||||
>>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
|
||||
True
|
||||
|
||||
You can also use alternative flow algorithms for computing edge
|
||||
connectivity. For instance, in dense networks the algorithm
|
||||
:meth:`shortest_augmenting_path` will usually perform better than
|
||||
the default :meth:`edmonds_karp` which is faster for sparse
|
||||
networks with highly skewed degree distributions. Alternative flow
|
||||
functions have to be explicitly imported from the flow package.
|
||||
|
||||
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
||||
>>> local_edge_connectivity(G, 0, 6, flow_func=shortest_augmenting_path)
|
||||
5
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a flow based implementation of edge connectivity. We compute the
|
||||
maximum flow using, by default, the :meth:`edmonds_karp` algorithm on an
|
||||
auxiliary digraph build from the original input graph:
|
||||
|
||||
If the input graph is undirected, we replace each edge (`u`,`v`) with
|
||||
two reciprocal arcs (`u`, `v`) and (`v`, `u`) and then we set the attribute
|
||||
'capacity' for each arc to 1. If the input graph is directed we simply
|
||||
add the 'capacity' attribute. This is an implementation of algorithm 1
|
||||
in [1]_.
|
||||
|
||||
The maximum flow in the auxiliary network is equal to the local edge
|
||||
connectivity because the value of a maximum s-t-flow is equal to the
|
||||
capacity of a minimum s-t-cut (Ford and Fulkerson theorem).
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`edge_connectivity`
|
||||
:meth:`local_node_connectivity`
|
||||
:meth:`node_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
||||
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
||||
|
||||
"""
|
||||
if flow_func is None:
|
||||
flow_func = default_flow_func
|
||||
|
||||
if auxiliary is None:
|
||||
H = build_auxiliary_edge_connectivity(G)
|
||||
else:
|
||||
H = auxiliary
|
||||
|
||||
kwargs = dict(flow_func=flow_func, residual=residual)
|
||||
if flow_func is shortest_augmenting_path:
|
||||
kwargs["cutoff"] = cutoff
|
||||
kwargs["two_phase"] = True
|
||||
elif flow_func is edmonds_karp:
|
||||
kwargs["cutoff"] = cutoff
|
||||
elif flow_func is dinitz:
|
||||
kwargs["cutoff"] = cutoff
|
||||
elif flow_func is boykov_kolmogorov:
|
||||
kwargs["cutoff"] = cutoff
|
||||
|
||||
return nx.maximum_flow_value(H, s, t, **kwargs)
|
||||
|
||||
|
||||
def edge_connectivity(G, s=None, t=None, flow_func=None, cutoff=None):
|
||||
r"""Returns the edge connectivity of the graph or digraph G.
|
||||
|
||||
The edge connectivity is equal to the minimum number of edges that
|
||||
must be removed to disconnect G or render it trivial. If source
|
||||
and target nodes are provided, this function returns the local edge
|
||||
connectivity: the minimum number of edges that must be removed to
|
||||
break all paths from source to target in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected or directed graph
|
||||
|
||||
s : node
|
||||
Source node. Optional. Default value: None.
|
||||
|
||||
t : node
|
||||
Target node. Optional. Default value: None.
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See below for details. The
|
||||
choice of the default function may change from version
|
||||
to version and should not be relied on. Default value: None.
|
||||
|
||||
cutoff : integer, float
|
||||
If specified, the maximum flow algorithm will terminate when the
|
||||
flow value reaches or exceeds the cutoff. This is only for the
|
||||
algorithms that support the cutoff parameter: e.g., :meth:`edmonds_karp`
|
||||
and :meth:`shortest_augmenting_path`. Other algorithms will ignore
|
||||
this parameter. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
K : integer
|
||||
Edge connectivity for G, or local edge connectivity if source
|
||||
and target were provided
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # Platonic icosahedral graph is 5-edge-connected
|
||||
>>> G = nx.icosahedral_graph()
|
||||
>>> nx.edge_connectivity(G)
|
||||
5
|
||||
|
||||
You can use alternative flow algorithms for the underlying
|
||||
maximum flow computation. In dense networks the algorithm
|
||||
:meth:`shortest_augmenting_path` will usually perform better
|
||||
than the default :meth:`edmonds_karp`, which is faster for
|
||||
sparse networks with highly skewed degree distributions.
|
||||
Alternative flow functions have to be explicitly imported
|
||||
from the flow package.
|
||||
|
||||
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
||||
>>> nx.edge_connectivity(G, flow_func=shortest_augmenting_path)
|
||||
5
|
||||
|
||||
If you specify a pair of nodes (source and target) as parameters,
|
||||
this function returns the value of local edge connectivity.
|
||||
|
||||
>>> nx.edge_connectivity(G, 3, 7)
|
||||
5
|
||||
|
||||
If you need to perform several local computations among different
|
||||
pairs of nodes on the same graph, it is recommended that you reuse
|
||||
the data structures used in the maximum flow computations. See
|
||||
:meth:`local_edge_connectivity` for details.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a flow based implementation of global edge connectivity.
|
||||
For undirected graphs the algorithm works by finding a 'small'
|
||||
dominating set of nodes of G (see algorithm 7 in [1]_ ) and
|
||||
computing local maximum flow (see :meth:`local_edge_connectivity`)
|
||||
between an arbitrary node in the dominating set and the rest of
|
||||
nodes in it. This is an implementation of algorithm 6 in [1]_ .
|
||||
For directed graphs, the algorithm does n calls to the maximum
|
||||
flow function. This is an implementation of algorithm 8 in [1]_ .
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`local_edge_connectivity`
|
||||
:meth:`local_node_connectivity`
|
||||
:meth:`node_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
:meth:`k_edge_components`
|
||||
:meth:`k_edge_subgraphs`
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
||||
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
||||
|
||||
"""
|
||||
if (s is not None and t is None) or (s is None and t is not None):
|
||||
raise nx.NetworkXError("Both source and target must be specified.")
|
||||
|
||||
# Local edge connectivity
|
||||
if s is not None and t is not None:
|
||||
if s not in G:
|
||||
raise nx.NetworkXError(f"node {s} not in graph")
|
||||
if t not in G:
|
||||
raise nx.NetworkXError(f"node {t} not in graph")
|
||||
return local_edge_connectivity(G, s, t, flow_func=flow_func, cutoff=cutoff)
|
||||
|
||||
# Global edge connectivity
|
||||
# reuse auxiliary digraph and residual network
|
||||
H = build_auxiliary_edge_connectivity(G)
|
||||
R = build_residual_network(H, "capacity")
|
||||
kwargs = dict(flow_func=flow_func, auxiliary=H, residual=R)
|
||||
|
||||
if G.is_directed():
|
||||
# Algorithm 8 in [1]
|
||||
if not nx.is_weakly_connected(G):
|
||||
return 0
|
||||
|
||||
# initial value for \lambda is minimum degree
|
||||
L = min(d for n, d in G.degree())
|
||||
nodes = list(G)
|
||||
n = len(nodes)
|
||||
|
||||
if cutoff is not None:
|
||||
L = min(cutoff, L)
|
||||
|
||||
for i in range(n):
|
||||
kwargs["cutoff"] = L
|
||||
try:
|
||||
L = min(L, local_edge_connectivity(G, nodes[i], nodes[i + 1], **kwargs))
|
||||
except IndexError: # last node!
|
||||
L = min(L, local_edge_connectivity(G, nodes[i], nodes[0], **kwargs))
|
||||
return L
|
||||
else: # undirected
|
||||
# Algorithm 6 in [1]
|
||||
if not nx.is_connected(G):
|
||||
return 0
|
||||
|
||||
# initial value for \lambda is minimum degree
|
||||
L = min(d for n, d in G.degree())
|
||||
|
||||
if cutoff is not None:
|
||||
L = min(cutoff, L)
|
||||
|
||||
# A dominating set is \lambda-covering
|
||||
# We need a dominating set with at least two nodes
|
||||
for node in G:
|
||||
D = nx.dominating_set(G, start_with=node)
|
||||
v = D.pop()
|
||||
if D:
|
||||
break
|
||||
else:
|
||||
# in complete graphs the dominating sets will always be of one node
|
||||
# thus we return min degree
|
||||
return L
|
||||
|
||||
for w in D:
|
||||
kwargs["cutoff"] = L
|
||||
L = min(L, local_edge_connectivity(G, v, w, **kwargs))
|
||||
|
||||
return L
|
599
venv/Lib/site-packages/networkx/algorithms/connectivity/cuts.py
Normal file
599
venv/Lib/site-packages/networkx/algorithms/connectivity/cuts.py
Normal file
|
@ -0,0 +1,599 @@
|
|||
"""
|
||||
Flow based cut algorithms
|
||||
"""
|
||||
import itertools
|
||||
import networkx as nx
|
||||
|
||||
# Define the default maximum flow function to use in all flow based
|
||||
# cut algorithms.
|
||||
from networkx.algorithms.flow import edmonds_karp
|
||||
from networkx.algorithms.flow import build_residual_network
|
||||
|
||||
default_flow_func = edmonds_karp
|
||||
|
||||
from .utils import build_auxiliary_node_connectivity, build_auxiliary_edge_connectivity
|
||||
|
||||
__all__ = [
|
||||
"minimum_st_node_cut",
|
||||
"minimum_node_cut",
|
||||
"minimum_st_edge_cut",
|
||||
"minimum_edge_cut",
|
||||
]
|
||||
|
||||
|
||||
def minimum_st_edge_cut(G, s, t, flow_func=None, auxiliary=None, residual=None):
|
||||
"""Returns the edges of the cut-set of a minimum (s, t)-cut.
|
||||
|
||||
This function returns the set of edges of minimum cardinality that,
|
||||
if removed, would destroy all paths among source and target in G.
|
||||
Edge weights are not considered. See :meth:`minimum_cut` for
|
||||
computing minimum cuts considering edge weights.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
s : node
|
||||
Source node for the flow.
|
||||
|
||||
t : node
|
||||
Sink node for the flow.
|
||||
|
||||
auxiliary : NetworkX DiGraph
|
||||
Auxiliary digraph to compute flow based node connectivity. It has
|
||||
to have a graph attribute called mapping with a dictionary mapping
|
||||
node names in G and in the auxiliary digraph. If provided
|
||||
it will be reused instead of recreated. Default value: None.
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See :meth:`node_connectivity` for
|
||||
details. The choice of the default function may change from version
|
||||
to version and should not be relied on. Default value: None.
|
||||
|
||||
residual : NetworkX DiGraph
|
||||
Residual network to compute maximum flow. If provided it will be
|
||||
reused instead of recreated. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cutset : set
|
||||
Set of edges that, if removed from the graph, will disconnect it.
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`minimum_cut`
|
||||
:meth:`minimum_node_cut`
|
||||
:meth:`minimum_edge_cut`
|
||||
:meth:`stoer_wagner`
|
||||
:meth:`node_connectivity`
|
||||
:meth:`edge_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
Examples
|
||||
--------
|
||||
This function is not imported in the base NetworkX namespace, so you
|
||||
have to explicitly import it from the connectivity package:
|
||||
|
||||
>>> from networkx.algorithms.connectivity import minimum_st_edge_cut
|
||||
|
||||
We use in this example the platonic icosahedral graph, which has edge
|
||||
connectivity 5.
|
||||
|
||||
>>> G = nx.icosahedral_graph()
|
||||
>>> len(minimum_st_edge_cut(G, 0, 6))
|
||||
5
|
||||
|
||||
If you need to compute local edge cuts on several pairs of
|
||||
nodes in the same graph, it is recommended that you reuse the
|
||||
data structures that NetworkX uses in the computation: the
|
||||
auxiliary digraph for edge connectivity, and the residual
|
||||
network for the underlying maximum flow computation.
|
||||
|
||||
Example of how to compute local edge cuts among all pairs of
|
||||
nodes of the platonic icosahedral graph reusing the data
|
||||
structures.
|
||||
|
||||
>>> import itertools
|
||||
>>> # You also have to explicitly import the function for
|
||||
>>> # building the auxiliary digraph from the connectivity package
|
||||
>>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity
|
||||
>>> H = build_auxiliary_edge_connectivity(G)
|
||||
>>> # And the function for building the residual network from the
|
||||
>>> # flow package
|
||||
>>> from networkx.algorithms.flow import build_residual_network
|
||||
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
||||
>>> R = build_residual_network(H, "capacity")
|
||||
>>> result = dict.fromkeys(G, dict())
|
||||
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
||||
>>> # as parameters
|
||||
>>> for u, v in itertools.combinations(G, 2):
|
||||
... k = len(minimum_st_edge_cut(G, u, v, auxiliary=H, residual=R))
|
||||
... result[u][v] = k
|
||||
>>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
|
||||
True
|
||||
|
||||
You can also use alternative flow algorithms for computing edge
|
||||
cuts. For instance, in dense networks the algorithm
|
||||
:meth:`shortest_augmenting_path` will usually perform better than
|
||||
the default :meth:`edmonds_karp` which is faster for sparse
|
||||
networks with highly skewed degree distributions. Alternative flow
|
||||
functions have to be explicitly imported from the flow package.
|
||||
|
||||
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
||||
>>> len(minimum_st_edge_cut(G, 0, 6, flow_func=shortest_augmenting_path))
|
||||
5
|
||||
|
||||
"""
|
||||
if flow_func is None:
|
||||
flow_func = default_flow_func
|
||||
|
||||
if auxiliary is None:
|
||||
H = build_auxiliary_edge_connectivity(G)
|
||||
else:
|
||||
H = auxiliary
|
||||
|
||||
kwargs = dict(capacity="capacity", flow_func=flow_func, residual=residual)
|
||||
|
||||
cut_value, partition = nx.minimum_cut(H, s, t, **kwargs)
|
||||
reachable, non_reachable = partition
|
||||
# Any edge in the original graph linking the two sets in the
|
||||
# partition is part of the edge cutset
|
||||
cutset = set()
|
||||
for u, nbrs in ((n, G[n]) for n in reachable):
|
||||
cutset.update((u, v) for v in nbrs if v in non_reachable)
|
||||
|
||||
return cutset
|
||||
|
||||
|
||||
def minimum_st_node_cut(G, s, t, flow_func=None, auxiliary=None, residual=None):
|
||||
r"""Returns a set of nodes of minimum cardinality that disconnect source
|
||||
from target in G.
|
||||
|
||||
This function returns the set of nodes of minimum cardinality that,
|
||||
if removed, would destroy all paths among source and target in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
s : node
|
||||
Source node.
|
||||
|
||||
t : node
|
||||
Target node.
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See below for details. The choice
|
||||
of the default function may change from version to version and
|
||||
should not be relied on. Default value: None.
|
||||
|
||||
auxiliary : NetworkX DiGraph
|
||||
Auxiliary digraph to compute flow based node connectivity. It has
|
||||
to have a graph attribute called mapping with a dictionary mapping
|
||||
node names in G and in the auxiliary digraph. If provided
|
||||
it will be reused instead of recreated. Default value: None.
|
||||
|
||||
residual : NetworkX DiGraph
|
||||
Residual network to compute maximum flow. If provided it will be
|
||||
reused instead of recreated. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cutset : set
|
||||
Set of nodes that, if removed, would destroy all paths between
|
||||
source and target in G.
|
||||
|
||||
Examples
|
||||
--------
|
||||
This function is not imported in the base NetworkX namespace, so you
|
||||
have to explicitly import it from the connectivity package:
|
||||
|
||||
>>> from networkx.algorithms.connectivity import minimum_st_node_cut
|
||||
|
||||
We use in this example the platonic icosahedral graph, which has node
|
||||
connectivity 5.
|
||||
|
||||
>>> G = nx.icosahedral_graph()
|
||||
>>> len(minimum_st_node_cut(G, 0, 6))
|
||||
5
|
||||
|
||||
If you need to compute local st cuts between several pairs of
|
||||
nodes in the same graph, it is recommended that you reuse the
|
||||
data structures that NetworkX uses in the computation: the
|
||||
auxiliary digraph for node connectivity and node cuts, and the
|
||||
residual network for the underlying maximum flow computation.
|
||||
|
||||
Example of how to compute local st node cuts reusing the data
|
||||
structures:
|
||||
|
||||
>>> # You also have to explicitly import the function for
|
||||
>>> # building the auxiliary digraph from the connectivity package
|
||||
>>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity
|
||||
>>> H = build_auxiliary_node_connectivity(G)
|
||||
>>> # And the function for building the residual network from the
|
||||
>>> # flow package
|
||||
>>> from networkx.algorithms.flow import build_residual_network
|
||||
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
||||
>>> R = build_residual_network(H, "capacity")
|
||||
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
||||
>>> # as parameters
|
||||
>>> len(minimum_st_node_cut(G, 0, 6, auxiliary=H, residual=R))
|
||||
5
|
||||
|
||||
You can also use alternative flow algorithms for computing minimum st
|
||||
node cuts. For instance, in dense networks the algorithm
|
||||
:meth:`shortest_augmenting_path` will usually perform better than
|
||||
the default :meth:`edmonds_karp` which is faster for sparse
|
||||
networks with highly skewed degree distributions. Alternative flow
|
||||
functions have to be explicitly imported from the flow package.
|
||||
|
||||
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
||||
>>> len(minimum_st_node_cut(G, 0, 6, flow_func=shortest_augmenting_path))
|
||||
5
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a flow based implementation of minimum node cut. The algorithm
|
||||
is based in solving a number of maximum flow computations to determine
|
||||
the capacity of the minimum cut on an auxiliary directed network that
|
||||
corresponds to the minimum node cut of G. It handles both directed
|
||||
and undirected graphs. This implementation is based on algorithm 11
|
||||
in [1]_.
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`minimum_node_cut`
|
||||
:meth:`minimum_edge_cut`
|
||||
:meth:`stoer_wagner`
|
||||
:meth:`node_connectivity`
|
||||
:meth:`edge_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
||||
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
||||
|
||||
"""
|
||||
if auxiliary is None:
|
||||
H = build_auxiliary_node_connectivity(G)
|
||||
else:
|
||||
H = auxiliary
|
||||
|
||||
mapping = H.graph.get("mapping", None)
|
||||
if mapping is None:
|
||||
raise nx.NetworkXError("Invalid auxiliary digraph.")
|
||||
if G.has_edge(s, t) or G.has_edge(t, s):
|
||||
return {}
|
||||
kwargs = dict(flow_func=flow_func, residual=residual, auxiliary=H)
|
||||
|
||||
# The edge cut in the auxiliary digraph corresponds to the node cut in the
|
||||
# original graph.
|
||||
edge_cut = minimum_st_edge_cut(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs)
|
||||
# Each node in the original graph maps to two nodes of the auxiliary graph
|
||||
node_cut = {H.nodes[node]["id"] for edge in edge_cut for node in edge}
|
||||
return node_cut - {s, t}
|
||||
|
||||
|
||||
def minimum_node_cut(G, s=None, t=None, flow_func=None):
|
||||
r"""Returns a set of nodes of minimum cardinality that disconnects G.
|
||||
|
||||
If source and target nodes are provided, this function returns the
|
||||
set of nodes of minimum cardinality that, if removed, would destroy
|
||||
all paths among source and target in G. If not, it returns a set
|
||||
of nodes of minimum cardinality that disconnects G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
s : node
|
||||
Source node. Optional. Default value: None.
|
||||
|
||||
t : node
|
||||
Target node. Optional. Default value: None.
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See below for details. The
|
||||
choice of the default function may change from version
|
||||
to version and should not be relied on. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cutset : set
|
||||
Set of nodes that, if removed, would disconnect G. If source
|
||||
and target nodes are provided, the set contains the nodes that
|
||||
if removed, would destroy all paths between source and target.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # Platonic icosahedral graph has node connectivity 5
|
||||
>>> G = nx.icosahedral_graph()
|
||||
>>> node_cut = nx.minimum_node_cut(G)
|
||||
>>> len(node_cut)
|
||||
5
|
||||
|
||||
You can use alternative flow algorithms for the underlying maximum
|
||||
flow computation. In dense networks the algorithm
|
||||
:meth:`shortest_augmenting_path` will usually perform better
|
||||
than the default :meth:`edmonds_karp`, which is faster for
|
||||
sparse networks with highly skewed degree distributions. Alternative
|
||||
flow functions have to be explicitly imported from the flow package.
|
||||
|
||||
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
||||
>>> node_cut == nx.minimum_node_cut(G, flow_func=shortest_augmenting_path)
|
||||
True
|
||||
|
||||
If you specify a pair of nodes (source and target) as parameters,
|
||||
this function returns a local st node cut.
|
||||
|
||||
>>> len(nx.minimum_node_cut(G, 3, 7))
|
||||
5
|
||||
|
||||
If you need to perform several local st cuts among different
|
||||
pairs of nodes on the same graph, it is recommended that you reuse
|
||||
the data structures used in the maximum flow computations. See
|
||||
:meth:`minimum_st_node_cut` for details.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a flow based implementation of minimum node cut. The algorithm
|
||||
is based in solving a number of maximum flow computations to determine
|
||||
the capacity of the minimum cut on an auxiliary directed network that
|
||||
corresponds to the minimum node cut of G. It handles both directed
|
||||
and undirected graphs. This implementation is based on algorithm 11
|
||||
in [1]_.
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`minimum_st_node_cut`
|
||||
:meth:`minimum_cut`
|
||||
:meth:`minimum_edge_cut`
|
||||
:meth:`stoer_wagner`
|
||||
:meth:`node_connectivity`
|
||||
:meth:`edge_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
||||
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
||||
|
||||
"""
|
||||
if (s is not None and t is None) or (s is None and t is not None):
|
||||
raise nx.NetworkXError("Both source and target must be specified.")
|
||||
|
||||
# Local minimum node cut.
|
||||
if s is not None and t is not None:
|
||||
if s not in G:
|
||||
raise nx.NetworkXError(f"node {s} not in graph")
|
||||
if t not in G:
|
||||
raise nx.NetworkXError(f"node {t} not in graph")
|
||||
return minimum_st_node_cut(G, s, t, flow_func=flow_func)
|
||||
|
||||
# Global minimum node cut.
|
||||
# Analog to the algorithm 11 for global node connectivity in [1].
|
||||
if G.is_directed():
|
||||
if not nx.is_weakly_connected(G):
|
||||
raise nx.NetworkXError("Input graph is not connected")
|
||||
iter_func = itertools.permutations
|
||||
|
||||
def neighbors(v):
|
||||
return itertools.chain.from_iterable([G.predecessors(v), G.successors(v)])
|
||||
|
||||
else:
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("Input graph is not connected")
|
||||
iter_func = itertools.combinations
|
||||
neighbors = G.neighbors
|
||||
|
||||
# Reuse the auxiliary digraph and the residual network.
|
||||
H = build_auxiliary_node_connectivity(G)
|
||||
R = build_residual_network(H, "capacity")
|
||||
kwargs = dict(flow_func=flow_func, auxiliary=H, residual=R)
|
||||
|
||||
# Choose a node with minimum degree.
|
||||
v = min(G, key=G.degree)
|
||||
# Initial node cutset is all neighbors of the node with minimum degree.
|
||||
min_cut = set(G[v])
|
||||
# Compute st node cuts between v and all its non-neighbors nodes in G.
|
||||
for w in set(G) - set(neighbors(v)) - {v}:
|
||||
this_cut = minimum_st_node_cut(G, v, w, **kwargs)
|
||||
if len(min_cut) >= len(this_cut):
|
||||
min_cut = this_cut
|
||||
# Also for non adjacent pairs of neighbors of v.
|
||||
for x, y in iter_func(neighbors(v), 2):
|
||||
if y in G[x]:
|
||||
continue
|
||||
this_cut = minimum_st_node_cut(G, x, y, **kwargs)
|
||||
if len(min_cut) >= len(this_cut):
|
||||
min_cut = this_cut
|
||||
|
||||
return min_cut
|
||||
|
||||
|
||||
def minimum_edge_cut(G, s=None, t=None, flow_func=None):
|
||||
r"""Returns a set of edges of minimum cardinality that disconnects G.
|
||||
|
||||
If source and target nodes are provided, this function returns the
|
||||
set of edges of minimum cardinality that, if removed, would break
|
||||
all paths among source and target in G. If not, it returns a set of
|
||||
edges of minimum cardinality that disconnects G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
s : node
|
||||
Source node. Optional. Default value: None.
|
||||
|
||||
t : node
|
||||
Target node. Optional. Default value: None.
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See below for details. The
|
||||
choice of the default function may change from version
|
||||
to version and should not be relied on. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cutset : set
|
||||
Set of edges that, if removed, would disconnect G. If source
|
||||
and target nodes are provided, the set contains the edges that
|
||||
if removed, would destroy all paths between source and target.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # Platonic icosahedral graph has edge connectivity 5
|
||||
>>> G = nx.icosahedral_graph()
|
||||
>>> len(nx.minimum_edge_cut(G))
|
||||
5
|
||||
|
||||
You can use alternative flow algorithms for the underlying
|
||||
maximum flow computation. In dense networks the algorithm
|
||||
:meth:`shortest_augmenting_path` will usually perform better
|
||||
than the default :meth:`edmonds_karp`, which is faster for
|
||||
sparse networks with highly skewed degree distributions.
|
||||
Alternative flow functions have to be explicitly imported
|
||||
from the flow package.
|
||||
|
||||
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
||||
>>> len(nx.minimum_edge_cut(G, flow_func=shortest_augmenting_path))
|
||||
5
|
||||
|
||||
If you specify a pair of nodes (source and target) as parameters,
|
||||
this function returns the value of local edge connectivity.
|
||||
|
||||
>>> nx.edge_connectivity(G, 3, 7)
|
||||
5
|
||||
|
||||
If you need to perform several local computations among different
|
||||
pairs of nodes on the same graph, it is recommended that you reuse
|
||||
the data structures used in the maximum flow computations. See
|
||||
:meth:`local_edge_connectivity` for details.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a flow based implementation of minimum edge cut. For
|
||||
undirected graphs the algorithm works by finding a 'small' dominating
|
||||
set of nodes of G (see algorithm 7 in [1]_) and computing the maximum
|
||||
flow between an arbitrary node in the dominating set and the rest of
|
||||
nodes in it. This is an implementation of algorithm 6 in [1]_. For
|
||||
directed graphs, the algorithm does n calls to the max flow function.
|
||||
The function raises an error if the directed graph is not weakly
|
||||
connected and returns an empty set if it is weakly connected.
|
||||
It is an implementation of algorithm 8 in [1]_.
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`minimum_st_edge_cut`
|
||||
:meth:`minimum_node_cut`
|
||||
:meth:`stoer_wagner`
|
||||
:meth:`node_connectivity`
|
||||
:meth:`edge_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms.
|
||||
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
||||
|
||||
"""
|
||||
if (s is not None and t is None) or (s is None and t is not None):
|
||||
raise nx.NetworkXError("Both source and target must be specified.")
|
||||
|
||||
# reuse auxiliary digraph and residual network
|
||||
H = build_auxiliary_edge_connectivity(G)
|
||||
R = build_residual_network(H, "capacity")
|
||||
kwargs = dict(flow_func=flow_func, residual=R, auxiliary=H)
|
||||
|
||||
# Local minimum edge cut if s and t are not None
|
||||
if s is not None and t is not None:
|
||||
if s not in G:
|
||||
raise nx.NetworkXError(f"node {s} not in graph")
|
||||
if t not in G:
|
||||
raise nx.NetworkXError(f"node {t} not in graph")
|
||||
return minimum_st_edge_cut(H, s, t, **kwargs)
|
||||
|
||||
# Global minimum edge cut
|
||||
# Analog to the algorithm for global edge connectivity
|
||||
if G.is_directed():
|
||||
# Based on algorithm 8 in [1]
|
||||
if not nx.is_weakly_connected(G):
|
||||
raise nx.NetworkXError("Input graph is not connected")
|
||||
|
||||
# Initial cutset is all edges of a node with minimum degree
|
||||
node = min(G, key=G.degree)
|
||||
min_cut = set(G.edges(node))
|
||||
nodes = list(G)
|
||||
n = len(nodes)
|
||||
for i in range(n):
|
||||
try:
|
||||
this_cut = minimum_st_edge_cut(H, nodes[i], nodes[i + 1], **kwargs)
|
||||
if len(this_cut) <= len(min_cut):
|
||||
min_cut = this_cut
|
||||
except IndexError: # Last node!
|
||||
this_cut = minimum_st_edge_cut(H, nodes[i], nodes[0], **kwargs)
|
||||
if len(this_cut) <= len(min_cut):
|
||||
min_cut = this_cut
|
||||
|
||||
return min_cut
|
||||
|
||||
else: # undirected
|
||||
# Based on algorithm 6 in [1]
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("Input graph is not connected")
|
||||
|
||||
# Initial cutset is all edges of a node with minimum degree
|
||||
node = min(G, key=G.degree)
|
||||
min_cut = set(G.edges(node))
|
||||
# A dominating set is \lambda-covering
|
||||
# We need a dominating set with at least two nodes
|
||||
for node in G:
|
||||
D = nx.dominating_set(G, start_with=node)
|
||||
v = D.pop()
|
||||
if D:
|
||||
break
|
||||
else:
|
||||
# in complete graphs the dominating set will always be of one node
|
||||
# thus we return min_cut, which now contains the edges of a node
|
||||
# with minimum degree
|
||||
return min_cut
|
||||
for w in D:
|
||||
this_cut = minimum_st_edge_cut(H, v, w, **kwargs)
|
||||
if len(this_cut) <= len(min_cut):
|
||||
min_cut = this_cut
|
||||
|
||||
return min_cut
|
|
@ -0,0 +1,393 @@
|
|||
"""Flow based node and edge disjoint paths."""
|
||||
import networkx as nx
|
||||
from networkx.exception import NetworkXNoPath
|
||||
|
||||
# Define the default maximum flow function to use for the undelying
|
||||
# maximum flow computations
|
||||
from networkx.algorithms.flow import edmonds_karp
|
||||
from networkx.algorithms.flow import preflow_push
|
||||
from networkx.algorithms.flow import shortest_augmenting_path
|
||||
|
||||
default_flow_func = edmonds_karp
|
||||
# Functions to build auxiliary data structures.
|
||||
from .utils import build_auxiliary_node_connectivity
|
||||
from .utils import build_auxiliary_edge_connectivity
|
||||
|
||||
from itertools import filterfalse as _filterfalse
|
||||
|
||||
__all__ = ["edge_disjoint_paths", "node_disjoint_paths"]
|
||||
|
||||
|
||||
def edge_disjoint_paths(
|
||||
G, s, t, flow_func=None, cutoff=None, auxiliary=None, residual=None
|
||||
):
|
||||
"""Returns the edges disjoint paths between source and target.
|
||||
|
||||
Edge disjoint paths are paths that do not share any edge. The
|
||||
number of edge disjoint paths between source and target is equal
|
||||
to their edge connectivity.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
s : node
|
||||
Source node for the flow.
|
||||
|
||||
t : node
|
||||
Sink node for the flow.
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. The choice of the default function
|
||||
may change from version to version and should not be relied on.
|
||||
Default value: None.
|
||||
|
||||
cutoff : int
|
||||
Maximum number of paths to yield. Some of the maximum flow
|
||||
algorithms, such as :meth:`edmonds_karp` (the default) and
|
||||
:meth:`shortest_augmenting_path` support the cutoff parameter,
|
||||
and will terminate when the flow value reaches or exceeds the
|
||||
cutoff. Other algorithms will ignore this parameter.
|
||||
Default value: None.
|
||||
|
||||
auxiliary : NetworkX DiGraph
|
||||
Auxiliary digraph to compute flow based edge connectivity. It has
|
||||
to have a graph attribute called mapping with a dictionary mapping
|
||||
node names in G and in the auxiliary digraph. If provided
|
||||
it will be reused instead of recreated. Default value: None.
|
||||
|
||||
residual : NetworkX DiGraph
|
||||
Residual network to compute maximum flow. If provided it will be
|
||||
reused instead of recreated. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
paths : generator
|
||||
A generator of edge independent paths.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNoPath
|
||||
If there is no path between source and target.
|
||||
|
||||
NetworkXError
|
||||
If source or target are not in the graph G.
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`node_disjoint_paths`
|
||||
:meth:`edge_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
Examples
|
||||
--------
|
||||
We use in this example the platonic icosahedral graph, which has node
|
||||
edge connectivity 5, thus there are 5 edge disjoint paths between any
|
||||
pair of nodes.
|
||||
|
||||
>>> G = nx.icosahedral_graph()
|
||||
>>> len(list(nx.edge_disjoint_paths(G, 0, 6)))
|
||||
5
|
||||
|
||||
|
||||
If you need to compute edge disjoint paths on several pairs of
|
||||
nodes in the same graph, it is recommended that you reuse the
|
||||
data structures that NetworkX uses in the computation: the
|
||||
auxiliary digraph for edge connectivity, and the residual
|
||||
network for the underlying maximum flow computation.
|
||||
|
||||
Example of how to compute edge disjoint paths among all pairs of
|
||||
nodes of the platonic icosahedral graph reusing the data
|
||||
structures.
|
||||
|
||||
>>> import itertools
|
||||
>>> # You also have to explicitly import the function for
|
||||
>>> # building the auxiliary digraph from the connectivity package
|
||||
>>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity
|
||||
>>> H = build_auxiliary_edge_connectivity(G)
|
||||
>>> # And the function for building the residual network from the
|
||||
>>> # flow package
|
||||
>>> from networkx.algorithms.flow import build_residual_network
|
||||
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
||||
>>> R = build_residual_network(H, "capacity")
|
||||
>>> result = {n: {} for n in G}
|
||||
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
||||
>>> # as arguments
|
||||
>>> for u, v in itertools.combinations(G, 2):
|
||||
... k = len(list(nx.edge_disjoint_paths(G, u, v, auxiliary=H, residual=R)))
|
||||
... result[u][v] = k
|
||||
>>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2))
|
||||
True
|
||||
|
||||
You can also use alternative flow algorithms for computing edge disjoint
|
||||
paths. For instance, in dense networks the algorithm
|
||||
:meth:`shortest_augmenting_path` will usually perform better than
|
||||
the default :meth:`edmonds_karp` which is faster for sparse
|
||||
networks with highly skewed degree distributions. Alternative flow
|
||||
functions have to be explicitly imported from the flow package.
|
||||
|
||||
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
||||
>>> len(list(nx.edge_disjoint_paths(G, 0, 6, flow_func=shortest_augmenting_path)))
|
||||
5
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a flow based implementation of edge disjoint paths. We compute
|
||||
the maximum flow between source and target on an auxiliary directed
|
||||
network. The saturated edges in the residual network after running the
|
||||
maximum flow algorithm correspond to edge disjoint paths between source
|
||||
and target in the original network. This function handles both directed
|
||||
and undirected graphs, and can use all flow algorithms from NetworkX flow
|
||||
package.
|
||||
|
||||
"""
|
||||
if s not in G:
|
||||
raise nx.NetworkXError(f"node {s} not in graph")
|
||||
if t not in G:
|
||||
raise nx.NetworkXError(f"node {t} not in graph")
|
||||
|
||||
if flow_func is None:
|
||||
flow_func = default_flow_func
|
||||
|
||||
if auxiliary is None:
|
||||
H = build_auxiliary_edge_connectivity(G)
|
||||
else:
|
||||
H = auxiliary
|
||||
|
||||
# Maximum possible edge disjoint paths
|
||||
possible = min(H.out_degree(s), H.in_degree(t))
|
||||
if not possible:
|
||||
raise NetworkXNoPath
|
||||
|
||||
if cutoff is None:
|
||||
cutoff = possible
|
||||
else:
|
||||
cutoff = min(cutoff, possible)
|
||||
|
||||
# Compute maximum flow between source and target. Flow functions in
|
||||
# NetworkX return a residual network.
|
||||
kwargs = dict(
|
||||
capacity="capacity", residual=residual, cutoff=cutoff, value_only=True
|
||||
)
|
||||
if flow_func is preflow_push:
|
||||
del kwargs["cutoff"]
|
||||
if flow_func is shortest_augmenting_path:
|
||||
kwargs["two_phase"] = True
|
||||
R = flow_func(H, s, t, **kwargs)
|
||||
|
||||
if R.graph["flow_value"] == 0:
|
||||
raise NetworkXNoPath
|
||||
|
||||
# Saturated edges in the residual network form the edge disjoint paths
|
||||
# between source and target
|
||||
cutset = [
|
||||
(u, v)
|
||||
for u, v, d in R.edges(data=True)
|
||||
if d["capacity"] == d["flow"] and d["flow"] > 0
|
||||
]
|
||||
# This is equivalent of what flow.utils.build_flow_dict returns, but
|
||||
# only for the nodes with saturated edges and without reporting 0 flows.
|
||||
flow_dict = {n: {} for edge in cutset for n in edge}
|
||||
for u, v in cutset:
|
||||
flow_dict[u][v] = 1
|
||||
|
||||
# Rebuild the edge disjoint paths from the flow dictionary.
|
||||
paths_found = 0
|
||||
for v in list(flow_dict[s]):
|
||||
if paths_found >= cutoff:
|
||||
# preflow_push does not support cutoff: we have to
|
||||
# keep track of the paths founds and stop at cutoff.
|
||||
break
|
||||
path = [s]
|
||||
if v == t:
|
||||
path.append(v)
|
||||
yield path
|
||||
continue
|
||||
u = v
|
||||
while u != t:
|
||||
path.append(u)
|
||||
try:
|
||||
u, _ = flow_dict[u].popitem()
|
||||
except KeyError:
|
||||
break
|
||||
else:
|
||||
path.append(t)
|
||||
yield path
|
||||
paths_found += 1
|
||||
|
||||
|
||||
def node_disjoint_paths(
|
||||
G, s, t, flow_func=None, cutoff=None, auxiliary=None, residual=None
|
||||
):
|
||||
r"""Computes node disjoint paths between source and target.
|
||||
|
||||
Node disjoint paths are paths that only share their first and last
|
||||
nodes. The number of node independent paths between two nodes is
|
||||
equal to their local node connectivity.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
s : node
|
||||
Source node.
|
||||
|
||||
t : node
|
||||
Target node.
|
||||
|
||||
flow_func : function
|
||||
A function for computing the maximum flow among a pair of nodes.
|
||||
The function has to accept at least three parameters: a Digraph,
|
||||
a source node, and a target node. And return a residual network
|
||||
that follows NetworkX conventions (see :meth:`maximum_flow` for
|
||||
details). If flow_func is None, the default maximum flow function
|
||||
(:meth:`edmonds_karp`) is used. See below for details. The choice
|
||||
of the default function may change from version to version and
|
||||
should not be relied on. Default value: None.
|
||||
|
||||
cutoff : int
|
||||
Maximum number of paths to yield. Some of the maximum flow
|
||||
algorithms, such as :meth:`edmonds_karp` (the default) and
|
||||
:meth:`shortest_augmenting_path` support the cutoff parameter,
|
||||
and will terminate when the flow value reaches or exceeds the
|
||||
cutoff. Other algorithms will ignore this parameter.
|
||||
Default value: None.
|
||||
|
||||
auxiliary : NetworkX DiGraph
|
||||
Auxiliary digraph to compute flow based node connectivity. It has
|
||||
to have a graph attribute called mapping with a dictionary mapping
|
||||
node names in G and in the auxiliary digraph. If provided
|
||||
it will be reused instead of recreated. Default value: None.
|
||||
|
||||
residual : NetworkX DiGraph
|
||||
Residual network to compute maximum flow. If provided it will be
|
||||
reused instead of recreated. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
paths : generator
|
||||
Generator of node disjoint paths.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNoPath
|
||||
If there is no path between source and target.
|
||||
|
||||
NetworkXError
|
||||
If source or target are not in the graph G.
|
||||
|
||||
Examples
|
||||
--------
|
||||
We use in this example the platonic icosahedral graph, which has node
|
||||
node connectivity 5, thus there are 5 node disjoint paths between any
|
||||
pair of non neighbor nodes.
|
||||
|
||||
>>> G = nx.icosahedral_graph()
|
||||
>>> len(list(nx.node_disjoint_paths(G, 0, 6)))
|
||||
5
|
||||
|
||||
If you need to compute node disjoint paths between several pairs of
|
||||
nodes in the same graph, it is recommended that you reuse the
|
||||
data structures that NetworkX uses in the computation: the
|
||||
auxiliary digraph for node connectivity and node cuts, and the
|
||||
residual network for the underlying maximum flow computation.
|
||||
|
||||
Example of how to compute node disjoint paths reusing the data
|
||||
structures:
|
||||
|
||||
>>> # You also have to explicitly import the function for
|
||||
>>> # building the auxiliary digraph from the connectivity package
|
||||
>>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity
|
||||
>>> H = build_auxiliary_node_connectivity(G)
|
||||
>>> # And the function for building the residual network from the
|
||||
>>> # flow package
|
||||
>>> from networkx.algorithms.flow import build_residual_network
|
||||
>>> # Note that the auxiliary digraph has an edge attribute named capacity
|
||||
>>> R = build_residual_network(H, "capacity")
|
||||
>>> # Reuse the auxiliary digraph and the residual network by passing them
|
||||
>>> # as arguments
|
||||
>>> len(list(nx.node_disjoint_paths(G, 0, 6, auxiliary=H, residual=R)))
|
||||
5
|
||||
|
||||
You can also use alternative flow algorithms for computing node disjoint
|
||||
paths. For instance, in dense networks the algorithm
|
||||
:meth:`shortest_augmenting_path` will usually perform better than
|
||||
the default :meth:`edmonds_karp` which is faster for sparse
|
||||
networks with highly skewed degree distributions. Alternative flow
|
||||
functions have to be explicitly imported from the flow package.
|
||||
|
||||
>>> from networkx.algorithms.flow import shortest_augmenting_path
|
||||
>>> len(list(nx.node_disjoint_paths(G, 0, 6, flow_func=shortest_augmenting_path)))
|
||||
5
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a flow based implementation of node disjoint paths. We compute
|
||||
the maximum flow between source and target on an auxiliary directed
|
||||
network. The saturated edges in the residual network after running the
|
||||
maximum flow algorithm correspond to node disjoint paths between source
|
||||
and target in the original network. This function handles both directed
|
||||
and undirected graphs, and can use all flow algorithms from NetworkX flow
|
||||
package.
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`edge_disjoint_paths`
|
||||
:meth:`node_connectivity`
|
||||
:meth:`maximum_flow`
|
||||
:meth:`edmonds_karp`
|
||||
:meth:`preflow_push`
|
||||
:meth:`shortest_augmenting_path`
|
||||
|
||||
"""
|
||||
if s not in G:
|
||||
raise nx.NetworkXError(f"node {s} not in graph")
|
||||
if t not in G:
|
||||
raise nx.NetworkXError(f"node {t} not in graph")
|
||||
|
||||
if auxiliary is None:
|
||||
H = build_auxiliary_node_connectivity(G)
|
||||
else:
|
||||
H = auxiliary
|
||||
|
||||
mapping = H.graph.get("mapping", None)
|
||||
if mapping is None:
|
||||
raise nx.NetworkXError("Invalid auxiliary digraph.")
|
||||
|
||||
# Maximum possible edge disjoint paths
|
||||
possible = min(H.out_degree(f"{mapping[s]}B"), H.in_degree(f"{mapping[t]}A"))
|
||||
if not possible:
|
||||
raise NetworkXNoPath
|
||||
|
||||
if cutoff is None:
|
||||
cutoff = possible
|
||||
else:
|
||||
cutoff = min(cutoff, possible)
|
||||
|
||||
kwargs = dict(flow_func=flow_func, residual=residual, auxiliary=H, cutoff=cutoff)
|
||||
|
||||
# The edge disjoint paths in the auxiliary digraph correspond to the node
|
||||
# disjoint paths in the original graph.
|
||||
paths_edges = edge_disjoint_paths(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs)
|
||||
for path in paths_edges:
|
||||
# Each node in the original graph maps to two nodes in auxiliary graph
|
||||
yield list(_unique_everseen(H.nodes[node]["id"] for node in path))
|
||||
|
||||
|
||||
def _unique_everseen(iterable):
|
||||
# Adapted from https://docs.python.org/3/library/itertools.html examples
|
||||
"List unique elements, preserving order. Remember all elements ever seen."
|
||||
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
||||
seen = set()
|
||||
seen_add = seen.add
|
||||
for element in _filterfalse(seen.__contains__, iterable):
|
||||
seen_add(element)
|
||||
yield element
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,582 @@
|
|||
"""
|
||||
Algorithms for finding k-edge-connected components and subgraphs.
|
||||
|
||||
A k-edge-connected component (k-edge-cc) is a maximal set of nodes in G, such
|
||||
that all pairs of node have an edge-connectivity of at least k.
|
||||
|
||||
A k-edge-connected subgraph (k-edge-subgraph) is a maximal set of nodes in G,
|
||||
such that the subgraph of G defined by the nodes has an edge-connectivity at
|
||||
least k.
|
||||
"""
|
||||
import networkx as nx
|
||||
from networkx.utils import arbitrary_element
|
||||
from networkx.utils import not_implemented_for
|
||||
from networkx.algorithms import bridges
|
||||
from functools import partial
|
||||
import itertools as it
|
||||
|
||||
__all__ = [
|
||||
"k_edge_components",
|
||||
"k_edge_subgraphs",
|
||||
"bridge_components",
|
||||
"EdgeComponentAuxGraph",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def k_edge_components(G, k):
|
||||
"""Generates nodes in each maximal k-edge-connected component in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
k : Integer
|
||||
Desired edge connectivity
|
||||
|
||||
Returns
|
||||
-------
|
||||
k_edge_components : a generator of k-edge-ccs. Each set of returned nodes
|
||||
will have k-edge-connectivity in the graph G.
|
||||
|
||||
See Also
|
||||
-------
|
||||
:func:`local_edge_connectivity`
|
||||
:func:`k_edge_subgraphs` : similar to this function, but the subgraph
|
||||
defined by the nodes must also have k-edge-connectivity.
|
||||
:func:`k_components` : similar to this function, but uses node-connectivity
|
||||
instead of edge-connectivity
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the input graph is a multigraph.
|
||||
|
||||
ValueError:
|
||||
If k is less than 1
|
||||
|
||||
Notes
|
||||
-----
|
||||
Attempts to use the most efficient implementation available based on k.
|
||||
If k=1, this is simply simply connected components for directed graphs and
|
||||
connected components for undirected graphs.
|
||||
If k=2 on an efficient bridge connected component algorithm from _[1] is
|
||||
run based on the chain decomposition.
|
||||
Otherwise, the algorithm from _[2] is used.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import itertools as it
|
||||
>>> from networkx.utils import pairwise
|
||||
>>> paths = [
|
||||
... (1, 2, 4, 3, 1, 4),
|
||||
... (5, 6, 7, 8, 5, 7, 8, 6),
|
||||
... ]
|
||||
>>> G = nx.Graph()
|
||||
>>> G.add_nodes_from(it.chain(*paths))
|
||||
>>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
|
||||
>>> # note this returns {1, 4} unlike k_edge_subgraphs
|
||||
>>> sorted(map(sorted, nx.k_edge_components(G, k=3)))
|
||||
[[1, 4], [2], [3], [5, 6, 7, 8]]
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://en.wikipedia.org/wiki/Bridge_%28graph_theory%29
|
||||
.. [2] Wang, Tianhao, et al. (2015) A simple algorithm for finding all
|
||||
k-edge-connected components.
|
||||
http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
|
||||
"""
|
||||
# Compute k-edge-ccs using the most efficient algorithms available.
|
||||
if k < 1:
|
||||
raise ValueError("k cannot be less than 1")
|
||||
if G.is_directed():
|
||||
if k == 1:
|
||||
return nx.strongly_connected_components(G)
|
||||
else:
|
||||
# TODO: investigate https://arxiv.org/abs/1412.6466 for k=2
|
||||
aux_graph = EdgeComponentAuxGraph.construct(G)
|
||||
return aux_graph.k_edge_components(k)
|
||||
else:
|
||||
if k == 1:
|
||||
return nx.connected_components(G)
|
||||
elif k == 2:
|
||||
return bridge_components(G)
|
||||
else:
|
||||
aux_graph = EdgeComponentAuxGraph.construct(G)
|
||||
return aux_graph.k_edge_components(k)
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def k_edge_subgraphs(G, k):
|
||||
"""Generates nodes in each maximal k-edge-connected subgraph in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
k : Integer
|
||||
Desired edge connectivity
|
||||
|
||||
Returns
|
||||
-------
|
||||
k_edge_subgraphs : a generator of k-edge-subgraphs
|
||||
Each k-edge-subgraph is a maximal set of nodes that defines a subgraph
|
||||
of G that is k-edge-connected.
|
||||
|
||||
See Also
|
||||
-------
|
||||
:func:`edge_connectivity`
|
||||
:func:`k_edge_components` : similar to this function, but nodes only
|
||||
need to have k-edge-connctivity within the graph G and the subgraphs
|
||||
might not be k-edge-connected.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the input graph is a multigraph.
|
||||
|
||||
ValueError:
|
||||
If k is less than 1
|
||||
|
||||
Notes
|
||||
-----
|
||||
Attempts to use the most efficient implementation available based on k.
|
||||
If k=1, or k=2 and the graph is undirected, then this simply calls
|
||||
`k_edge_components`. Otherwise the algorithm from _[1] is used.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import itertools as it
|
||||
>>> from networkx.utils import pairwise
|
||||
>>> paths = [
|
||||
... (1, 2, 4, 3, 1, 4),
|
||||
... (5, 6, 7, 8, 5, 7, 8, 6),
|
||||
... ]
|
||||
>>> G = nx.Graph()
|
||||
>>> G.add_nodes_from(it.chain(*paths))
|
||||
>>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
|
||||
>>> # note this does not return {1, 4} unlike k_edge_components
|
||||
>>> sorted(map(sorted, nx.k_edge_subgraphs(G, k=3)))
|
||||
[[1], [2], [3], [4], [5, 6, 7, 8]]
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Zhou, Liu, et al. (2012) Finding maximal k-edge-connected subgraphs
|
||||
from a large graph. ACM International Conference on Extending Database
|
||||
Technology 2012 480-–491.
|
||||
https://openproceedings.org/2012/conf/edbt/ZhouLYLCL12.pdf
|
||||
"""
|
||||
if k < 1:
|
||||
raise ValueError("k cannot be less than 1")
|
||||
if G.is_directed():
|
||||
if k <= 1:
|
||||
# For directed graphs ,
|
||||
# When k == 1, k-edge-ccs and k-edge-subgraphs are the same
|
||||
return k_edge_components(G, k)
|
||||
else:
|
||||
return _k_edge_subgraphs_nodes(G, k)
|
||||
else:
|
||||
if k <= 2:
|
||||
# For undirected graphs,
|
||||
# when k <= 2, k-edge-ccs and k-edge-subgraphs are the same
|
||||
return k_edge_components(G, k)
|
||||
else:
|
||||
return _k_edge_subgraphs_nodes(G, k)
|
||||
|
||||
|
||||
def _k_edge_subgraphs_nodes(G, k):
|
||||
"""Helper to get the nodes from the subgraphs.
|
||||
|
||||
This allows k_edge_subgraphs to return a generator.
|
||||
"""
|
||||
for C in general_k_edge_subgraphs(G, k):
|
||||
yield set(C.nodes())
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
def bridge_components(G):
|
||||
"""Finds all bridge-connected components G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
bridge_components : a generator of 2-edge-connected components
|
||||
|
||||
|
||||
See Also
|
||||
--------
|
||||
:func:`k_edge_subgraphs` : this function is a special case for an
|
||||
undirected graph where k=2.
|
||||
:func:`biconnected_components` : similar to this function, but is defined
|
||||
using 2-node-connectivity instead of 2-edge-connectivity.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the input graph is directed or a multigraph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Bridge-connected components are also known as 2-edge-connected components.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> # The barbell graph with parameter zero has a single bridge
|
||||
>>> G = nx.barbell_graph(5, 0)
|
||||
>>> from networkx.algorithms.connectivity.edge_kcomponents import bridge_components
|
||||
>>> sorted(map(sorted, bridge_components(G)))
|
||||
[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
|
||||
"""
|
||||
H = G.copy()
|
||||
H.remove_edges_from(bridges(G))
|
||||
yield from nx.connected_components(H)
|
||||
|
||||
|
||||
class EdgeComponentAuxGraph:
|
||||
r"""A simple algorithm to find all k-edge-connected components in a graph.
|
||||
|
||||
Constructing the AuxillaryGraph (which may take some time) allows for the
|
||||
k-edge-ccs to be found in linear time for arbitrary k.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This implementation is based on [1]_. The idea is to construct an auxiliary
|
||||
graph from which the k-edge-ccs can be extracted in linear time. The
|
||||
auxiliary graph is constructed in $O(|V|\cdot F)$ operations, where F is the
|
||||
complexity of max flow. Querying the components takes an additional $O(|V|)$
|
||||
operations. This algorithm can be slow for large graphs, but it handles an
|
||||
arbitrary k and works for both directed and undirected inputs.
|
||||
|
||||
The undirected case for k=1 is exactly connected components.
|
||||
The undirected case for k=2 is exactly bridge connected components.
|
||||
The directed case for k=1 is exactly strongly connected components.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Wang, Tianhao, et al. (2015) A simple algorithm for finding all
|
||||
k-edge-connected components.
|
||||
http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import itertools as it
|
||||
>>> from networkx.utils import pairwise
|
||||
>>> from networkx.algorithms.connectivity import EdgeComponentAuxGraph
|
||||
>>> # Build an interesting graph with multiple levels of k-edge-ccs
|
||||
>>> paths = [
|
||||
... (1, 2, 3, 4, 1, 3, 4, 2), # a 3-edge-cc (a 4 clique)
|
||||
... (5, 6, 7, 5), # a 2-edge-cc (a 3 clique)
|
||||
... (1, 5), # combine first two ccs into a 1-edge-cc
|
||||
... (0,), # add an additional disconnected 1-edge-cc
|
||||
... ]
|
||||
>>> G = nx.Graph()
|
||||
>>> G.add_nodes_from(it.chain(*paths))
|
||||
>>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
|
||||
>>> # Constructing the AuxGraph takes about O(n ** 4)
|
||||
>>> aux_graph = EdgeComponentAuxGraph.construct(G)
|
||||
>>> # Once constructed, querying takes O(n)
|
||||
>>> sorted(map(sorted, aux_graph.k_edge_components(k=1)))
|
||||
[[0], [1, 2, 3, 4, 5, 6, 7]]
|
||||
>>> sorted(map(sorted, aux_graph.k_edge_components(k=2)))
|
||||
[[0], [1, 2, 3, 4], [5, 6, 7]]
|
||||
>>> sorted(map(sorted, aux_graph.k_edge_components(k=3)))
|
||||
[[0], [1, 2, 3, 4], [5], [6], [7]]
|
||||
>>> sorted(map(sorted, aux_graph.k_edge_components(k=4)))
|
||||
[[0], [1], [2], [3], [4], [5], [6], [7]]
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> # The auxiliary graph is primarilly used for k-edge-ccs but it
|
||||
>>> # can also speed up the queries of k-edge-subgraphs by refining the
|
||||
>>> # search space.
|
||||
>>> import itertools as it
|
||||
>>> from networkx.utils import pairwise
|
||||
>>> from networkx.algorithms.connectivity import EdgeComponentAuxGraph
|
||||
>>> paths = [
|
||||
... (1, 2, 4, 3, 1, 4),
|
||||
... ]
|
||||
>>> G = nx.Graph()
|
||||
>>> G.add_nodes_from(it.chain(*paths))
|
||||
>>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
|
||||
>>> aux_graph = EdgeComponentAuxGraph.construct(G)
|
||||
>>> sorted(map(sorted, aux_graph.k_edge_subgraphs(k=3)))
|
||||
[[1], [2], [3], [4]]
|
||||
>>> sorted(map(sorted, aux_graph.k_edge_components(k=3)))
|
||||
[[1, 4], [2], [3]]
|
||||
"""
|
||||
|
||||
# @not_implemented_for('multigraph') # TODO: fix decor for classmethods
|
||||
@classmethod
|
||||
def construct(EdgeComponentAuxGraph, G):
|
||||
"""Builds an auxiliary graph encoding edge-connectivity between nodes.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Given G=(V, E), initialize an empty auxiliary graph A.
|
||||
Choose an arbitrary source node s. Initialize a set N of available
|
||||
nodes (that can be used as the sink). The algorithm picks an
|
||||
arbitrary node t from N - {s}, and then computes the minimum st-cut
|
||||
(S, T) with value w. If G is directed the the minimum of the st-cut or
|
||||
the ts-cut is used instead. Then, the edge (s, t) is added to the
|
||||
auxiliary graph with weight w. The algorithm is called recursively
|
||||
first using S as the available nodes and s as the source, and then
|
||||
using T and t. Recursion stops when the source is the only available
|
||||
node.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
"""
|
||||
# workaround for classmethod decorator
|
||||
not_implemented_for("multigraph")(lambda G: G)(G)
|
||||
|
||||
def _recursive_build(H, A, source, avail):
|
||||
# Terminate once the flow has been compute to every node.
|
||||
if {source} == avail:
|
||||
return
|
||||
# pick an arbitrary node as the sink
|
||||
sink = arbitrary_element(avail - {source})
|
||||
# find the minimum cut and its weight
|
||||
value, (S, T) = nx.minimum_cut(H, source, sink)
|
||||
if H.is_directed():
|
||||
# check if the reverse direction has a smaller cut
|
||||
value_, (T_, S_) = nx.minimum_cut(H, sink, source)
|
||||
if value_ < value:
|
||||
value, S, T = value_, S_, T_
|
||||
# add edge with weight of cut to the aux graph
|
||||
A.add_edge(source, sink, weight=value)
|
||||
# recursively call until all but one node is used
|
||||
_recursive_build(H, A, source, avail.intersection(S))
|
||||
_recursive_build(H, A, sink, avail.intersection(T))
|
||||
|
||||
# Copy input to ensure all edges have unit capacity
|
||||
H = G.__class__()
|
||||
H.add_nodes_from(G.nodes())
|
||||
H.add_edges_from(G.edges(), capacity=1)
|
||||
|
||||
# A is the auxiliary graph to be constructed
|
||||
# It is a weighted undirected tree
|
||||
A = nx.Graph()
|
||||
|
||||
# Pick an arbitrary node as the source
|
||||
if H.number_of_nodes() > 0:
|
||||
source = arbitrary_element(H.nodes())
|
||||
# Initialize a set of elements that can be chosen as the sink
|
||||
avail = set(H.nodes())
|
||||
|
||||
# This constructs A
|
||||
_recursive_build(H, A, source, avail)
|
||||
|
||||
# This class is a container the holds the auxiliary graph A and
|
||||
# provides access the the k_edge_components function.
|
||||
self = EdgeComponentAuxGraph()
|
||||
self.A = A
|
||||
self.H = H
|
||||
return self
|
||||
|
||||
def k_edge_components(self, k):
|
||||
"""Queries the auxiliary graph for k-edge-connected components.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
k : Integer
|
||||
Desired edge connectivity
|
||||
|
||||
Returns
|
||||
-------
|
||||
k_edge_components : a generator of k-edge-ccs
|
||||
|
||||
Notes
|
||||
-----
|
||||
Given the auxiliary graph, the k-edge-connected components can be
|
||||
determined in linear time by removing all edges with weights less than
|
||||
k from the auxiliary graph. The resulting connected components are the
|
||||
k-edge-ccs in the original graph.
|
||||
"""
|
||||
if k < 1:
|
||||
raise ValueError("k cannot be less than 1")
|
||||
A = self.A
|
||||
# "traverse the auxiliary graph A and delete all edges with weights less
|
||||
# than k"
|
||||
aux_weights = nx.get_edge_attributes(A, "weight")
|
||||
# Create a relevant graph with the auxiliary edges with weights >= k
|
||||
R = nx.Graph()
|
||||
R.add_nodes_from(A.nodes())
|
||||
R.add_edges_from(e for e, w in aux_weights.items() if w >= k)
|
||||
|
||||
# Return the nodes that are k-edge-connected in the original graph
|
||||
yield from nx.connected_components(R)
|
||||
|
||||
def k_edge_subgraphs(self, k):
|
||||
"""Queries the auxiliary graph for k-edge-connected subgraphs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
k : Integer
|
||||
Desired edge connectivity
|
||||
|
||||
Returns
|
||||
-------
|
||||
k_edge_subgraphs : a generator of k-edge-subgraphs
|
||||
|
||||
Notes
|
||||
-----
|
||||
Refines the k-edge-ccs into k-edge-subgraphs. The running time is more
|
||||
than $O(|V|)$.
|
||||
|
||||
For single values of k it is faster to use `nx.k_edge_subgraphs`.
|
||||
But for multiple values of k, it can be faster to build AuxGraph and
|
||||
then use this method.
|
||||
"""
|
||||
if k < 1:
|
||||
raise ValueError("k cannot be less than 1")
|
||||
H = self.H
|
||||
A = self.A
|
||||
# "traverse the auxiliary graph A and delete all edges with weights less
|
||||
# than k"
|
||||
aux_weights = nx.get_edge_attributes(A, "weight")
|
||||
# Create a relevant graph with the auxiliary edges with weights >= k
|
||||
R = nx.Graph()
|
||||
R.add_nodes_from(A.nodes())
|
||||
R.add_edges_from(e for e, w in aux_weights.items() if w >= k)
|
||||
|
||||
# Return the components whose subgraphs are k-edge-connected
|
||||
for cc in nx.connected_components(R):
|
||||
if len(cc) < k:
|
||||
# Early return optimization
|
||||
for node in cc:
|
||||
yield {node}
|
||||
else:
|
||||
# Call subgraph solution to refine the results
|
||||
C = H.subgraph(cc)
|
||||
yield from k_edge_subgraphs(C, k)
|
||||
|
||||
|
||||
def _low_degree_nodes(G, k, nbunch=None):
|
||||
"""Helper for finding nodes with degree less than k."""
|
||||
# Nodes with degree less than k cannot be k-edge-connected.
|
||||
if G.is_directed():
|
||||
# Consider both in and out degree in the directed case
|
||||
seen = set()
|
||||
for node, degree in G.out_degree(nbunch):
|
||||
if degree < k:
|
||||
seen.add(node)
|
||||
yield node
|
||||
for node, degree in G.in_degree(nbunch):
|
||||
if node not in seen and degree < k:
|
||||
seen.add(node)
|
||||
yield node
|
||||
else:
|
||||
# Only the degree matters in the undirected case
|
||||
for node, degree in G.degree(nbunch):
|
||||
if degree < k:
|
||||
yield node
|
||||
|
||||
|
||||
def _high_degree_components(G, k):
|
||||
"""Helper for filtering components that can't be k-edge-connected.
|
||||
|
||||
Removes and generates each node with degree less than k. Then generates
|
||||
remaining components where all nodes have degree at least k.
|
||||
"""
|
||||
# Iteravely remove parts of the graph that are not k-edge-connected
|
||||
H = G.copy()
|
||||
singletons = set(_low_degree_nodes(H, k))
|
||||
while singletons:
|
||||
# Only search neighbors of removed nodes
|
||||
nbunch = set(it.chain.from_iterable(map(H.neighbors, singletons)))
|
||||
nbunch.difference_update(singletons)
|
||||
H.remove_nodes_from(singletons)
|
||||
for node in singletons:
|
||||
yield {node}
|
||||
singletons = set(_low_degree_nodes(H, k, nbunch))
|
||||
|
||||
# Note: remaining connected components may not be k-edge-connected
|
||||
if G.is_directed():
|
||||
yield from nx.strongly_connected_components(H)
|
||||
else:
|
||||
yield from nx.connected_components(H)
|
||||
|
||||
|
||||
def general_k_edge_subgraphs(G, k):
|
||||
"""General algorithm to find all maximal k-edge-connected subgraphs in G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
k_edge_subgraphs : a generator of nx.Graphs that are k-edge-subgraphs
|
||||
Each k-edge-subgraph is a maximal set of nodes that defines a subgraph
|
||||
of G that is k-edge-connected.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Implementation of the basic algorithm from _[1]. The basic idea is to find
|
||||
a global minimum cut of the graph. If the cut value is at least k, then the
|
||||
graph is a k-edge-connected subgraph and can be added to the results.
|
||||
Otherwise, the cut is used to split the graph in two and the procedure is
|
||||
applied recursively. If the graph is just a single node, then it is also
|
||||
added to the results. At the end, each result is either guaranteed to be
|
||||
a single node or a subgraph of G that is k-edge-connected.
|
||||
|
||||
This implementation contains optimizations for reducing the number of calls
|
||||
to max-flow, but there are other optimizations in _[1] that could be
|
||||
implemented.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Zhou, Liu, et al. (2012) Finding maximal k-edge-connected subgraphs
|
||||
from a large graph. ACM International Conference on Extending Database
|
||||
Technology 2012 480-–491.
|
||||
https://openproceedings.org/2012/conf/edbt/ZhouLYLCL12.pdf
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> from networkx.utils import pairwise
|
||||
>>> paths = [
|
||||
... (11, 12, 13, 14, 11, 13, 14, 12), # a 4-clique
|
||||
... (21, 22, 23, 24, 21, 23, 24, 22), # another 4-clique
|
||||
... # connect the cliques with high degree but low connectivity
|
||||
... (50, 13),
|
||||
... (12, 50, 22),
|
||||
... (13, 102, 23),
|
||||
... (14, 101, 24),
|
||||
... ]
|
||||
>>> G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
|
||||
>>> sorted(map(len, k_edge_subgraphs(G, k=3)))
|
||||
[1, 1, 1, 4, 4]
|
||||
"""
|
||||
if k < 1:
|
||||
raise ValueError("k cannot be less than 1")
|
||||
|
||||
# Node pruning optimization (incorporates early return)
|
||||
# find_ccs is either connected_components/strongly_connected_components
|
||||
find_ccs = partial(_high_degree_components, k=k)
|
||||
|
||||
# Quick return optimization
|
||||
if G.number_of_nodes() < k:
|
||||
for node in G.nodes():
|
||||
yield G.subgraph([node]).copy()
|
||||
return
|
||||
|
||||
# Intermediate results
|
||||
R0 = {G.subgraph(cc).copy() for cc in find_ccs(G)}
|
||||
# Subdivide CCs in the intermediate results until they are k-conn
|
||||
while R0:
|
||||
G1 = R0.pop()
|
||||
if G1.number_of_nodes() == 1:
|
||||
yield G1
|
||||
else:
|
||||
# Find a global minimum cut
|
||||
cut_edges = nx.minimum_edge_cut(G1)
|
||||
cut_value = len(cut_edges)
|
||||
if cut_value < k:
|
||||
# G1 is not k-edge-connected, so subdivide it
|
||||
G1.remove_edges_from(cut_edges)
|
||||
for cc in find_ccs(G1):
|
||||
R0.add(G1.subgraph(cc).copy())
|
||||
else:
|
||||
# Otherwise we found a k-edge-connected subgraph
|
||||
yield G1
|
|
@ -0,0 +1,224 @@
|
|||
"""
|
||||
Moody and White algorithm for k-components
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from itertools import combinations
|
||||
from operator import itemgetter
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
# Define the default maximum flow function.
|
||||
from networkx.algorithms.flow import edmonds_karp
|
||||
|
||||
default_flow_func = edmonds_karp
|
||||
|
||||
__all__ = ["k_components"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
def k_components(G, flow_func=None):
|
||||
r"""Returns the k-component structure of a graph G.
|
||||
|
||||
A `k`-component is a maximal subgraph of a graph G that has, at least,
|
||||
node connectivity `k`: we need to remove at least `k` nodes to break it
|
||||
into more components. `k`-components have an inherent hierarchical
|
||||
structure because they are nested in terms of connectivity: a connected
|
||||
graph can contain several 2-components, each of which can contain
|
||||
one or more 3-components, and so forth.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
flow_func : function
|
||||
Function to perform the underlying flow computations. Default value
|
||||
:meth:`edmonds_karp`. This function performs better in sparse graphs with
|
||||
right tailed degree distributions. :meth:`shortest_augmenting_path` will
|
||||
perform better in denser graphs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
k_components : dict
|
||||
Dictionary with all connectivity levels `k` in the input Graph as keys
|
||||
and a list of sets of nodes that form a k-component of level `k` as
|
||||
values.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the input graph is directed.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # Petersen graph has 10 nodes and it is triconnected, thus all
|
||||
>>> # nodes are in a single component on all three connectivity levels
|
||||
>>> G = nx.petersen_graph()
|
||||
>>> k_components = nx.k_components(G)
|
||||
|
||||
Notes
|
||||
-----
|
||||
Moody and White [1]_ (appendix A) provide an algorithm for identifying
|
||||
k-components in a graph, which is based on Kanevsky's algorithm [2]_
|
||||
for finding all minimum-size node cut-sets of a graph (implemented in
|
||||
:meth:`all_node_cuts` function):
|
||||
|
||||
1. Compute node connectivity, k, of the input graph G.
|
||||
|
||||
2. Identify all k-cutsets at the current level of connectivity using
|
||||
Kanevsky's algorithm.
|
||||
|
||||
3. Generate new graph components based on the removal of
|
||||
these cutsets. Nodes in a cutset belong to both sides
|
||||
of the induced cut.
|
||||
|
||||
4. If the graph is neither complete nor trivial, return to 1;
|
||||
else end.
|
||||
|
||||
This implementation also uses some heuristics (see [3]_ for details)
|
||||
to speed up the computation.
|
||||
|
||||
See also
|
||||
--------
|
||||
node_connectivity
|
||||
all_node_cuts
|
||||
biconnected_components : special case of this function when k=2
|
||||
k_edge_components : similar to this function, but uses edge-connectivity
|
||||
instead of node-connectivity
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Moody, J. and D. White (2003). Social cohesion and embeddedness:
|
||||
A hierarchical conception of social groups.
|
||||
American Sociological Review 68(1), 103--28.
|
||||
http://www2.asanet.org/journals/ASRFeb03MoodyWhite.pdf
|
||||
|
||||
.. [2] Kanevsky, A. (1993). Finding all minimum-size separating vertex
|
||||
sets in a graph. Networks 23(6), 533--541.
|
||||
http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract
|
||||
|
||||
.. [3] Torrents, J. and F. Ferraro (2015). Structural Cohesion:
|
||||
Visualization and Heuristics for Fast Computation.
|
||||
https://arxiv.org/pdf/1503.04476v1
|
||||
|
||||
"""
|
||||
# Dictionary with connectivity level (k) as keys and a list of
|
||||
# sets of nodes that form a k-component as values. Note that
|
||||
# k-compoents can overlap (but only k - 1 nodes).
|
||||
k_components = defaultdict(list)
|
||||
# Define default flow function
|
||||
if flow_func is None:
|
||||
flow_func = default_flow_func
|
||||
# Bicomponents as a base to check for higher order k-components
|
||||
for component in nx.connected_components(G):
|
||||
# isolated nodes have connectivity 0
|
||||
comp = set(component)
|
||||
if len(comp) > 1:
|
||||
k_components[1].append(comp)
|
||||
bicomponents = [G.subgraph(c) for c in nx.biconnected_components(G)]
|
||||
for bicomponent in bicomponents:
|
||||
bicomp = set(bicomponent)
|
||||
# avoid considering dyads as bicomponents
|
||||
if len(bicomp) > 2:
|
||||
k_components[2].append(bicomp)
|
||||
for B in bicomponents:
|
||||
if len(B) <= 2:
|
||||
continue
|
||||
k = nx.node_connectivity(B, flow_func=flow_func)
|
||||
if k > 2:
|
||||
k_components[k].append(set(B))
|
||||
# Perform cuts in a DFS like order.
|
||||
cuts = list(nx.all_node_cuts(B, k=k, flow_func=flow_func))
|
||||
stack = [(k, _generate_partition(B, cuts, k))]
|
||||
while stack:
|
||||
(parent_k, partition) = stack[-1]
|
||||
try:
|
||||
nodes = next(partition)
|
||||
C = B.subgraph(nodes)
|
||||
this_k = nx.node_connectivity(C, flow_func=flow_func)
|
||||
if this_k > parent_k and this_k > 2:
|
||||
k_components[this_k].append(set(C))
|
||||
cuts = list(nx.all_node_cuts(C, k=this_k, flow_func=flow_func))
|
||||
if cuts:
|
||||
stack.append((this_k, _generate_partition(C, cuts, this_k)))
|
||||
except StopIteration:
|
||||
stack.pop()
|
||||
|
||||
# This is necessary because k-components may only be reported at their
|
||||
# maximum k level. But we want to return a dictionary in which keys are
|
||||
# connectivity levels and values list of sets of components, without
|
||||
# skipping any connectivity level. Also, it's possible that subsets of
|
||||
# an already detected k-component appear at a level k. Checking for this
|
||||
# in the while loop above penalizes the common case. Thus we also have to
|
||||
# _consolidate all connectivity levels in _reconstruct_k_components.
|
||||
return _reconstruct_k_components(k_components)
|
||||
|
||||
|
||||
def _consolidate(sets, k):
|
||||
"""Merge sets that share k or more elements.
|
||||
|
||||
See: http://rosettacode.org/wiki/Set_consolidation
|
||||
|
||||
The iterative python implementation posted there is
|
||||
faster than this because of the overhead of building a
|
||||
Graph and calling nx.connected_components, but it's not
|
||||
clear for us if we can use it in NetworkX because there
|
||||
is no licence for the code.
|
||||
|
||||
"""
|
||||
G = nx.Graph()
|
||||
nodes = {i: s for i, s in enumerate(sets)}
|
||||
G.add_nodes_from(nodes)
|
||||
G.add_edges_from(
|
||||
(u, v) for u, v in combinations(nodes, 2) if len(nodes[u] & nodes[v]) >= k
|
||||
)
|
||||
for component in nx.connected_components(G):
|
||||
yield set.union(*[nodes[n] for n in component])
|
||||
|
||||
|
||||
def _generate_partition(G, cuts, k):
|
||||
def has_nbrs_in_partition(G, node, partition):
|
||||
for n in G[node]:
|
||||
if n in partition:
|
||||
return True
|
||||
return False
|
||||
|
||||
components = []
|
||||
nodes = {n for n, d in G.degree() if d > k} - {n for cut in cuts for n in cut}
|
||||
H = G.subgraph(nodes)
|
||||
for cc in nx.connected_components(H):
|
||||
component = set(cc)
|
||||
for cut in cuts:
|
||||
for node in cut:
|
||||
if has_nbrs_in_partition(G, node, cc):
|
||||
component.add(node)
|
||||
if len(component) < G.order():
|
||||
components.append(component)
|
||||
yield from _consolidate(components, k + 1)
|
||||
|
||||
|
||||
def _reconstruct_k_components(k_comps):
|
||||
result = dict()
|
||||
max_k = max(k_comps)
|
||||
for k in reversed(range(1, max_k + 1)):
|
||||
if k == max_k:
|
||||
result[k] = list(_consolidate(k_comps[k], k))
|
||||
elif k not in k_comps:
|
||||
result[k] = list(_consolidate(result[k + 1], k))
|
||||
else:
|
||||
nodes_at_k = set.union(*k_comps[k])
|
||||
to_add = [c for c in result[k + 1] if any(n not in nodes_at_k for n in c)]
|
||||
if to_add:
|
||||
result[k] = list(_consolidate(k_comps[k] + to_add, k))
|
||||
else:
|
||||
result[k] = list(_consolidate(k_comps[k], k))
|
||||
return result
|
||||
|
||||
|
||||
def build_k_number_dict(kcomps):
|
||||
result = {}
|
||||
for k, comps in sorted(kcomps.items(), key=itemgetter(0)):
|
||||
for comp in comps:
|
||||
for node in comp:
|
||||
result[node] = k
|
||||
return result
|
|
@ -0,0 +1,230 @@
|
|||
"""
|
||||
Kanevsky all minimum node k cutsets algorithm.
|
||||
"""
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
from itertools import combinations
|
||||
from operator import itemgetter
|
||||
|
||||
import networkx as nx
|
||||
from .utils import build_auxiliary_node_connectivity
|
||||
from networkx.algorithms.flow import (
|
||||
build_residual_network,
|
||||
edmonds_karp,
|
||||
shortest_augmenting_path,
|
||||
)
|
||||
|
||||
default_flow_func = edmonds_karp
|
||||
|
||||
|
||||
__all__ = ["all_node_cuts"]
|
||||
|
||||
|
||||
def all_node_cuts(G, k=None, flow_func=None):
|
||||
r"""Returns all minimum k cutsets of an undirected graph G.
|
||||
|
||||
This implementation is based on Kanevsky's algorithm [1]_ for finding all
|
||||
minimum-size node cut-sets of an undirected graph G; ie the set (or sets)
|
||||
of nodes of cardinality equal to the node connectivity of G. Thus if
|
||||
removed, would break G into two or more connected components.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
k : Integer
|
||||
Node connectivity of the input graph. If k is None, then it is
|
||||
computed. Default value: None.
|
||||
|
||||
flow_func : function
|
||||
Function to perform the underlying flow computations. Default value
|
||||
edmonds_karp. This function performs better in sparse graphs with
|
||||
right tailed degree distributions. shortest_augmenting_path will
|
||||
perform better in denser graphs.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
cuts : a generator of node cutsets
|
||||
Each node cutset has cardinality equal to the node connectivity of
|
||||
the input graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # A two-dimensional grid graph has 4 cutsets of cardinality 2
|
||||
>>> G = nx.grid_2d_graph(5, 5)
|
||||
>>> cutsets = list(nx.all_node_cuts(G))
|
||||
>>> len(cutsets)
|
||||
4
|
||||
>>> all(2 == len(cutset) for cutset in cutsets)
|
||||
True
|
||||
>>> nx.node_connectivity(G)
|
||||
2
|
||||
|
||||
Notes
|
||||
-----
|
||||
This implementation is based on the sequential algorithm for finding all
|
||||
minimum-size separating vertex sets in a graph [1]_. The main idea is to
|
||||
compute minimum cuts using local maximum flow computations among a set
|
||||
of nodes of highest degree and all other non-adjacent nodes in the Graph.
|
||||
Once we find a minimum cut, we add an edge between the high degree
|
||||
node and the target node of the local maximum flow computation to make
|
||||
sure that we will not find that minimum cut again.
|
||||
|
||||
See also
|
||||
--------
|
||||
node_connectivity
|
||||
edmonds_karp
|
||||
shortest_augmenting_path
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Kanevsky, A. (1993). Finding all minimum-size separating vertex
|
||||
sets in a graph. Networks 23(6), 533--541.
|
||||
http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract
|
||||
|
||||
"""
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("Input graph is disconnected.")
|
||||
|
||||
# Address some corner cases first.
|
||||
# For complete Graphs
|
||||
if nx.density(G) == 1:
|
||||
for cut_set in combinations(G, len(G) - 1):
|
||||
yield set(cut_set)
|
||||
return
|
||||
# Initialize data structures.
|
||||
# Keep track of the cuts already computed so we do not repeat them.
|
||||
seen = []
|
||||
# Even-Tarjan reduction is what we call auxiliary digraph
|
||||
# for node connectivity.
|
||||
H = build_auxiliary_node_connectivity(G)
|
||||
H_nodes = H.nodes # for speed
|
||||
mapping = H.graph["mapping"]
|
||||
# Keep a copy of original predecessors, H will be modified later.
|
||||
# Shallow copy is enough.
|
||||
original_H_pred = copy.copy(H._pred)
|
||||
R = build_residual_network(H, "capacity")
|
||||
kwargs = dict(capacity="capacity", residual=R)
|
||||
# Define default flow function
|
||||
if flow_func is None:
|
||||
flow_func = default_flow_func
|
||||
if flow_func is shortest_augmenting_path:
|
||||
kwargs["two_phase"] = True
|
||||
# Begin the actual algorithm
|
||||
# step 1: Find node connectivity k of G
|
||||
if k is None:
|
||||
k = nx.node_connectivity(G, flow_func=flow_func)
|
||||
# step 2:
|
||||
# Find k nodes with top degree, call it X:
|
||||
X = {n for n, d in sorted(G.degree(), key=itemgetter(1), reverse=True)[:k]}
|
||||
# Check if X is a k-node-cutset
|
||||
if _is_separating_set(G, X):
|
||||
seen.append(X)
|
||||
yield X
|
||||
|
||||
for x in X:
|
||||
# step 3: Compute local connectivity flow of x with all other
|
||||
# non adjacent nodes in G
|
||||
non_adjacent = set(G) - X - set(G[x])
|
||||
for v in non_adjacent:
|
||||
# step 4: compute maximum flow in an Even-Tarjan reduction H of G
|
||||
# and step 5: build the associated residual network R
|
||||
R = flow_func(H, f"{mapping[x]}B", f"{mapping[v]}A", **kwargs)
|
||||
flow_value = R.graph["flow_value"]
|
||||
|
||||
if flow_value == k:
|
||||
# Find the nodes incident to the flow.
|
||||
E1 = flowed_edges = [
|
||||
(u, w) for (u, w, d) in R.edges(data=True) if d["flow"] != 0
|
||||
]
|
||||
VE1 = incident_nodes = {n for edge in E1 for n in edge}
|
||||
# Remove saturated edges form the residual network.
|
||||
# Note that reversed edges are introduced with capacity 0
|
||||
# in the residual graph and they need to be removed too.
|
||||
saturated_edges = [
|
||||
(u, w, d)
|
||||
for (u, w, d) in R.edges(data=True)
|
||||
if d["capacity"] == d["flow"] or d["capacity"] == 0
|
||||
]
|
||||
R.remove_edges_from(saturated_edges)
|
||||
R_closure = nx.transitive_closure(R)
|
||||
# step 6: shrink the strongly connected components of
|
||||
# residual flow network R and call it L.
|
||||
L = nx.condensation(R)
|
||||
cmap = L.graph["mapping"]
|
||||
inv_cmap = defaultdict(list)
|
||||
for n, scc in cmap.items():
|
||||
inv_cmap[scc].append(n)
|
||||
# Find the incident nodes in the condensed graph.
|
||||
VE1 = {cmap[n] for n in VE1}
|
||||
# step 7: Compute all antichains of L;
|
||||
# they map to closed sets in H.
|
||||
# Any edge in H that links a closed set is part of a cutset.
|
||||
for antichain in nx.antichains(L):
|
||||
# Only antichains that are subsets of incident nodes counts.
|
||||
# Lemma 8 in reference.
|
||||
if not set(antichain).issubset(VE1):
|
||||
continue
|
||||
# Nodes in an antichain of the condensation graph of
|
||||
# the residual network map to a closed set of nodes that
|
||||
# define a node partition of the auxiliary digraph H
|
||||
# through taking all of antichain's predecessors in the
|
||||
# transitive closure.
|
||||
S = set()
|
||||
for scc in antichain:
|
||||
S.update(inv_cmap[scc])
|
||||
S_ancestors = set()
|
||||
for n in S:
|
||||
S_ancestors.update(R_closure._pred[n])
|
||||
S.update(S_ancestors)
|
||||
if f"{mapping[x]}B" not in S or f"{mapping[v]}A" in S:
|
||||
continue
|
||||
# Find the cutset that links the node partition (S,~S) in H
|
||||
cutset = set()
|
||||
for u in S:
|
||||
cutset.update((u, w) for w in original_H_pred[u] if w not in S)
|
||||
# The edges in H that form the cutset are internal edges
|
||||
# (ie edges that represent a node of the original graph G)
|
||||
if any([H_nodes[u]["id"] != H_nodes[w]["id"] for u, w in cutset]):
|
||||
continue
|
||||
node_cut = {H_nodes[u]["id"] for u, _ in cutset}
|
||||
|
||||
if len(node_cut) == k:
|
||||
# The cut is invalid if it includes internal edges of
|
||||
# end nodes. The other half of Lemma 8 in ref.
|
||||
if x in node_cut or v in node_cut:
|
||||
continue
|
||||
if node_cut not in seen:
|
||||
yield node_cut
|
||||
seen.append(node_cut)
|
||||
|
||||
# Add an edge (x, v) to make sure that we do not
|
||||
# find this cutset again. This is equivalent
|
||||
# of adding the edge in the input graph
|
||||
# G.add_edge(x, v) and then regenerate H and R:
|
||||
# Add edges to the auxiliary digraph.
|
||||
# See build_residual_network for convention we used
|
||||
# in residual graphs.
|
||||
H.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1)
|
||||
H.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1)
|
||||
# Add edges to the residual network.
|
||||
R.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1)
|
||||
R.add_edge(f"{mapping[v]}A", f"{mapping[x]}B", capacity=0)
|
||||
R.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1)
|
||||
R.add_edge(f"{mapping[x]}A", f"{mapping[v]}B", capacity=0)
|
||||
|
||||
# Add again the saturated edges to reuse the residual network
|
||||
R.add_edges_from(saturated_edges)
|
||||
|
||||
|
||||
def _is_separating_set(G, cut):
|
||||
"""Assumes that the input graph is connected"""
|
||||
if len(cut) == len(G) - 1:
|
||||
return True
|
||||
|
||||
H = nx.restricted_view(G, cut, [])
|
||||
if nx.is_connected(H):
|
||||
return False
|
||||
return True
|
|
@ -0,0 +1,150 @@
|
|||
"""
|
||||
Stoer-Wagner minimum cut algorithm.
|
||||
"""
|
||||
from itertools import islice
|
||||
|
||||
import networkx as nx
|
||||
from ...utils import BinaryHeap
|
||||
from ...utils import not_implemented_for
|
||||
from ...utils import arbitrary_element
|
||||
|
||||
__all__ = ["stoer_wagner"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
def stoer_wagner(G, weight="weight", heap=BinaryHeap):
|
||||
r"""Returns the weighted minimum edge cut using the Stoer-Wagner algorithm.
|
||||
|
||||
Determine the minimum edge cut of a connected graph using the
|
||||
Stoer-Wagner algorithm. In weighted cases, all weights must be
|
||||
nonnegative.
|
||||
|
||||
The running time of the algorithm depends on the type of heaps used:
|
||||
|
||||
============== =============================================
|
||||
Type of heap Running time
|
||||
============== =============================================
|
||||
Binary heap $O(n (m + n) \log n)$
|
||||
Fibonacci heap $O(nm + n^2 \log n)$
|
||||
Pairing heap $O(2^{2 \sqrt{\log \log n}} nm + n^2 \log n)$
|
||||
============== =============================================
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Edges of the graph are expected to have an attribute named by the
|
||||
weight parameter below. If this attribute is not present, the edge is
|
||||
considered to have unit weight.
|
||||
|
||||
weight : string
|
||||
Name of the weight attribute of the edges. If the attribute is not
|
||||
present, unit weight is assumed. Default value: 'weight'.
|
||||
|
||||
heap : class
|
||||
Type of heap to be used in the algorithm. It should be a subclass of
|
||||
:class:`MinHeap` or implement a compatible interface.
|
||||
|
||||
If a stock heap implementation is to be used, :class:`BinaryHeap` is
|
||||
recommended over :class:`PairingHeap` for Python implementations without
|
||||
optimized attribute accesses (e.g., CPython) despite a slower
|
||||
asymptotic running time. For Python implementations with optimized
|
||||
attribute accesses (e.g., PyPy), :class:`PairingHeap` provides better
|
||||
performance. Default value: :class:`BinaryHeap`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cut_value : integer or float
|
||||
The sum of weights of edges in a minimum cut.
|
||||
|
||||
partition : pair of node lists
|
||||
A partitioning of the nodes that defines a minimum cut.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or a multigraph.
|
||||
|
||||
NetworkXError
|
||||
If the graph has less than two nodes, is not connected or has a
|
||||
negative-weighted edge.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.Graph()
|
||||
>>> G.add_edge("x", "a", weight=3)
|
||||
>>> G.add_edge("x", "b", weight=1)
|
||||
>>> G.add_edge("a", "c", weight=3)
|
||||
>>> G.add_edge("b", "c", weight=5)
|
||||
>>> G.add_edge("b", "d", weight=4)
|
||||
>>> G.add_edge("d", "e", weight=2)
|
||||
>>> G.add_edge("c", "y", weight=2)
|
||||
>>> G.add_edge("e", "y", weight=3)
|
||||
>>> cut_value, partition = nx.stoer_wagner(G)
|
||||
>>> cut_value
|
||||
4
|
||||
"""
|
||||
n = len(G)
|
||||
if n < 2:
|
||||
raise nx.NetworkXError("graph has less than two nodes.")
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("graph is not connected.")
|
||||
|
||||
# Make a copy of the graph for internal use.
|
||||
G = nx.Graph(
|
||||
(u, v, {"weight": e.get(weight, 1)}) for u, v, e in G.edges(data=True) if u != v
|
||||
)
|
||||
|
||||
for u, v, e in G.edges(data=True):
|
||||
if e["weight"] < 0:
|
||||
raise nx.NetworkXError("graph has a negative-weighted edge.")
|
||||
|
||||
cut_value = float("inf")
|
||||
nodes = set(G)
|
||||
contractions = [] # contracted node pairs
|
||||
|
||||
# Repeatedly pick a pair of nodes to contract until only one node is left.
|
||||
for i in range(n - 1):
|
||||
# Pick an arbitrary node u and create a set A = {u}.
|
||||
u = arbitrary_element(G)
|
||||
A = {u}
|
||||
# Repeatedly pick the node "most tightly connected" to A and add it to
|
||||
# A. The tightness of connectivity of a node not in A is defined by the
|
||||
# of edges connecting it to nodes in A.
|
||||
h = heap() # min-heap emulating a max-heap
|
||||
for v, e in G[u].items():
|
||||
h.insert(v, -e["weight"])
|
||||
# Repeat until all but one node has been added to A.
|
||||
for j in range(n - i - 2):
|
||||
u = h.pop()[0]
|
||||
A.add(u)
|
||||
for v, e in G[u].items():
|
||||
if v not in A:
|
||||
h.insert(v, h.get(v, 0) - e["weight"])
|
||||
# A and the remaining node v define a "cut of the phase". There is a
|
||||
# minimum cut of the original graph that is also a cut of the phase.
|
||||
# Due to contractions in earlier phases, v may in fact represent
|
||||
# multiple nodes in the original graph.
|
||||
v, w = h.min()
|
||||
w = -w
|
||||
if w < cut_value:
|
||||
cut_value = w
|
||||
best_phase = i
|
||||
# Contract v and the last node added to A.
|
||||
contractions.append((u, v))
|
||||
for w, e in G[v].items():
|
||||
if w != u:
|
||||
if w not in G[u]:
|
||||
G.add_edge(u, w, weight=e["weight"])
|
||||
else:
|
||||
G[u][w]["weight"] += e["weight"]
|
||||
G.remove_node(v)
|
||||
|
||||
# Recover the optimal partitioning from the contractions.
|
||||
G = nx.Graph(islice(contractions, best_phase))
|
||||
v = contractions[best_phase][1]
|
||||
G.add_node(v)
|
||||
reachable = set(nx.single_source_shortest_path_length(G, v))
|
||||
partition = (list(reachable), list(nodes - reachable))
|
||||
|
||||
return cut_value, partition
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,418 @@
|
|||
import itertools
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import flow
|
||||
from networkx.algorithms.connectivity import local_edge_connectivity
|
||||
from networkx.algorithms.connectivity import local_node_connectivity
|
||||
|
||||
flow_funcs = [
|
||||
flow.boykov_kolmogorov,
|
||||
flow.dinitz,
|
||||
flow.edmonds_karp,
|
||||
flow.preflow_push,
|
||||
flow.shortest_augmenting_path,
|
||||
]
|
||||
|
||||
|
||||
# helper functions for tests
|
||||
|
||||
|
||||
def _generate_no_biconnected(max_attempts=50):
|
||||
attempts = 0
|
||||
while True:
|
||||
G = nx.fast_gnp_random_graph(100, 0.0575, seed=42)
|
||||
if nx.is_connected(G) and not nx.is_biconnected(G):
|
||||
attempts = 0
|
||||
yield G
|
||||
else:
|
||||
if attempts >= max_attempts:
|
||||
msg = f"Tried {max_attempts} times: no suitable Graph."
|
||||
raise Exception(msg)
|
||||
else:
|
||||
attempts += 1
|
||||
|
||||
|
||||
def test_average_connectivity():
|
||||
# figure 1 from:
|
||||
# Beineke, L., O. Oellermann, and R. Pippert (2002). The average
|
||||
# connectivity of a graph. Discrete mathematics 252(1-3), 31-45
|
||||
# http://www.sciencedirect.com/science/article/pii/S0012365X01001807
|
||||
G1 = nx.path_graph(3)
|
||||
G1.add_edges_from([(1, 3), (1, 4)])
|
||||
G2 = nx.path_graph(3)
|
||||
G2.add_edges_from([(1, 3), (1, 4), (0, 3), (0, 4), (3, 4)])
|
||||
G3 = nx.Graph()
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert nx.average_node_connectivity(G1, **kwargs) == 1, errmsg
|
||||
assert nx.average_node_connectivity(G2, **kwargs) == 2.2, errmsg
|
||||
assert nx.average_node_connectivity(G3, **kwargs) == 0, errmsg
|
||||
|
||||
|
||||
def test_average_connectivity_directed():
|
||||
G = nx.DiGraph([(1, 3), (1, 4), (1, 5)])
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert nx.average_node_connectivity(G) == 0.25, errmsg
|
||||
|
||||
|
||||
def test_articulation_points():
|
||||
Ggen = _generate_no_biconnected()
|
||||
for flow_func in flow_funcs:
|
||||
for i in range(3):
|
||||
G = next(Ggen)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert nx.node_connectivity(G, flow_func=flow_func) == 1, errmsg
|
||||
|
||||
|
||||
def test_brandes_erlebach():
|
||||
# Figure 1 chapter 7: Connectivity
|
||||
# http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf
|
||||
G = nx.Graph()
|
||||
G.add_edges_from(
|
||||
[
|
||||
(1, 2),
|
||||
(1, 3),
|
||||
(1, 4),
|
||||
(1, 5),
|
||||
(2, 3),
|
||||
(2, 6),
|
||||
(3, 4),
|
||||
(3, 6),
|
||||
(4, 6),
|
||||
(4, 7),
|
||||
(5, 7),
|
||||
(6, 8),
|
||||
(6, 9),
|
||||
(7, 8),
|
||||
(7, 10),
|
||||
(8, 11),
|
||||
(9, 10),
|
||||
(9, 11),
|
||||
(10, 11),
|
||||
]
|
||||
)
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert 3 == local_edge_connectivity(G, 1, 11, **kwargs), errmsg
|
||||
assert 3 == nx.edge_connectivity(G, 1, 11, **kwargs), errmsg
|
||||
assert 2 == local_node_connectivity(G, 1, 11, **kwargs), errmsg
|
||||
assert 2 == nx.node_connectivity(G, 1, 11, **kwargs), errmsg
|
||||
assert 2 == nx.edge_connectivity(G, **kwargs), errmsg
|
||||
assert 2 == nx.node_connectivity(G, **kwargs), errmsg
|
||||
if flow_func is flow.preflow_push:
|
||||
assert 3 == nx.edge_connectivity(G, 1, 11, cutoff=2, **kwargs), errmsg
|
||||
else:
|
||||
assert 2 == nx.edge_connectivity(G, 1, 11, cutoff=2, **kwargs), errmsg
|
||||
|
||||
|
||||
def test_white_harary_1():
|
||||
# Figure 1b white and harary (2001)
|
||||
# # http://eclectic.ss.uci.edu/~drwhite/sm-w23.PDF
|
||||
# A graph with high adhesion (edge connectivity) and low cohesion
|
||||
# (vertex connectivity)
|
||||
G = nx.disjoint_union(nx.complete_graph(4), nx.complete_graph(4))
|
||||
G.remove_node(7)
|
||||
for i in range(4, 7):
|
||||
G.add_edge(0, i)
|
||||
G = nx.disjoint_union(G, nx.complete_graph(4))
|
||||
G.remove_node(G.order() - 1)
|
||||
for i in range(7, 10):
|
||||
G.add_edge(0, i)
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert 1 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
||||
|
||||
|
||||
def test_white_harary_2():
|
||||
# Figure 8 white and harary (2001)
|
||||
# # http://eclectic.ss.uci.edu/~drwhite/sm-w23.PDF
|
||||
G = nx.disjoint_union(nx.complete_graph(4), nx.complete_graph(4))
|
||||
G.add_edge(0, 4)
|
||||
# kappa <= lambda <= delta
|
||||
assert 3 == min(nx.core_number(G).values())
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert 1 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert 1 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
||||
|
||||
|
||||
def test_complete_graphs():
|
||||
for n in range(5, 20, 5):
|
||||
for flow_func in flow_funcs:
|
||||
G = nx.complete_graph(n)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert n - 1 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert n - 1 == nx.node_connectivity(
|
||||
G.to_directed(), flow_func=flow_func
|
||||
), errmsg
|
||||
assert n - 1 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert n - 1 == nx.edge_connectivity(
|
||||
G.to_directed(), flow_func=flow_func
|
||||
), errmsg
|
||||
|
||||
|
||||
def test_empty_graphs():
|
||||
for k in range(5, 25, 5):
|
||||
G = nx.empty_graph(k)
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert 0 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert 0 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
||||
|
||||
|
||||
def test_petersen():
|
||||
G = nx.petersen_graph()
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert 3 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
||||
|
||||
|
||||
def test_tutte():
|
||||
G = nx.tutte_graph()
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert 3 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
||||
|
||||
|
||||
def test_dodecahedral():
|
||||
G = nx.dodecahedral_graph()
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert 3 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert 3 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
||||
|
||||
|
||||
def test_octahedral():
|
||||
G = nx.octahedral_graph()
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert 4 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert 4 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
||||
|
||||
|
||||
def test_icosahedral():
|
||||
G = nx.icosahedral_graph()
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert 5 == nx.node_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert 5 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
||||
|
||||
|
||||
def test_missing_source():
|
||||
G = nx.path_graph(4)
|
||||
for flow_func in flow_funcs:
|
||||
pytest.raises(
|
||||
nx.NetworkXError, nx.node_connectivity, G, 10, 1, flow_func=flow_func
|
||||
)
|
||||
|
||||
|
||||
def test_missing_target():
|
||||
G = nx.path_graph(4)
|
||||
for flow_func in flow_funcs:
|
||||
pytest.raises(
|
||||
nx.NetworkXError, nx.node_connectivity, G, 1, 10, flow_func=flow_func
|
||||
)
|
||||
|
||||
|
||||
def test_edge_missing_source():
|
||||
G = nx.path_graph(4)
|
||||
for flow_func in flow_funcs:
|
||||
pytest.raises(
|
||||
nx.NetworkXError, nx.edge_connectivity, G, 10, 1, flow_func=flow_func
|
||||
)
|
||||
|
||||
|
||||
def test_edge_missing_target():
|
||||
G = nx.path_graph(4)
|
||||
for flow_func in flow_funcs:
|
||||
pytest.raises(
|
||||
nx.NetworkXError, nx.edge_connectivity, G, 1, 10, flow_func=flow_func
|
||||
)
|
||||
|
||||
|
||||
def test_not_weakly_connected():
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, [1, 2, 3])
|
||||
nx.add_path(G, [4, 5])
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert nx.node_connectivity(G) == 0, errmsg
|
||||
assert nx.edge_connectivity(G) == 0, errmsg
|
||||
|
||||
|
||||
def test_not_connected():
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, [1, 2, 3])
|
||||
nx.add_path(G, [4, 5])
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert nx.node_connectivity(G) == 0, errmsg
|
||||
assert nx.edge_connectivity(G) == 0, errmsg
|
||||
|
||||
|
||||
def test_directed_edge_connectivity():
|
||||
G = nx.cycle_graph(10, create_using=nx.DiGraph()) # only one direction
|
||||
D = nx.cycle_graph(10).to_directed() # 2 reciprocal edges
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
assert 1 == nx.edge_connectivity(G, flow_func=flow_func), errmsg
|
||||
assert 1 == local_edge_connectivity(G, 1, 4, flow_func=flow_func), errmsg
|
||||
assert 1 == nx.edge_connectivity(G, 1, 4, flow_func=flow_func), errmsg
|
||||
assert 2 == nx.edge_connectivity(D, flow_func=flow_func), errmsg
|
||||
assert 2 == local_edge_connectivity(D, 1, 4, flow_func=flow_func), errmsg
|
||||
assert 2 == nx.edge_connectivity(D, 1, 4, flow_func=flow_func), errmsg
|
||||
|
||||
|
||||
def test_cutoff():
|
||||
G = nx.complete_graph(5)
|
||||
for local_func in [local_edge_connectivity, local_node_connectivity]:
|
||||
for flow_func in flow_funcs:
|
||||
if flow_func is flow.preflow_push:
|
||||
# cutoff is not supported by preflow_push
|
||||
continue
|
||||
for cutoff in [3, 2, 1]:
|
||||
result = local_func(G, 0, 4, flow_func=flow_func, cutoff=cutoff)
|
||||
assert cutoff == result, f"cutoff error in {flow_func.__name__}"
|
||||
|
||||
|
||||
def test_invalid_auxiliary():
|
||||
G = nx.complete_graph(5)
|
||||
pytest.raises(nx.NetworkXError, local_node_connectivity, G, 0, 3, auxiliary=G)
|
||||
|
||||
|
||||
def test_interface_only_source():
|
||||
G = nx.complete_graph(5)
|
||||
for interface_func in [nx.node_connectivity, nx.edge_connectivity]:
|
||||
pytest.raises(nx.NetworkXError, interface_func, G, s=0)
|
||||
|
||||
|
||||
def test_interface_only_target():
|
||||
G = nx.complete_graph(5)
|
||||
for interface_func in [nx.node_connectivity, nx.edge_connectivity]:
|
||||
pytest.raises(nx.NetworkXError, interface_func, G, t=3)
|
||||
|
||||
|
||||
def test_edge_connectivity_flow_vs_stoer_wagner():
|
||||
graph_funcs = [nx.icosahedral_graph, nx.octahedral_graph, nx.dodecahedral_graph]
|
||||
for graph_func in graph_funcs:
|
||||
G = graph_func()
|
||||
assert nx.stoer_wagner(G)[0] == nx.edge_connectivity(G)
|
||||
|
||||
|
||||
class TestAllPairsNodeConnectivity:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.path = nx.path_graph(7)
|
||||
cls.directed_path = nx.path_graph(7, create_using=nx.DiGraph())
|
||||
cls.cycle = nx.cycle_graph(7)
|
||||
cls.directed_cycle = nx.cycle_graph(7, create_using=nx.DiGraph())
|
||||
cls.gnp = nx.gnp_random_graph(30, 0.1, seed=42)
|
||||
cls.directed_gnp = nx.gnp_random_graph(30, 0.1, directed=True, seed=42)
|
||||
cls.K20 = nx.complete_graph(20)
|
||||
cls.K10 = nx.complete_graph(10)
|
||||
cls.K5 = nx.complete_graph(5)
|
||||
cls.G_list = [
|
||||
cls.path,
|
||||
cls.directed_path,
|
||||
cls.cycle,
|
||||
cls.directed_cycle,
|
||||
cls.gnp,
|
||||
cls.directed_gnp,
|
||||
cls.K10,
|
||||
cls.K5,
|
||||
cls.K20,
|
||||
]
|
||||
|
||||
def test_cycles(self):
|
||||
K_undir = nx.all_pairs_node_connectivity(self.cycle)
|
||||
for source in K_undir:
|
||||
for target, k in K_undir[source].items():
|
||||
assert k == 2
|
||||
K_dir = nx.all_pairs_node_connectivity(self.directed_cycle)
|
||||
for source in K_dir:
|
||||
for target, k in K_dir[source].items():
|
||||
assert k == 1
|
||||
|
||||
def test_complete(self):
|
||||
for G in [self.K10, self.K5, self.K20]:
|
||||
K = nx.all_pairs_node_connectivity(G)
|
||||
for source in K:
|
||||
for target, k in K[source].items():
|
||||
assert k == len(G) - 1
|
||||
|
||||
def test_paths(self):
|
||||
K_undir = nx.all_pairs_node_connectivity(self.path)
|
||||
for source in K_undir:
|
||||
for target, k in K_undir[source].items():
|
||||
assert k == 1
|
||||
K_dir = nx.all_pairs_node_connectivity(self.directed_path)
|
||||
for source in K_dir:
|
||||
for target, k in K_dir[source].items():
|
||||
if source < target:
|
||||
assert k == 1
|
||||
else:
|
||||
assert k == 0
|
||||
|
||||
def test_all_pairs_connectivity_nbunch(self):
|
||||
G = nx.complete_graph(5)
|
||||
nbunch = [0, 2, 3]
|
||||
C = nx.all_pairs_node_connectivity(G, nbunch=nbunch)
|
||||
assert len(C) == len(nbunch)
|
||||
|
||||
def test_all_pairs_connectivity_icosahedral(self):
|
||||
G = nx.icosahedral_graph()
|
||||
C = nx.all_pairs_node_connectivity(G)
|
||||
assert all(5 == C[u][v] for u, v in itertools.combinations(G, 2))
|
||||
|
||||
def test_all_pairs_connectivity(self):
|
||||
G = nx.Graph()
|
||||
nodes = [0, 1, 2, 3]
|
||||
nx.add_path(G, nodes)
|
||||
A = {n: {} for n in G}
|
||||
for u, v in itertools.combinations(nodes, 2):
|
||||
A[u][v] = A[v][u] = nx.node_connectivity(G, u, v)
|
||||
C = nx.all_pairs_node_connectivity(G)
|
||||
assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
|
||||
(k, sorted(v)) for k, v in C.items()
|
||||
)
|
||||
|
||||
def test_all_pairs_connectivity_directed(self):
|
||||
G = nx.DiGraph()
|
||||
nodes = [0, 1, 2, 3]
|
||||
nx.add_path(G, nodes)
|
||||
A = {n: {} for n in G}
|
||||
for u, v in itertools.permutations(nodes, 2):
|
||||
A[u][v] = nx.node_connectivity(G, u, v)
|
||||
C = nx.all_pairs_node_connectivity(G)
|
||||
assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
|
||||
(k, sorted(v)) for k, v in C.items()
|
||||
)
|
||||
|
||||
def test_all_pairs_connectivity_nbunch_combinations(self):
|
||||
G = nx.complete_graph(5)
|
||||
nbunch = [0, 2, 3]
|
||||
A = {n: {} for n in nbunch}
|
||||
for u, v in itertools.combinations(nbunch, 2):
|
||||
A[u][v] = A[v][u] = nx.node_connectivity(G, u, v)
|
||||
C = nx.all_pairs_node_connectivity(G, nbunch=nbunch)
|
||||
assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
|
||||
(k, sorted(v)) for k, v in C.items()
|
||||
)
|
||||
|
||||
def test_all_pairs_connectivity_nbunch_iter(self):
|
||||
G = nx.complete_graph(5)
|
||||
nbunch = [0, 2, 3]
|
||||
A = {n: {} for n in nbunch}
|
||||
for u, v in itertools.combinations(nbunch, 2):
|
||||
A[u][v] = A[v][u] = nx.node_connectivity(G, u, v)
|
||||
C = nx.all_pairs_node_connectivity(G, nbunch=iter(nbunch))
|
||||
assert sorted((k, sorted(v)) for k, v in A.items()) == sorted(
|
||||
(k, sorted(v)) for k, v in C.items()
|
||||
)
|
|
@ -0,0 +1,310 @@
|
|||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import flow
|
||||
from networkx.algorithms.connectivity import minimum_st_edge_cut
|
||||
from networkx.algorithms.connectivity import minimum_st_node_cut
|
||||
from networkx.utils import arbitrary_element
|
||||
|
||||
flow_funcs = [
|
||||
flow.boykov_kolmogorov,
|
||||
flow.dinitz,
|
||||
flow.edmonds_karp,
|
||||
flow.preflow_push,
|
||||
flow.shortest_augmenting_path,
|
||||
]
|
||||
|
||||
# Tests for node and edge cutsets
|
||||
|
||||
|
||||
def _generate_no_biconnected(max_attempts=50):
|
||||
attempts = 0
|
||||
while True:
|
||||
G = nx.fast_gnp_random_graph(100, 0.0575, seed=42)
|
||||
if nx.is_connected(G) and not nx.is_biconnected(G):
|
||||
attempts = 0
|
||||
yield G
|
||||
else:
|
||||
if attempts >= max_attempts:
|
||||
msg = f"Tried {attempts} times: no suitable Graph."
|
||||
raise Exception(msg)
|
||||
else:
|
||||
attempts += 1
|
||||
|
||||
|
||||
def test_articulation_points():
|
||||
Ggen = _generate_no_biconnected()
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
for i in range(1): # change 1 to 3 or more for more realizations.
|
||||
G = next(Ggen)
|
||||
cut = nx.minimum_node_cut(G, flow_func=flow_func)
|
||||
assert len(cut) == 1, errmsg
|
||||
assert cut.pop() in set(nx.articulation_points(G)), errmsg
|
||||
|
||||
|
||||
def test_brandes_erlebach_book():
|
||||
# Figure 1 chapter 7: Connectivity
|
||||
# http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf
|
||||
G = nx.Graph()
|
||||
G.add_edges_from(
|
||||
[
|
||||
(1, 2),
|
||||
(1, 3),
|
||||
(1, 4),
|
||||
(1, 5),
|
||||
(2, 3),
|
||||
(2, 6),
|
||||
(3, 4),
|
||||
(3, 6),
|
||||
(4, 6),
|
||||
(4, 7),
|
||||
(5, 7),
|
||||
(6, 8),
|
||||
(6, 9),
|
||||
(7, 8),
|
||||
(7, 10),
|
||||
(8, 11),
|
||||
(9, 10),
|
||||
(9, 11),
|
||||
(10, 11),
|
||||
]
|
||||
)
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge cutsets
|
||||
assert 3 == len(nx.minimum_edge_cut(G, 1, 11, **kwargs)), errmsg
|
||||
edge_cut = nx.minimum_edge_cut(G, **kwargs)
|
||||
# Node 5 has only two edges
|
||||
assert 2 == len(edge_cut), errmsg
|
||||
H = G.copy()
|
||||
H.remove_edges_from(edge_cut)
|
||||
assert not nx.is_connected(H), errmsg
|
||||
# node cuts
|
||||
assert {6, 7} == minimum_st_node_cut(G, 1, 11, **kwargs), errmsg
|
||||
assert {6, 7} == nx.minimum_node_cut(G, 1, 11, **kwargs), errmsg
|
||||
node_cut = nx.minimum_node_cut(G, **kwargs)
|
||||
assert 2 == len(node_cut), errmsg
|
||||
H = G.copy()
|
||||
H.remove_nodes_from(node_cut)
|
||||
assert not nx.is_connected(H), errmsg
|
||||
|
||||
|
||||
def test_white_harary_paper():
|
||||
# Figure 1b white and harary (2001)
|
||||
# http://eclectic.ss.uci.edu/~drwhite/sm-w23.PDF
|
||||
# A graph with high adhesion (edge connectivity) and low cohesion
|
||||
# (node connectivity)
|
||||
G = nx.disjoint_union(nx.complete_graph(4), nx.complete_graph(4))
|
||||
G.remove_node(7)
|
||||
for i in range(4, 7):
|
||||
G.add_edge(0, i)
|
||||
G = nx.disjoint_union(G, nx.complete_graph(4))
|
||||
G.remove_node(G.order() - 1)
|
||||
for i in range(7, 10):
|
||||
G.add_edge(0, i)
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge cuts
|
||||
edge_cut = nx.minimum_edge_cut(G, **kwargs)
|
||||
assert 3 == len(edge_cut), errmsg
|
||||
H = G.copy()
|
||||
H.remove_edges_from(edge_cut)
|
||||
assert not nx.is_connected(H), errmsg
|
||||
# node cuts
|
||||
node_cut = nx.minimum_node_cut(G, **kwargs)
|
||||
assert {0} == node_cut, errmsg
|
||||
H = G.copy()
|
||||
H.remove_nodes_from(node_cut)
|
||||
assert not nx.is_connected(H), errmsg
|
||||
|
||||
|
||||
def test_petersen_cutset():
|
||||
G = nx.petersen_graph()
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge cuts
|
||||
edge_cut = nx.minimum_edge_cut(G, **kwargs)
|
||||
assert 3 == len(edge_cut), errmsg
|
||||
H = G.copy()
|
||||
H.remove_edges_from(edge_cut)
|
||||
assert not nx.is_connected(H), errmsg
|
||||
# node cuts
|
||||
node_cut = nx.minimum_node_cut(G, **kwargs)
|
||||
assert 3 == len(node_cut), errmsg
|
||||
H = G.copy()
|
||||
H.remove_nodes_from(node_cut)
|
||||
assert not nx.is_connected(H), errmsg
|
||||
|
||||
|
||||
def test_octahedral_cutset():
|
||||
G = nx.octahedral_graph()
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge cuts
|
||||
edge_cut = nx.minimum_edge_cut(G, **kwargs)
|
||||
assert 4 == len(edge_cut), errmsg
|
||||
H = G.copy()
|
||||
H.remove_edges_from(edge_cut)
|
||||
assert not nx.is_connected(H), errmsg
|
||||
# node cuts
|
||||
node_cut = nx.minimum_node_cut(G, **kwargs)
|
||||
assert 4 == len(node_cut), errmsg
|
||||
H = G.copy()
|
||||
H.remove_nodes_from(node_cut)
|
||||
assert not nx.is_connected(H), errmsg
|
||||
|
||||
|
||||
def test_icosahedral_cutset():
|
||||
G = nx.icosahedral_graph()
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge cuts
|
||||
edge_cut = nx.minimum_edge_cut(G, **kwargs)
|
||||
assert 5 == len(edge_cut), errmsg
|
||||
H = G.copy()
|
||||
H.remove_edges_from(edge_cut)
|
||||
assert not nx.is_connected(H), errmsg
|
||||
# node cuts
|
||||
node_cut = nx.minimum_node_cut(G, **kwargs)
|
||||
assert 5 == len(node_cut), errmsg
|
||||
H = G.copy()
|
||||
H.remove_nodes_from(node_cut)
|
||||
assert not nx.is_connected(H), errmsg
|
||||
|
||||
|
||||
def test_node_cutset_exception():
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([(1, 2), (3, 4)])
|
||||
for flow_func in flow_funcs:
|
||||
pytest.raises(nx.NetworkXError, nx.minimum_node_cut, G, flow_func=flow_func)
|
||||
|
||||
|
||||
def test_node_cutset_random_graphs():
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
for i in range(3):
|
||||
G = nx.fast_gnp_random_graph(50, 0.25, seed=42)
|
||||
if not nx.is_connected(G):
|
||||
ccs = iter(nx.connected_components(G))
|
||||
start = arbitrary_element(next(ccs))
|
||||
G.add_edges_from((start, arbitrary_element(c)) for c in ccs)
|
||||
cutset = nx.minimum_node_cut(G, flow_func=flow_func)
|
||||
assert nx.node_connectivity(G) == len(cutset), errmsg
|
||||
G.remove_nodes_from(cutset)
|
||||
assert not nx.is_connected(G), errmsg
|
||||
|
||||
|
||||
def test_edge_cutset_random_graphs():
|
||||
for flow_func in flow_funcs:
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
for i in range(3):
|
||||
G = nx.fast_gnp_random_graph(50, 0.25, seed=42)
|
||||
if not nx.is_connected(G):
|
||||
ccs = iter(nx.connected_components(G))
|
||||
start = arbitrary_element(next(ccs))
|
||||
G.add_edges_from((start, arbitrary_element(c)) for c in ccs)
|
||||
cutset = nx.minimum_edge_cut(G, flow_func=flow_func)
|
||||
assert nx.edge_connectivity(G) == len(cutset), errmsg
|
||||
G.remove_edges_from(cutset)
|
||||
assert not nx.is_connected(G), errmsg
|
||||
|
||||
|
||||
def test_empty_graphs():
|
||||
G = nx.Graph()
|
||||
D = nx.DiGraph()
|
||||
for interface_func in [nx.minimum_node_cut, nx.minimum_edge_cut]:
|
||||
for flow_func in flow_funcs:
|
||||
pytest.raises(
|
||||
nx.NetworkXPointlessConcept, interface_func, G, flow_func=flow_func
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXPointlessConcept, interface_func, D, flow_func=flow_func
|
||||
)
|
||||
|
||||
|
||||
def test_unbounded():
|
||||
G = nx.complete_graph(5)
|
||||
for flow_func in flow_funcs:
|
||||
assert 4 == len(minimum_st_edge_cut(G, 1, 4, flow_func=flow_func))
|
||||
|
||||
|
||||
def test_missing_source():
|
||||
G = nx.path_graph(4)
|
||||
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
||||
for flow_func in flow_funcs:
|
||||
pytest.raises(
|
||||
nx.NetworkXError, interface_func, G, 10, 1, flow_func=flow_func
|
||||
)
|
||||
|
||||
|
||||
def test_missing_target():
|
||||
G = nx.path_graph(4)
|
||||
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
||||
for flow_func in flow_funcs:
|
||||
pytest.raises(
|
||||
nx.NetworkXError, interface_func, G, 1, 10, flow_func=flow_func
|
||||
)
|
||||
|
||||
|
||||
def test_not_weakly_connected():
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, [1, 2, 3])
|
||||
nx.add_path(G, [4, 5])
|
||||
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
||||
for flow_func in flow_funcs:
|
||||
pytest.raises(nx.NetworkXError, interface_func, G, flow_func=flow_func)
|
||||
|
||||
|
||||
def test_not_connected():
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, [1, 2, 3])
|
||||
nx.add_path(G, [4, 5])
|
||||
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
||||
for flow_func in flow_funcs:
|
||||
pytest.raises(nx.NetworkXError, interface_func, G, flow_func=flow_func)
|
||||
|
||||
|
||||
def tests_min_cut_complete():
|
||||
G = nx.complete_graph(5)
|
||||
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
||||
for flow_func in flow_funcs:
|
||||
assert 4 == len(interface_func(G, flow_func=flow_func))
|
||||
|
||||
|
||||
def tests_min_cut_complete_directed():
|
||||
G = nx.complete_graph(5)
|
||||
G = G.to_directed()
|
||||
for interface_func in [nx.minimum_edge_cut, nx.minimum_node_cut]:
|
||||
for flow_func in flow_funcs:
|
||||
assert 4 == len(interface_func(G, flow_func=flow_func))
|
||||
|
||||
|
||||
def tests_minimum_st_node_cut():
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([0, 1, 2, 3, 7, 8, 11, 12])
|
||||
G.add_edges_from([(7, 11), (1, 11), (1, 12), (12, 8), (0, 1)])
|
||||
nodelist = minimum_st_node_cut(G, 7, 11)
|
||||
assert nodelist == {}
|
||||
|
||||
|
||||
def test_invalid_auxiliary():
|
||||
G = nx.complete_graph(5)
|
||||
pytest.raises(nx.NetworkXError, minimum_st_node_cut, G, 0, 3, auxiliary=G)
|
||||
|
||||
|
||||
def test_interface_only_source():
|
||||
G = nx.complete_graph(5)
|
||||
for interface_func in [nx.minimum_node_cut, nx.minimum_edge_cut]:
|
||||
pytest.raises(nx.NetworkXError, interface_func, G, s=0)
|
||||
|
||||
|
||||
def test_interface_only_target():
|
||||
G = nx.complete_graph(5)
|
||||
for interface_func in [nx.minimum_node_cut, nx.minimum_edge_cut]:
|
||||
pytest.raises(nx.NetworkXError, interface_func, G, t=3)
|
|
@ -0,0 +1,249 @@
|
|||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import flow
|
||||
from networkx.utils import pairwise
|
||||
|
||||
flow_funcs = [
|
||||
flow.boykov_kolmogorov,
|
||||
flow.edmonds_karp,
|
||||
flow.dinitz,
|
||||
flow.preflow_push,
|
||||
flow.shortest_augmenting_path,
|
||||
]
|
||||
|
||||
|
||||
def is_path(G, path):
|
||||
return all(v in G[u] for u, v in pairwise(path))
|
||||
|
||||
|
||||
def are_edge_disjoint_paths(G, paths):
|
||||
if not paths:
|
||||
return False
|
||||
for path in paths:
|
||||
assert is_path(G, path)
|
||||
paths_edges = [list(pairwise(p)) for p in paths]
|
||||
num_of_edges = sum(len(e) for e in paths_edges)
|
||||
num_unique_edges = len(set.union(*[set(es) for es in paths_edges]))
|
||||
if num_of_edges == num_unique_edges:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def are_node_disjoint_paths(G, paths):
|
||||
if not paths:
|
||||
return False
|
||||
for path in paths:
|
||||
assert is_path(G, path)
|
||||
# first and last nodes are source and target
|
||||
st = {paths[0][0], paths[0][-1]}
|
||||
num_of_nodes = len([n for path in paths for n in path if n not in st])
|
||||
num_unique_nodes = len({n for path in paths for n in path if n not in st})
|
||||
if num_of_nodes == num_unique_nodes:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def test_graph_from_pr_2053():
|
||||
G = nx.Graph()
|
||||
G.add_edges_from(
|
||||
[
|
||||
("A", "B"),
|
||||
("A", "D"),
|
||||
("A", "F"),
|
||||
("A", "G"),
|
||||
("B", "C"),
|
||||
("B", "D"),
|
||||
("B", "G"),
|
||||
("C", "D"),
|
||||
("C", "E"),
|
||||
("C", "Z"),
|
||||
("D", "E"),
|
||||
("D", "F"),
|
||||
("E", "F"),
|
||||
("E", "Z"),
|
||||
("F", "Z"),
|
||||
("G", "Z"),
|
||||
]
|
||||
)
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge disjoint paths
|
||||
edge_paths = list(nx.edge_disjoint_paths(G, "A", "Z", **kwargs))
|
||||
assert are_edge_disjoint_paths(G, edge_paths), errmsg
|
||||
assert nx.edge_connectivity(G, "A", "Z") == len(edge_paths), errmsg
|
||||
# node disjoint paths
|
||||
node_paths = list(nx.node_disjoint_paths(G, "A", "Z", **kwargs))
|
||||
assert are_node_disjoint_paths(G, node_paths), errmsg
|
||||
assert nx.node_connectivity(G, "A", "Z") == len(node_paths), errmsg
|
||||
|
||||
|
||||
def test_florentine_families():
|
||||
G = nx.florentine_families_graph()
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge disjoint paths
|
||||
edge_dpaths = list(nx.edge_disjoint_paths(G, "Medici", "Strozzi", **kwargs))
|
||||
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
||||
assert nx.edge_connectivity(G, "Medici", "Strozzi") == len(edge_dpaths), errmsg
|
||||
# node disjoint paths
|
||||
node_dpaths = list(nx.node_disjoint_paths(G, "Medici", "Strozzi", **kwargs))
|
||||
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
||||
assert nx.node_connectivity(G, "Medici", "Strozzi") == len(node_dpaths), errmsg
|
||||
|
||||
|
||||
def test_karate():
|
||||
G = nx.karate_club_graph()
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge disjoint paths
|
||||
edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 33, **kwargs))
|
||||
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
||||
assert nx.edge_connectivity(G, 0, 33) == len(edge_dpaths), errmsg
|
||||
# node disjoint paths
|
||||
node_dpaths = list(nx.node_disjoint_paths(G, 0, 33, **kwargs))
|
||||
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
||||
assert nx.node_connectivity(G, 0, 33) == len(node_dpaths), errmsg
|
||||
|
||||
|
||||
def test_petersen_disjoint_paths():
|
||||
G = nx.petersen_graph()
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge disjoint paths
|
||||
edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 6, **kwargs))
|
||||
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
||||
assert 3 == len(edge_dpaths), errmsg
|
||||
# node disjoint paths
|
||||
node_dpaths = list(nx.node_disjoint_paths(G, 0, 6, **kwargs))
|
||||
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
||||
assert 3 == len(node_dpaths), errmsg
|
||||
|
||||
|
||||
def test_octahedral_disjoint_paths():
|
||||
G = nx.octahedral_graph()
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge disjoint paths
|
||||
edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 5, **kwargs))
|
||||
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
||||
assert 4 == len(edge_dpaths), errmsg
|
||||
# node disjoint paths
|
||||
node_dpaths = list(nx.node_disjoint_paths(G, 0, 5, **kwargs))
|
||||
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
||||
assert 4 == len(node_dpaths), errmsg
|
||||
|
||||
|
||||
def test_icosahedral_disjoint_paths():
|
||||
G = nx.icosahedral_graph()
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
# edge disjoint paths
|
||||
edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 6, **kwargs))
|
||||
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
||||
assert 5 == len(edge_dpaths), errmsg
|
||||
# node disjoint paths
|
||||
node_dpaths = list(nx.node_disjoint_paths(G, 0, 6, **kwargs))
|
||||
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
||||
assert 5 == len(node_dpaths), errmsg
|
||||
|
||||
|
||||
def test_cutoff_disjoint_paths():
|
||||
G = nx.icosahedral_graph()
|
||||
for flow_func in flow_funcs:
|
||||
kwargs = dict(flow_func=flow_func)
|
||||
errmsg = f"Assertion failed in function: {flow_func.__name__}"
|
||||
for cutoff in [2, 4]:
|
||||
kwargs["cutoff"] = cutoff
|
||||
# edge disjoint paths
|
||||
edge_dpaths = list(nx.edge_disjoint_paths(G, 0, 6, **kwargs))
|
||||
assert are_edge_disjoint_paths(G, edge_dpaths), errmsg
|
||||
assert cutoff == len(edge_dpaths), errmsg
|
||||
# node disjoint paths
|
||||
node_dpaths = list(nx.node_disjoint_paths(G, 0, 6, **kwargs))
|
||||
assert are_node_disjoint_paths(G, node_dpaths), errmsg
|
||||
assert cutoff == len(node_dpaths), errmsg
|
||||
|
||||
|
||||
def test_missing_source_edge_paths():
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.path_graph(4)
|
||||
list(nx.edge_disjoint_paths(G, 10, 1))
|
||||
|
||||
|
||||
def test_missing_source_node_paths():
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.path_graph(4)
|
||||
list(nx.node_disjoint_paths(G, 10, 1))
|
||||
|
||||
|
||||
def test_missing_target_edge_paths():
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.path_graph(4)
|
||||
list(nx.edge_disjoint_paths(G, 1, 10))
|
||||
|
||||
|
||||
def test_missing_target_node_paths():
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.path_graph(4)
|
||||
list(nx.node_disjoint_paths(G, 1, 10))
|
||||
|
||||
|
||||
def test_not_weakly_connected_edges():
|
||||
with pytest.raises(nx.NetworkXNoPath):
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, [1, 2, 3])
|
||||
nx.add_path(G, [4, 5])
|
||||
list(nx.edge_disjoint_paths(G, 1, 5))
|
||||
|
||||
|
||||
def test_not_weakly_connected_nodes():
|
||||
with pytest.raises(nx.NetworkXNoPath):
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, [1, 2, 3])
|
||||
nx.add_path(G, [4, 5])
|
||||
list(nx.node_disjoint_paths(G, 1, 5))
|
||||
|
||||
|
||||
def test_not_connected_edges():
|
||||
with pytest.raises(nx.NetworkXNoPath):
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, [1, 2, 3])
|
||||
nx.add_path(G, [4, 5])
|
||||
list(nx.edge_disjoint_paths(G, 1, 5))
|
||||
|
||||
|
||||
def test_not_connected_nodes():
|
||||
with pytest.raises(nx.NetworkXNoPath):
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, [1, 2, 3])
|
||||
nx.add_path(G, [4, 5])
|
||||
list(nx.node_disjoint_paths(G, 1, 5))
|
||||
|
||||
|
||||
def test_isolated_edges():
|
||||
with pytest.raises(nx.NetworkXNoPath):
|
||||
G = nx.Graph()
|
||||
G.add_node(1)
|
||||
nx.add_path(G, [4, 5])
|
||||
list(nx.edge_disjoint_paths(G, 1, 5))
|
||||
|
||||
|
||||
def test_isolated_nodes():
|
||||
with pytest.raises(nx.NetworkXNoPath):
|
||||
G = nx.Graph()
|
||||
G.add_node(1)
|
||||
nx.add_path(G, [4, 5])
|
||||
list(nx.node_disjoint_paths(G, 1, 5))
|
||||
|
||||
|
||||
def test_invalid_auxiliary():
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.complete_graph(5)
|
||||
list(nx.node_disjoint_paths(G, 0, 3, auxiliary=G))
|
|
@ -0,0 +1,495 @@
|
|||
import random
|
||||
import networkx as nx
|
||||
import itertools as it
|
||||
from networkx.utils import pairwise
|
||||
import pytest
|
||||
from networkx.algorithms.connectivity import k_edge_augmentation
|
||||
from networkx.algorithms.connectivity.edge_augmentation import (
|
||||
collapse,
|
||||
complement_edges,
|
||||
is_locally_k_edge_connected,
|
||||
is_k_edge_connected,
|
||||
_unpack_available_edges,
|
||||
)
|
||||
|
||||
# This should be set to the largest k for which an efficient algorithm is
|
||||
# explicitly defined.
|
||||
MAX_EFFICIENT_K = 2
|
||||
|
||||
|
||||
def tarjan_bridge_graph():
|
||||
# graph from tarjan paper
|
||||
# RE Tarjan - "A note on finding the bridges of a graph"
|
||||
# Information Processing Letters, 1974 - Elsevier
|
||||
# doi:10.1016/0020-0190(74)90003-9.
|
||||
# define 2-connected components and bridges
|
||||
ccs = [
|
||||
(1, 2, 4, 3, 1, 4),
|
||||
(5, 6, 7, 5),
|
||||
(8, 9, 10, 8),
|
||||
(17, 18, 16, 15, 17),
|
||||
(11, 12, 14, 13, 11, 14),
|
||||
]
|
||||
bridges = [(4, 8), (3, 5), (3, 17)]
|
||||
G = nx.Graph(it.chain(*(pairwise(path) for path in ccs + bridges)))
|
||||
return G
|
||||
|
||||
|
||||
def test_weight_key():
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
G.add_edges_from([(3, 8), (1, 2), (2, 3)])
|
||||
impossible = {(3, 6), (3, 9)}
|
||||
rng = random.Random(0)
|
||||
avail_uv = list(set(complement_edges(G)) - impossible)
|
||||
avail = [(u, v, {"cost": rng.random()}) for u, v in avail_uv]
|
||||
|
||||
_augment_and_check(G, k=1)
|
||||
_augment_and_check(G, k=1, avail=avail_uv)
|
||||
_augment_and_check(G, k=1, avail=avail, weight="cost")
|
||||
|
||||
_check_augmentations(G, avail, weight="cost")
|
||||
|
||||
|
||||
def test_is_locally_k_edge_connected_exceptions():
|
||||
pytest.raises(nx.NetworkXNotImplemented, is_k_edge_connected, nx.DiGraph(), k=0)
|
||||
pytest.raises(nx.NetworkXNotImplemented, is_k_edge_connected, nx.MultiGraph(), k=0)
|
||||
pytest.raises(ValueError, is_k_edge_connected, nx.Graph(), k=0)
|
||||
|
||||
|
||||
def test_is_k_edge_connected():
|
||||
G = nx.barbell_graph(10, 0)
|
||||
assert is_k_edge_connected(G, k=1)
|
||||
assert not is_k_edge_connected(G, k=2)
|
||||
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([5, 15])
|
||||
assert not is_k_edge_connected(G, k=1)
|
||||
assert not is_k_edge_connected(G, k=2)
|
||||
|
||||
G = nx.complete_graph(5)
|
||||
assert is_k_edge_connected(G, k=1)
|
||||
assert is_k_edge_connected(G, k=2)
|
||||
assert is_k_edge_connected(G, k=3)
|
||||
assert is_k_edge_connected(G, k=4)
|
||||
|
||||
|
||||
def test_is_k_edge_connected_exceptions():
|
||||
pytest.raises(
|
||||
nx.NetworkXNotImplemented, is_locally_k_edge_connected, nx.DiGraph(), 1, 2, k=0
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXNotImplemented,
|
||||
is_locally_k_edge_connected,
|
||||
nx.MultiGraph(),
|
||||
1,
|
||||
2,
|
||||
k=0,
|
||||
)
|
||||
pytest.raises(ValueError, is_locally_k_edge_connected, nx.Graph(), 1, 2, k=0)
|
||||
|
||||
|
||||
def test_is_locally_k_edge_connected():
|
||||
G = nx.barbell_graph(10, 0)
|
||||
assert is_locally_k_edge_connected(G, 5, 15, k=1)
|
||||
assert not is_locally_k_edge_connected(G, 5, 15, k=2)
|
||||
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([5, 15])
|
||||
assert not is_locally_k_edge_connected(G, 5, 15, k=2)
|
||||
|
||||
|
||||
def test_null_graph():
|
||||
G = nx.Graph()
|
||||
_check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
|
||||
|
||||
|
||||
def test_cliques():
|
||||
for n in range(1, 10):
|
||||
G = nx.complete_graph(n)
|
||||
_check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
|
||||
|
||||
|
||||
def test_clique_and_node():
|
||||
for n in range(1, 10):
|
||||
G = nx.complete_graph(n)
|
||||
G.add_node(n + 1)
|
||||
_check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
|
||||
|
||||
|
||||
def test_point_graph():
|
||||
G = nx.Graph()
|
||||
G.add_node(1)
|
||||
_check_augmentations(G, max_k=MAX_EFFICIENT_K + 2)
|
||||
|
||||
|
||||
def test_edgeless_graph():
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([1, 2, 3, 4])
|
||||
_check_augmentations(G)
|
||||
|
||||
|
||||
def test_invalid_k():
|
||||
G = nx.Graph()
|
||||
pytest.raises(ValueError, list, k_edge_augmentation(G, k=-1))
|
||||
pytest.raises(ValueError, list, k_edge_augmentation(G, k=0))
|
||||
|
||||
|
||||
def test_unfeasible():
|
||||
G = tarjan_bridge_graph()
|
||||
pytest.raises(nx.NetworkXUnfeasible, list, k_edge_augmentation(G, k=1, avail=[]))
|
||||
|
||||
pytest.raises(nx.NetworkXUnfeasible, list, k_edge_augmentation(G, k=2, avail=[]))
|
||||
|
||||
pytest.raises(
|
||||
nx.NetworkXUnfeasible, list, k_edge_augmentation(G, k=2, avail=[(7, 9)])
|
||||
)
|
||||
|
||||
# partial solutions should not error if real solutions are infeasible
|
||||
aug_edges = list(k_edge_augmentation(G, k=2, avail=[(7, 9)], partial=True))
|
||||
assert aug_edges == [(7, 9)]
|
||||
|
||||
_check_augmentations(G, avail=[], max_k=MAX_EFFICIENT_K + 2)
|
||||
|
||||
_check_augmentations(G, avail=[(7, 9)], max_k=MAX_EFFICIENT_K + 2)
|
||||
|
||||
|
||||
def test_tarjan():
|
||||
G = tarjan_bridge_graph()
|
||||
|
||||
aug_edges = set(_augment_and_check(G, k=2)[0])
|
||||
print(f"aug_edges = {aug_edges!r}")
|
||||
# can't assert edge exactly equality due to non-determinant edge order
|
||||
# but we do know the size of the solution must be 3
|
||||
assert len(aug_edges) == 3
|
||||
|
||||
avail = [
|
||||
(9, 7),
|
||||
(8, 5),
|
||||
(2, 10),
|
||||
(6, 13),
|
||||
(11, 18),
|
||||
(1, 17),
|
||||
(2, 3),
|
||||
(16, 17),
|
||||
(18, 14),
|
||||
(15, 14),
|
||||
]
|
||||
aug_edges = set(_augment_and_check(G, avail=avail, k=2)[0])
|
||||
|
||||
# Can't assert exact length since approximation depends on the order of a
|
||||
# dict traversal.
|
||||
assert len(aug_edges) <= 3 * 2
|
||||
|
||||
_check_augmentations(G, avail)
|
||||
|
||||
|
||||
def test_configuration():
|
||||
# seeds = [2718183590, 2470619828, 1694705158, 3001036531, 2401251497]
|
||||
seeds = [1001, 1002, 1003, 1004]
|
||||
for seed in seeds:
|
||||
deg_seq = nx.random_powerlaw_tree_sequence(20, seed=seed, tries=5000)
|
||||
G = nx.Graph(nx.configuration_model(deg_seq, seed=seed))
|
||||
G.remove_edges_from(nx.selfloop_edges(G))
|
||||
_check_augmentations(G)
|
||||
|
||||
|
||||
def test_shell():
|
||||
# seeds = [2057382236, 3331169846, 1840105863, 476020778, 2247498425]
|
||||
seeds = [18]
|
||||
for seed in seeds:
|
||||
constructor = [(12, 70, 0.8), (15, 40, 0.6)]
|
||||
G = nx.random_shell_graph(constructor, seed=seed)
|
||||
_check_augmentations(G)
|
||||
|
||||
|
||||
def test_karate():
|
||||
G = nx.karate_club_graph()
|
||||
_check_augmentations(G)
|
||||
|
||||
|
||||
def test_star():
|
||||
G = nx.star_graph(3)
|
||||
_check_augmentations(G)
|
||||
|
||||
G = nx.star_graph(5)
|
||||
_check_augmentations(G)
|
||||
|
||||
G = nx.star_graph(10)
|
||||
_check_augmentations(G)
|
||||
|
||||
|
||||
def test_barbell():
|
||||
G = nx.barbell_graph(5, 0)
|
||||
_check_augmentations(G)
|
||||
|
||||
G = nx.barbell_graph(5, 2)
|
||||
_check_augmentations(G)
|
||||
|
||||
G = nx.barbell_graph(5, 3)
|
||||
_check_augmentations(G)
|
||||
|
||||
G = nx.barbell_graph(5, 4)
|
||||
_check_augmentations(G)
|
||||
|
||||
|
||||
def test_bridge():
|
||||
G = nx.Graph([(2393, 2257), (2393, 2685), (2685, 2257), (1758, 2257)])
|
||||
_check_augmentations(G)
|
||||
|
||||
|
||||
def test_gnp_augmentation():
|
||||
rng = random.Random(0)
|
||||
G = nx.gnp_random_graph(30, 0.005, seed=0)
|
||||
# Randomly make edges available
|
||||
avail = {
|
||||
(u, v): 1 + rng.random() for u, v in complement_edges(G) if rng.random() < 0.25
|
||||
}
|
||||
_check_augmentations(G, avail)
|
||||
|
||||
|
||||
def _assert_solution_properties(G, aug_edges, avail_dict=None):
|
||||
""" Checks that aug_edges are consistently formatted """
|
||||
if avail_dict is not None:
|
||||
assert all(
|
||||
e in avail_dict for e in aug_edges
|
||||
), "when avail is specified aug-edges should be in avail"
|
||||
|
||||
unique_aug = set(map(tuple, map(sorted, aug_edges)))
|
||||
unique_aug = list(map(tuple, map(sorted, aug_edges)))
|
||||
assert len(aug_edges) == len(unique_aug), "edges should be unique"
|
||||
|
||||
assert not any(u == v for u, v in unique_aug), "should be no self-edges"
|
||||
|
||||
assert not any(
|
||||
G.has_edge(u, v) for u, v in unique_aug
|
||||
), "aug edges and G.edges should be disjoint"
|
||||
|
||||
|
||||
def _augment_and_check(
|
||||
G, k, avail=None, weight=None, verbose=False, orig_k=None, max_aug_k=None
|
||||
):
|
||||
"""
|
||||
Does one specific augmentation and checks for properties of the result
|
||||
"""
|
||||
if orig_k is None:
|
||||
try:
|
||||
orig_k = nx.edge_connectivity(G)
|
||||
except nx.NetworkXPointlessConcept:
|
||||
orig_k = 0
|
||||
info = {}
|
||||
try:
|
||||
if avail is not None:
|
||||
# ensure avail is in dict form
|
||||
avail_dict = dict(zip(*_unpack_available_edges(avail, weight=weight)))
|
||||
else:
|
||||
avail_dict = None
|
||||
try:
|
||||
# Find the augmentation if possible
|
||||
generator = nx.k_edge_augmentation(G, k=k, weight=weight, avail=avail)
|
||||
assert not isinstance(generator, list), "should always return an iter"
|
||||
aug_edges = []
|
||||
for edge in generator:
|
||||
aug_edges.append(edge)
|
||||
except nx.NetworkXUnfeasible:
|
||||
infeasible = True
|
||||
info["infeasible"] = True
|
||||
assert len(aug_edges) == 0, "should not generate anything if unfeasible"
|
||||
|
||||
if avail is None:
|
||||
n_nodes = G.number_of_nodes()
|
||||
assert n_nodes <= k, (
|
||||
"unconstrained cases are only unfeasible if |V| <= k. "
|
||||
f"Got |V|={n_nodes} and k={k}"
|
||||
)
|
||||
else:
|
||||
if max_aug_k is None:
|
||||
G_aug_all = G.copy()
|
||||
G_aug_all.add_edges_from(avail_dict.keys())
|
||||
try:
|
||||
max_aug_k = nx.edge_connectivity(G_aug_all)
|
||||
except nx.NetworkXPointlessConcept:
|
||||
max_aug_k = 0
|
||||
|
||||
assert max_aug_k < k, (
|
||||
"avail should only be unfeasible if using all edges "
|
||||
"does not achieve k-edge-connectivity"
|
||||
)
|
||||
|
||||
# Test for a partial solution
|
||||
partial_edges = list(
|
||||
nx.k_edge_augmentation(G, k=k, weight=weight, partial=True, avail=avail)
|
||||
)
|
||||
|
||||
info["n_partial_edges"] = len(partial_edges)
|
||||
|
||||
if avail_dict is None:
|
||||
assert set(partial_edges) == set(
|
||||
complement_edges(G)
|
||||
), "unweighted partial solutions should be the complement"
|
||||
elif len(avail_dict) > 0:
|
||||
H = G.copy()
|
||||
|
||||
# Find the partial / full augmented connectivity
|
||||
H.add_edges_from(partial_edges)
|
||||
partial_conn = nx.edge_connectivity(H)
|
||||
|
||||
H.add_edges_from(set(avail_dict.keys()))
|
||||
full_conn = nx.edge_connectivity(H)
|
||||
|
||||
# Full connectivity should be no better than our partial
|
||||
# solution.
|
||||
assert (
|
||||
partial_conn == full_conn
|
||||
), "adding more edges should not increase k-conn"
|
||||
|
||||
# Find the new edge-connectivity after adding the augmenting edges
|
||||
aug_edges = partial_edges
|
||||
else:
|
||||
infeasible = False
|
||||
|
||||
# Find the weight of the augmentation
|
||||
num_edges = len(aug_edges)
|
||||
if avail is not None:
|
||||
total_weight = sum([avail_dict[e] for e in aug_edges])
|
||||
else:
|
||||
total_weight = num_edges
|
||||
|
||||
info["total_weight"] = total_weight
|
||||
info["num_edges"] = num_edges
|
||||
|
||||
# Find the new edge-connectivity after adding the augmenting edges
|
||||
G_aug = G.copy()
|
||||
G_aug.add_edges_from(aug_edges)
|
||||
try:
|
||||
aug_k = nx.edge_connectivity(G_aug)
|
||||
except nx.NetworkXPointlessConcept:
|
||||
aug_k = 0
|
||||
info["aug_k"] = aug_k
|
||||
|
||||
# Do checks
|
||||
if not infeasible and orig_k < k:
|
||||
assert info["aug_k"] >= k, f"connectivity should increase to k={k} or more"
|
||||
|
||||
assert info["aug_k"] >= orig_k, "augmenting should never reduce connectivity"
|
||||
|
||||
_assert_solution_properties(G, aug_edges, avail_dict)
|
||||
|
||||
except Exception:
|
||||
info["failed"] = True
|
||||
print(f"edges = {list(G.edges())}")
|
||||
print(f"nodes = {list(G.nodes())}")
|
||||
print(f"aug_edges = {list(aug_edges)}")
|
||||
print(f"info = {info}")
|
||||
raise
|
||||
else:
|
||||
if verbose:
|
||||
print(f"info = {info}")
|
||||
|
||||
if infeasible:
|
||||
aug_edges = None
|
||||
return aug_edges, info
|
||||
|
||||
|
||||
def _check_augmentations(G, avail=None, max_k=None, weight=None, verbose=False):
|
||||
""" Helper to check weighted/unweighted cases with multiple values of k """
|
||||
# Using all available edges, find the maximum edge-connectivity
|
||||
try:
|
||||
orig_k = nx.edge_connectivity(G)
|
||||
except nx.NetworkXPointlessConcept:
|
||||
orig_k = 0
|
||||
|
||||
if avail is not None:
|
||||
all_aug_edges = _unpack_available_edges(avail, weight=weight)[0]
|
||||
G_aug_all = G.copy()
|
||||
G_aug_all.add_edges_from(all_aug_edges)
|
||||
try:
|
||||
max_aug_k = nx.edge_connectivity(G_aug_all)
|
||||
except nx.NetworkXPointlessConcept:
|
||||
max_aug_k = 0
|
||||
else:
|
||||
max_aug_k = G.number_of_nodes() - 1
|
||||
|
||||
if max_k is None:
|
||||
max_k = min(4, max_aug_k)
|
||||
|
||||
avail_uniform = {e: 1 for e in complement_edges(G)}
|
||||
|
||||
if verbose:
|
||||
print("\n=== CHECK_AUGMENTATION ===")
|
||||
print(f"G.number_of_nodes = {G.number_of_nodes()!r}")
|
||||
print(f"G.number_of_edges = {G.number_of_edges()!r}")
|
||||
print(f"max_k = {max_k!r}")
|
||||
print(f"max_aug_k = {max_aug_k!r}")
|
||||
print(f"orig_k = {orig_k!r}")
|
||||
|
||||
# check augmentation for multiple values of k
|
||||
for k in range(1, max_k + 1):
|
||||
if verbose:
|
||||
print("---------------")
|
||||
print(f"Checking k = {k}")
|
||||
|
||||
# Check the unweighted version
|
||||
if verbose:
|
||||
print("unweighted case")
|
||||
aug_edges1, info1 = _augment_and_check(G, k=k, verbose=verbose, orig_k=orig_k)
|
||||
|
||||
# Check that the weighted version with all available edges and uniform
|
||||
# weights gives a similar solution to the unweighted case.
|
||||
if verbose:
|
||||
print("weighted uniform case")
|
||||
aug_edges2, info2 = _augment_and_check(
|
||||
G,
|
||||
k=k,
|
||||
avail=avail_uniform,
|
||||
verbose=verbose,
|
||||
orig_k=orig_k,
|
||||
max_aug_k=G.number_of_nodes() - 1,
|
||||
)
|
||||
|
||||
# Check the weighted version
|
||||
if avail is not None:
|
||||
if verbose:
|
||||
print("weighted case")
|
||||
aug_edges3, info3 = _augment_and_check(
|
||||
G,
|
||||
k=k,
|
||||
avail=avail,
|
||||
weight=weight,
|
||||
verbose=verbose,
|
||||
max_aug_k=max_aug_k,
|
||||
orig_k=orig_k,
|
||||
)
|
||||
|
||||
if aug_edges1 is not None:
|
||||
# Check approximation ratios
|
||||
if k == 1:
|
||||
# when k=1, both solutions should be optimal
|
||||
assert info2["total_weight"] == info1["total_weight"]
|
||||
if k == 2:
|
||||
# when k=2, the weighted version is an approximation
|
||||
if orig_k == 0:
|
||||
# the approximation ratio is 3 if G is not connected
|
||||
assert info2["total_weight"] <= info1["total_weight"] * 3
|
||||
else:
|
||||
# the approximation ratio is 2 if G is was connected
|
||||
assert info2["total_weight"] <= info1["total_weight"] * 2
|
||||
_check_unconstrained_bridge_property(G, info1)
|
||||
|
||||
|
||||
def _check_unconstrained_bridge_property(G, info1):
|
||||
# Check Theorem 5 from Eswaran and Tarjan. (1975) Augmentation problems
|
||||
import math
|
||||
|
||||
bridge_ccs = list(nx.connectivity.bridge_components(G))
|
||||
# condense G into an forest C
|
||||
C = collapse(G, bridge_ccs)
|
||||
|
||||
p = len([n for n, d in C.degree() if d == 1]) # leafs
|
||||
q = len([n for n, d in C.degree() if d == 0]) # isolated
|
||||
if p + q > 1:
|
||||
size_target = int(math.ceil(p / 2.0)) + q
|
||||
size_aug = info1["num_edges"]
|
||||
assert (
|
||||
size_aug == size_target
|
||||
), "augmentation size is different from what theory predicts"
|
|
@ -0,0 +1,485 @@
|
|||
import networkx as nx
|
||||
import itertools as it
|
||||
import pytest
|
||||
from networkx.utils import pairwise
|
||||
from networkx.algorithms.connectivity import bridge_components, EdgeComponentAuxGraph
|
||||
from networkx.algorithms.connectivity.edge_kcomponents import general_k_edge_subgraphs
|
||||
|
||||
|
||||
# ----------------
|
||||
# Helper functions
|
||||
# ----------------
|
||||
|
||||
|
||||
def fset(list_of_sets):
|
||||
""" allows == to be used for list of sets """
|
||||
return set(map(frozenset, list_of_sets))
|
||||
|
||||
|
||||
def _assert_subgraph_edge_connectivity(G, ccs_subgraph, k):
|
||||
"""
|
||||
tests properties of k-edge-connected subgraphs
|
||||
|
||||
the actual edge connectivity should be no less than k unless the cc is a
|
||||
single node.
|
||||
"""
|
||||
for cc in ccs_subgraph:
|
||||
C = G.subgraph(cc)
|
||||
if len(cc) > 1:
|
||||
connectivity = nx.edge_connectivity(C)
|
||||
assert connectivity >= k
|
||||
|
||||
|
||||
def _memo_connectivity(G, u, v, memo):
|
||||
edge = (u, v)
|
||||
if edge in memo:
|
||||
return memo[edge]
|
||||
if not G.is_directed():
|
||||
redge = (v, u)
|
||||
if redge in memo:
|
||||
return memo[redge]
|
||||
memo[edge] = nx.edge_connectivity(G, *edge)
|
||||
return memo[edge]
|
||||
|
||||
|
||||
def _all_pairs_connectivity(G, cc, k, memo):
|
||||
# Brute force check
|
||||
for u, v in it.combinations(cc, 2):
|
||||
# Use a memoization dict to save on computation
|
||||
connectivity = _memo_connectivity(G, u, v, memo)
|
||||
if G.is_directed():
|
||||
connectivity = min(connectivity, _memo_connectivity(G, v, u, memo))
|
||||
assert connectivity >= k
|
||||
|
||||
|
||||
def _assert_local_cc_edge_connectivity(G, ccs_local, k, memo):
|
||||
"""
|
||||
tests properties of k-edge-connected components
|
||||
|
||||
the local edge connectivity between each pair of nodes in the the original
|
||||
graph should be no less than k unless the cc is a single node.
|
||||
"""
|
||||
for cc in ccs_local:
|
||||
if len(cc) > 1:
|
||||
# Strategy for testing a bit faster: If the subgraph has high edge
|
||||
# connectivity then it must have local connectivity
|
||||
C = G.subgraph(cc)
|
||||
connectivity = nx.edge_connectivity(C)
|
||||
if connectivity < k:
|
||||
# Otherwise do the brute force (with memoization) check
|
||||
_all_pairs_connectivity(G, cc, k, memo)
|
||||
|
||||
|
||||
# Helper function
|
||||
def _check_edge_connectivity(G):
|
||||
"""
|
||||
Helper - generates all k-edge-components using the aux graph. Checks the
|
||||
both local and subgraph edge connectivity of each cc. Also checks that
|
||||
alternate methods of computing the k-edge-ccs generate the same result.
|
||||
"""
|
||||
# Construct the auxiliary graph that can be used to make each k-cc or k-sub
|
||||
aux_graph = EdgeComponentAuxGraph.construct(G)
|
||||
|
||||
# memoize the local connectivity in this graph
|
||||
memo = {}
|
||||
|
||||
for k in it.count(1):
|
||||
# Test "local" k-edge-components and k-edge-subgraphs
|
||||
ccs_local = fset(aux_graph.k_edge_components(k))
|
||||
ccs_subgraph = fset(aux_graph.k_edge_subgraphs(k))
|
||||
|
||||
# Check connectivity properties that should be garuenteed by the
|
||||
# algorithms.
|
||||
_assert_local_cc_edge_connectivity(G, ccs_local, k, memo)
|
||||
_assert_subgraph_edge_connectivity(G, ccs_subgraph, k)
|
||||
|
||||
if k == 1 or k == 2 and not G.is_directed():
|
||||
assert (
|
||||
ccs_local == ccs_subgraph
|
||||
), "Subgraphs and components should be the same when k == 1 or (k == 2 and not G.directed())"
|
||||
|
||||
if G.is_directed():
|
||||
# Test special case methods are the same as the aux graph
|
||||
if k == 1:
|
||||
alt_sccs = fset(nx.strongly_connected_components(G))
|
||||
assert alt_sccs == ccs_local, "k=1 failed alt"
|
||||
assert alt_sccs == ccs_subgraph, "k=1 failed alt"
|
||||
else:
|
||||
# Test special case methods are the same as the aux graph
|
||||
if k == 1:
|
||||
alt_ccs = fset(nx.connected_components(G))
|
||||
assert alt_ccs == ccs_local, "k=1 failed alt"
|
||||
assert alt_ccs == ccs_subgraph, "k=1 failed alt"
|
||||
elif k == 2:
|
||||
alt_bridge_ccs = fset(bridge_components(G))
|
||||
assert alt_bridge_ccs == ccs_local, "k=2 failed alt"
|
||||
assert alt_bridge_ccs == ccs_subgraph, "k=2 failed alt"
|
||||
# if new methods for k == 3 or k == 4 are implemented add them here
|
||||
|
||||
# Check the general subgraph method works by itself
|
||||
alt_subgraph_ccs = fset(
|
||||
[set(C.nodes()) for C in general_k_edge_subgraphs(G, k=k)]
|
||||
)
|
||||
assert alt_subgraph_ccs == ccs_subgraph, "alt subgraph method failed"
|
||||
|
||||
# Stop once k is larger than all special case methods
|
||||
# and we cannot break down ccs any further.
|
||||
if k > 2 and all(len(cc) == 1 for cc in ccs_local):
|
||||
break
|
||||
|
||||
|
||||
# ----------------
|
||||
# Misc tests
|
||||
# ----------------
|
||||
|
||||
|
||||
def test_zero_k_exception():
|
||||
G = nx.Graph()
|
||||
# functions that return generators error immediately
|
||||
pytest.raises(ValueError, nx.k_edge_components, G, k=0)
|
||||
pytest.raises(ValueError, nx.k_edge_subgraphs, G, k=0)
|
||||
|
||||
# actual generators only error when you get the first item
|
||||
aux_graph = EdgeComponentAuxGraph.construct(G)
|
||||
pytest.raises(ValueError, list, aux_graph.k_edge_components(k=0))
|
||||
pytest.raises(ValueError, list, aux_graph.k_edge_subgraphs(k=0))
|
||||
|
||||
pytest.raises(ValueError, list, general_k_edge_subgraphs(G, k=0))
|
||||
|
||||
|
||||
def test_empty_input():
|
||||
G = nx.Graph()
|
||||
assert [] == list(nx.k_edge_components(G, k=5))
|
||||
assert [] == list(nx.k_edge_subgraphs(G, k=5))
|
||||
|
||||
G = nx.DiGraph()
|
||||
assert [] == list(nx.k_edge_components(G, k=5))
|
||||
assert [] == list(nx.k_edge_subgraphs(G, k=5))
|
||||
|
||||
|
||||
def test_not_implemented():
|
||||
G = nx.MultiGraph()
|
||||
pytest.raises(nx.NetworkXNotImplemented, EdgeComponentAuxGraph.construct, G)
|
||||
pytest.raises(nx.NetworkXNotImplemented, nx.k_edge_components, G, k=2)
|
||||
pytest.raises(nx.NetworkXNotImplemented, nx.k_edge_subgraphs, G, k=2)
|
||||
pytest.raises(nx.NetworkXNotImplemented, bridge_components, G)
|
||||
pytest.raises(nx.NetworkXNotImplemented, bridge_components, nx.DiGraph())
|
||||
|
||||
|
||||
def test_general_k_edge_subgraph_quick_return():
|
||||
# tests quick return optimization
|
||||
G = nx.Graph()
|
||||
G.add_node(0)
|
||||
subgraphs = list(general_k_edge_subgraphs(G, k=1))
|
||||
assert len(subgraphs) == 1
|
||||
for subgraph in subgraphs:
|
||||
assert subgraph.number_of_nodes() == 1
|
||||
|
||||
G.add_node(1)
|
||||
subgraphs = list(general_k_edge_subgraphs(G, k=1))
|
||||
assert len(subgraphs) == 2
|
||||
for subgraph in subgraphs:
|
||||
assert subgraph.number_of_nodes() == 1
|
||||
|
||||
|
||||
# ----------------
|
||||
# Undirected tests
|
||||
# ----------------
|
||||
|
||||
|
||||
def test_random_gnp():
|
||||
# seeds = [1550709854, 1309423156, 4208992358, 2785630813, 1915069929]
|
||||
seeds = [12, 13]
|
||||
|
||||
for seed in seeds:
|
||||
G = nx.gnp_random_graph(20, 0.2, seed=seed)
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_configuration():
|
||||
# seeds = [2718183590, 2470619828, 1694705158, 3001036531, 2401251497]
|
||||
seeds = [14, 15]
|
||||
for seed in seeds:
|
||||
deg_seq = nx.random_powerlaw_tree_sequence(20, seed=seed, tries=5000)
|
||||
G = nx.Graph(nx.configuration_model(deg_seq, seed=seed))
|
||||
G.remove_edges_from(nx.selfloop_edges(G))
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_shell():
|
||||
# seeds = [2057382236, 3331169846, 1840105863, 476020778, 2247498425]
|
||||
seeds = [20]
|
||||
for seed in seeds:
|
||||
constructor = [(12, 70, 0.8), (15, 40, 0.6)]
|
||||
G = nx.random_shell_graph(constructor, seed=seed)
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_karate():
|
||||
G = nx.karate_club_graph()
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_tarjan_bridge():
|
||||
# graph from tarjan paper
|
||||
# RE Tarjan - "A note on finding the bridges of a graph"
|
||||
# Information Processing Letters, 1974 - Elsevier
|
||||
# doi:10.1016/0020-0190(74)90003-9.
|
||||
# define 2-connected components and bridges
|
||||
ccs = [
|
||||
(1, 2, 4, 3, 1, 4),
|
||||
(5, 6, 7, 5),
|
||||
(8, 9, 10, 8),
|
||||
(17, 18, 16, 15, 17),
|
||||
(11, 12, 14, 13, 11, 14),
|
||||
]
|
||||
bridges = [(4, 8), (3, 5), (3, 17)]
|
||||
G = nx.Graph(it.chain(*(pairwise(path) for path in ccs + bridges)))
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_bridge_cc():
|
||||
# define 2-connected components and bridges
|
||||
cc2 = [(1, 2, 4, 3, 1, 4), (8, 9, 10, 8), (11, 12, 13, 11)]
|
||||
bridges = [(4, 8), (3, 5), (20, 21), (22, 23, 24)]
|
||||
G = nx.Graph(it.chain(*(pairwise(path) for path in cc2 + bridges)))
|
||||
bridge_ccs = fset(bridge_components(G))
|
||||
target_ccs = fset(
|
||||
[{1, 2, 3, 4}, {5}, {8, 9, 10}, {11, 12, 13}, {20}, {21}, {22}, {23}, {24}]
|
||||
)
|
||||
assert bridge_ccs == target_ccs
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_undirected_aux_graph():
|
||||
# Graph similar to the one in
|
||||
# http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
|
||||
a, b, c, d, e, f, g, h, i = "abcdefghi"
|
||||
paths = [
|
||||
(a, d, b, f, c),
|
||||
(a, e, b),
|
||||
(a, e, b, c, g, b, a),
|
||||
(c, b),
|
||||
(f, g, f),
|
||||
(h, i),
|
||||
]
|
||||
G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
|
||||
aux_graph = EdgeComponentAuxGraph.construct(G)
|
||||
|
||||
components_1 = fset(aux_graph.k_edge_subgraphs(k=1))
|
||||
target_1 = fset([{a, b, c, d, e, f, g}, {h, i}])
|
||||
assert target_1 == components_1
|
||||
|
||||
# Check that the undirected case for k=1 agrees with CCs
|
||||
alt_1 = fset(nx.k_edge_subgraphs(G, k=1))
|
||||
assert alt_1 == components_1
|
||||
|
||||
components_2 = fset(aux_graph.k_edge_subgraphs(k=2))
|
||||
target_2 = fset([{a, b, c, d, e, f, g}, {h}, {i}])
|
||||
assert target_2 == components_2
|
||||
|
||||
# Check that the undirected case for k=2 agrees with bridge components
|
||||
alt_2 = fset(nx.k_edge_subgraphs(G, k=2))
|
||||
assert alt_2 == components_2
|
||||
|
||||
components_3 = fset(aux_graph.k_edge_subgraphs(k=3))
|
||||
target_3 = fset([{a}, {b, c, f, g}, {d}, {e}, {h}, {i}])
|
||||
assert target_3 == components_3
|
||||
|
||||
components_4 = fset(aux_graph.k_edge_subgraphs(k=4))
|
||||
target_4 = fset([{a}, {b}, {c}, {d}, {e}, {f}, {g}, {h}, {i}])
|
||||
assert target_4 == components_4
|
||||
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_local_subgraph_difference():
|
||||
paths = [
|
||||
(11, 12, 13, 14, 11, 13, 14, 12), # first 4-clique
|
||||
(21, 22, 23, 24, 21, 23, 24, 22), # second 4-clique
|
||||
# paths connecting each node of the 4 cliques
|
||||
(11, 101, 21),
|
||||
(12, 102, 22),
|
||||
(13, 103, 23),
|
||||
(14, 104, 24),
|
||||
]
|
||||
G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
|
||||
aux_graph = EdgeComponentAuxGraph.construct(G)
|
||||
|
||||
# Each clique is returned separately in k-edge-subgraphs
|
||||
subgraph_ccs = fset(aux_graph.k_edge_subgraphs(3))
|
||||
subgraph_target = fset(
|
||||
[{101}, {102}, {103}, {104}, {21, 22, 23, 24}, {11, 12, 13, 14}]
|
||||
)
|
||||
assert subgraph_ccs == subgraph_target
|
||||
|
||||
# But in k-edge-ccs they are returned together
|
||||
# because they are locally 3-edge-connected
|
||||
local_ccs = fset(aux_graph.k_edge_components(3))
|
||||
local_target = fset([{101}, {102}, {103}, {104}, {11, 12, 13, 14, 21, 22, 23, 24}])
|
||||
assert local_ccs == local_target
|
||||
|
||||
|
||||
def test_local_subgraph_difference_directed():
|
||||
dipaths = [(1, 2, 3, 4, 1), (1, 3, 1)]
|
||||
G = nx.DiGraph(it.chain(*[pairwise(path) for path in dipaths]))
|
||||
|
||||
assert fset(nx.k_edge_components(G, k=1)) == fset(nx.k_edge_subgraphs(G, k=1))
|
||||
|
||||
# Unlike undirected graphs, when k=2, for directed graphs there is a case
|
||||
# where the k-edge-ccs are not the same as the k-edge-subgraphs.
|
||||
# (in directed graphs ccs and subgraphs are the same when k=2)
|
||||
assert fset(nx.k_edge_components(G, k=2)) != fset(nx.k_edge_subgraphs(G, k=2))
|
||||
|
||||
assert fset(nx.k_edge_components(G, k=3)) == fset(nx.k_edge_subgraphs(G, k=3))
|
||||
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_triangles():
|
||||
paths = [
|
||||
(11, 12, 13, 11), # first 3-clique
|
||||
(21, 22, 23, 21), # second 3-clique
|
||||
(11, 21), # connected by an edge
|
||||
]
|
||||
G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
|
||||
|
||||
# subgraph and ccs are the same in all cases here
|
||||
assert fset(nx.k_edge_components(G, k=1)) == fset(nx.k_edge_subgraphs(G, k=1))
|
||||
|
||||
assert fset(nx.k_edge_components(G, k=2)) == fset(nx.k_edge_subgraphs(G, k=2))
|
||||
|
||||
assert fset(nx.k_edge_components(G, k=3)) == fset(nx.k_edge_subgraphs(G, k=3))
|
||||
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_four_clique():
|
||||
paths = [
|
||||
(11, 12, 13, 14, 11, 13, 14, 12), # first 4-clique
|
||||
(21, 22, 23, 24, 21, 23, 24, 22), # second 4-clique
|
||||
# paths connecting the 4 cliques such that they are
|
||||
# 3-connected in G, but not in the subgraph.
|
||||
# Case where the nodes bridging them do not have degree less than 3.
|
||||
(100, 13),
|
||||
(12, 100, 22),
|
||||
(13, 200, 23),
|
||||
(14, 300, 24),
|
||||
]
|
||||
G = nx.Graph(it.chain(*[pairwise(path) for path in paths]))
|
||||
|
||||
# The subgraphs and ccs are different for k=3
|
||||
local_ccs = fset(nx.k_edge_components(G, k=3))
|
||||
subgraphs = fset(nx.k_edge_subgraphs(G, k=3))
|
||||
assert local_ccs != subgraphs
|
||||
|
||||
# The cliques ares in the same cc
|
||||
clique1 = frozenset(paths[0])
|
||||
clique2 = frozenset(paths[1])
|
||||
assert clique1.union(clique2).union({100}) in local_ccs
|
||||
|
||||
# but different subgraphs
|
||||
assert clique1 in subgraphs
|
||||
assert clique2 in subgraphs
|
||||
|
||||
assert G.degree(100) == 3
|
||||
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_five_clique():
|
||||
# Make a graph that can be disconnected less than 4 edges, but no node has
|
||||
# degree less than 4.
|
||||
G = nx.disjoint_union(nx.complete_graph(5), nx.complete_graph(5))
|
||||
paths = [
|
||||
# add aux-connections
|
||||
(1, 100, 6),
|
||||
(2, 100, 7),
|
||||
(3, 200, 8),
|
||||
(4, 200, 100),
|
||||
]
|
||||
G.add_edges_from(it.chain(*[pairwise(path) for path in paths]))
|
||||
assert min(dict(nx.degree(G)).values()) == 4
|
||||
|
||||
# For k=3 they are the same
|
||||
assert fset(nx.k_edge_components(G, k=3)) == fset(nx.k_edge_subgraphs(G, k=3))
|
||||
|
||||
# For k=4 they are the different
|
||||
# the aux nodes are in the same CC as clique 1 but no the same subgraph
|
||||
assert fset(nx.k_edge_components(G, k=4)) != fset(nx.k_edge_subgraphs(G, k=4))
|
||||
|
||||
# For k=5 they are not the same
|
||||
assert fset(nx.k_edge_components(G, k=5)) != fset(nx.k_edge_subgraphs(G, k=5))
|
||||
|
||||
# For k=6 they are the same
|
||||
assert fset(nx.k_edge_components(G, k=6)) == fset(nx.k_edge_subgraphs(G, k=6))
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
# ----------------
|
||||
# Undirected tests
|
||||
# ----------------
|
||||
|
||||
|
||||
def test_directed_aux_graph():
|
||||
# Graph similar to the one in
|
||||
# http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264
|
||||
a, b, c, d, e, f, g, h, i = "abcdefghi"
|
||||
dipaths = [
|
||||
(a, d, b, f, c),
|
||||
(a, e, b),
|
||||
(a, e, b, c, g, b, a),
|
||||
(c, b),
|
||||
(f, g, f),
|
||||
(h, i),
|
||||
]
|
||||
G = nx.DiGraph(it.chain(*[pairwise(path) for path in dipaths]))
|
||||
aux_graph = EdgeComponentAuxGraph.construct(G)
|
||||
|
||||
components_1 = fset(aux_graph.k_edge_subgraphs(k=1))
|
||||
target_1 = fset([{a, b, c, d, e, f, g}, {h}, {i}])
|
||||
assert target_1 == components_1
|
||||
|
||||
# Check that the directed case for k=1 agrees with SCCs
|
||||
alt_1 = fset(nx.strongly_connected_components(G))
|
||||
assert alt_1 == components_1
|
||||
|
||||
components_2 = fset(aux_graph.k_edge_subgraphs(k=2))
|
||||
target_2 = fset([{i}, {e}, {d}, {b, c, f, g}, {h}, {a}])
|
||||
assert target_2 == components_2
|
||||
|
||||
components_3 = fset(aux_graph.k_edge_subgraphs(k=3))
|
||||
target_3 = fset([{a}, {b}, {c}, {d}, {e}, {f}, {g}, {h}, {i}])
|
||||
assert target_3 == components_3
|
||||
|
||||
|
||||
def test_random_gnp_directed():
|
||||
# seeds = [3894723670, 500186844, 267231174, 2181982262, 1116750056]
|
||||
seeds = [21]
|
||||
for seed in seeds:
|
||||
G = nx.gnp_random_graph(20, 0.2, directed=True, seed=seed)
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_configuration_directed():
|
||||
# seeds = [671221681, 2403749451, 124433910, 672335939, 1193127215]
|
||||
seeds = [67]
|
||||
for seed in seeds:
|
||||
deg_seq = nx.random_powerlaw_tree_sequence(20, seed=seed, tries=5000)
|
||||
G = nx.DiGraph(nx.configuration_model(deg_seq, seed=seed))
|
||||
G.remove_edges_from(nx.selfloop_edges(G))
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_shell_directed():
|
||||
# seeds = [3134027055, 4079264063, 1350769518, 1405643020, 530038094]
|
||||
seeds = [31]
|
||||
for seed in seeds:
|
||||
constructor = [(12, 70, 0.8), (15, 40, 0.6)]
|
||||
G = nx.random_shell_graph(constructor, seed=seed).to_directed()
|
||||
_check_edge_connectivity(G)
|
||||
|
||||
|
||||
def test_karate_directed():
|
||||
G = nx.karate_club_graph().to_directed()
|
||||
_check_edge_connectivity(G)
|
|
@ -0,0 +1,295 @@
|
|||
# Test for Moody and White k-components algorithm
|
||||
import pytest
|
||||
import networkx as nx
|
||||
from networkx.algorithms.connectivity.kcomponents import (
|
||||
build_k_number_dict,
|
||||
_consolidate,
|
||||
)
|
||||
|
||||
##
|
||||
# A nice synthetic graph
|
||||
##
|
||||
|
||||
|
||||
def torrents_and_ferraro_graph():
|
||||
# Graph from https://arxiv.org/pdf/1503.04476v1 p.26
|
||||
G = nx.convert_node_labels_to_integers(
|
||||
nx.grid_graph([5, 5]), label_attribute="labels"
|
||||
)
|
||||
rlabels = nx.get_node_attributes(G, "labels")
|
||||
labels = {v: k for k, v in rlabels.items()}
|
||||
|
||||
for nodes in [(labels[(0, 4)], labels[(1, 4)]), (labels[(3, 4)], labels[(4, 4)])]:
|
||||
new_node = G.order() + 1
|
||||
# Petersen graph is triconnected
|
||||
P = nx.petersen_graph()
|
||||
G = nx.disjoint_union(G, P)
|
||||
# Add two edges between the grid and P
|
||||
G.add_edge(new_node + 1, nodes[0])
|
||||
G.add_edge(new_node, nodes[1])
|
||||
# K5 is 4-connected
|
||||
K = nx.complete_graph(5)
|
||||
G = nx.disjoint_union(G, K)
|
||||
# Add three edges between P and K5
|
||||
G.add_edge(new_node + 2, new_node + 11)
|
||||
G.add_edge(new_node + 3, new_node + 12)
|
||||
G.add_edge(new_node + 4, new_node + 13)
|
||||
# Add another K5 sharing a node
|
||||
G = nx.disjoint_union(G, K)
|
||||
nbrs = G[new_node + 10]
|
||||
G.remove_node(new_node + 10)
|
||||
for nbr in nbrs:
|
||||
G.add_edge(new_node + 17, nbr)
|
||||
# This edge makes the graph biconnected; it's
|
||||
# needed because K5s share only one node.
|
||||
G.add_edge(new_node + 16, new_node + 8)
|
||||
|
||||
for nodes in [(labels[(0, 0)], labels[(1, 0)]), (labels[(3, 0)], labels[(4, 0)])]:
|
||||
new_node = G.order() + 1
|
||||
# Petersen graph is triconnected
|
||||
P = nx.petersen_graph()
|
||||
G = nx.disjoint_union(G, P)
|
||||
# Add two edges between the grid and P
|
||||
G.add_edge(new_node + 1, nodes[0])
|
||||
G.add_edge(new_node, nodes[1])
|
||||
# K5 is 4-connected
|
||||
K = nx.complete_graph(5)
|
||||
G = nx.disjoint_union(G, K)
|
||||
# Add three edges between P and K5
|
||||
G.add_edge(new_node + 2, new_node + 11)
|
||||
G.add_edge(new_node + 3, new_node + 12)
|
||||
G.add_edge(new_node + 4, new_node + 13)
|
||||
# Add another K5 sharing two nodes
|
||||
G = nx.disjoint_union(G, K)
|
||||
nbrs = G[new_node + 10]
|
||||
G.remove_node(new_node + 10)
|
||||
for nbr in nbrs:
|
||||
G.add_edge(new_node + 17, nbr)
|
||||
nbrs2 = G[new_node + 9]
|
||||
G.remove_node(new_node + 9)
|
||||
for nbr in nbrs2:
|
||||
G.add_edge(new_node + 18, nbr)
|
||||
return G
|
||||
|
||||
|
||||
def test_directed():
|
||||
with pytest.raises(nx.NetworkXNotImplemented):
|
||||
G = nx.gnp_random_graph(10, 0.2, directed=True, seed=42)
|
||||
nx.k_components(G)
|
||||
|
||||
|
||||
# Helper function
|
||||
def _check_connectivity(G, k_components):
|
||||
for k, components in k_components.items():
|
||||
if k < 3:
|
||||
continue
|
||||
# check that k-components have node connectivity >= k.
|
||||
for component in components:
|
||||
C = G.subgraph(component)
|
||||
K = nx.node_connectivity(C)
|
||||
assert K >= k
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_torrents_and_ferraro_graph():
|
||||
G = torrents_and_ferraro_graph()
|
||||
result = nx.k_components(G)
|
||||
_check_connectivity(G, result)
|
||||
|
||||
# In this example graph there are 8 3-components, 4 with 15 nodes
|
||||
# and 4 with 5 nodes.
|
||||
assert len(result[3]) == 8
|
||||
assert len([c for c in result[3] if len(c) == 15]) == 4
|
||||
assert len([c for c in result[3] if len(c) == 5]) == 4
|
||||
# There are also 8 4-components all with 5 nodes.
|
||||
assert len(result[4]) == 8
|
||||
assert all(len(c) == 5 for c in result[4])
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_random_gnp():
|
||||
G = nx.gnp_random_graph(50, 0.2, seed=42)
|
||||
result = nx.k_components(G)
|
||||
_check_connectivity(G, result)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_shell():
|
||||
constructor = [(20, 80, 0.8), (80, 180, 0.6)]
|
||||
G = nx.random_shell_graph(constructor, seed=42)
|
||||
result = nx.k_components(G)
|
||||
_check_connectivity(G, result)
|
||||
|
||||
|
||||
def test_configuration():
|
||||
deg_seq = nx.random_powerlaw_tree_sequence(100, tries=5, seed=72)
|
||||
G = nx.Graph(nx.configuration_model(deg_seq))
|
||||
G.remove_edges_from(nx.selfloop_edges(G))
|
||||
result = nx.k_components(G)
|
||||
_check_connectivity(G, result)
|
||||
|
||||
|
||||
def test_karate():
|
||||
G = nx.karate_club_graph()
|
||||
result = nx.k_components(G)
|
||||
_check_connectivity(G, result)
|
||||
|
||||
|
||||
def test_karate_component_number():
|
||||
karate_k_num = {
|
||||
0: 4,
|
||||
1: 4,
|
||||
2: 4,
|
||||
3: 4,
|
||||
4: 3,
|
||||
5: 3,
|
||||
6: 3,
|
||||
7: 4,
|
||||
8: 4,
|
||||
9: 2,
|
||||
10: 3,
|
||||
11: 1,
|
||||
12: 2,
|
||||
13: 4,
|
||||
14: 2,
|
||||
15: 2,
|
||||
16: 2,
|
||||
17: 2,
|
||||
18: 2,
|
||||
19: 3,
|
||||
20: 2,
|
||||
21: 2,
|
||||
22: 2,
|
||||
23: 3,
|
||||
24: 3,
|
||||
25: 3,
|
||||
26: 2,
|
||||
27: 3,
|
||||
28: 3,
|
||||
29: 3,
|
||||
30: 4,
|
||||
31: 3,
|
||||
32: 4,
|
||||
33: 4,
|
||||
}
|
||||
G = nx.karate_club_graph()
|
||||
k_components = nx.k_components(G)
|
||||
k_num = build_k_number_dict(k_components)
|
||||
assert karate_k_num == k_num
|
||||
|
||||
|
||||
def test_davis_southern_women():
|
||||
G = nx.davis_southern_women_graph()
|
||||
result = nx.k_components(G)
|
||||
_check_connectivity(G, result)
|
||||
|
||||
|
||||
def test_davis_southern_women_detail_3_and_4():
|
||||
solution = {
|
||||
3: [
|
||||
{
|
||||
"Nora Fayette",
|
||||
"E10",
|
||||
"Myra Liddel",
|
||||
"E12",
|
||||
"E14",
|
||||
"Frances Anderson",
|
||||
"Evelyn Jefferson",
|
||||
"Ruth DeSand",
|
||||
"Helen Lloyd",
|
||||
"Eleanor Nye",
|
||||
"E9",
|
||||
"E8",
|
||||
"E5",
|
||||
"E4",
|
||||
"E7",
|
||||
"E6",
|
||||
"E1",
|
||||
"Verne Sanderson",
|
||||
"E3",
|
||||
"E2",
|
||||
"Theresa Anderson",
|
||||
"Pearl Oglethorpe",
|
||||
"Katherina Rogers",
|
||||
"Brenda Rogers",
|
||||
"E13",
|
||||
"Charlotte McDowd",
|
||||
"Sylvia Avondale",
|
||||
"Laura Mandeville",
|
||||
}
|
||||
],
|
||||
4: [
|
||||
{
|
||||
"Nora Fayette",
|
||||
"E10",
|
||||
"Verne Sanderson",
|
||||
"E12",
|
||||
"Frances Anderson",
|
||||
"Evelyn Jefferson",
|
||||
"Ruth DeSand",
|
||||
"Helen Lloyd",
|
||||
"Eleanor Nye",
|
||||
"E9",
|
||||
"E8",
|
||||
"E5",
|
||||
"E4",
|
||||
"E7",
|
||||
"E6",
|
||||
"Myra Liddel",
|
||||
"E3",
|
||||
"Theresa Anderson",
|
||||
"Katherina Rogers",
|
||||
"Brenda Rogers",
|
||||
"Charlotte McDowd",
|
||||
"Sylvia Avondale",
|
||||
"Laura Mandeville",
|
||||
}
|
||||
],
|
||||
}
|
||||
G = nx.davis_southern_women_graph()
|
||||
result = nx.k_components(G)
|
||||
for k, components in result.items():
|
||||
if k < 3:
|
||||
continue
|
||||
assert len(components) == len(solution[k])
|
||||
for component in components:
|
||||
assert component in solution[k]
|
||||
|
||||
|
||||
def test_set_consolidation_rosettacode():
|
||||
# Tests from http://rosettacode.org/wiki/Set_consolidation
|
||||
def list_of_sets_equal(result, solution):
|
||||
assert {frozenset(s) for s in result} == {frozenset(s) for s in solution}
|
||||
|
||||
question = [{"A", "B"}, {"C", "D"}]
|
||||
solution = [{"A", "B"}, {"C", "D"}]
|
||||
list_of_sets_equal(_consolidate(question, 1), solution)
|
||||
question = [{"A", "B"}, {"B", "C"}]
|
||||
solution = [{"A", "B", "C"}]
|
||||
list_of_sets_equal(_consolidate(question, 1), solution)
|
||||
question = [{"A", "B"}, {"C", "D"}, {"D", "B"}]
|
||||
solution = [{"A", "C", "B", "D"}]
|
||||
list_of_sets_equal(_consolidate(question, 1), solution)
|
||||
question = [{"H", "I", "K"}, {"A", "B"}, {"C", "D"}, {"D", "B"}, {"F", "G", "H"}]
|
||||
solution = [{"A", "C", "B", "D"}, {"G", "F", "I", "H", "K"}]
|
||||
list_of_sets_equal(_consolidate(question, 1), solution)
|
||||
question = [
|
||||
{"A", "H"},
|
||||
{"H", "I", "K"},
|
||||
{"A", "B"},
|
||||
{"C", "D"},
|
||||
{"D", "B"},
|
||||
{"F", "G", "H"},
|
||||
]
|
||||
solution = [{"A", "C", "B", "D", "G", "F", "I", "H", "K"}]
|
||||
list_of_sets_equal(_consolidate(question, 1), solution)
|
||||
question = [
|
||||
{"H", "I", "K"},
|
||||
{"A", "B"},
|
||||
{"C", "D"},
|
||||
{"D", "B"},
|
||||
{"F", "G", "H"},
|
||||
{"A", "H"},
|
||||
]
|
||||
solution = [{"A", "C", "B", "D", "G", "F", "I", "H", "K"}]
|
||||
list_of_sets_equal(_consolidate(question, 1), solution)
|
|
@ -0,0 +1,266 @@
|
|||
# Jordi Torrents
|
||||
# Test for k-cutsets
|
||||
import itertools
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import flow
|
||||
from networkx.algorithms.connectivity.kcutsets import _is_separating_set
|
||||
|
||||
MAX_CUTSETS_TO_TEST = 4 # originally 100. cut to decrease testing time
|
||||
|
||||
flow_funcs = [
|
||||
flow.boykov_kolmogorov,
|
||||
flow.dinitz,
|
||||
flow.edmonds_karp,
|
||||
flow.preflow_push,
|
||||
flow.shortest_augmenting_path,
|
||||
]
|
||||
|
||||
|
||||
##
|
||||
# Some nice synthetic graphs
|
||||
##
|
||||
def graph_example_1():
|
||||
G = nx.convert_node_labels_to_integers(
|
||||
nx.grid_graph([5, 5]), label_attribute="labels"
|
||||
)
|
||||
rlabels = nx.get_node_attributes(G, "labels")
|
||||
labels = {v: k for k, v in rlabels.items()}
|
||||
|
||||
for nodes in [
|
||||
(labels[(0, 0)], labels[(1, 0)]),
|
||||
(labels[(0, 4)], labels[(1, 4)]),
|
||||
(labels[(3, 0)], labels[(4, 0)]),
|
||||
(labels[(3, 4)], labels[(4, 4)]),
|
||||
]:
|
||||
new_node = G.order() + 1
|
||||
# Petersen graph is triconnected
|
||||
P = nx.petersen_graph()
|
||||
G = nx.disjoint_union(G, P)
|
||||
# Add two edges between the grid and P
|
||||
G.add_edge(new_node + 1, nodes[0])
|
||||
G.add_edge(new_node, nodes[1])
|
||||
# K5 is 4-connected
|
||||
K = nx.complete_graph(5)
|
||||
G = nx.disjoint_union(G, K)
|
||||
# Add three edges between P and K5
|
||||
G.add_edge(new_node + 2, new_node + 11)
|
||||
G.add_edge(new_node + 3, new_node + 12)
|
||||
G.add_edge(new_node + 4, new_node + 13)
|
||||
# Add another K5 sharing a node
|
||||
G = nx.disjoint_union(G, K)
|
||||
nbrs = G[new_node + 10]
|
||||
G.remove_node(new_node + 10)
|
||||
for nbr in nbrs:
|
||||
G.add_edge(new_node + 17, nbr)
|
||||
G.add_edge(new_node + 16, new_node + 5)
|
||||
return G
|
||||
|
||||
|
||||
def torrents_and_ferraro_graph():
|
||||
G = nx.convert_node_labels_to_integers(
|
||||
nx.grid_graph([5, 5]), label_attribute="labels"
|
||||
)
|
||||
rlabels = nx.get_node_attributes(G, "labels")
|
||||
labels = {v: k for k, v in rlabels.items()}
|
||||
|
||||
for nodes in [(labels[(0, 4)], labels[(1, 4)]), (labels[(3, 4)], labels[(4, 4)])]:
|
||||
new_node = G.order() + 1
|
||||
# Petersen graph is triconnected
|
||||
P = nx.petersen_graph()
|
||||
G = nx.disjoint_union(G, P)
|
||||
# Add two edges between the grid and P
|
||||
G.add_edge(new_node + 1, nodes[0])
|
||||
G.add_edge(new_node, nodes[1])
|
||||
# K5 is 4-connected
|
||||
K = nx.complete_graph(5)
|
||||
G = nx.disjoint_union(G, K)
|
||||
# Add three edges between P and K5
|
||||
G.add_edge(new_node + 2, new_node + 11)
|
||||
G.add_edge(new_node + 3, new_node + 12)
|
||||
G.add_edge(new_node + 4, new_node + 13)
|
||||
# Add another K5 sharing a node
|
||||
G = nx.disjoint_union(G, K)
|
||||
nbrs = G[new_node + 10]
|
||||
G.remove_node(new_node + 10)
|
||||
for nbr in nbrs:
|
||||
G.add_edge(new_node + 17, nbr)
|
||||
# Commenting this makes the graph not biconnected !!
|
||||
# This stupid mistake make one reviewer very angry :P
|
||||
G.add_edge(new_node + 16, new_node + 8)
|
||||
|
||||
for nodes in [(labels[(0, 0)], labels[(1, 0)]), (labels[(3, 0)], labels[(4, 0)])]:
|
||||
new_node = G.order() + 1
|
||||
# Petersen graph is triconnected
|
||||
P = nx.petersen_graph()
|
||||
G = nx.disjoint_union(G, P)
|
||||
# Add two edges between the grid and P
|
||||
G.add_edge(new_node + 1, nodes[0])
|
||||
G.add_edge(new_node, nodes[1])
|
||||
# K5 is 4-connected
|
||||
K = nx.complete_graph(5)
|
||||
G = nx.disjoint_union(G, K)
|
||||
# Add three edges between P and K5
|
||||
G.add_edge(new_node + 2, new_node + 11)
|
||||
G.add_edge(new_node + 3, new_node + 12)
|
||||
G.add_edge(new_node + 4, new_node + 13)
|
||||
# Add another K5 sharing two nodes
|
||||
G = nx.disjoint_union(G, K)
|
||||
nbrs = G[new_node + 10]
|
||||
G.remove_node(new_node + 10)
|
||||
for nbr in nbrs:
|
||||
G.add_edge(new_node + 17, nbr)
|
||||
nbrs2 = G[new_node + 9]
|
||||
G.remove_node(new_node + 9)
|
||||
for nbr in nbrs2:
|
||||
G.add_edge(new_node + 18, nbr)
|
||||
return G
|
||||
|
||||
|
||||
# Helper function
|
||||
def _check_separating_sets(G):
|
||||
for cc in nx.connected_components(G):
|
||||
if len(cc) < 3:
|
||||
continue
|
||||
Gc = G.subgraph(cc)
|
||||
node_conn = nx.node_connectivity(Gc)
|
||||
all_cuts = nx.all_node_cuts(Gc)
|
||||
# Only test a limited number of cut sets to reduce test time.
|
||||
for cut in itertools.islice(all_cuts, MAX_CUTSETS_TO_TEST):
|
||||
assert node_conn == len(cut)
|
||||
assert not nx.is_connected(nx.restricted_view(G, cut, []))
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_torrents_and_ferraro_graph():
|
||||
G = torrents_and_ferraro_graph()
|
||||
_check_separating_sets(G)
|
||||
|
||||
|
||||
def test_example_1():
|
||||
G = graph_example_1()
|
||||
_check_separating_sets(G)
|
||||
|
||||
|
||||
def test_random_gnp():
|
||||
G = nx.gnp_random_graph(100, 0.1, seed=42)
|
||||
_check_separating_sets(G)
|
||||
|
||||
|
||||
def test_shell():
|
||||
constructor = [(20, 80, 0.8), (80, 180, 0.6)]
|
||||
G = nx.random_shell_graph(constructor, seed=42)
|
||||
_check_separating_sets(G)
|
||||
|
||||
|
||||
def test_configuration():
|
||||
deg_seq = nx.random_powerlaw_tree_sequence(100, tries=5, seed=72)
|
||||
G = nx.Graph(nx.configuration_model(deg_seq))
|
||||
G.remove_edges_from(nx.selfloop_edges(G))
|
||||
_check_separating_sets(G)
|
||||
|
||||
|
||||
def test_karate():
|
||||
G = nx.karate_club_graph()
|
||||
_check_separating_sets(G)
|
||||
|
||||
|
||||
def _generate_no_biconnected(max_attempts=50):
|
||||
attempts = 0
|
||||
while True:
|
||||
G = nx.fast_gnp_random_graph(100, 0.0575, seed=42)
|
||||
if nx.is_connected(G) and not nx.is_biconnected(G):
|
||||
attempts = 0
|
||||
yield G
|
||||
else:
|
||||
if attempts >= max_attempts:
|
||||
msg = f"Tried {attempts} times: no suitable Graph."
|
||||
raise Exception(msg)
|
||||
else:
|
||||
attempts += 1
|
||||
|
||||
|
||||
def test_articulation_points():
|
||||
Ggen = _generate_no_biconnected()
|
||||
for i in range(1): # change 1 to 3 or more for more realizations.
|
||||
G = next(Ggen)
|
||||
articulation_points = list({a} for a in nx.articulation_points(G))
|
||||
for cut in nx.all_node_cuts(G):
|
||||
assert cut in articulation_points
|
||||
|
||||
|
||||
def test_grid_2d_graph():
|
||||
# All minimum node cuts of a 2d grid
|
||||
# are the four pairs of nodes that are
|
||||
# neighbors of the four corner nodes.
|
||||
G = nx.grid_2d_graph(5, 5)
|
||||
solution = [{(0, 1), (1, 0)}, {(3, 0), (4, 1)}, {(3, 4), (4, 3)}, {(0, 3), (1, 4)}]
|
||||
for cut in nx.all_node_cuts(G):
|
||||
assert cut in solution
|
||||
|
||||
|
||||
def test_disconnected_graph():
|
||||
G = nx.fast_gnp_random_graph(100, 0.01, seed=42)
|
||||
cuts = nx.all_node_cuts(G)
|
||||
pytest.raises(nx.NetworkXError, next, cuts)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_alternative_flow_functions():
|
||||
graphs = [nx.grid_2d_graph(4, 4), nx.cycle_graph(5)]
|
||||
for G in graphs:
|
||||
node_conn = nx.node_connectivity(G)
|
||||
for flow_func in flow_funcs:
|
||||
all_cuts = nx.all_node_cuts(G, flow_func=flow_func)
|
||||
# Only test a limited number of cut sets to reduce test time.
|
||||
for cut in itertools.islice(all_cuts, MAX_CUTSETS_TO_TEST):
|
||||
assert node_conn == len(cut)
|
||||
assert not nx.is_connected(nx.restricted_view(G, cut, []))
|
||||
|
||||
|
||||
def test_is_separating_set_complete_graph():
|
||||
G = nx.complete_graph(5)
|
||||
assert _is_separating_set(G, {0, 1, 2, 3})
|
||||
|
||||
|
||||
def test_is_separating_set():
|
||||
for i in [5, 10, 15]:
|
||||
G = nx.star_graph(i)
|
||||
max_degree_node = max(G, key=G.degree)
|
||||
assert _is_separating_set(G, {max_degree_node})
|
||||
|
||||
|
||||
def test_non_repeated_cuts():
|
||||
# The algorithm was repeating the cut {0, 1} for the giant biconnected
|
||||
# component of the Karate club graph.
|
||||
K = nx.karate_club_graph()
|
||||
bcc = max(list(nx.biconnected_components(K)), key=len)
|
||||
G = K.subgraph(bcc)
|
||||
solution = [{32, 33}, {2, 33}, {0, 3}, {0, 1}, {29, 33}]
|
||||
cuts = list(nx.all_node_cuts(G))
|
||||
if len(solution) != len(cuts):
|
||||
print(nx.info(G))
|
||||
print(f"Solution: {solution}")
|
||||
print(f"Result: {cuts}")
|
||||
assert len(solution) == len(cuts)
|
||||
for cut in cuts:
|
||||
assert cut in solution
|
||||
|
||||
|
||||
def test_cycle_graph():
|
||||
G = nx.cycle_graph(5)
|
||||
solution = [{0, 2}, {0, 3}, {1, 3}, {1, 4}, {2, 4}]
|
||||
cuts = list(nx.all_node_cuts(G))
|
||||
assert len(solution) == len(cuts)
|
||||
for cut in cuts:
|
||||
assert cut in solution
|
||||
|
||||
|
||||
def test_complete_graph():
|
||||
G = nx.complete_graph(5)
|
||||
solution = [{0, 1, 2, 3}, {0, 1, 2, 4}, {0, 1, 3, 4}, {0, 2, 3, 4}, {1, 2, 3, 4}]
|
||||
cuts = list(nx.all_node_cuts(G))
|
||||
assert len(solution) == len(cuts)
|
||||
for cut in cuts:
|
||||
assert cut in solution
|
|
@ -0,0 +1,100 @@
|
|||
from itertools import chain
|
||||
import networkx as nx
|
||||
import pytest
|
||||
|
||||
|
||||
def _check_partition(G, cut_value, partition, weight):
|
||||
assert isinstance(partition, tuple)
|
||||
assert len(partition) == 2
|
||||
assert isinstance(partition[0], list)
|
||||
assert isinstance(partition[1], list)
|
||||
assert len(partition[0]) > 0
|
||||
assert len(partition[1]) > 0
|
||||
assert sum(map(len, partition)) == len(G)
|
||||
assert set(chain.from_iterable(partition)) == set(G)
|
||||
partition = tuple(map(set, partition))
|
||||
w = 0
|
||||
for u, v, e in G.edges(data=True):
|
||||
if (u in partition[0]) == (v in partition[1]):
|
||||
w += e.get(weight, 1)
|
||||
assert w == cut_value
|
||||
|
||||
|
||||
def _test_stoer_wagner(G, answer, weight="weight"):
|
||||
cut_value, partition = nx.stoer_wagner(G, weight, heap=nx.utils.PairingHeap)
|
||||
assert cut_value == answer
|
||||
_check_partition(G, cut_value, partition, weight)
|
||||
cut_value, partition = nx.stoer_wagner(G, weight, heap=nx.utils.BinaryHeap)
|
||||
assert cut_value == answer
|
||||
_check_partition(G, cut_value, partition, weight)
|
||||
|
||||
|
||||
def test_graph1():
|
||||
G = nx.Graph()
|
||||
G.add_edge("x", "a", weight=3)
|
||||
G.add_edge("x", "b", weight=1)
|
||||
G.add_edge("a", "c", weight=3)
|
||||
G.add_edge("b", "c", weight=5)
|
||||
G.add_edge("b", "d", weight=4)
|
||||
G.add_edge("d", "e", weight=2)
|
||||
G.add_edge("c", "y", weight=2)
|
||||
G.add_edge("e", "y", weight=3)
|
||||
_test_stoer_wagner(G, 4)
|
||||
|
||||
|
||||
def test_graph2():
|
||||
G = nx.Graph()
|
||||
G.add_edge("x", "a")
|
||||
G.add_edge("x", "b")
|
||||
G.add_edge("a", "c")
|
||||
G.add_edge("b", "c")
|
||||
G.add_edge("b", "d")
|
||||
G.add_edge("d", "e")
|
||||
G.add_edge("c", "y")
|
||||
G.add_edge("e", "y")
|
||||
_test_stoer_wagner(G, 2)
|
||||
|
||||
|
||||
def test_graph3():
|
||||
# Source:
|
||||
# Stoer, M. and Wagner, F. (1997). "A simple min-cut algorithm". Journal of
|
||||
# the ACM 44 (4), 585-591.
|
||||
G = nx.Graph()
|
||||
G.add_edge(1, 2, weight=2)
|
||||
G.add_edge(1, 5, weight=3)
|
||||
G.add_edge(2, 3, weight=3)
|
||||
G.add_edge(2, 5, weight=2)
|
||||
G.add_edge(2, 6, weight=2)
|
||||
G.add_edge(3, 4, weight=4)
|
||||
G.add_edge(3, 7, weight=2)
|
||||
G.add_edge(4, 7, weight=2)
|
||||
G.add_edge(4, 8, weight=2)
|
||||
G.add_edge(5, 6, weight=3)
|
||||
G.add_edge(6, 7, weight=1)
|
||||
G.add_edge(7, 8, weight=3)
|
||||
_test_stoer_wagner(G, 4)
|
||||
|
||||
|
||||
def test_weight_name():
|
||||
G = nx.Graph()
|
||||
G.add_edge(1, 2, weight=1, cost=8)
|
||||
G.add_edge(1, 3, cost=2)
|
||||
G.add_edge(2, 3, cost=4)
|
||||
_test_stoer_wagner(G, 6, weight="cost")
|
||||
|
||||
|
||||
def test_exceptions():
|
||||
G = nx.Graph()
|
||||
pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
|
||||
G.add_node(1)
|
||||
pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
|
||||
G.add_node(2)
|
||||
pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
|
||||
G.add_edge(1, 2, weight=-2)
|
||||
pytest.raises(nx.NetworkXError, nx.stoer_wagner, G)
|
||||
G = nx.DiGraph()
|
||||
pytest.raises(nx.NetworkXNotImplemented, nx.stoer_wagner, G)
|
||||
G = nx.MultiGraph()
|
||||
pytest.raises(nx.NetworkXNotImplemented, nx.stoer_wagner, G)
|
||||
G = nx.MultiDiGraph()
|
||||
pytest.raises(nx.NetworkXNotImplemented, nx.stoer_wagner, G)
|
|
@ -0,0 +1,85 @@
|
|||
"""
|
||||
Utilities for connectivity package
|
||||
"""
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["build_auxiliary_node_connectivity", "build_auxiliary_edge_connectivity"]
|
||||
|
||||
|
||||
def build_auxiliary_node_connectivity(G):
|
||||
r"""Creates a directed graph D from an undirected graph G to compute flow
|
||||
based node connectivity.
|
||||
|
||||
For an undirected graph G having `n` nodes and `m` edges we derive a
|
||||
directed graph D with `2n` nodes and `2m+n` arcs by replacing each
|
||||
original node `v` with two nodes `vA`, `vB` linked by an (internal)
|
||||
arc in D. Then for each edge (`u`, `v`) in G we add two arcs (`uB`, `vA`)
|
||||
and (`vB`, `uA`) in D. Finally we set the attribute capacity = 1 for each
|
||||
arc in D [1]_.
|
||||
|
||||
For a directed graph having `n` nodes and `m` arcs we derive a
|
||||
directed graph D with `2n` nodes and `m+n` arcs by replacing each
|
||||
original node `v` with two nodes `vA`, `vB` linked by an (internal)
|
||||
arc (`vA`, `vB`) in D. Then for each arc (`u`, `v`) in G we add one
|
||||
arc (`uB`, `vA`) in D. Finally we set the attribute capacity = 1 for
|
||||
each arc in D.
|
||||
|
||||
A dictionary with a mapping between nodes in the original graph and the
|
||||
auxiliary digraph is stored as a graph attribute: H.graph['mapping'].
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Kammer, Frank and Hanjo Taubig. Graph Connectivity. in Brandes and
|
||||
Erlebach, 'Network Analysis: Methodological Foundations', Lecture
|
||||
Notes in Computer Science, Volume 3418, Springer-Verlag, 2005.
|
||||
http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf
|
||||
|
||||
"""
|
||||
directed = G.is_directed()
|
||||
|
||||
mapping = {}
|
||||
H = nx.DiGraph()
|
||||
|
||||
for i, node in enumerate(G):
|
||||
mapping[node] = i
|
||||
H.add_node(f"{i}A", id=node)
|
||||
H.add_node(f"{i}B", id=node)
|
||||
H.add_edge(f"{i}A", f"{i}B", capacity=1)
|
||||
|
||||
edges = []
|
||||
for (source, target) in G.edges():
|
||||
edges.append((f"{mapping[source]}B", f"{mapping[target]}A"))
|
||||
if not directed:
|
||||
edges.append((f"{mapping[target]}B", f"{mapping[source]}A"))
|
||||
H.add_edges_from(edges, capacity=1)
|
||||
|
||||
# Store mapping as graph attribute
|
||||
H.graph["mapping"] = mapping
|
||||
return H
|
||||
|
||||
|
||||
def build_auxiliary_edge_connectivity(G):
|
||||
"""Auxiliary digraph for computing flow based edge connectivity
|
||||
|
||||
If the input graph is undirected, we replace each edge (`u`,`v`) with
|
||||
two reciprocal arcs (`u`, `v`) and (`v`, `u`) and then we set the attribute
|
||||
'capacity' for each arc to 1. If the input graph is directed we simply
|
||||
add the 'capacity' attribute. Part of algorithm 1 in [1]_ .
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms. (this is a
|
||||
chapter, look for the reference of the book).
|
||||
http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
|
||||
"""
|
||||
if G.is_directed():
|
||||
H = nx.DiGraph()
|
||||
H.add_nodes_from(G.nodes())
|
||||
H.add_edges_from(G.edges(), capacity=1)
|
||||
return H
|
||||
else:
|
||||
H = nx.DiGraph()
|
||||
H.add_nodes_from(G.nodes())
|
||||
for (source, target) in G.edges():
|
||||
H.add_edges_from([(source, target), (target, source)], capacity=1)
|
||||
return H
|
Loading…
Add table
Add a link
Reference in a new issue