Fixed database typo and removed unnecessary class identifier.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-14 10:10:37 -04:00
parent 00ad49a143
commit 45fb349a7d
5098 changed files with 952558 additions and 85 deletions

View file

@ -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__,
],
[],
)

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()
)

View file

@ -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)

View file

@ -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))

View file

@ -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"

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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