Restore 0.1.5 version from stash
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
"""Approximations of graph properties and Heuristic methods for optimization.
|
||||
|
||||
The functions in this class are not imported into the top-level ``networkx``
|
||||
namespace so the easiest way to use them is with::
|
||||
|
||||
>>> from networkx.algorithms import approximation
|
||||
|
||||
Another option is to import the specific function with
|
||||
``from networkx.algorithms.approximation import function_name``.
|
||||
|
||||
"""
|
||||
|
||||
from networkx.algorithms.approximation.clustering_coefficient import *
|
||||
from networkx.algorithms.approximation.clique import *
|
||||
from networkx.algorithms.approximation.connectivity import *
|
||||
from networkx.algorithms.approximation.distance_measures import *
|
||||
from networkx.algorithms.approximation.dominating_set import *
|
||||
from networkx.algorithms.approximation.kcomponents import *
|
||||
from networkx.algorithms.approximation.matching import *
|
||||
from networkx.algorithms.approximation.ramsey import *
|
||||
from networkx.algorithms.approximation.steinertree import *
|
||||
from networkx.algorithms.approximation.traveling_salesman import *
|
||||
from networkx.algorithms.approximation.treewidth import *
|
||||
from networkx.algorithms.approximation.vertex_cover import *
|
||||
from networkx.algorithms.approximation.maxcut import *
|
||||
from networkx.algorithms.approximation.density import *
|
||||
@@ -0,0 +1,259 @@
|
||||
"""Functions for computing large cliques and maximum independent sets."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import ramsey
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = [
|
||||
"clique_removal",
|
||||
"max_clique",
|
||||
"large_clique_size",
|
||||
"maximum_independent_set",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def maximum_independent_set(G):
|
||||
"""Returns an approximate maximum independent set.
|
||||
|
||||
Independent set or stable set is a set of vertices in a graph, no two of
|
||||
which are adjacent. That is, it is a set I of vertices such that for every
|
||||
two vertices in I, there is no edge connecting the two. Equivalently, each
|
||||
edge in the graph has at most one endpoint in I. The size of an independent
|
||||
set is the number of vertices it contains [1]_.
|
||||
|
||||
A maximum independent set is a largest independent set for a given graph G
|
||||
and its size is denoted $\\alpha(G)$. The problem of finding such a set is called
|
||||
the maximum independent set problem and is an NP-hard optimization problem.
|
||||
As such, it is unlikely that there exists an efficient algorithm for finding
|
||||
a maximum independent set of a graph.
|
||||
|
||||
The Independent Set algorithm is based on [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
iset : Set
|
||||
The apx-maximum independent set
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(10)
|
||||
>>> nx.approximation.maximum_independent_set(G)
|
||||
{0, 2, 4, 6, 9}
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Finds the $O(|V|/(log|V|)^2)$ apx of independent set in the worst case.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] `Wikipedia: Independent set
|
||||
<https://en.wikipedia.org/wiki/Independent_set_(graph_theory)>`_
|
||||
.. [2] Boppana, R., & Halldórsson, M. M. (1992).
|
||||
Approximating maximum independent sets by excluding subgraphs.
|
||||
BIT Numerical Mathematics, 32(2), 180–196. Springer.
|
||||
"""
|
||||
iset, _ = clique_removal(G)
|
||||
return iset
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def max_clique(G):
|
||||
r"""Find the Maximum Clique
|
||||
|
||||
Finds the $O(|V|/(log|V|)^2)$ apx of maximum clique/independent set
|
||||
in the worst case.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
clique : set
|
||||
The apx-maximum clique of the graph
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(10)
|
||||
>>> nx.approximation.max_clique(G)
|
||||
{8, 9}
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
A clique in an undirected graph G = (V, E) is a subset of the vertex set
|
||||
`C \subseteq V` such that for every two vertices in C there exists an edge
|
||||
connecting the two. This is equivalent to saying that the subgraph
|
||||
induced by C is complete (in some cases, the term clique may also refer
|
||||
to the subgraph).
|
||||
|
||||
A maximum clique is a clique of the largest possible size in a given graph.
|
||||
The clique number `\omega(G)` of a graph G is the number of
|
||||
vertices in a maximum clique in G. The intersection number of
|
||||
G is the smallest number of cliques that together cover all edges of G.
|
||||
|
||||
https://en.wikipedia.org/wiki/Maximum_clique
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Boppana, R., & Halldórsson, M. M. (1992).
|
||||
Approximating maximum independent sets by excluding subgraphs.
|
||||
BIT Numerical Mathematics, 32(2), 180–196. Springer.
|
||||
doi:10.1007/BF01994876
|
||||
"""
|
||||
# finding the maximum clique in a graph is equivalent to finding
|
||||
# the independent set in the complementary graph
|
||||
cgraph = nx.complement(G)
|
||||
iset, _ = clique_removal(cgraph)
|
||||
return iset
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def clique_removal(G):
|
||||
r"""Repeatedly remove cliques from the graph.
|
||||
|
||||
Results in a $O(|V|/(\log |V|)^2)$ approximation of maximum clique
|
||||
and independent set. Returns the largest independent set found, along
|
||||
with found maximal cliques.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
max_ind_cliques : (set, list) tuple
|
||||
2-tuple of Maximal Independent Set and list of maximal cliques (sets).
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(10)
|
||||
>>> nx.approximation.clique_removal(G)
|
||||
({0, 2, 4, 6, 9}, [{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}])
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Boppana, R., & Halldórsson, M. M. (1992).
|
||||
Approximating maximum independent sets by excluding subgraphs.
|
||||
BIT Numerical Mathematics, 32(2), 180–196. Springer.
|
||||
"""
|
||||
graph = G.copy()
|
||||
c_i, i_i = ramsey.ramsey_R2(graph)
|
||||
cliques = [c_i]
|
||||
isets = [i_i]
|
||||
while graph:
|
||||
graph.remove_nodes_from(c_i)
|
||||
c_i, i_i = ramsey.ramsey_R2(graph)
|
||||
if c_i:
|
||||
cliques.append(c_i)
|
||||
if i_i:
|
||||
isets.append(i_i)
|
||||
# Determine the largest independent set as measured by cardinality.
|
||||
maxiset = max(isets, key=len)
|
||||
return maxiset, cliques
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def large_clique_size(G):
|
||||
"""Find the size of a large clique in a graph.
|
||||
|
||||
A *clique* is a subset of nodes in which each pair of nodes is
|
||||
adjacent. This function is a heuristic for finding the size of a
|
||||
large clique in the graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
k: integer
|
||||
The size of a large clique in the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(10)
|
||||
>>> nx.approximation.large_clique_size(G)
|
||||
2
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This implementation is from [1]_. Its worst case time complexity is
|
||||
:math:`O(n d^2)`, where *n* is the number of nodes in the graph and
|
||||
*d* is the maximum degree.
|
||||
|
||||
This function is a heuristic, which means it may work well in
|
||||
practice, but there is no rigorous mathematical guarantee on the
|
||||
ratio between the returned number and the actual largest clique size
|
||||
in the graph.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Pattabiraman, Bharath, et al.
|
||||
"Fast Algorithms for the Maximum Clique Problem on Massive Graphs
|
||||
with Applications to Overlapping Community Detection."
|
||||
*Internet Mathematics* 11.4-5 (2015): 421--448.
|
||||
<https://doi.org/10.1080/15427951.2014.986778>
|
||||
|
||||
See also
|
||||
--------
|
||||
|
||||
:func:`networkx.algorithms.approximation.clique.max_clique`
|
||||
A function that returns an approximate maximum clique with a
|
||||
guarantee on the approximation ratio.
|
||||
|
||||
:mod:`networkx.algorithms.clique`
|
||||
Functions for finding the exact maximum clique in a graph.
|
||||
|
||||
"""
|
||||
degrees = G.degree
|
||||
|
||||
def _clique_heuristic(G, U, size, best_size):
|
||||
if not U:
|
||||
return max(best_size, size)
|
||||
u = max(U, key=degrees)
|
||||
U.remove(u)
|
||||
N_prime = {v for v in G[u] if degrees[v] >= best_size}
|
||||
return _clique_heuristic(G, U & N_prime, size + 1, best_size)
|
||||
|
||||
best_size = 0
|
||||
nodes = (u for u in G if degrees[u] >= best_size)
|
||||
for u in nodes:
|
||||
neighbors = {v for v in G[u] if degrees[v] >= best_size}
|
||||
best_size = _clique_heuristic(G, neighbors, 1, best_size)
|
||||
return best_size
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for, py_random_state
|
||||
|
||||
__all__ = ["average_clustering"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@py_random_state(2)
|
||||
@nx._dispatchable(name="approximate_average_clustering")
|
||||
def average_clustering(G, trials=1000, seed=None):
|
||||
r"""Estimates the average clustering coefficient of G.
|
||||
|
||||
The local clustering of each node in `G` is the fraction of triangles
|
||||
that actually exist over all possible triangles in its neighborhood.
|
||||
The average clustering coefficient of a graph `G` is the mean of
|
||||
local clusterings.
|
||||
|
||||
This function finds an approximate average clustering coefficient
|
||||
for G by repeating `n` times (defined in `trials`) the following
|
||||
experiment: choose a node at random, choose two of its neighbors
|
||||
at random, and check if they are connected. The approximate
|
||||
coefficient is the fraction of triangles found over the number
|
||||
of trials [1]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
trials : integer
|
||||
Number of trials to perform (default 1000).
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
c : float
|
||||
Approximated average clustering coefficient.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import approximation
|
||||
>>> G = nx.erdos_renyi_graph(10, 0.2, seed=10)
|
||||
>>> approximation.average_clustering(G, trials=1000, seed=10)
|
||||
0.214
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If G is directed.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Schank, Thomas, and Dorothea Wagner. Approximating clustering
|
||||
coefficient and transitivity. Universität Karlsruhe, Fakultät für
|
||||
Informatik, 2004.
|
||||
https://doi.org/10.5445/IR/1000001239
|
||||
|
||||
"""
|
||||
n = len(G)
|
||||
triangles = 0
|
||||
nodes = list(G)
|
||||
for i in [int(seed.random() * n) for i in range(trials)]:
|
||||
nbrs = list(G[nodes[i]])
|
||||
if len(nbrs) < 2:
|
||||
continue
|
||||
u, v = seed.sample(nbrs, 2)
|
||||
if u in G[v]:
|
||||
triangles += 1
|
||||
return triangles / trials
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
"""Fast approximation for node connectivity"""
|
||||
|
||||
import itertools
|
||||
from operator import itemgetter
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = [
|
||||
"local_node_connectivity",
|
||||
"node_connectivity",
|
||||
"all_pairs_node_connectivity",
|
||||
]
|
||||
|
||||
|
||||
@nx._dispatchable(name="approximate_local_node_connectivity")
|
||||
def local_node_connectivity(G, source, target, cutoff=None):
|
||||
"""Compute node connectivity between source and target.
|
||||
|
||||
Pairwise or local node connectivity between two distinct and nonadjacent
|
||||
nodes is the minimum number of nodes that must be removed (minimum
|
||||
separating cutset) to disconnect them. By Menger's theorem, this is equal
|
||||
to the number of node independent paths (paths that share no nodes other
|
||||
than source and target). Which is what we compute in this function.
|
||||
|
||||
This algorithm is a fast approximation that gives an strict lower
|
||||
bound on the actual number of node independent paths between two nodes [1]_.
|
||||
It works for both directed and undirected graphs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
G : NetworkX graph
|
||||
|
||||
source : node
|
||||
Starting node for node connectivity
|
||||
|
||||
target : node
|
||||
Ending node for node connectivity
|
||||
|
||||
cutoff : integer
|
||||
Maximum node connectivity to consider. If None, the minimum degree
|
||||
of source or target is used as a cutoff. Default value None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
k: integer
|
||||
pairwise node connectivity
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # Platonic octahedral graph has node connectivity 4
|
||||
>>> # for each non adjacent node pair
|
||||
>>> from networkx.algorithms import approximation as approx
|
||||
>>> G = nx.octahedral_graph()
|
||||
>>> approx.local_node_connectivity(G, 0, 5)
|
||||
4
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm [1]_ finds node independents paths between two nodes by
|
||||
computing their shortest path using BFS, marking the nodes of the path
|
||||
found as 'used' and then searching other shortest paths excluding the
|
||||
nodes marked as used until no more paths exist. It is not exact because
|
||||
a shortest path could use nodes that, if the path were longer, may belong
|
||||
to two different node independent paths. Thus it only guarantees an
|
||||
strict lower bound on node connectivity.
|
||||
|
||||
Note that the authors propose a further refinement, losing accuracy and
|
||||
gaining speed, which is not implemented yet.
|
||||
|
||||
See also
|
||||
--------
|
||||
all_pairs_node_connectivity
|
||||
node_connectivity
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
|
||||
Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
|
||||
http://eclectic.ss.uci.edu/~drwhite/working.pdf
|
||||
|
||||
"""
|
||||
if target == source:
|
||||
raise nx.NetworkXError("source and target have to be different nodes.")
|
||||
|
||||
# Maximum possible node independent paths
|
||||
if G.is_directed():
|
||||
possible = min(G.out_degree(source), G.in_degree(target))
|
||||
else:
|
||||
possible = min(G.degree(source), G.degree(target))
|
||||
|
||||
K = 0
|
||||
if not possible:
|
||||
return K
|
||||
|
||||
if cutoff is None:
|
||||
cutoff = float("inf")
|
||||
|
||||
exclude = set()
|
||||
for i in range(min(possible, cutoff)):
|
||||
try:
|
||||
path = _bidirectional_shortest_path(G, source, target, exclude)
|
||||
exclude.update(set(path))
|
||||
K += 1
|
||||
except nx.NetworkXNoPath:
|
||||
break
|
||||
|
||||
return K
|
||||
|
||||
|
||||
@nx._dispatchable(name="approximate_node_connectivity")
|
||||
def node_connectivity(G, s=None, t=None):
|
||||
r"""Returns an approximation for 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. By Menger's theorem,
|
||||
this is equal to the number of node independent paths (paths that
|
||||
share no nodes other than source and target).
|
||||
|
||||
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.
|
||||
|
||||
This algorithm is based on a fast approximation that gives an strict lower
|
||||
bound on the actual number of node independent paths between two nodes [1]_.
|
||||
It works for both directed and undirected graphs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
s : node
|
||||
Source node. Optional. Default value: None.
|
||||
|
||||
t : node
|
||||
Target node. Optional. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
K : integer
|
||||
Node connectivity of G, or local node connectivity if source
|
||||
and target are provided.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # Platonic octahedral graph is 4-node-connected
|
||||
>>> from networkx.algorithms import approximation as approx
|
||||
>>> G = nx.octahedral_graph()
|
||||
>>> approx.node_connectivity(G)
|
||||
4
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm [1]_ finds node independents paths between two nodes by
|
||||
computing their shortest path using BFS, marking the nodes of the path
|
||||
found as 'used' and then searching other shortest paths excluding the
|
||||
nodes marked as used until no more paths exist. It is not exact because
|
||||
a shortest path could use nodes that, if the path were longer, may belong
|
||||
to two different node independent paths. Thus it only guarantees an
|
||||
strict lower bound on node connectivity.
|
||||
|
||||
See also
|
||||
--------
|
||||
all_pairs_node_connectivity
|
||||
local_node_connectivity
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
|
||||
Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
|
||||
http://eclectic.ss.uci.edu/~drwhite/working.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)
|
||||
|
||||
# Global node connectivity
|
||||
if G.is_directed():
|
||||
connected_func = nx.is_weakly_connected
|
||||
iter_func = itertools.permutations
|
||||
|
||||
def neighbors(v):
|
||||
return itertools.chain(G.predecessors(v), G.successors(v))
|
||||
|
||||
else:
|
||||
connected_func = nx.is_connected
|
||||
iter_func = itertools.combinations
|
||||
neighbors = G.neighbors
|
||||
|
||||
if not connected_func(G):
|
||||
return 0
|
||||
|
||||
# Choose a node with minimum degree
|
||||
v, minimum_degree = min(G.degree(), key=itemgetter(1))
|
||||
# Node connectivity is bounded by minimum degree
|
||||
K = minimum_degree
|
||||
# compute local node connectivity with all non-neighbors nodes
|
||||
# and store the minimum
|
||||
for w in set(G) - set(neighbors(v)) - {v}:
|
||||
K = min(K, local_node_connectivity(G, v, w, cutoff=K))
|
||||
# Same for non adjacent pairs of neighbors of v
|
||||
for x, y in iter_func(neighbors(v), 2):
|
||||
if y not in G[x] and x != y:
|
||||
K = min(K, local_node_connectivity(G, x, y, cutoff=K))
|
||||
return K
|
||||
|
||||
|
||||
@nx._dispatchable(name="approximate_all_pairs_node_connectivity")
|
||||
def all_pairs_node_connectivity(G, nbunch=None, cutoff=None):
|
||||
"""Compute node connectivity between all pairs of nodes.
|
||||
|
||||
Pairwise or local node connectivity between two distinct and nonadjacent
|
||||
nodes is the minimum number of nodes that must be removed (minimum
|
||||
separating cutset) to disconnect them. By Menger's theorem, this is equal
|
||||
to the number of node independent paths (paths that share no nodes other
|
||||
than source and target). Which is what we compute in this function.
|
||||
|
||||
This algorithm is a fast approximation that gives an strict lower
|
||||
bound on the actual number of node independent paths between two nodes [1]_.
|
||||
It works for both directed and undirected graphs.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
nbunch: container
|
||||
Container of nodes. If provided node connectivity will be computed
|
||||
only over pairs of nodes in nbunch.
|
||||
|
||||
cutoff : integer
|
||||
Maximum node connectivity to consider. If None, the minimum degree
|
||||
of source or target is used as a cutoff in each pair of nodes.
|
||||
Default value None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
K : dictionary
|
||||
Dictionary, keyed by source and target, of pairwise node connectivity
|
||||
|
||||
Examples
|
||||
--------
|
||||
A 3 node cycle with one extra node attached has connectivity 2 between all
|
||||
nodes in the cycle and connectivity 1 between the extra node and the rest:
|
||||
|
||||
>>> G = nx.cycle_graph(3)
|
||||
>>> G.add_edge(2, 3)
|
||||
>>> import pprint # for nice dictionary formatting
|
||||
>>> pprint.pprint(nx.all_pairs_node_connectivity(G))
|
||||
{0: {1: 2, 2: 2, 3: 1},
|
||||
1: {0: 2, 2: 2, 3: 1},
|
||||
2: {0: 2, 1: 2, 3: 1},
|
||||
3: {0: 1, 1: 1, 2: 1}}
|
||||
|
||||
See Also
|
||||
--------
|
||||
local_node_connectivity
|
||||
node_connectivity
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
|
||||
Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
|
||||
http://eclectic.ss.uci.edu/~drwhite/working.pdf
|
||||
"""
|
||||
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}
|
||||
|
||||
for u, v in iter_func(nbunch, 2):
|
||||
k = local_node_connectivity(G, u, v, cutoff=cutoff)
|
||||
all_pairs[u][v] = k
|
||||
if not directed:
|
||||
all_pairs[v][u] = k
|
||||
|
||||
return all_pairs
|
||||
|
||||
|
||||
def _bidirectional_shortest_path(G, source, target, exclude):
|
||||
"""Returns shortest path between source and target ignoring nodes in the
|
||||
container 'exclude'.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
G : NetworkX graph
|
||||
|
||||
source : node
|
||||
Starting node for path
|
||||
|
||||
target : node
|
||||
Ending node for path
|
||||
|
||||
exclude: container
|
||||
Container for nodes to exclude from the search for shortest paths
|
||||
|
||||
Returns
|
||||
-------
|
||||
path: list
|
||||
Shortest path between source and target ignoring nodes in 'exclude'
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNoPath
|
||||
If there is no path or if nodes are adjacent and have only one path
|
||||
between them
|
||||
|
||||
Notes
|
||||
-----
|
||||
This function and its helper are originally from
|
||||
networkx.algorithms.shortest_paths.unweighted and are modified to
|
||||
accept the extra parameter 'exclude', which is a container for nodes
|
||||
already used in other paths that should be ignored.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
|
||||
Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
|
||||
http://eclectic.ss.uci.edu/~drwhite/working.pdf
|
||||
|
||||
"""
|
||||
# call helper to do the real work
|
||||
results = _bidirectional_pred_succ(G, source, target, exclude)
|
||||
pred, succ, w = results
|
||||
|
||||
# build path from pred+w+succ
|
||||
path = []
|
||||
# from source to w
|
||||
while w is not None:
|
||||
path.append(w)
|
||||
w = pred[w]
|
||||
path.reverse()
|
||||
# from w to target
|
||||
w = succ[path[-1]]
|
||||
while w is not None:
|
||||
path.append(w)
|
||||
w = succ[w]
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def _bidirectional_pred_succ(G, source, target, exclude):
|
||||
# does BFS from both source and target and meets in the middle
|
||||
# excludes nodes in the container "exclude" from the search
|
||||
|
||||
# handle either directed or undirected
|
||||
if G.is_directed():
|
||||
Gpred = G.predecessors
|
||||
Gsucc = G.successors
|
||||
else:
|
||||
Gpred = G.neighbors
|
||||
Gsucc = G.neighbors
|
||||
|
||||
# predecessor and successors in search
|
||||
pred = {source: None}
|
||||
succ = {target: None}
|
||||
|
||||
# initialize fringes, start with forward
|
||||
forward_fringe = [source]
|
||||
reverse_fringe = [target]
|
||||
|
||||
level = 0
|
||||
|
||||
while forward_fringe and reverse_fringe:
|
||||
# Make sure that we iterate one step forward and one step backwards
|
||||
# thus source and target will only trigger "found path" when they are
|
||||
# adjacent and then they can be safely included in the container 'exclude'
|
||||
level += 1
|
||||
if level % 2 != 0:
|
||||
this_level = forward_fringe
|
||||
forward_fringe = []
|
||||
for v in this_level:
|
||||
for w in Gsucc(v):
|
||||
if w in exclude:
|
||||
continue
|
||||
if w not in pred:
|
||||
forward_fringe.append(w)
|
||||
pred[w] = v
|
||||
if w in succ:
|
||||
return pred, succ, w # found path
|
||||
else:
|
||||
this_level = reverse_fringe
|
||||
reverse_fringe = []
|
||||
for v in this_level:
|
||||
for w in Gpred(v):
|
||||
if w in exclude:
|
||||
continue
|
||||
if w not in succ:
|
||||
succ[w] = v
|
||||
reverse_fringe.append(w)
|
||||
if w in pred:
|
||||
return pred, succ, w # found path
|
||||
|
||||
raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
"""Fast algorithms for the densest subgraph problem"""
|
||||
|
||||
import math
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["densest_subgraph"]
|
||||
|
||||
|
||||
def _greedy_plus_plus(G, iterations):
|
||||
if G.number_of_edges() == 0:
|
||||
return 0.0, set()
|
||||
if iterations < 1:
|
||||
raise ValueError(
|
||||
f"The number of iterations must be an integer >= 1. Provided: {iterations}"
|
||||
)
|
||||
|
||||
loads = {node: 0 for node in G.nodes} # Load vector for Greedy++.
|
||||
best_density = 0.0 # Highest density encountered.
|
||||
best_subgraph = set() # Nodes of the best subgraph found.
|
||||
|
||||
for _ in range(iterations):
|
||||
# Initialize heap for fast access to minimum weighted degree.
|
||||
heap = nx.utils.BinaryHeap()
|
||||
|
||||
# Compute initial weighted degrees and add nodes to the heap.
|
||||
for node, degree in G.degree:
|
||||
heap.insert(node, loads[node] + degree)
|
||||
# Set up tracking for current graph state.
|
||||
remaining_nodes = set(G.nodes)
|
||||
num_edges = G.number_of_edges()
|
||||
current_degrees = dict(G.degree)
|
||||
|
||||
while remaining_nodes:
|
||||
num_nodes = len(remaining_nodes)
|
||||
|
||||
# Current density of the (implicit) graph
|
||||
current_density = num_edges / num_nodes
|
||||
|
||||
# Update the best density.
|
||||
if current_density > best_density:
|
||||
best_density = current_density
|
||||
best_subgraph = set(remaining_nodes)
|
||||
|
||||
# Pop the node with the smallest weighted degree.
|
||||
node, _ = heap.pop()
|
||||
if node not in remaining_nodes:
|
||||
continue # Skip nodes already removed.
|
||||
|
||||
# Update the load of the popped node.
|
||||
loads[node] += current_degrees[node]
|
||||
|
||||
# Update neighbors' degrees and the heap.
|
||||
for neighbor in G.neighbors(node):
|
||||
if neighbor in remaining_nodes:
|
||||
current_degrees[neighbor] -= 1
|
||||
num_edges -= 1
|
||||
heap.insert(neighbor, loads[neighbor] + current_degrees[neighbor])
|
||||
|
||||
# Remove the node from the remaining nodes.
|
||||
remaining_nodes.remove(node)
|
||||
|
||||
return best_density, best_subgraph
|
||||
|
||||
|
||||
def _fractional_peeling(G, b, x, node_to_idx, edge_to_idx):
|
||||
"""
|
||||
Optimized fractional peeling using NumPy arrays.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : networkx.Graph
|
||||
The input graph.
|
||||
b : numpy.ndarray
|
||||
Induced load vector.
|
||||
x : numpy.ndarray
|
||||
Fractional edge values.
|
||||
node_to_idx : dict
|
||||
Mapping from node to index.
|
||||
edge_to_idx : dict
|
||||
Mapping from edge to index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
best_density : float
|
||||
The best density found.
|
||||
best_subgraph : set
|
||||
The subset of nodes defining the densest subgraph.
|
||||
"""
|
||||
heap = nx.utils.BinaryHeap()
|
||||
|
||||
remaining_nodes = set(G.nodes)
|
||||
|
||||
# Initialize heap with b values
|
||||
for idx, node in enumerate(G):
|
||||
heap.insert(node, b[idx])
|
||||
|
||||
num_edges = G.number_of_edges()
|
||||
|
||||
best_density = 0.0
|
||||
best_subgraph = set()
|
||||
|
||||
while remaining_nodes:
|
||||
num_nodes = len(remaining_nodes)
|
||||
current_density = num_edges / num_nodes
|
||||
|
||||
if current_density > best_density:
|
||||
best_density = current_density
|
||||
best_subgraph = set(remaining_nodes)
|
||||
|
||||
# Pop the node with the smallest b
|
||||
node, _ = heap.pop()
|
||||
while node not in remaining_nodes:
|
||||
node, _ = heap.pop() # Clean the heap from stale values
|
||||
|
||||
# Update neighbors b values by subtracting fractional x value
|
||||
for neighbor in G.neighbors(node):
|
||||
if neighbor in remaining_nodes:
|
||||
neighbor_idx = node_to_idx[neighbor]
|
||||
# Take off fractional value
|
||||
b[neighbor_idx] -= x[edge_to_idx[(neighbor, node)]]
|
||||
num_edges -= 1
|
||||
heap.insert(neighbor, b[neighbor_idx])
|
||||
|
||||
remaining_nodes.remove(node) # peel off node
|
||||
|
||||
return best_density, best_subgraph
|
||||
|
||||
|
||||
def _fista(G, iterations):
|
||||
if G.number_of_edges() == 0:
|
||||
return 0.0, set()
|
||||
if iterations < 1:
|
||||
raise ValueError(
|
||||
f"The number of iterations must be an integer >= 1. Provided: {iterations}"
|
||||
)
|
||||
import numpy as np
|
||||
|
||||
# 1. Node Mapping: Assign a unique index to each node and edge
|
||||
node_to_idx = {node: idx for idx, node in enumerate(G)}
|
||||
num_nodes = G.number_of_nodes()
|
||||
num_undirected_edges = G.number_of_edges()
|
||||
|
||||
# 2. Edge Mapping: Assign a unique index to each bidirectional edge
|
||||
bidirectional_edges = [(u, v) for u, v in G.edges] + [(v, u) for u, v in G.edges]
|
||||
edge_to_idx = {edge: idx for idx, edge in enumerate(bidirectional_edges)}
|
||||
|
||||
num_edges = len(bidirectional_edges)
|
||||
|
||||
# 3. Reverse Edge Mapping: Map each (bidirectional) edge to its reverse edge index
|
||||
reverse_edge_idx = np.empty(num_edges, dtype=np.int32)
|
||||
for idx in range(num_undirected_edges):
|
||||
reverse_edge_idx[idx] = num_undirected_edges + idx
|
||||
for idx in range(num_undirected_edges, 2 * num_undirected_edges):
|
||||
reverse_edge_idx[idx] = idx - num_undirected_edges
|
||||
|
||||
# 4. Initialize Variables as NumPy Arrays
|
||||
x = np.full(num_edges, 0.5, dtype=np.float32)
|
||||
y = x.copy()
|
||||
z = np.zeros(num_edges, dtype=np.float32)
|
||||
b = np.zeros(num_nodes, dtype=np.float32) # Induced load vector
|
||||
tk = 1.0 # Momentum term
|
||||
|
||||
# 5. Precompute Edge Source Indices
|
||||
edge_src_indices = np.array(
|
||||
[node_to_idx[u] for u, _ in bidirectional_edges], dtype=np.int32
|
||||
)
|
||||
|
||||
# 6. Compute Learning Rate
|
||||
max_degree = max(deg for _, deg in G.degree)
|
||||
# 0.9 for floating point errs when max_degree is very large
|
||||
learning_rate = 0.9 / max_degree
|
||||
|
||||
# 7. Iterative Updates
|
||||
for _ in range(iterations):
|
||||
# 7a. Update b: sum y over outgoing edges for each node
|
||||
b[:] = 0.0 # Reset b to zero
|
||||
np.add.at(b, edge_src_indices, y) # b_u = \sum_{v : (u,v) \in E(G)} y_{uv}
|
||||
|
||||
# 7b. Compute z, z_{uv} = y_{uv} - 2 * learning_rate * b_u
|
||||
z = y - 2.0 * learning_rate * b[edge_src_indices]
|
||||
|
||||
# 7c. Update Momentum Term
|
||||
tknew = (1.0 + math.sqrt(1 + 4.0 * tk**2)) / 2.0
|
||||
|
||||
# 7d. Update x in a vectorized manner, x_{uv} = (z_{uv} - z_{vu} + 1.0) / 2.0
|
||||
new_xuv = (z - z[reverse_edge_idx] + 1.0) / 2.0
|
||||
clamped_x = np.clip(new_xuv, 0.0, 1.0) # Clamp x_{uv} between 0 and 1
|
||||
|
||||
# Update y using the FISTA update formula (similar to gradient descent)
|
||||
y = (
|
||||
clamped_x
|
||||
+ ((tk - 1.0) / tknew) * (clamped_x - x)
|
||||
+ (tk / tknew) * (clamped_x - y)
|
||||
)
|
||||
|
||||
# Update x
|
||||
x = clamped_x
|
||||
|
||||
# Update tk, the momemntum term
|
||||
tk = tknew
|
||||
|
||||
# Rebalance the b values! Otherwise performance is a bit suboptimal.
|
||||
b[:] = 0.0
|
||||
np.add.at(b, edge_src_indices, x) # b_u = \sum_{v : (u,v) \in E(G)} x_{uv}
|
||||
|
||||
# Extract the actual (approximate) dense subgraph.
|
||||
return _fractional_peeling(G, b, x, node_to_idx, edge_to_idx)
|
||||
|
||||
|
||||
ALGORITHMS = {"greedy++": _greedy_plus_plus, "fista": _fista}
|
||||
|
||||
|
||||
@nx.utils.not_implemented_for("directed")
|
||||
@nx.utils.not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def densest_subgraph(G, iterations=1, *, method="fista"):
|
||||
r"""Returns an approximate densest subgraph for a graph `G`.
|
||||
|
||||
This function runs an iterative algorithm to find the densest subgraph,
|
||||
and returns both the density and the subgraph. For a discussion on the
|
||||
notion of density used and the different algorithms available on
|
||||
networkx, please see the Notes section below.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph.
|
||||
|
||||
iterations : int, optional (default=1)
|
||||
Number of iterations to use for the iterative algorithm. Can be
|
||||
specified positionally or as a keyword argument.
|
||||
|
||||
method : string, optional (default='fista')
|
||||
The algorithm to use to approximate the densest subgraph. Supported
|
||||
options: 'greedy++' by Boob et al. [2]_ and 'fista' by Harb et al. [3]_.
|
||||
Must be specified as a keyword argument. Other inputs produce a
|
||||
ValueError.
|
||||
|
||||
Returns
|
||||
-------
|
||||
d : float
|
||||
The density of the approximate subgraph found.
|
||||
|
||||
S : set
|
||||
The subset of nodes defining the approximate densest subgraph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.star_graph(4)
|
||||
>>> nx.approximation.densest_subgraph(G, iterations=1)
|
||||
(0.8, {0, 1, 2, 3, 4})
|
||||
|
||||
Notes
|
||||
-----
|
||||
**Problem Definition:**
|
||||
The densest subgraph problem (DSG) asks to find the subgraph
|
||||
$S \subseteq V(G)$ with maximum density. For a subset of the nodes of
|
||||
$G$, $S \subseteq V(G)$, define $E(S) = \{ (u,v) : (u,v)\in E(G),
|
||||
u\in S, v\in S \}$ as the set of edges with both endpoints in $S$.
|
||||
The density of $S$ is defined as $|E(S)|/|S|$, the ratio between the
|
||||
edges in the subgraph $G[S]$ and the number of nodes in that subgraph.
|
||||
Note that this is different from the standard graph theoretic definition
|
||||
of density, defined as $\frac{2|E(S)|}{|S|(|S|-1)}$, for historical
|
||||
reasons.
|
||||
|
||||
**Exact Algorithms:**
|
||||
The densest subgraph problem is polynomial time solvable using maximum
|
||||
flow, commonly referred to as Goldberg's algorithm. However, the
|
||||
algorithm is quite involved. It first binary searches on the optimal
|
||||
density, $d^\ast$. For a guess of the density $d$, it sets up a flow
|
||||
network $G'$ with size $O(m)$. The maximum flow solution either
|
||||
informs the algorithm that no subgraph with density $d$ exists, or it
|
||||
provides a subgraph with density at least $d$. However, this is
|
||||
inherently bottlenecked by the maximum flow algorithm. For example, [2]_
|
||||
notes that Goldberg’s algorithm was not feasible on many large graphs
|
||||
even though they used a highly optimized maximum flow library.
|
||||
|
||||
**Charikar's Greedy Peeling:**
|
||||
While exact solution algorithms are quite involved, there are several
|
||||
known approximation algorithms for the densest subgraph problem.
|
||||
|
||||
Charikar [1]_ described a very simple 1/2-approximation algorithm for DSG
|
||||
known as the greedy "peeling" algorithm. The algorithm creates an
|
||||
ordering of the nodes as follows. The first node $v_1$ is the one with
|
||||
the smallest degree in $G$ (ties broken arbitrarily). It selects
|
||||
$v_2$ to be the smallest degree node in $G \setminus v_1$. Letting
|
||||
$G_i$ be the graph after removing $v_1, ..., v_i$ (with $G_0=G$),
|
||||
the algorithm returns the graph among $G_0, ..., G_n$ with the highest
|
||||
density.
|
||||
|
||||
**Greedy++:**
|
||||
Boob et al. [2]_ generalized this algorithm into Greedy++, an iterative
|
||||
algorithm that runs several rounds of "peeling". In fact, Greedy++ with 1
|
||||
iteration is precisely Charikar's algorithm. The algorithm converges to a
|
||||
$(1-\epsilon)$ approximate densest subgraph in $O(\Delta(G)\log
|
||||
n/\epsilon^2)$ iterations, where $\Delta(G)$ is the maximum degree,
|
||||
and $n$ is the number of nodes in $G$. The algorithm also has other
|
||||
desirable properties as shown by [4]_ and [5]_.
|
||||
|
||||
**FISTA Algorithm:**
|
||||
Harb et al. [3]_ gave a faster and more scalable algorithm using ideas
|
||||
from quadratic programming for the densest subgraph, which is based on a
|
||||
fast iterative shrinkage-thresholding algorithm (FISTA) algorithm. It is
|
||||
known that computing the densest subgraph can be formulated as the
|
||||
following convex optimization problem:
|
||||
|
||||
Minimize $\sum_{u \in V(G)} b_u^2$
|
||||
|
||||
Subject to:
|
||||
|
||||
$b_u = \sum_{v: \{u,v\} \in E(G)} x_{uv}$ for all $u \in V(G)$
|
||||
|
||||
$x_{uv} + x_{vu} = 1.0$ for all $\{u,v\} \in E(G)$
|
||||
|
||||
$x_{uv} \geq 0, x_{vu} \geq 0$ for all $\{u,v\} \in E(G)$
|
||||
|
||||
Here, $x_{uv}$ represents the fraction of edge $\{u,v\}$ assigned to
|
||||
$u$, and $x_{vu}$ to $v$.
|
||||
|
||||
The FISTA algorithm efficiently solves this convex program using gradient
|
||||
descent with projections. For a learning rate $\alpha$, the algorithm
|
||||
does:
|
||||
|
||||
1. **Initialization**: Set $x^{(0)}_{uv} = x^{(0)}_{vu} = 0.5$ for all
|
||||
edges as a feasible solution.
|
||||
|
||||
2. **Gradient Update**: For iteration $k\geq 1$, set
|
||||
$x^{(k+1)}_{uv} = x^{(k)}_{uv} - 2 \alpha \sum_{v: \{u,v\} \in E(G)}
|
||||
x^{(k)}_{uv}$. However, now $x^{(k+1)}_{uv}$ might be infeasible!
|
||||
To ensure feasibility, we project $x^{(k+1)}_{uv}$.
|
||||
|
||||
3. **Projection to the Feasible Set**: Compute
|
||||
$b^{(k+1)}_u = \sum_{v: \{u,v\} \in E(G)} x^{(k)}_{uv}$ for all
|
||||
nodes $u$. Define $z^{(k+1)}_{uv} = x^{(k+1)}_{uv} - 2 \alpha
|
||||
b^{(k+1)}_u$. Update $x^{(k+1)}_{uv} =
|
||||
CLAMP((z^{(k+1)}_{uv} - z^{(k+1)}_{vu} + 1.0) / 2.0)$, where
|
||||
$CLAMP(x) = \max(0, \min(1, x))$.
|
||||
|
||||
With a learning rate of $\alpha=1/\Delta(G)$, where $\Delta(G)$ is
|
||||
the maximum degree, the algorithm converges to the optimum solution of
|
||||
the convex program.
|
||||
|
||||
**Fractional Peeling:**
|
||||
To obtain a **discrete** subgraph, we use fractional peeling, an
|
||||
adaptation of the standard peeling algorithm which peels the minimum
|
||||
degree vertex in each iteration, and returns the densest subgraph found
|
||||
along the way. Here, we instead peel the vertex with the smallest
|
||||
induced load $b_u$:
|
||||
|
||||
1. Compute $b_u$ and $x_{uv}$.
|
||||
|
||||
2. Iteratively remove the vertex with the smallest $b_u$, updating its
|
||||
neighbors' load by $x_{vu}$.
|
||||
|
||||
Fractional peeling transforms the approximately optimal fractional
|
||||
values $b_u, x_{uv}$ into a discrete subgraph. Unlike traditional
|
||||
peeling, which removes the lowest-degree node, this method accounts for
|
||||
fractional edge contributions from the convex program.
|
||||
|
||||
This approach is both scalable and theoretically sound, ensuring a quick
|
||||
approximation of the densest subgraph while leveraging fractional load
|
||||
balancing.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Charikar, Moses. "Greedy approximation algorithms for finding dense
|
||||
components in a graph." In International workshop on approximation
|
||||
algorithms for combinatorial optimization, pp. 84-95. Berlin, Heidelberg:
|
||||
Springer Berlin Heidelberg, 2000.
|
||||
|
||||
.. [2] Boob, Digvijay, Yu Gao, Richard Peng, Saurabh Sawlani, Charalampos
|
||||
Tsourakakis, Di Wang, and Junxing Wang. "Flowless: Extracting densest
|
||||
subgraphs without flow computations." In Proceedings of The Web Conference
|
||||
2020, pp. 573-583. 2020.
|
||||
|
||||
.. [3] Harb, Elfarouk, Kent Quanrud, and Chandra Chekuri. "Faster and scalable
|
||||
algorithms for densest subgraph and decomposition." Advances in Neural
|
||||
Information Processing Systems 35 (2022): 26966-26979.
|
||||
|
||||
.. [4] Harb, Elfarouk, Kent Quanrud, and Chandra Chekuri. "Convergence to
|
||||
lexicographically optimal base in a (contra) polymatroid and applications
|
||||
to densest subgraph and tree packing." arXiv preprint arXiv:2305.02987
|
||||
(2023).
|
||||
|
||||
.. [5] Chekuri, Chandra, Kent Quanrud, and Manuel R. Torres. "Densest
|
||||
subgraph: Supermodularity, iterative peeling, and flow." In Proceedings of
|
||||
the 2022 Annual ACM-SIAM Symposium on Discrete Algorithms (SODA), pp.
|
||||
1531-1555. Society for Industrial and Applied Mathematics, 2022.
|
||||
"""
|
||||
try:
|
||||
algo = ALGORITHMS[method]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"{method} is not a valid choice for an algorithm.") from e
|
||||
|
||||
return algo(G, iterations)
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
"""Distance measures approximated metrics."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils.decorators import py_random_state
|
||||
|
||||
__all__ = ["diameter"]
|
||||
|
||||
|
||||
@py_random_state(1)
|
||||
@nx._dispatchable(name="approximate_diameter")
|
||||
def diameter(G, seed=None):
|
||||
"""Returns a lower bound on the diameter of the graph G.
|
||||
|
||||
The function computes a lower bound on the diameter (i.e., the maximum eccentricity)
|
||||
of a directed or undirected graph G. The procedure used varies depending on the graph
|
||||
being directed or not.
|
||||
|
||||
If G is an `undirected` graph, then the function uses the `2-sweep` algorithm [1]_.
|
||||
The main idea is to pick the farthest node from a random node and return its eccentricity.
|
||||
|
||||
Otherwise, if G is a `directed` graph, the function uses the `2-dSweep` algorithm [2]_,
|
||||
The procedure starts by selecting a random source node $s$ from which it performs a
|
||||
forward and a backward BFS. Let $a_1$ and $a_2$ be the farthest nodes in the forward and
|
||||
backward cases, respectively. Then, it computes the backward eccentricity of $a_1$ using
|
||||
a backward BFS and the forward eccentricity of $a_2$ using a forward BFS.
|
||||
Finally, it returns the best lower bound between the two.
|
||||
|
||||
In both cases, the time complexity is linear with respect to the size of G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
d : integer
|
||||
Lower Bound on the Diameter of G
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(10) # undirected graph
|
||||
>>> nx.diameter(G)
|
||||
9
|
||||
>>> G = nx.cycle_graph(3, create_using=nx.DiGraph) # directed graph
|
||||
>>> nx.diameter(G)
|
||||
2
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the graph is empty or
|
||||
If the graph is undirected and not connected or
|
||||
If the graph is directed and not strongly connected.
|
||||
|
||||
See Also
|
||||
--------
|
||||
networkx.algorithms.distance_measures.diameter
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Magnien, Clémence, Matthieu Latapy, and Michel Habib.
|
||||
*Fast computation of empirically tight bounds for the diameter of massive graphs.*
|
||||
Journal of Experimental Algorithmics (JEA), 2009.
|
||||
https://arxiv.org/pdf/0904.2728.pdf
|
||||
.. [2] Crescenzi, Pierluigi, Roberto Grossi, Leonardo Lanzi, and Andrea Marino.
|
||||
*On computing the diameter of real-world directed (weighted) graphs.*
|
||||
International Symposium on Experimental Algorithms. Springer, Berlin, Heidelberg, 2012.
|
||||
https://courses.cs.ut.ee/MTAT.03.238/2014_fall/uploads/Main/diameter.pdf
|
||||
"""
|
||||
# if G is empty
|
||||
if not G:
|
||||
raise nx.NetworkXError("Expected non-empty NetworkX graph!")
|
||||
# if there's only a node
|
||||
if G.number_of_nodes() == 1:
|
||||
return 0
|
||||
# if G is directed
|
||||
if G.is_directed():
|
||||
return _two_sweep_directed(G, seed)
|
||||
# else if G is undirected
|
||||
return _two_sweep_undirected(G, seed)
|
||||
|
||||
|
||||
def _two_sweep_undirected(G, seed):
|
||||
"""Helper function for finding a lower bound on the diameter
|
||||
for undirected Graphs.
|
||||
|
||||
The idea is to pick the farthest node from a random node
|
||||
and return its eccentricity.
|
||||
|
||||
``G`` is a NetworkX undirected graph.
|
||||
|
||||
.. note::
|
||||
|
||||
``seed`` is a random.Random or numpy.random.RandomState instance
|
||||
"""
|
||||
# select a random source node
|
||||
source = seed.choice(list(G))
|
||||
# get the distances to the other nodes
|
||||
distances = nx.shortest_path_length(G, source)
|
||||
# if some nodes have not been visited, then the graph is not connected
|
||||
if len(distances) != len(G):
|
||||
raise nx.NetworkXError("Graph not connected.")
|
||||
# take a node that is (one of) the farthest nodes from the source
|
||||
*_, node = distances
|
||||
# return the eccentricity of the node
|
||||
return nx.eccentricity(G, node)
|
||||
|
||||
|
||||
def _two_sweep_directed(G, seed):
|
||||
"""Helper function for finding a lower bound on the diameter
|
||||
for directed Graphs.
|
||||
|
||||
It implements 2-dSweep, the directed version of the 2-sweep algorithm.
|
||||
The algorithm follows the following steps.
|
||||
1. Select a source node $s$ at random.
|
||||
2. Perform a forward BFS from $s$ to select a node $a_1$ at the maximum
|
||||
distance from the source, and compute $LB_1$, the backward eccentricity of $a_1$.
|
||||
3. Perform a backward BFS from $s$ to select a node $a_2$ at the maximum
|
||||
distance from the source, and compute $LB_2$, the forward eccentricity of $a_2$.
|
||||
4. Return the maximum between $LB_1$ and $LB_2$.
|
||||
|
||||
``G`` is a NetworkX directed graph.
|
||||
|
||||
.. note::
|
||||
|
||||
``seed`` is a random.Random or numpy.random.RandomState instance
|
||||
"""
|
||||
# get a new digraph G' with the edges reversed in the opposite direction
|
||||
G_reversed = G.reverse()
|
||||
# select a random source node
|
||||
source = seed.choice(list(G))
|
||||
# compute forward distances from source
|
||||
forward_distances = nx.shortest_path_length(G, source)
|
||||
# compute backward distances from source
|
||||
backward_distances = nx.shortest_path_length(G_reversed, source)
|
||||
# if either the source can't reach every node or not every node
|
||||
# can reach the source, then the graph is not strongly connected
|
||||
n = len(G)
|
||||
if len(forward_distances) != n or len(backward_distances) != n:
|
||||
raise nx.NetworkXError("DiGraph not strongly connected.")
|
||||
# take a node a_1 at the maximum distance from the source in G
|
||||
*_, a_1 = forward_distances
|
||||
# take a node a_2 at the maximum distance from the source in G_reversed
|
||||
*_, a_2 = backward_distances
|
||||
# return the max between the backward eccentricity of a_1 and the forward eccentricity of a_2
|
||||
return max(nx.eccentricity(G_reversed, a_1), nx.eccentricity(G, a_2))
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
"""Functions for finding node and edge dominating sets.
|
||||
|
||||
A `dominating set`_ for an undirected graph *G* with vertex set *V*
|
||||
and edge set *E* is a subset *D* of *V* such that every vertex not in
|
||||
*D* is adjacent to at least one member of *D*. An `edge dominating set`_
|
||||
is a subset *F* of *E* such that every edge not in *F* is
|
||||
incident to an endpoint of at least one edge in *F*.
|
||||
|
||||
.. _dominating set: https://en.wikipedia.org/wiki/Dominating_set
|
||||
.. _edge dominating set: https://en.wikipedia.org/wiki/Edge_dominating_set
|
||||
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
|
||||
from ...utils import not_implemented_for
|
||||
from ..matching import maximal_matching
|
||||
|
||||
__all__ = ["min_weighted_dominating_set", "min_edge_dominating_set"]
|
||||
|
||||
|
||||
# TODO Why doesn't this algorithm work for directed graphs?
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(node_attrs="weight")
|
||||
def min_weighted_dominating_set(G, weight=None):
|
||||
r"""Returns a dominating set that approximates the minimum weight node
|
||||
dominating set.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph.
|
||||
|
||||
weight : string
|
||||
The node attribute storing the weight of an node. If provided,
|
||||
the node attribute with this key must be a number for each
|
||||
node. If not provided, each node is assumed to have weight one.
|
||||
|
||||
Returns
|
||||
-------
|
||||
min_weight_dominating_set : set
|
||||
A set of nodes, the sum of whose weights is no more than `(\log
|
||||
w(V)) w(V^*)`, where `w(V)` denotes the sum of the weights of
|
||||
each node in the graph and `w(V^*)` denotes the sum of the
|
||||
weights of each node in the minimum weight dominating set.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.Graph([(0, 1), (0, 4), (1, 4), (1, 2), (2, 3), (3, 4), (2, 5)])
|
||||
>>> nx.approximation.min_weighted_dominating_set(G)
|
||||
{1, 2, 4}
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If G is directed.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm computes an approximate minimum weighted dominating
|
||||
set for the graph `G`. The returned solution has weight `(\log
|
||||
w(V)) w(V^*)`, where `w(V)` denotes the sum of the weights of each
|
||||
node in the graph and `w(V^*)` denotes the sum of the weights of
|
||||
each node in the minimum weight dominating set for the graph.
|
||||
|
||||
This implementation of the algorithm runs in $O(m)$ time, where $m$
|
||||
is the number of edges in the graph.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Vazirani, Vijay V.
|
||||
*Approximation Algorithms*.
|
||||
Springer Science & Business Media, 2001.
|
||||
|
||||
"""
|
||||
# The unique dominating set for the null graph is the empty set.
|
||||
if len(G) == 0:
|
||||
return set()
|
||||
|
||||
# This is the dominating set that will eventually be returned.
|
||||
dom_set = set()
|
||||
|
||||
def _cost(node_and_neighborhood):
|
||||
"""Returns the cost-effectiveness of greedily choosing the given
|
||||
node.
|
||||
|
||||
`node_and_neighborhood` is a two-tuple comprising a node and its
|
||||
closed neighborhood.
|
||||
|
||||
"""
|
||||
v, neighborhood = node_and_neighborhood
|
||||
return G.nodes[v].get(weight, 1) / len(neighborhood - dom_set)
|
||||
|
||||
# This is a set of all vertices not already covered by the
|
||||
# dominating set.
|
||||
vertices = set(G)
|
||||
# This is a dictionary mapping each node to the closed neighborhood
|
||||
# of that node.
|
||||
neighborhoods = {v: {v} | set(G[v]) for v in G}
|
||||
|
||||
# Continue until all vertices are adjacent to some node in the
|
||||
# dominating set.
|
||||
while vertices:
|
||||
# Find the most cost-effective node to add, along with its
|
||||
# closed neighborhood.
|
||||
dom_node, min_set = min(neighborhoods.items(), key=_cost)
|
||||
# Add the node to the dominating set and reduce the remaining
|
||||
# set of nodes to cover.
|
||||
dom_set.add(dom_node)
|
||||
del neighborhoods[dom_node]
|
||||
vertices -= min_set
|
||||
|
||||
return dom_set
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def min_edge_dominating_set(G):
|
||||
r"""Returns minimum cardinality edge dominating set.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
min_edge_dominating_set : set
|
||||
Returns a set of dominating edges whose size is no more than 2 * OPT.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.petersen_graph()
|
||||
>>> nx.approximation.min_edge_dominating_set(G)
|
||||
{(0, 1), (4, 9), (6, 8), (5, 7), (2, 3)}
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If the input graph `G` is empty.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm computes an approximate solution to the edge dominating set
|
||||
problem. The result is no more than 2 * OPT in terms of size of the set.
|
||||
Runtime of the algorithm is $O(|E|)$.
|
||||
"""
|
||||
if not G:
|
||||
raise ValueError("Expected non-empty NetworkX graph!")
|
||||
return maximal_matching(G)
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
"""Fast approximation for k-component structure"""
|
||||
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping
|
||||
from functools import cached_property
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import local_node_connectivity
|
||||
from networkx.exception import NetworkXError
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = ["k_components"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(name="approximate_k_components")
|
||||
def k_components(G, min_density=0.95):
|
||||
r"""Returns the approximate 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.
|
||||
|
||||
This implementation is based on the fast heuristics to approximate
|
||||
the `k`-component structure of a graph [1]_. Which, in turn, it is based on
|
||||
a fast approximation algorithm for finding good lower bounds of the number
|
||||
of node independent paths between two nodes [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
min_density : Float
|
||||
Density relaxation threshold. Default value 0.95
|
||||
|
||||
Returns
|
||||
-------
|
||||
k_components : dict
|
||||
Dictionary with connectivity level `k` as key and a list of
|
||||
sets of nodes that form a k-component of level `k` as values.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If G 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
|
||||
>>> from networkx.algorithms import approximation as apxa
|
||||
>>> G = nx.petersen_graph()
|
||||
>>> k_components = apxa.k_components(G)
|
||||
|
||||
Notes
|
||||
-----
|
||||
The logic of the approximation algorithm for computing the `k`-component
|
||||
structure [1]_ is based on repeatedly applying simple and fast algorithms
|
||||
for `k`-cores and biconnected components in order to narrow down the
|
||||
number of pairs of nodes over which we have to compute White and Newman's
|
||||
approximation algorithm for finding node independent paths [2]_. More
|
||||
formally, this algorithm is based on Whitney's theorem, which states
|
||||
an inclusion relation among node connectivity, edge connectivity, and
|
||||
minimum degree for any graph G. This theorem implies that every
|
||||
`k`-component is nested inside a `k`-edge-component, which in turn,
|
||||
is contained in a `k`-core. Thus, this algorithm computes node independent
|
||||
paths among pairs of nodes in each biconnected part of each `k`-core,
|
||||
and repeats this procedure for each `k` from 3 to the maximal core number
|
||||
of a node in the input graph.
|
||||
|
||||
Because, in practice, many nodes of the core of level `k` inside a
|
||||
bicomponent actually are part of a component of level k, the auxiliary
|
||||
graph needed for the algorithm is likely to be very dense. Thus, we use
|
||||
a complement graph data structure (see `AntiGraph`) to save memory.
|
||||
AntiGraph only stores information of the edges that are *not* present
|
||||
in the actual auxiliary graph. When applying algorithms to this
|
||||
complement graph data structure, it behaves as if it were the dense
|
||||
version.
|
||||
|
||||
See also
|
||||
--------
|
||||
k_components
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Torrents, J. and F. Ferraro (2015) Structural Cohesion:
|
||||
Visualization and Heuristics for Fast Computation.
|
||||
https://arxiv.org/pdf/1503.04476v1
|
||||
|
||||
.. [2] White, Douglas R., and Mark Newman (2001) A Fast Algorithm for
|
||||
Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
|
||||
https://www.santafe.edu/research/results/working-papers/fast-approximation-algorithms-for-finding-node-ind
|
||||
|
||||
.. [3] Moody, J. and D. White (2003). Social cohesion and embeddedness:
|
||||
A hierarchical conception of social groups.
|
||||
American Sociological Review 68(1), 103--28.
|
||||
https://doi.org/10.2307/3088904
|
||||
|
||||
"""
|
||||
# Dictionary with connectivity level (k) as keys and a list of
|
||||
# sets of nodes that form a k-component as values
|
||||
k_components = defaultdict(list)
|
||||
# make a few functions local for speed
|
||||
node_connectivity = local_node_connectivity
|
||||
k_core = nx.k_core
|
||||
core_number = nx.core_number
|
||||
biconnected_components = nx.biconnected_components
|
||||
combinations = itertools.combinations
|
||||
# Exact solution for k = {1,2}
|
||||
# There is a linear time algorithm for triconnectivity, if we had an
|
||||
# implementation available we could start from k = 4.
|
||||
for component in nx.connected_components(G):
|
||||
# isolated nodes have connectivity 0
|
||||
comp = set(component)
|
||||
if len(comp) > 1:
|
||||
k_components[1].append(comp)
|
||||
for bicomponent in nx.biconnected_components(G):
|
||||
# avoid considering dyads as bicomponents
|
||||
bicomp = set(bicomponent)
|
||||
if len(bicomp) > 2:
|
||||
k_components[2].append(bicomp)
|
||||
# There is no k-component of k > maximum core number
|
||||
# \kappa(G) <= \lambda(G) <= \delta(G)
|
||||
g_cnumber = core_number(G)
|
||||
max_core = max(g_cnumber.values())
|
||||
for k in range(3, max_core + 1):
|
||||
C = k_core(G, k, core_number=g_cnumber)
|
||||
for nodes in biconnected_components(C):
|
||||
# Build a subgraph SG induced by the nodes that are part of
|
||||
# each biconnected component of the k-core subgraph C.
|
||||
if len(nodes) < k:
|
||||
continue
|
||||
SG = G.subgraph(nodes)
|
||||
# Build auxiliary graph
|
||||
H = _AntiGraph()
|
||||
H.add_nodes_from(SG.nodes())
|
||||
for u, v in combinations(SG, 2):
|
||||
K = node_connectivity(SG, u, v, cutoff=k)
|
||||
if k > K:
|
||||
H.add_edge(u, v)
|
||||
for h_nodes in biconnected_components(H):
|
||||
if len(h_nodes) <= k:
|
||||
continue
|
||||
SH = H.subgraph(h_nodes)
|
||||
for Gc in _cliques_heuristic(SG, SH, k, min_density):
|
||||
for k_nodes in biconnected_components(Gc):
|
||||
Gk = nx.k_core(SG.subgraph(k_nodes), k)
|
||||
if len(Gk) <= k:
|
||||
continue
|
||||
k_components[k].append(set(Gk))
|
||||
return k_components
|
||||
|
||||
|
||||
def _cliques_heuristic(G, H, k, min_density):
|
||||
h_cnumber = nx.core_number(H)
|
||||
for i, c_value in enumerate(sorted(set(h_cnumber.values()), reverse=True)):
|
||||
cands = {n for n, c in h_cnumber.items() if c == c_value}
|
||||
# Skip checking for overlap for the highest core value
|
||||
if i == 0:
|
||||
overlap = False
|
||||
else:
|
||||
overlap = set.intersection(
|
||||
*[{x for x in H[n] if x not in cands} for n in cands]
|
||||
)
|
||||
if overlap and len(overlap) < k:
|
||||
SH = H.subgraph(cands | overlap)
|
||||
else:
|
||||
SH = H.subgraph(cands)
|
||||
sh_cnumber = nx.core_number(SH)
|
||||
SG = nx.k_core(G.subgraph(SH), k)
|
||||
while not (_same(sh_cnumber) and nx.density(SH) >= min_density):
|
||||
# This subgraph must be writable => .copy()
|
||||
SH = H.subgraph(SG).copy()
|
||||
if len(SH) <= k:
|
||||
break
|
||||
sh_cnumber = nx.core_number(SH)
|
||||
sh_deg = dict(SH.degree())
|
||||
min_deg = min(sh_deg.values())
|
||||
SH.remove_nodes_from(n for n, d in sh_deg.items() if d == min_deg)
|
||||
SG = nx.k_core(G.subgraph(SH), k)
|
||||
else:
|
||||
yield SG
|
||||
|
||||
|
||||
def _same(measure, tol=0):
|
||||
vals = set(measure.values())
|
||||
if (max(vals) - min(vals)) <= tol:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class _AntiGraph(nx.Graph):
|
||||
"""
|
||||
Class for complement graphs.
|
||||
|
||||
The main goal is to be able to work with big and dense graphs with
|
||||
a low memory footprint.
|
||||
|
||||
In this class you add the edges that *do not exist* in the dense graph,
|
||||
the report methods of the class return the neighbors, the edges and
|
||||
the degree as if it was the dense graph. Thus it's possible to use
|
||||
an instance of this class with some of NetworkX functions. In this
|
||||
case we only use k-core, connected_components, and biconnected_components.
|
||||
"""
|
||||
|
||||
all_edge_dict = {"weight": 1}
|
||||
|
||||
def single_edge_dict(self):
|
||||
return self.all_edge_dict
|
||||
|
||||
edge_attr_dict_factory = single_edge_dict # type: ignore[assignment]
|
||||
|
||||
def __getitem__(self, n):
|
||||
"""Returns a dict of neighbors of node n in the dense graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : node
|
||||
A node in the graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
adj_dict : dictionary
|
||||
The adjacency dictionary for nodes connected to n.
|
||||
|
||||
"""
|
||||
all_edge_dict = self.all_edge_dict
|
||||
return {
|
||||
node: all_edge_dict for node in set(self._adj) - set(self._adj[n]) - {n}
|
||||
}
|
||||
|
||||
def neighbors(self, n):
|
||||
"""Returns an iterator over all neighbors of node n in the
|
||||
dense graph.
|
||||
"""
|
||||
try:
|
||||
return iter(set(self._adj) - set(self._adj[n]) - {n})
|
||||
except KeyError as err:
|
||||
raise NetworkXError(f"The node {n} is not in the graph.") from err
|
||||
|
||||
class AntiAtlasView(Mapping):
|
||||
"""An adjacency inner dict for AntiGraph"""
|
||||
|
||||
def __init__(self, graph, node):
|
||||
self._graph = graph
|
||||
self._atlas = graph._adj[node]
|
||||
self._node = node
|
||||
|
||||
def __len__(self):
|
||||
return len(self._graph) - len(self._atlas) - 1
|
||||
|
||||
def __iter__(self):
|
||||
return (n for n in self._graph if n not in self._atlas and n != self._node)
|
||||
|
||||
def __getitem__(self, nbr):
|
||||
nbrs = set(self._graph._adj) - set(self._atlas) - {self._node}
|
||||
if nbr in nbrs:
|
||||
return self._graph.all_edge_dict
|
||||
raise KeyError(nbr)
|
||||
|
||||
class AntiAdjacencyView(AntiAtlasView):
|
||||
"""An adjacency outer dict for AntiGraph"""
|
||||
|
||||
def __init__(self, graph):
|
||||
self._graph = graph
|
||||
self._atlas = graph._adj
|
||||
|
||||
def __len__(self):
|
||||
return len(self._atlas)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._graph)
|
||||
|
||||
def __getitem__(self, node):
|
||||
if node not in self._graph:
|
||||
raise KeyError(node)
|
||||
return self._graph.AntiAtlasView(self._graph, node)
|
||||
|
||||
@cached_property
|
||||
def adj(self):
|
||||
return self.AntiAdjacencyView(self)
|
||||
|
||||
def subgraph(self, nodes):
|
||||
"""This subgraph method returns a full AntiGraph. Not a View"""
|
||||
nodes = set(nodes)
|
||||
G = _AntiGraph()
|
||||
G.add_nodes_from(nodes)
|
||||
for n in G:
|
||||
Gnbrs = G.adjlist_inner_dict_factory()
|
||||
G._adj[n] = Gnbrs
|
||||
for nbr, d in self._adj[n].items():
|
||||
if nbr in G._adj:
|
||||
Gnbrs[nbr] = d
|
||||
G._adj[nbr][n] = d
|
||||
G.graph = self.graph
|
||||
return G
|
||||
|
||||
class AntiDegreeView(nx.reportviews.DegreeView):
|
||||
def __iter__(self):
|
||||
all_nodes = set(self._succ)
|
||||
for n in self._nodes:
|
||||
nbrs = all_nodes - set(self._succ[n]) - {n}
|
||||
yield (n, len(nbrs))
|
||||
|
||||
def __getitem__(self, n):
|
||||
nbrs = set(self._succ) - set(self._succ[n]) - {n}
|
||||
# AntiGraph is a ThinGraph so all edges have weight 1
|
||||
return len(nbrs) + (n in nbrs)
|
||||
|
||||
@cached_property
|
||||
def degree(self):
|
||||
"""Returns an iterator for (node, degree) and degree for single node.
|
||||
|
||||
The node degree is the number of edges adjacent to the node.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nbunch : iterable container, optional (default=all nodes)
|
||||
A container of nodes. The container will be iterated
|
||||
through once.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
The edge attribute that holds the numerical value used
|
||||
as a weight. If None, then each edge has weight 1.
|
||||
The degree is the sum of the edge weights adjacent to the node.
|
||||
|
||||
Returns
|
||||
-------
|
||||
deg:
|
||||
Degree of the node, if a single node is passed as argument.
|
||||
nd_iter : an iterator
|
||||
The iterator returns two-tuples of (node, degree).
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> G.degree(0) # node 0 with degree 1
|
||||
1
|
||||
>>> list(G.degree([0, 1]))
|
||||
[(0, 1), (1, 2)]
|
||||
|
||||
"""
|
||||
return self.AntiDegreeView(self)
|
||||
|
||||
def adjacency(self):
|
||||
"""Returns an iterator of (node, adjacency set) tuples for all nodes
|
||||
in the dense graph.
|
||||
|
||||
This is the fastest way to look at every edge.
|
||||
For directed graphs, only outgoing adjacencies are included.
|
||||
|
||||
Returns
|
||||
-------
|
||||
adj_iter : iterator
|
||||
An iterator of (node, adjacency set) for all nodes in
|
||||
the graph.
|
||||
|
||||
"""
|
||||
for n in self._adj:
|
||||
yield (n, set(self._adj) - set(self._adj[n]) - {n})
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
**************
|
||||
Graph Matching
|
||||
**************
|
||||
|
||||
Given a graph G = (V,E), a matching M in G is a set of pairwise non-adjacent
|
||||
edges; that is, no two edges share a common vertex.
|
||||
|
||||
`Wikipedia: Matching <https://en.wikipedia.org/wiki/Matching_(graph_theory)>`_
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["min_maximal_matching"]
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def min_maximal_matching(G):
|
||||
r"""Returns the minimum maximal matching of G. That is, out of all maximal
|
||||
matchings of the graph G, the smallest is returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
min_maximal_matching : set
|
||||
Returns a set of edges such that no two edges share a common endpoint
|
||||
and every edge not in the set shares some common endpoint in the set.
|
||||
Cardinality will be 2*OPT in the worst case.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm computes an approximate solution for the minimum maximal
|
||||
cardinality matching problem. The solution is no more than 2 * OPT in size.
|
||||
Runtime is $O(|E|)$.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Vazirani, Vijay Approximation Algorithms (2001)
|
||||
"""
|
||||
return nx.maximal_matching(G)
|
||||
@@ -0,0 +1,143 @@
|
||||
import networkx as nx
|
||||
from networkx.utils.decorators import not_implemented_for, py_random_state
|
||||
|
||||
__all__ = ["randomized_partitioning", "one_exchange"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@py_random_state(1)
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def randomized_partitioning(G, seed=None, p=0.5, weight=None):
|
||||
"""Compute a random partitioning of the graph nodes and its cut value.
|
||||
|
||||
A partitioning is calculated by observing each node
|
||||
and deciding to add it to the partition with probability `p`,
|
||||
returning a random cut and its corresponding value (the
|
||||
sum of weights of edges connecting different partitions).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
p : scalar
|
||||
Probability for each node to be part of the first partition.
|
||||
Should be in [0,1]
|
||||
|
||||
weight : object
|
||||
Edge attribute key to use as weight. If not specified, edges
|
||||
have weight one.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cut_size : scalar
|
||||
Value of the minimum cut.
|
||||
|
||||
partition : pair of node sets
|
||||
A partitioning of the nodes that defines a minimum cut.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.complete_graph(5)
|
||||
>>> cut_size, partition = nx.approximation.randomized_partitioning(G, seed=1)
|
||||
>>> cut_size
|
||||
6
|
||||
>>> partition
|
||||
({0, 3, 4}, {1, 2})
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
"""
|
||||
cut = {node for node in G.nodes() if seed.random() < p}
|
||||
cut_size = nx.algorithms.cut_size(G, cut, weight=weight)
|
||||
partition = (cut, G.nodes - cut)
|
||||
return cut_size, partition
|
||||
|
||||
|
||||
def _swap_node_partition(cut, node):
|
||||
return cut - {node} if node in cut else cut.union({node})
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@py_random_state(2)
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def one_exchange(G, initial_cut=None, seed=None, weight=None):
|
||||
"""Compute a partitioning of the graphs nodes and the corresponding cut value.
|
||||
|
||||
Use a greedy one exchange strategy to find a locally maximal cut
|
||||
and its value, it works by finding the best node (one that gives
|
||||
the highest gain to the cut value) to add to the current cut
|
||||
and repeats this process until no improvement can be made.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : networkx Graph
|
||||
Graph to find a maximum cut for.
|
||||
|
||||
initial_cut : set
|
||||
Cut to use as a starting point. If not supplied the algorithm
|
||||
starts with an empty cut.
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
weight : object
|
||||
Edge attribute key to use as weight. If not specified, edges
|
||||
have weight one.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cut_value : scalar
|
||||
Value of the maximum cut.
|
||||
|
||||
partition : pair of node sets
|
||||
A partitioning of the nodes that defines a maximum cut.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.complete_graph(5)
|
||||
>>> curr_cut_size, partition = nx.approximation.one_exchange(G, seed=1)
|
||||
>>> curr_cut_size
|
||||
6
|
||||
>>> partition
|
||||
({0, 2}, {1, 3, 4})
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
"""
|
||||
if initial_cut is None:
|
||||
initial_cut = set()
|
||||
cut = set(initial_cut)
|
||||
current_cut_size = nx.algorithms.cut_size(G, cut, weight=weight)
|
||||
while True:
|
||||
nodes = list(G.nodes())
|
||||
# Shuffling the nodes ensures random tie-breaks in the following call to max
|
||||
seed.shuffle(nodes)
|
||||
best_node_to_swap = max(
|
||||
nodes,
|
||||
key=lambda v: nx.algorithms.cut_size(
|
||||
G, _swap_node_partition(cut, v), weight=weight
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
potential_cut = _swap_node_partition(cut, best_node_to_swap)
|
||||
potential_cut_size = nx.algorithms.cut_size(G, potential_cut, weight=weight)
|
||||
|
||||
if potential_cut_size > current_cut_size:
|
||||
cut = potential_cut
|
||||
current_cut_size = potential_cut_size
|
||||
else:
|
||||
break
|
||||
|
||||
partition = (cut, G.nodes - cut)
|
||||
return current_cut_size, partition
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Ramsey numbers.
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
from ...utils import arbitrary_element
|
||||
|
||||
__all__ = ["ramsey_R2"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def ramsey_R2(G):
|
||||
r"""Compute the largest clique and largest independent set in `G`.
|
||||
|
||||
This can be used to estimate bounds for the 2-color
|
||||
Ramsey number `R(2;s,t)` for `G`.
|
||||
|
||||
This is a recursive implementation which could run into trouble
|
||||
for large recursions. Note that self-loop edges are ignored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
max_pair : (set, set) tuple
|
||||
Maximum clique, Maximum independent set.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
"""
|
||||
if not G:
|
||||
return set(), set()
|
||||
|
||||
node = arbitrary_element(G)
|
||||
nbrs = (nbr for nbr in nx.all_neighbors(G, node) if nbr != node)
|
||||
nnbrs = nx.non_neighbors(G, node)
|
||||
c_1, i_1 = ramsey_R2(G.subgraph(nbrs).copy())
|
||||
c_2, i_2 = ramsey_R2(G.subgraph(nnbrs).copy())
|
||||
|
||||
c_1.add(node)
|
||||
i_2.add(node)
|
||||
# Choose the larger of the two cliques and the larger of the two
|
||||
# independent sets, according to cardinality.
|
||||
return max(c_1, c_2, key=len), max(i_1, i_2, key=len)
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
from itertools import chain
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for, pairwise
|
||||
|
||||
__all__ = ["metric_closure", "steiner_tree"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(edge_attrs="weight", returns_graph=True)
|
||||
def metric_closure(G, weight="weight"):
|
||||
"""Return the metric closure of a graph.
|
||||
|
||||
The metric closure of a graph *G* is the complete graph in which each edge
|
||||
is weighted by the shortest path distance between the nodes in *G* .
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
NetworkX graph
|
||||
Metric closure of the graph `G`.
|
||||
|
||||
Notes
|
||||
-----
|
||||
.. deprecated:: 3.6
|
||||
`metric_closure` is deprecated and will be removed in NetworkX 3.8.
|
||||
Use :func:`networkx.all_pairs_shortest_path_length` instead.
|
||||
|
||||
"""
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"metric_closure is deprecated and will be removed in NetworkX 3.8.\n"
|
||||
"Use nx.all_pairs_shortest_path_length instead.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=5,
|
||||
)
|
||||
|
||||
M = nx.Graph()
|
||||
|
||||
Gnodes = set(G)
|
||||
|
||||
# check for connected graph while processing first node
|
||||
all_paths_iter = nx.all_pairs_dijkstra(G, weight=weight)
|
||||
u, (distance, path) = next(all_paths_iter)
|
||||
if len(G) != len(distance):
|
||||
msg = "G is not a connected graph. metric_closure is not defined."
|
||||
raise nx.NetworkXError(msg)
|
||||
Gnodes.remove(u)
|
||||
for v in Gnodes:
|
||||
M.add_edge(u, v, distance=distance[v], path=path[v])
|
||||
|
||||
# first node done -- now process the rest
|
||||
for u, (distance, path) in all_paths_iter:
|
||||
Gnodes.remove(u)
|
||||
for v in Gnodes:
|
||||
M.add_edge(u, v, distance=distance[v], path=path[v])
|
||||
|
||||
return M
|
||||
|
||||
|
||||
def _mehlhorn_steiner_tree(G, terminal_nodes, weight):
|
||||
distances, paths = nx.multi_source_dijkstra(G, terminal_nodes, weight=weight)
|
||||
|
||||
d_1 = {}
|
||||
s = {}
|
||||
for v in G.nodes():
|
||||
s[v] = paths[v][0]
|
||||
d_1[(v, s[v])] = distances[v]
|
||||
|
||||
# G1-G4 names match those from the Mehlhorn 1988 paper.
|
||||
G_1_prime = nx.Graph()
|
||||
# iterate over all edges to complete d1
|
||||
for u, v, data in G.edges(data=True):
|
||||
su, sv = s[u], s[v]
|
||||
weight_here = d_1[(u, su)] + data.get(weight, 1) + d_1[(v, sv)]
|
||||
if not G_1_prime.has_edge(su, sv):
|
||||
G_1_prime.add_edge(su, sv, weight_d1=weight_here)
|
||||
else:
|
||||
new_weight = min(weight_here, G_1_prime[su][sv]["weight_d1"])
|
||||
G_1_prime.add_edge(su, sv, weight_d1=new_weight)
|
||||
|
||||
G_2 = nx.minimum_spanning_edges(G_1_prime, data=True, weight="weight_d1")
|
||||
|
||||
G_3 = nx.Graph()
|
||||
for u, v, _ in G_2:
|
||||
path = nx.shortest_path(G, u, v, weight=weight)
|
||||
for n1, n2 in pairwise(path):
|
||||
G_3.add_edge(n1, n2, weight=G[n1][n2].get(weight, 1))
|
||||
|
||||
G_3_mst = list(nx.minimum_spanning_edges(G_3, data=False, weight=weight))
|
||||
if G.is_multigraph():
|
||||
G_3_mst = (
|
||||
(u, v, min(G[u][v], key=lambda k: G[u][v][k].get(weight, 1)))
|
||||
for u, v in G_3_mst
|
||||
)
|
||||
G_4 = G.edge_subgraph(G_3_mst).copy()
|
||||
_remove_nonterminal_leaves(G_4, terminal_nodes)
|
||||
return G_4.edges()
|
||||
|
||||
|
||||
def _kou_steiner_tree(G, terminal_nodes, weight):
|
||||
# Compute the metric closure only for terminal nodes
|
||||
# Create a complete graph H from the metric edges
|
||||
H = nx.Graph()
|
||||
unvisited_terminals = set(terminal_nodes)
|
||||
|
||||
# check for connected graph while processing first node
|
||||
u = unvisited_terminals.pop()
|
||||
distances, paths = nx.single_source_dijkstra(G, source=u, weight=weight)
|
||||
if len(G) != len(distances):
|
||||
msg = "G is not a connected graph."
|
||||
raise nx.NetworkXError(msg)
|
||||
for v in unvisited_terminals:
|
||||
H.add_edge(u, v, distance=distances[v], path=paths[v])
|
||||
|
||||
# first node done -- now process the rest
|
||||
for u in unvisited_terminals.copy():
|
||||
distances, paths = nx.single_source_dijkstra(G, source=u, weight=weight)
|
||||
unvisited_terminals.remove(u)
|
||||
for v in unvisited_terminals:
|
||||
H.add_edge(u, v, distance=distances[v], path=paths[v])
|
||||
|
||||
# Use the 'distance' attribute of each edge provided by H.
|
||||
mst_edges = nx.minimum_spanning_edges(H, weight="distance", data=True)
|
||||
|
||||
# Create an iterator over each edge in each shortest path; repeats are okay
|
||||
mst_all_edges = chain.from_iterable(pairwise(d["path"]) for u, v, d in mst_edges)
|
||||
if G.is_multigraph():
|
||||
mst_all_edges = (
|
||||
(u, v, min(G[u][v], key=lambda k: G[u][v][k].get(weight, 1)))
|
||||
for u, v in mst_all_edges
|
||||
)
|
||||
|
||||
# Find the MST again, over this new set of edges
|
||||
G_S = G.edge_subgraph(mst_all_edges)
|
||||
T_S = nx.minimum_spanning_edges(G_S, weight="weight", data=False)
|
||||
|
||||
# Leaf nodes that are not terminal might still remain; remove them here
|
||||
T_H = G.edge_subgraph(T_S).copy()
|
||||
_remove_nonterminal_leaves(T_H, terminal_nodes)
|
||||
|
||||
return T_H.edges()
|
||||
|
||||
|
||||
def _remove_nonterminal_leaves(G, terminals):
|
||||
terminal_set = set(terminals)
|
||||
leaves = {n for n in G if len(set(G[n]) - {n}) == 1}
|
||||
nonterminal_leaves = leaves - terminal_set
|
||||
|
||||
while nonterminal_leaves:
|
||||
# Removing a node may create new non-terminal leaves, so we limit
|
||||
# search for candidate non-terminal nodes to neighbors of current
|
||||
# non-terminal nodes
|
||||
candidate_leaves = set.union(*(set(G[n]) for n in nonterminal_leaves))
|
||||
candidate_leaves -= nonterminal_leaves | terminal_set
|
||||
# Remove current set of non-terminal nodes
|
||||
G.remove_nodes_from(nonterminal_leaves)
|
||||
# Find any new non-terminal nodes from the set of candidates
|
||||
leaves = {n for n in candidate_leaves if len(set(G[n]) - {n}) == 1}
|
||||
nonterminal_leaves = leaves - terminal_set
|
||||
|
||||
|
||||
ALGORITHMS = {
|
||||
"kou": _kou_steiner_tree,
|
||||
"mehlhorn": _mehlhorn_steiner_tree,
|
||||
}
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
|
||||
def steiner_tree(G, terminal_nodes, weight="weight", method=None):
|
||||
r"""Return an approximation to the minimum Steiner tree of a graph.
|
||||
|
||||
The minimum Steiner tree of `G` w.r.t a set of `terminal_nodes` (also *S*)
|
||||
is a tree within `G` that spans those nodes and has minimum size (sum of
|
||||
edge weights) among all such trees.
|
||||
|
||||
The approximation algorithm is specified with the `method` keyword
|
||||
argument. All three available algorithms produce a tree whose weight is
|
||||
within a ``(2 - (2 / l))`` factor of the weight of the optimal Steiner tree,
|
||||
where ``l`` is the minimum number of leaf nodes across all possible Steiner
|
||||
trees.
|
||||
|
||||
* ``"kou"`` [2]_ (runtime $O(|S| |V|^2)$) computes the minimum spanning tree of
|
||||
the subgraph of the metric closure of *G* induced by the terminal nodes,
|
||||
where the metric closure of *G* is the complete graph in which each edge is
|
||||
weighted by the shortest path distance between the nodes in *G*.
|
||||
|
||||
* ``"mehlhorn"`` [3]_ (runtime $O(|E|+|V|\log|V|)$) modifies Kou et al.'s
|
||||
algorithm, beginning by finding the closest terminal node for each
|
||||
non-terminal. This data is used to create a complete graph containing only
|
||||
the terminal nodes, in which edge is weighted with the shortest path
|
||||
distance between them. The algorithm then proceeds in the same way as Kou
|
||||
et al..
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
terminal_nodes : list
|
||||
A list of terminal nodes for which minimum steiner tree is
|
||||
to be found.
|
||||
|
||||
weight : string (default = 'weight')
|
||||
Use the edge attribute specified by this string as the edge weight.
|
||||
Any edge attribute not present defaults to 1.
|
||||
|
||||
method : string, optional (default = 'mehlhorn')
|
||||
The algorithm to use to approximate the Steiner tree.
|
||||
Supported options: 'kou', 'mehlhorn'.
|
||||
Other inputs produce a ValueError.
|
||||
|
||||
Returns
|
||||
-------
|
||||
NetworkX graph
|
||||
Approximation to the minimum steiner tree of `G` induced by
|
||||
`terminal_nodes` .
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If `G` is directed.
|
||||
|
||||
ValueError
|
||||
If the specified `method` is not supported.
|
||||
|
||||
Notes
|
||||
-----
|
||||
For multigraphs, the edge between two nodes with minimum weight is the
|
||||
edge put into the Steiner tree.
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Steiner_tree_problem on Wikipedia.
|
||||
https://en.wikipedia.org/wiki/Steiner_tree_problem
|
||||
.. [2] Kou, L., G. Markowsky, and L. Berman. 1981.
|
||||
‘A Fast Algorithm for Steiner Trees’.
|
||||
Acta Informatica 15 (2): 141–45.
|
||||
https://doi.org/10.1007/BF00288961.
|
||||
.. [3] Mehlhorn, Kurt. 1988.
|
||||
‘A Faster Approximation Algorithm for the Steiner Problem in Graphs’.
|
||||
Information Processing Letters 27 (3): 125–28.
|
||||
https://doi.org/10.1016/0020-0190(88)90066-X.
|
||||
"""
|
||||
if method is None:
|
||||
method = "mehlhorn"
|
||||
|
||||
try:
|
||||
algo = ALGORITHMS[method]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"{method} is not a valid choice for an algorithm.") from e
|
||||
|
||||
edges = algo(G, terminal_nodes, weight)
|
||||
# For multigraph we should add the minimal weight edge keys
|
||||
if G.is_multigraph():
|
||||
edges = (
|
||||
(u, v, min(G[u][v], key=lambda k: G[u][v][k][weight])) for u, v in edges
|
||||
)
|
||||
T = G.edge_subgraph(edges)
|
||||
return T
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import average_clustering
|
||||
|
||||
# This approximation has to be exact in regular graphs
|
||||
# with no triangles or with all possible triangles.
|
||||
|
||||
|
||||
def test_petersen():
|
||||
# Actual coefficient is 0
|
||||
G = nx.petersen_graph()
|
||||
assert average_clustering(G, trials=len(G) // 2) == nx.average_clustering(G)
|
||||
|
||||
|
||||
def test_petersen_seed():
|
||||
# Actual coefficient is 0
|
||||
G = nx.petersen_graph()
|
||||
assert average_clustering(G, trials=len(G) // 2, seed=1) == nx.average_clustering(G)
|
||||
|
||||
|
||||
def test_tetrahedral():
|
||||
# Actual coefficient is 1
|
||||
G = nx.tetrahedral_graph()
|
||||
assert average_clustering(G, trials=len(G) // 2) == nx.average_clustering(G)
|
||||
|
||||
|
||||
def test_dodecahedral():
|
||||
# Actual coefficient is 0
|
||||
G = nx.dodecahedral_graph()
|
||||
assert average_clustering(G, trials=len(G) // 2) == nx.average_clustering(G)
|
||||
|
||||
|
||||
def test_empty():
|
||||
G = nx.empty_graph(5)
|
||||
assert average_clustering(G, trials=len(G) // 2) == 0
|
||||
|
||||
|
||||
def test_complete():
|
||||
G = nx.complete_graph(5)
|
||||
assert average_clustering(G, trials=len(G) // 2) == 1
|
||||
G = nx.complete_graph(7)
|
||||
assert average_clustering(G, trials=len(G) // 2) == 1
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
"""Unit tests for the :mod:`networkx.algorithms.approximation.clique` module."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import (
|
||||
clique_removal,
|
||||
large_clique_size,
|
||||
max_clique,
|
||||
maximum_independent_set,
|
||||
)
|
||||
|
||||
|
||||
def is_independent_set(G, nodes):
|
||||
"""Returns True if and only if `nodes` is a clique in `G`.
|
||||
|
||||
`G` is a NetworkX graph. `nodes` is an iterable of nodes in
|
||||
`G`.
|
||||
|
||||
"""
|
||||
return G.subgraph(nodes).number_of_edges() == 0
|
||||
|
||||
|
||||
def is_clique(G, nodes):
|
||||
"""Returns True if and only if `nodes` is an independent set
|
||||
in `G`.
|
||||
|
||||
`G` is an undirected simple graph. `nodes` is an iterable of
|
||||
nodes in `G`.
|
||||
|
||||
"""
|
||||
H = G.subgraph(nodes)
|
||||
n = len(H)
|
||||
return H.number_of_edges() == n * (n - 1) // 2
|
||||
|
||||
|
||||
class TestCliqueRemoval:
|
||||
"""Unit tests for the
|
||||
:func:`~networkx.algorithms.approximation.clique_removal` function.
|
||||
|
||||
"""
|
||||
|
||||
def test_trivial_graph(self):
|
||||
G = nx.trivial_graph()
|
||||
independent_set, cliques = clique_removal(G)
|
||||
assert is_independent_set(G, independent_set)
|
||||
assert all(is_clique(G, clique) for clique in cliques)
|
||||
# In fact, we should only have 1-cliques, that is, singleton nodes.
|
||||
assert all(len(clique) == 1 for clique in cliques)
|
||||
|
||||
def test_complete_graph(self):
|
||||
G = nx.complete_graph(10)
|
||||
independent_set, cliques = clique_removal(G)
|
||||
assert is_independent_set(G, independent_set)
|
||||
assert all(is_clique(G, clique) for clique in cliques)
|
||||
|
||||
def test_barbell_graph(self):
|
||||
G = nx.barbell_graph(10, 5)
|
||||
independent_set, cliques = clique_removal(G)
|
||||
assert is_independent_set(G, independent_set)
|
||||
assert all(is_clique(G, clique) for clique in cliques)
|
||||
|
||||
|
||||
class TestMaxClique:
|
||||
"""Unit tests for the :func:`networkx.algorithms.approximation.max_clique`
|
||||
function.
|
||||
|
||||
"""
|
||||
|
||||
def test_null_graph(self):
|
||||
G = nx.null_graph()
|
||||
assert len(max_clique(G)) == 0
|
||||
|
||||
def test_complete_graph(self):
|
||||
graph = nx.complete_graph(30)
|
||||
# this should return the entire graph
|
||||
mc = max_clique(graph)
|
||||
assert 30 == len(mc)
|
||||
|
||||
def test_maximal_by_cardinality(self):
|
||||
"""Tests that the maximal clique is computed according to maximum
|
||||
cardinality of the sets.
|
||||
|
||||
For more information, see pull request #1531.
|
||||
|
||||
"""
|
||||
G = nx.complete_graph(5)
|
||||
G.add_edge(4, 5)
|
||||
clique = max_clique(G)
|
||||
assert len(clique) > 1
|
||||
|
||||
G = nx.lollipop_graph(30, 2)
|
||||
clique = max_clique(G)
|
||||
assert len(clique) > 2
|
||||
|
||||
|
||||
def test_large_clique_size():
|
||||
G = nx.complete_graph(9)
|
||||
nx.add_cycle(G, [9, 10, 11])
|
||||
G.add_edge(8, 9)
|
||||
G.add_edge(1, 12)
|
||||
G.add_node(13)
|
||||
|
||||
assert large_clique_size(G) == 9
|
||||
G.remove_node(5)
|
||||
assert large_clique_size(G) == 8
|
||||
G.remove_edge(2, 3)
|
||||
assert large_clique_size(G) == 7
|
||||
|
||||
|
||||
def test_independent_set():
|
||||
# smoke test
|
||||
G = nx.Graph()
|
||||
assert len(maximum_independent_set(G)) == 0
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import approximation as approx
|
||||
|
||||
|
||||
def test_global_node_connectivity():
|
||||
# Figure 1 chapter on Connectivity
|
||||
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),
|
||||
]
|
||||
)
|
||||
assert 2 == approx.local_node_connectivity(G, 1, 11)
|
||||
assert 2 == approx.node_connectivity(G)
|
||||
assert 2 == approx.node_connectivity(G, 1, 11)
|
||||
|
||||
|
||||
def test_white_harary1():
|
||||
# Figure 1b white and harary (2001)
|
||||
# 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)
|
||||
assert 1 == approx.node_connectivity(G)
|
||||
|
||||
|
||||
def test_complete_graphs():
|
||||
for n in range(5, 25, 5):
|
||||
G = nx.complete_graph(n)
|
||||
assert n - 1 == approx.node_connectivity(G)
|
||||
assert n - 1 == approx.node_connectivity(G, 0, 3)
|
||||
|
||||
|
||||
def test_empty_graphs():
|
||||
for k in range(5, 25, 5):
|
||||
G = nx.empty_graph(k)
|
||||
assert 0 == approx.node_connectivity(G)
|
||||
assert 0 == approx.node_connectivity(G, 0, 3)
|
||||
|
||||
|
||||
def test_petersen():
|
||||
G = nx.petersen_graph()
|
||||
assert 3 == approx.node_connectivity(G)
|
||||
assert 3 == approx.node_connectivity(G, 0, 5)
|
||||
|
||||
|
||||
# Approximation fails with tutte graph
|
||||
# def test_tutte():
|
||||
# G = nx.tutte_graph()
|
||||
# assert_equal(3, approx.node_connectivity(G))
|
||||
|
||||
|
||||
def test_dodecahedral():
|
||||
G = nx.dodecahedral_graph()
|
||||
assert 3 == approx.node_connectivity(G)
|
||||
assert 3 == approx.node_connectivity(G, 0, 5)
|
||||
|
||||
|
||||
def test_octahedral():
|
||||
G = nx.octahedral_graph()
|
||||
assert 4 == approx.node_connectivity(G)
|
||||
assert 4 == approx.node_connectivity(G, 0, 5)
|
||||
|
||||
|
||||
# Approximation can fail with icosahedral graph depending
|
||||
# on iteration order.
|
||||
# def test_icosahedral():
|
||||
# G=nx.icosahedral_graph()
|
||||
# assert_equal(5, approx.node_connectivity(G))
|
||||
# assert_equal(5, approx.node_connectivity(G, 0, 5))
|
||||
|
||||
|
||||
def test_only_source():
|
||||
G = nx.complete_graph(5)
|
||||
pytest.raises(nx.NetworkXError, approx.node_connectivity, G, s=0)
|
||||
|
||||
|
||||
def test_only_target():
|
||||
G = nx.complete_graph(5)
|
||||
pytest.raises(nx.NetworkXError, approx.node_connectivity, G, t=0)
|
||||
|
||||
|
||||
def test_missing_source():
|
||||
G = nx.path_graph(4)
|
||||
pytest.raises(nx.NetworkXError, approx.node_connectivity, G, 10, 1)
|
||||
|
||||
|
||||
def test_missing_target():
|
||||
G = nx.path_graph(4)
|
||||
pytest.raises(nx.NetworkXError, approx.node_connectivity, G, 1, 10)
|
||||
|
||||
|
||||
def test_source_equals_target():
|
||||
G = nx.complete_graph(5)
|
||||
pytest.raises(nx.NetworkXError, approx.local_node_connectivity, G, 0, 0)
|
||||
|
||||
|
||||
def test_directed_node_connectivity():
|
||||
G = nx.cycle_graph(10, create_using=nx.DiGraph()) # only one direction
|
||||
D = nx.cycle_graph(10).to_directed() # 2 reciprocal edges
|
||||
assert 1 == approx.node_connectivity(G)
|
||||
assert 1 == approx.node_connectivity(G, 1, 4)
|
||||
assert 2 == approx.node_connectivity(D)
|
||||
assert 2 == approx.node_connectivity(D, 1, 4)
|
||||
|
||||
|
||||
class TestAllPairsNodeConnectivityApprox:
|
||||
@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)
|
||||
cls.directed_gnp = nx.gnp_random_graph(30, 0.1, directed=True)
|
||||
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 = approx.all_pairs_node_connectivity(self.cycle)
|
||||
for source in K_undir:
|
||||
for target, k in K_undir[source].items():
|
||||
assert k == 2
|
||||
K_dir = approx.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 = approx.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 = approx.all_pairs_node_connectivity(self.path)
|
||||
for source in K_undir:
|
||||
for target, k in K_undir[source].items():
|
||||
assert k == 1
|
||||
K_dir = approx.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_cutoff(self):
|
||||
for G in [self.K10, self.K5, self.K20]:
|
||||
for mp in [2, 3, 4]:
|
||||
paths = approx.all_pairs_node_connectivity(G, cutoff=mp)
|
||||
for source in paths:
|
||||
for target, K in paths[source].items():
|
||||
assert K == mp
|
||||
|
||||
def test_all_pairs_connectivity_nbunch(self):
|
||||
G = nx.complete_graph(5)
|
||||
nbunch = [0, 2, 3]
|
||||
C = approx.all_pairs_node_connectivity(G, nbunch=nbunch)
|
||||
assert len(C) == len(nbunch)
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
import networkx.algorithms.approximation as approx
|
||||
|
||||
|
||||
def close_cliques_example(d=12, D=300, h=24, k=2):
|
||||
"""
|
||||
Hard example from Harb, Elfarouk, Kent Quanrud, and Chandra Chekuri.
|
||||
"Faster and scalable algorithms for densest subgraph and decomposition."
|
||||
Advances in Neural Information Processing Systems 35 (2022): 26966-26979.
|
||||
"""
|
||||
Kh = nx.complete_graph(h)
|
||||
KdD = nx.complete_bipartite_graph(d, D)
|
||||
G = nx.disjoint_union_all([KdD] + [Kh for _ in range(k)])
|
||||
best_density = d * D / (d + D) # of the complete bipartite graph
|
||||
return G, best_density, set(KdD.nodes)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("iterations", (1, 3))
|
||||
@pytest.mark.parametrize("n", range(4, 7))
|
||||
@pytest.mark.parametrize("method", ("greedy++", "fista"))
|
||||
def test_star(n, iterations, method):
|
||||
if method == "fista":
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
G = nx.star_graph(n)
|
||||
# The densest subgraph of a star network is the entire graph.
|
||||
# The peeling algorithm would peel all the vertices with degree 1,
|
||||
# and so should discover the densest subgraph in one iteration!
|
||||
d, S = approx.densest_subgraph(G, iterations=iterations, method=method)
|
||||
|
||||
assert d == pytest.approx(G.number_of_edges() / G.number_of_nodes())
|
||||
assert S == set(G) # The entire graph!
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ("greedy++", "fista"))
|
||||
def test_greedy_plus_plus_complete_graph(method):
|
||||
if method == "fista":
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
G = nx.complete_graph(4)
|
||||
# The density of a complete graph network is the entire graph: C(4, 2)/4
|
||||
# where C(n, 2) is n*(n-1)//2. The peeling algorithm would find
|
||||
# the densest subgraph in one iteration!
|
||||
d, S = approx.densest_subgraph(G, iterations=1, method=method)
|
||||
|
||||
assert d == pytest.approx(6 / 4) # The density, 4/5=0.8.
|
||||
assert S == {0, 1, 2, 3} # The entire graph!
|
||||
|
||||
|
||||
def test_greedy_plus_plus_close_cliques():
|
||||
G, best_density, densest_set = close_cliques_example()
|
||||
# NOTE: iterations=185 fails to ID the densest subgraph
|
||||
greedy_pp, S_pp = approx.densest_subgraph(G, iterations=186, method="greedy++")
|
||||
|
||||
assert greedy_pp == pytest.approx(best_density)
|
||||
assert S_pp == densest_set
|
||||
|
||||
|
||||
def test_fista_close_cliques():
|
||||
pytest.importorskip("numpy")
|
||||
G, best_density, best_set = close_cliques_example()
|
||||
# NOTE: iterations=12 fails to ID the densest subgraph
|
||||
density, dense_set = approx.densest_subgraph(G, iterations=13, method="fista")
|
||||
|
||||
assert density == pytest.approx(best_density)
|
||||
assert dense_set == best_set
|
||||
|
||||
|
||||
def bipartite_and_clique_example(d=5, D=200, k=2):
|
||||
"""
|
||||
Hard example from: Boob, Digvijay, Yu Gao, Richard Peng, Saurabh Sawlani,
|
||||
Charalampos Tsourakakis, Di Wang, and Junxing Wang. "Flowless: Extracting
|
||||
densest subgraphs without flow computations." In Proceedings of The Web
|
||||
Conference 2020, pp. 573-583. 2020.
|
||||
"""
|
||||
B = nx.complete_bipartite_graph(d, D)
|
||||
H = [nx.complete_graph(d + 2) for _ in range(k)]
|
||||
G = nx.disjoint_union_all([B] + H)
|
||||
|
||||
best_density = d * D / (d + D) # of the complete bipartite graph
|
||||
correct_one_round_density = (2 * d * D + (d + 1) * (d + 2) * k) / (
|
||||
2 * d + 2 * D + 2 * k * (d + 2)
|
||||
)
|
||||
best_subgraph = set(B.nodes)
|
||||
return G, best_density, best_subgraph, correct_one_round_density
|
||||
|
||||
|
||||
def test_greedy_plus_plus_bipartite_and_clique():
|
||||
G, best_density, best_subgraph, correct_one_iter_density = (
|
||||
bipartite_and_clique_example()
|
||||
)
|
||||
one_round_density, S_one = approx.densest_subgraph(
|
||||
G, iterations=1, method="greedy++"
|
||||
)
|
||||
assert one_round_density == pytest.approx(correct_one_iter_density)
|
||||
assert S_one == set(G.nodes)
|
||||
|
||||
ten_round_density, S_ten = approx.densest_subgraph(
|
||||
G, iterations=10, method="greedy++"
|
||||
)
|
||||
assert ten_round_density == pytest.approx(best_density)
|
||||
assert S_ten == best_subgraph
|
||||
|
||||
|
||||
def test_fista_bipartite_and_clique():
|
||||
pytest.importorskip("numpy")
|
||||
G, best_density, best_subgraph, _ = bipartite_and_clique_example()
|
||||
|
||||
ten_round_density, S_ten = approx.densest_subgraph(G, iterations=10, method="fista")
|
||||
assert ten_round_density == pytest.approx(best_density)
|
||||
assert S_ten == best_subgraph
|
||||
|
||||
|
||||
def test_fista_big_dataset():
|
||||
pytest.importorskip("numpy")
|
||||
G, best_density, best_subgraph = close_cliques_example(d=30, D=2000, h=60, k=20)
|
||||
|
||||
# Note: iterations=12 fails to identify densest subgraph
|
||||
density, dense_set = approx.densest_subgraph(G, iterations=13, method="fista")
|
||||
|
||||
assert density == pytest.approx(best_density)
|
||||
assert dense_set == best_subgraph
|
||||
|
||||
|
||||
@pytest.mark.parametrize("iterations", (1, 3))
|
||||
def test_greedy_plus_plus_edgeless_cornercase(iterations):
|
||||
G = nx.Graph()
|
||||
assert approx.densest_subgraph(G, iterations=iterations, method="greedy++") == (
|
||||
0,
|
||||
set(),
|
||||
)
|
||||
G.add_nodes_from(range(4))
|
||||
assert approx.densest_subgraph(G, iterations=iterations, method="greedy++") == (
|
||||
0,
|
||||
set(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("labels", ((1, 2, 3), ("a", "b", "c")))
|
||||
def test_gh_8271(labels):
|
||||
"""Test for graphs with nonstandard node labels."""
|
||||
pytest.importorskip("numpy")
|
||||
G = nx.complete_graph(labels)
|
||||
assert approx.densest_subgraph(G, method="fista") == (1, set(labels))
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
"""Unit tests for the :mod:`networkx.algorithms.approximation.distance_measures` module."""
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import diameter
|
||||
|
||||
|
||||
class TestDiameter:
|
||||
"""Unit tests for the approximate diameter function
|
||||
:func:`~networkx.algorithms.approximation.distance_measures.diameter`.
|
||||
"""
|
||||
|
||||
def test_null_graph(self):
|
||||
"""Test empty graph."""
|
||||
G = nx.null_graph()
|
||||
with pytest.raises(
|
||||
nx.NetworkXError, match="Expected non-empty NetworkX graph!"
|
||||
):
|
||||
diameter(G)
|
||||
|
||||
def test_undirected_non_connected(self):
|
||||
"""Test an undirected disconnected graph."""
|
||||
graph = nx.path_graph(10)
|
||||
graph.remove_edge(3, 4)
|
||||
with pytest.raises(nx.NetworkXError, match="Graph not connected."):
|
||||
diameter(graph)
|
||||
|
||||
def test_directed_non_strongly_connected(self):
|
||||
"""Test a directed non strongly connected graph."""
|
||||
graph = nx.path_graph(10, create_using=nx.DiGraph())
|
||||
with pytest.raises(nx.NetworkXError, match="DiGraph not strongly connected."):
|
||||
diameter(graph)
|
||||
|
||||
def test_complete_undirected_graph(self):
|
||||
"""Test a complete undirected graph."""
|
||||
graph = nx.complete_graph(10)
|
||||
assert diameter(graph) == 1
|
||||
|
||||
def test_complete_directed_graph(self):
|
||||
"""Test a complete directed graph."""
|
||||
graph = nx.complete_graph(10, create_using=nx.DiGraph())
|
||||
assert diameter(graph) == 1
|
||||
|
||||
def test_undirected_path_graph(self):
|
||||
"""Test an undirected path graph with 10 nodes."""
|
||||
graph = nx.path_graph(10)
|
||||
assert diameter(graph) == 9
|
||||
|
||||
def test_directed_path_graph(self):
|
||||
"""Test a directed path graph with 10 nodes."""
|
||||
graph = nx.path_graph(10).to_directed()
|
||||
assert diameter(graph) == 9
|
||||
|
||||
def test_single_node(self):
|
||||
"""Test a graph which contains just a node."""
|
||||
graph = nx.Graph()
|
||||
graph.add_node(1)
|
||||
assert diameter(graph) == 0
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import (
|
||||
min_edge_dominating_set,
|
||||
min_weighted_dominating_set,
|
||||
)
|
||||
|
||||
|
||||
class TestMinWeightDominatingSet:
|
||||
def test_min_weighted_dominating_set(self):
|
||||
graph = nx.Graph()
|
||||
graph.add_edge(1, 2)
|
||||
graph.add_edge(1, 5)
|
||||
graph.add_edge(2, 3)
|
||||
graph.add_edge(2, 5)
|
||||
graph.add_edge(3, 4)
|
||||
graph.add_edge(3, 6)
|
||||
graph.add_edge(5, 6)
|
||||
|
||||
vertices = {1, 2, 3, 4, 5, 6}
|
||||
# due to ties, this might be hard to test tight bounds
|
||||
dom_set = min_weighted_dominating_set(graph)
|
||||
for vertex in vertices - dom_set:
|
||||
neighbors = set(graph.neighbors(vertex))
|
||||
assert len(neighbors & dom_set) > 0, "Non dominating set found!"
|
||||
|
||||
def test_star_graph(self):
|
||||
"""Tests that an approximate dominating set for the star graph,
|
||||
even when the center node does not have the smallest integer
|
||||
label, gives just the center node.
|
||||
|
||||
For more information, see #1527.
|
||||
|
||||
"""
|
||||
# Create a star graph in which the center node has the highest
|
||||
# label instead of the lowest.
|
||||
G = nx.star_graph(10)
|
||||
G = nx.relabel_nodes(G, {0: 9, 9: 0})
|
||||
assert min_weighted_dominating_set(G) == {9}
|
||||
|
||||
def test_null_graph(self):
|
||||
"""Tests that the unique dominating set for the null graph is an empty set"""
|
||||
G = nx.Graph()
|
||||
assert min_weighted_dominating_set(G) == set()
|
||||
|
||||
def test_min_edge_dominating_set(self):
|
||||
graph = nx.path_graph(5)
|
||||
dom_set = min_edge_dominating_set(graph)
|
||||
|
||||
# this is a crappy way to test, but good enough for now.
|
||||
for edge in graph.edges():
|
||||
if edge in dom_set:
|
||||
continue
|
||||
else:
|
||||
u, v = edge
|
||||
found = False
|
||||
for dom_edge in dom_set:
|
||||
found |= u == dom_edge[0] or u == dom_edge[1]
|
||||
assert found, "Non adjacent edge found!"
|
||||
|
||||
graph = nx.complete_graph(10)
|
||||
dom_set = min_edge_dominating_set(graph)
|
||||
|
||||
# this is a crappy way to test, but good enough for now.
|
||||
for edge in graph.edges():
|
||||
if edge in dom_set:
|
||||
continue
|
||||
else:
|
||||
u, v = edge
|
||||
found = False
|
||||
for dom_edge in dom_set:
|
||||
found |= u == dom_edge[0] or u == dom_edge[1]
|
||||
assert found, "Non adjacent edge found!"
|
||||
|
||||
graph = nx.Graph() # empty Networkx graph
|
||||
with pytest.raises(ValueError, match="Expected non-empty NetworkX graph!"):
|
||||
min_edge_dominating_set(graph)
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
# Test for approximation to k-components algorithm
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import k_components
|
||||
from networkx.algorithms.approximation.kcomponents import _AntiGraph, _same
|
||||
|
||||
|
||||
def build_k_number_dict(k_components):
|
||||
k_num = {}
|
||||
for k, comps in sorted(k_components.items()):
|
||||
for comp in comps:
|
||||
for node in comp:
|
||||
k_num[node] = k
|
||||
return k_num
|
||||
|
||||
|
||||
##
|
||||
# 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_connectivity(G):
|
||||
result = k_components(G)
|
||||
for k, components in result.items():
|
||||
if k < 3:
|
||||
continue
|
||||
for component in components:
|
||||
C = G.subgraph(component)
|
||||
K = nx.node_connectivity(C)
|
||||
assert K >= k
|
||||
|
||||
|
||||
def test_torrents_and_ferraro_graph():
|
||||
G = torrents_and_ferraro_graph()
|
||||
_check_connectivity(G)
|
||||
|
||||
|
||||
def test_example_1():
|
||||
G = graph_example_1()
|
||||
_check_connectivity(G)
|
||||
|
||||
|
||||
def test_karate_0():
|
||||
G = nx.karate_club_graph()
|
||||
_check_connectivity(G)
|
||||
|
||||
|
||||
def test_karate_1():
|
||||
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,
|
||||
}
|
||||
approx_karate_k_num = karate_k_num.copy()
|
||||
approx_karate_k_num[24] = 2
|
||||
approx_karate_k_num[25] = 2
|
||||
G = nx.karate_club_graph()
|
||||
k_comps = k_components(G)
|
||||
k_num = build_k_number_dict(k_comps)
|
||||
assert k_num in (karate_k_num, approx_karate_k_num)
|
||||
|
||||
|
||||
def test_example_1_detail_3_and_4():
|
||||
G = graph_example_1()
|
||||
result = k_components(G)
|
||||
# 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])
|
||||
# Finally check that the k-components detected have actually node
|
||||
# connectivity >= k.
|
||||
for k, components in result.items():
|
||||
if k < 3:
|
||||
continue
|
||||
for component in components:
|
||||
K = nx.node_connectivity(G.subgraph(component))
|
||||
assert K >= k
|
||||
|
||||
|
||||
def test_directed():
|
||||
with pytest.raises(nx.NetworkXNotImplemented):
|
||||
G = nx.gnp_random_graph(10, 0.4, directed=True)
|
||||
kc = k_components(G)
|
||||
|
||||
|
||||
def test_same():
|
||||
equal = {"A": 2, "B": 2, "C": 2}
|
||||
slightly_different = {"A": 2, "B": 1, "C": 2}
|
||||
different = {"A": 2, "B": 8, "C": 18}
|
||||
assert _same(equal)
|
||||
assert not _same(slightly_different)
|
||||
assert _same(slightly_different, tol=1)
|
||||
assert not _same(different)
|
||||
assert not _same(different, tol=4)
|
||||
|
||||
|
||||
class TestAntiGraph:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.Gnp = nx.gnp_random_graph(20, 0.8, seed=42)
|
||||
cls.Anp = _AntiGraph(nx.complement(cls.Gnp))
|
||||
cls.Gd = nx.davis_southern_women_graph()
|
||||
cls.Ad = _AntiGraph(nx.complement(cls.Gd))
|
||||
cls.Gk = nx.karate_club_graph()
|
||||
cls.Ak = _AntiGraph(nx.complement(cls.Gk))
|
||||
cls.GA = [(cls.Gnp, cls.Anp), (cls.Gd, cls.Ad), (cls.Gk, cls.Ak)]
|
||||
|
||||
def test_size(self):
|
||||
for G, A in self.GA:
|
||||
n = G.order()
|
||||
s = len(list(G.edges())) + len(list(A.edges()))
|
||||
assert s == (n * (n - 1)) / 2
|
||||
|
||||
def test_degree(self):
|
||||
for G, A in self.GA:
|
||||
assert sorted(G.degree()) == sorted(A.degree())
|
||||
|
||||
def test_core_number(self):
|
||||
for G, A in self.GA:
|
||||
assert nx.core_number(G) == nx.core_number(A)
|
||||
|
||||
def test_connected_components(self):
|
||||
# ccs are same unless isolated nodes or any node has degree=len(G)-1
|
||||
# graphs in self.GA avoid this problem
|
||||
for G, A in self.GA:
|
||||
gc = [set(c) for c in nx.connected_components(G)]
|
||||
ac = [set(c) for c in nx.connected_components(A)]
|
||||
for comp in ac:
|
||||
assert comp in gc
|
||||
|
||||
def test_adj(self):
|
||||
for G, A in self.GA:
|
||||
for n, nbrs in G.adj.items():
|
||||
a_adj = sorted((n, sorted(ad)) for n, ad in A.adj.items())
|
||||
g_adj = sorted((n, sorted(ad)) for n, ad in G.adj.items())
|
||||
assert a_adj == g_adj
|
||||
|
||||
def test_adjacency(self):
|
||||
for G, A in self.GA:
|
||||
a_adj = list(A.adjacency())
|
||||
for n, nbrs in G.adjacency():
|
||||
assert (n, set(nbrs)) in a_adj
|
||||
|
||||
def test_neighbors(self):
|
||||
for G, A in self.GA:
|
||||
node = list(G.nodes())[0]
|
||||
assert set(G.neighbors(node)) == set(A.neighbors(node))
|
||||
|
||||
def test_node_not_in_graph(self):
|
||||
for G, A in self.GA:
|
||||
node = "non_existent_node"
|
||||
pytest.raises(nx.NetworkXError, A.neighbors, node)
|
||||
pytest.raises(nx.NetworkXError, G.neighbors, node)
|
||||
|
||||
def test_degree_thingraph(self):
|
||||
for G, A in self.GA:
|
||||
node = list(G.nodes())[0]
|
||||
nodes = list(G.nodes())[1:4]
|
||||
assert G.degree(node) == A.degree(node)
|
||||
assert sum(d for n, d in G.degree()) == sum(d for n, d in A.degree())
|
||||
# AntiGraph is a ThinGraph, so all the weights are 1
|
||||
assert sum(d for n, d in A.degree()) == sum(
|
||||
d for n, d in A.degree(weight="weight")
|
||||
)
|
||||
assert sum(d for n, d in G.degree(nodes)) == sum(
|
||||
d for n, d in A.degree(nodes)
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import networkx as nx
|
||||
import networkx.algorithms.approximation as a
|
||||
|
||||
|
||||
def test_min_maximal_matching():
|
||||
# smoke test
|
||||
G = nx.Graph()
|
||||
assert len(a.min_maximal_matching(G)) == 0
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import maxcut
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"f", (nx.approximation.randomized_partitioning, nx.approximation.one_exchange)
|
||||
)
|
||||
@pytest.mark.parametrize("graph_constructor", (nx.DiGraph, nx.MultiGraph))
|
||||
def test_raises_on_directed_and_multigraphs(f, graph_constructor):
|
||||
G = graph_constructor([(0, 1), (1, 2)])
|
||||
with pytest.raises(nx.NetworkXNotImplemented):
|
||||
f(G)
|
||||
|
||||
|
||||
def _is_valid_cut(G, set1, set2):
|
||||
union = set1.union(set2)
|
||||
assert union == set(G.nodes)
|
||||
assert len(set1) + len(set2) == G.number_of_nodes()
|
||||
|
||||
|
||||
def _cut_is_locally_optimal(G, cut_size, set1):
|
||||
# test if cut can be locally improved
|
||||
for i, node in enumerate(set1):
|
||||
cut_size_without_node = nx.algorithms.cut_size(
|
||||
G, set1 - {node}, weight="weight"
|
||||
)
|
||||
assert cut_size_without_node <= cut_size
|
||||
|
||||
|
||||
def test_random_partitioning():
|
||||
G = nx.complete_graph(5)
|
||||
_, (set1, set2) = maxcut.randomized_partitioning(G, seed=5)
|
||||
_is_valid_cut(G, set1, set2)
|
||||
|
||||
|
||||
def test_random_partitioning_all_to_one():
|
||||
G = nx.complete_graph(5)
|
||||
_, (set1, set2) = maxcut.randomized_partitioning(G, p=1)
|
||||
_is_valid_cut(G, set1, set2)
|
||||
assert len(set1) == G.number_of_nodes()
|
||||
assert len(set2) == 0
|
||||
|
||||
|
||||
def test_one_exchange_basic():
|
||||
G = nx.complete_graph(5)
|
||||
random.seed(5)
|
||||
for u, v, w in G.edges(data=True):
|
||||
w["weight"] = random.randrange(-100, 100, 1) / 10
|
||||
|
||||
initial_cut = set(random.sample(sorted(G.nodes()), k=5))
|
||||
cut_size, (set1, set2) = maxcut.one_exchange(
|
||||
G, initial_cut, weight="weight", seed=5
|
||||
)
|
||||
|
||||
_is_valid_cut(G, set1, set2)
|
||||
_cut_is_locally_optimal(G, cut_size, set1)
|
||||
|
||||
|
||||
def test_one_exchange_optimal():
|
||||
# Greedy one exchange should find the optimal solution for this graph (14)
|
||||
G = nx.Graph()
|
||||
G.add_edge(1, 2, weight=3)
|
||||
G.add_edge(1, 3, weight=3)
|
||||
G.add_edge(1, 4, weight=3)
|
||||
G.add_edge(1, 5, weight=3)
|
||||
G.add_edge(2, 3, weight=5)
|
||||
|
||||
cut_size, (set1, set2) = maxcut.one_exchange(G, weight="weight", seed=5)
|
||||
|
||||
_is_valid_cut(G, set1, set2)
|
||||
_cut_is_locally_optimal(G, cut_size, set1)
|
||||
# check global optimality
|
||||
assert cut_size == 14
|
||||
|
||||
|
||||
def test_negative_weights():
|
||||
G = nx.complete_graph(5)
|
||||
random.seed(5)
|
||||
for u, v, w in G.edges(data=True):
|
||||
w["weight"] = -1 * random.random()
|
||||
|
||||
initial_cut = set(random.sample(sorted(G.nodes()), k=5))
|
||||
cut_size, (set1, set2) = maxcut.one_exchange(G, initial_cut, weight="weight")
|
||||
|
||||
# make sure it is a valid cut
|
||||
_is_valid_cut(G, set1, set2)
|
||||
# check local optimality
|
||||
_cut_is_locally_optimal(G, cut_size, set1)
|
||||
# test that all nodes are in the same partition
|
||||
assert len(set1) == len(G.nodes) or len(set2) == len(G.nodes)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import networkx as nx
|
||||
import networkx.algorithms.approximation as apxa
|
||||
|
||||
|
||||
def test_ramsey():
|
||||
# this should only find the complete graph
|
||||
graph = nx.complete_graph(10)
|
||||
c, i = apxa.ramsey_R2(graph)
|
||||
cdens = nx.density(graph.subgraph(c))
|
||||
assert cdens == 1.0, "clique not correctly found by ramsey!"
|
||||
idens = nx.density(graph.subgraph(i))
|
||||
assert idens == 0.0, "i-set not correctly found by ramsey!"
|
||||
|
||||
# this trivial graph has no cliques. should just find i-sets
|
||||
graph = nx.trivial_graph()
|
||||
c, i = apxa.ramsey_R2(graph)
|
||||
assert c == {0}, "clique not correctly found by ramsey!"
|
||||
assert i == {0}, "i-set not correctly found by ramsey!"
|
||||
|
||||
graph = nx.barbell_graph(10, 5, nx.Graph())
|
||||
c, i = apxa.ramsey_R2(graph)
|
||||
cdens = nx.density(graph.subgraph(c))
|
||||
assert cdens == 1.0, "clique not correctly found by ramsey!"
|
||||
idens = nx.density(graph.subgraph(i))
|
||||
assert idens == 0.0, "i-set not correctly found by ramsey!"
|
||||
|
||||
# add self-loops and test again
|
||||
graph.add_edges_from([(n, n) for n in range(0, len(graph), 2)])
|
||||
cc, ii = apxa.ramsey_R2(graph)
|
||||
assert cc == c
|
||||
assert ii == i
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation.steinertree import (
|
||||
_remove_nonterminal_leaves,
|
||||
metric_closure,
|
||||
steiner_tree,
|
||||
)
|
||||
from networkx.utils import edges_equal
|
||||
|
||||
|
||||
@pytest.fixture(params=["kou", "mehlhorn"])
|
||||
def method(request):
|
||||
return request.param
|
||||
|
||||
|
||||
class TestSteinerTree:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
G1 = nx.Graph()
|
||||
G1.add_edge(1, 2, weight=10)
|
||||
G1.add_edge(2, 3, weight=10)
|
||||
G1.add_edge(3, 4, weight=10)
|
||||
G1.add_edge(4, 5, weight=10)
|
||||
G1.add_edge(5, 6, weight=10)
|
||||
G1.add_edge(2, 7, weight=1)
|
||||
G1.add_edge(7, 5, weight=1)
|
||||
|
||||
G2 = nx.Graph()
|
||||
G2.add_edge(0, 5, weight=6)
|
||||
G2.add_edge(1, 2, weight=2)
|
||||
G2.add_edge(1, 5, weight=3)
|
||||
G2.add_edge(2, 4, weight=4)
|
||||
G2.add_edge(3, 5, weight=5)
|
||||
G2.add_edge(4, 5, weight=1)
|
||||
|
||||
G3 = nx.Graph()
|
||||
G3.add_edge(1, 2, weight=8)
|
||||
G3.add_edge(1, 9, weight=3)
|
||||
G3.add_edge(1, 8, weight=6)
|
||||
G3.add_edge(1, 10, weight=2)
|
||||
G3.add_edge(1, 14, weight=3)
|
||||
G3.add_edge(2, 3, weight=6)
|
||||
G3.add_edge(3, 4, weight=3)
|
||||
G3.add_edge(3, 10, weight=2)
|
||||
G3.add_edge(3, 11, weight=1)
|
||||
G3.add_edge(4, 5, weight=1)
|
||||
G3.add_edge(4, 11, weight=1)
|
||||
G3.add_edge(5, 6, weight=4)
|
||||
G3.add_edge(5, 11, weight=2)
|
||||
G3.add_edge(5, 12, weight=1)
|
||||
G3.add_edge(5, 13, weight=3)
|
||||
G3.add_edge(6, 7, weight=2)
|
||||
G3.add_edge(6, 12, weight=3)
|
||||
G3.add_edge(6, 13, weight=1)
|
||||
G3.add_edge(7, 8, weight=3)
|
||||
G3.add_edge(7, 9, weight=3)
|
||||
G3.add_edge(7, 11, weight=5)
|
||||
G3.add_edge(7, 13, weight=2)
|
||||
G3.add_edge(7, 14, weight=4)
|
||||
G3.add_edge(8, 9, weight=2)
|
||||
G3.add_edge(9, 14, weight=1)
|
||||
G3.add_edge(10, 11, weight=2)
|
||||
G3.add_edge(10, 14, weight=1)
|
||||
G3.add_edge(11, 12, weight=1)
|
||||
G3.add_edge(11, 14, weight=7)
|
||||
G3.add_edge(12, 14, weight=3)
|
||||
G3.add_edge(12, 15, weight=1)
|
||||
G3.add_edge(13, 14, weight=4)
|
||||
G3.add_edge(13, 15, weight=1)
|
||||
G3.add_edge(14, 15, weight=2)
|
||||
|
||||
G4 = nx.Graph()
|
||||
G4.add_edge(0, 2, my_weight=2)
|
||||
G4.add_edge(0, 1, my_weight=0.1)
|
||||
G4.add_edge(1, 2, my_weight=0.1)
|
||||
G4.add_edge(2, 3, my_weight=1)
|
||||
G4.add_edge(2, 4)
|
||||
|
||||
cls.G1 = G1
|
||||
cls.G2 = G2
|
||||
cls.G3 = G3
|
||||
cls.G4 = G4
|
||||
|
||||
cls.G1_term_nodes = [1, 2, 3, 4, 5]
|
||||
cls.G2_term_nodes = [0, 2, 3]
|
||||
cls.G3_term_nodes = [1, 3, 5, 6, 8, 10, 11, 12, 13]
|
||||
cls.G4_term_nodes = [0, 3, 4]
|
||||
|
||||
def test_connected_metric_closure(self):
|
||||
G = self.G1.copy()
|
||||
G.add_node(100)
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
with pytest.deprecated_call():
|
||||
metric_closure(G)
|
||||
|
||||
def test_metric_closure(self):
|
||||
with pytest.deprecated_call():
|
||||
M = metric_closure(self.G1)
|
||||
mc = [
|
||||
(1, 2, {"distance": 10, "path": [1, 2]}),
|
||||
(1, 3, {"distance": 20, "path": [1, 2, 3]}),
|
||||
(1, 4, {"distance": 22, "path": [1, 2, 7, 5, 4]}),
|
||||
(1, 5, {"distance": 12, "path": [1, 2, 7, 5]}),
|
||||
(1, 6, {"distance": 22, "path": [1, 2, 7, 5, 6]}),
|
||||
(1, 7, {"distance": 11, "path": [1, 2, 7]}),
|
||||
(2, 3, {"distance": 10, "path": [2, 3]}),
|
||||
(2, 4, {"distance": 12, "path": [2, 7, 5, 4]}),
|
||||
(2, 5, {"distance": 2, "path": [2, 7, 5]}),
|
||||
(2, 6, {"distance": 12, "path": [2, 7, 5, 6]}),
|
||||
(2, 7, {"distance": 1, "path": [2, 7]}),
|
||||
(3, 4, {"distance": 10, "path": [3, 4]}),
|
||||
(3, 5, {"distance": 12, "path": [3, 2, 7, 5]}),
|
||||
(3, 6, {"distance": 22, "path": [3, 2, 7, 5, 6]}),
|
||||
(3, 7, {"distance": 11, "path": [3, 2, 7]}),
|
||||
(4, 5, {"distance": 10, "path": [4, 5]}),
|
||||
(4, 6, {"distance": 20, "path": [4, 5, 6]}),
|
||||
(4, 7, {"distance": 11, "path": [4, 5, 7]}),
|
||||
(5, 6, {"distance": 10, "path": [5, 6]}),
|
||||
(5, 7, {"distance": 1, "path": [5, 7]}),
|
||||
(6, 7, {"distance": 11, "path": [6, 5, 7]}),
|
||||
]
|
||||
assert edges_equal(list(M.edges(data=True)), mc)
|
||||
|
||||
def test_steiner_tree(self, method):
|
||||
valid_steiner_trees = [
|
||||
[
|
||||
[
|
||||
(1, 2, {"weight": 10}),
|
||||
(2, 3, {"weight": 10}),
|
||||
(2, 7, {"weight": 1}),
|
||||
(3, 4, {"weight": 10}),
|
||||
(5, 7, {"weight": 1}),
|
||||
],
|
||||
[
|
||||
(1, 2, {"weight": 10}),
|
||||
(2, 7, {"weight": 1}),
|
||||
(3, 4, {"weight": 10}),
|
||||
(4, 5, {"weight": 10}),
|
||||
(5, 7, {"weight": 1}),
|
||||
],
|
||||
[
|
||||
(1, 2, {"weight": 10}),
|
||||
(2, 3, {"weight": 10}),
|
||||
(2, 7, {"weight": 1}),
|
||||
(4, 5, {"weight": 10}),
|
||||
(5, 7, {"weight": 1}),
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
(0, 5, {"weight": 6}),
|
||||
(1, 2, {"weight": 2}),
|
||||
(1, 5, {"weight": 3}),
|
||||
(3, 5, {"weight": 5}),
|
||||
],
|
||||
[
|
||||
(0, 5, {"weight": 6}),
|
||||
(4, 2, {"weight": 4}),
|
||||
(4, 5, {"weight": 1}),
|
||||
(3, 5, {"weight": 5}),
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
(1, 10, {"weight": 2}),
|
||||
(3, 10, {"weight": 2}),
|
||||
(3, 11, {"weight": 1}),
|
||||
(5, 12, {"weight": 1}),
|
||||
(6, 13, {"weight": 1}),
|
||||
(8, 9, {"weight": 2}),
|
||||
(9, 14, {"weight": 1}),
|
||||
(10, 14, {"weight": 1}),
|
||||
(11, 12, {"weight": 1}),
|
||||
(12, 15, {"weight": 1}),
|
||||
(13, 15, {"weight": 1}),
|
||||
]
|
||||
],
|
||||
]
|
||||
for G, term_nodes, valid_trees in zip(
|
||||
[self.G1, self.G2, self.G3],
|
||||
[self.G1_term_nodes, self.G2_term_nodes, self.G3_term_nodes],
|
||||
valid_steiner_trees,
|
||||
):
|
||||
S = steiner_tree(G, term_nodes, method=method)
|
||||
assert any(
|
||||
edges_equal(list(S.edges(data=True)), valid_tree)
|
||||
for valid_tree in valid_trees
|
||||
)
|
||||
|
||||
def test_multigraph_steiner_tree(self, method):
|
||||
G = nx.MultiGraph()
|
||||
G.add_edges_from(
|
||||
[
|
||||
(1, 2, 0, {"weight": 1}),
|
||||
(2, 3, 0, {"weight": 999}),
|
||||
(2, 3, 1, {"weight": 1}),
|
||||
(3, 4, 0, {"weight": 1}),
|
||||
(3, 5, 0, {"weight": 1}),
|
||||
]
|
||||
)
|
||||
terminal_nodes = [2, 4, 5]
|
||||
expected_edges = [
|
||||
(2, 3, 1, {"weight": 1}), # edge with key 1 has lower weight
|
||||
(3, 4, 0, {"weight": 1}),
|
||||
(3, 5, 0, {"weight": 1}),
|
||||
]
|
||||
S = steiner_tree(G, terminal_nodes, method=method)
|
||||
assert edges_equal(S.edges(data=True, keys=True), expected_edges)
|
||||
|
||||
def test_remove_nonterminal_leaves(self):
|
||||
G = nx.path_graph(10)
|
||||
_remove_nonterminal_leaves(G, [4, 5, 6])
|
||||
|
||||
assert list(G) == [4, 5, 6] # only the terminal nodes are left
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("weight", "expected_edges"),
|
||||
[
|
||||
(
|
||||
None,
|
||||
[
|
||||
(0, 2, {"my_weight": 2}),
|
||||
(2, 3, {"my_weight": 1}),
|
||||
(2, 4, {}),
|
||||
],
|
||||
),
|
||||
(
|
||||
"my_weight",
|
||||
[
|
||||
(0, 1, {"my_weight": 0.1}),
|
||||
(1, 2, {"my_weight": 0.1}),
|
||||
(2, 3, {"my_weight": 1}),
|
||||
(2, 4, {}),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_weighted(self, method, weight, expected_edges):
|
||||
G = self.G4
|
||||
terminal_nodes = self.G4_term_nodes
|
||||
|
||||
S = steiner_tree(G, terminal_nodes, method=method, weight=weight)
|
||||
assert edges_equal(list(S.edges(data=True)), expected_edges)
|
||||
|
||||
|
||||
def test_steiner_tree_weight_attribute(method):
|
||||
G = nx.star_graph(4)
|
||||
# Add an edge attribute that is named something other than "weight"
|
||||
nx.set_edge_attributes(G, {e: 10 for e in G.edges}, name="distance")
|
||||
H = nx.approximation.steiner_tree(G, [1, 3], method=method, weight="distance")
|
||||
assert nx.utils.edges_equal(H.edges, [(0, 1), (0, 3)])
|
||||
|
||||
|
||||
def test_steiner_tree_multigraph_weight_attribute(method):
|
||||
G = nx.cycle_graph(3, create_using=nx.MultiGraph)
|
||||
nx.set_edge_attributes(G, {e: 10 for e in G.edges}, name="distance")
|
||||
G.add_edge(2, 0, distance=5)
|
||||
H = nx.approximation.steiner_tree(G, list(G), method=method, weight="distance")
|
||||
assert len(H.edges) == 2 and H.has_edge(2, 0, key=1)
|
||||
assert sum(dist for *_, dist in H.edges(data="distance")) == 15
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", (None, "mehlhorn", "kou"))
|
||||
def test_steiner_tree_methods(method):
|
||||
G = nx.star_graph(4)
|
||||
expected = nx.Graph([(0, 1), (0, 3)])
|
||||
st = nx.approximation.steiner_tree(G, [1, 3], method=method)
|
||||
assert nx.utils.edges_equal(st.edges, expected.edges)
|
||||
|
||||
|
||||
def test_steiner_tree_method_invalid():
|
||||
G = nx.star_graph(4)
|
||||
with pytest.raises(
|
||||
ValueError, match="invalid_method is not a valid choice for an algorithm."
|
||||
):
|
||||
nx.approximation.steiner_tree(G, terminal_nodes=[1, 3], method="invalid_method")
|
||||
|
||||
|
||||
def test_steiner_tree_remove_non_terminal_leaves_self_loop_edges():
|
||||
# To verify that the last step of the steiner tree approximation
|
||||
# behaves in the case where a non-terminal leaf has a self loop edge
|
||||
G = nx.path_graph(10)
|
||||
|
||||
# Add self loops to the terminal nodes
|
||||
G.add_edges_from([(2, 2), (3, 3), (4, 4), (7, 7), (8, 8)])
|
||||
|
||||
# Remove non-terminal leaves
|
||||
_remove_nonterminal_leaves(G, [4, 5, 6, 7])
|
||||
|
||||
# The terminal nodes should be left
|
||||
assert list(G) == [4, 5, 6, 7] # only the terminal nodes are left
|
||||
|
||||
|
||||
def test_steiner_tree_non_terminal_leaves_multigraph_self_loop_edges():
|
||||
# To verify that the last step of the steiner tree approximation
|
||||
# behaves in the case where a non-terminal leaf has a self loop edge
|
||||
G = nx.MultiGraph()
|
||||
G.add_edges_from([(i, i + 1) for i in range(10)])
|
||||
G.add_edges_from([(2, 2), (3, 3), (4, 4), (4, 4), (7, 7)])
|
||||
|
||||
# Remove non-terminal leaves
|
||||
_remove_nonterminal_leaves(G, [4, 5, 6, 7])
|
||||
|
||||
# Only the terminal nodes should be left
|
||||
assert list(G) == [4, 5, 6, 7]
|
||||
+1014
File diff suppressed because it is too large
Load Diff
+274
@@ -0,0 +1,274 @@
|
||||
import itertools
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import (
|
||||
treewidth_min_degree,
|
||||
treewidth_min_fill_in,
|
||||
)
|
||||
from networkx.algorithms.approximation.treewidth import (
|
||||
MinDegreeHeuristic,
|
||||
min_fill_in_heuristic,
|
||||
)
|
||||
|
||||
|
||||
def is_tree_decomp(graph, decomp):
|
||||
"""Check if the given tree decomposition is valid."""
|
||||
for x in graph.nodes():
|
||||
appear_once = False
|
||||
for bag in decomp.nodes():
|
||||
if x in bag:
|
||||
appear_once = True
|
||||
break
|
||||
assert appear_once
|
||||
|
||||
# Check if each connected pair of nodes are at least once together in a bag
|
||||
for x, y in graph.edges():
|
||||
appear_together = False
|
||||
for bag in decomp.nodes():
|
||||
if x in bag and y in bag:
|
||||
appear_together = True
|
||||
break
|
||||
assert appear_together
|
||||
|
||||
# Check if the nodes associated with vertex v form a connected subset of T
|
||||
for v in graph.nodes():
|
||||
subset = []
|
||||
for bag in decomp.nodes():
|
||||
if v in bag:
|
||||
subset.append(bag)
|
||||
sub_graph = decomp.subgraph(subset)
|
||||
assert nx.is_connected(sub_graph)
|
||||
|
||||
|
||||
class TestTreewidthMinDegree:
|
||||
"""Unit tests for the min_degree function"""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Setup for different kinds of trees"""
|
||||
cls.complete = nx.Graph()
|
||||
cls.complete.add_edge(1, 2)
|
||||
cls.complete.add_edge(2, 3)
|
||||
cls.complete.add_edge(1, 3)
|
||||
|
||||
cls.small_tree = nx.Graph()
|
||||
cls.small_tree.add_edge(1, 3)
|
||||
cls.small_tree.add_edge(4, 3)
|
||||
cls.small_tree.add_edge(2, 3)
|
||||
cls.small_tree.add_edge(3, 5)
|
||||
cls.small_tree.add_edge(5, 6)
|
||||
cls.small_tree.add_edge(5, 7)
|
||||
cls.small_tree.add_edge(6, 7)
|
||||
|
||||
cls.deterministic_graph = nx.Graph()
|
||||
cls.deterministic_graph.add_edge(0, 1) # deg(0) = 1
|
||||
|
||||
cls.deterministic_graph.add_edge(1, 2) # deg(1) = 2
|
||||
|
||||
cls.deterministic_graph.add_edge(2, 3)
|
||||
cls.deterministic_graph.add_edge(2, 4) # deg(2) = 3
|
||||
|
||||
cls.deterministic_graph.add_edge(3, 4)
|
||||
cls.deterministic_graph.add_edge(3, 5)
|
||||
cls.deterministic_graph.add_edge(3, 6) # deg(3) = 4
|
||||
|
||||
cls.deterministic_graph.add_edge(4, 5)
|
||||
cls.deterministic_graph.add_edge(4, 6)
|
||||
cls.deterministic_graph.add_edge(4, 7) # deg(4) = 5
|
||||
|
||||
cls.deterministic_graph.add_edge(5, 6)
|
||||
cls.deterministic_graph.add_edge(5, 7)
|
||||
cls.deterministic_graph.add_edge(5, 8)
|
||||
cls.deterministic_graph.add_edge(5, 9) # deg(5) = 6
|
||||
|
||||
cls.deterministic_graph.add_edge(6, 7)
|
||||
cls.deterministic_graph.add_edge(6, 8)
|
||||
cls.deterministic_graph.add_edge(6, 9) # deg(6) = 6
|
||||
|
||||
cls.deterministic_graph.add_edge(7, 8)
|
||||
cls.deterministic_graph.add_edge(7, 9) # deg(7) = 5
|
||||
|
||||
cls.deterministic_graph.add_edge(8, 9) # deg(8) = 4
|
||||
|
||||
def test_petersen_graph(self):
|
||||
"""Test Petersen graph tree decomposition result"""
|
||||
G = nx.petersen_graph()
|
||||
_, decomp = treewidth_min_degree(G)
|
||||
is_tree_decomp(G, decomp)
|
||||
|
||||
def test_small_tree_treewidth(self):
|
||||
"""Test small tree
|
||||
|
||||
Test if the computed treewidth of the known self.small_tree is 2.
|
||||
As we know which value we can expect from our heuristic, values other
|
||||
than two are regressions
|
||||
"""
|
||||
G = self.small_tree
|
||||
# the order of removal should be [1,2,4]3[5,6,7]
|
||||
# (with [] denoting any order of the containing nodes)
|
||||
# resulting in treewidth 2 for the heuristic
|
||||
treewidth, _ = treewidth_min_fill_in(G)
|
||||
assert treewidth == 2
|
||||
|
||||
def test_heuristic_abort(self):
|
||||
"""Test heuristic abort condition for fully connected graph"""
|
||||
graph = {}
|
||||
for u in self.complete:
|
||||
graph[u] = set()
|
||||
for v in self.complete[u]:
|
||||
if u != v: # ignore self-loop
|
||||
graph[u].add(v)
|
||||
|
||||
deg_heuristic = MinDegreeHeuristic(graph)
|
||||
node = deg_heuristic.best_node(graph)
|
||||
if node is None:
|
||||
pass
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_empty_graph(self):
|
||||
"""Test empty graph"""
|
||||
G = nx.Graph()
|
||||
_, _ = treewidth_min_degree(G)
|
||||
|
||||
def test_two_component_graph(self):
|
||||
G = nx.Graph()
|
||||
G.add_node(1)
|
||||
G.add_node(2)
|
||||
treewidth, _ = treewidth_min_degree(G)
|
||||
assert treewidth == 0
|
||||
|
||||
def test_not_sortable_nodes(self):
|
||||
G = nx.Graph([(0, "a")])
|
||||
treewidth_min_degree(G)
|
||||
|
||||
def test_heuristic_first_steps(self):
|
||||
"""Test first steps of min_degree heuristic"""
|
||||
graph = {
|
||||
n: set(self.deterministic_graph[n]) - {n} for n in self.deterministic_graph
|
||||
}
|
||||
deg_heuristic = MinDegreeHeuristic(graph)
|
||||
elim_node = deg_heuristic.best_node(graph)
|
||||
steps = []
|
||||
|
||||
while elim_node is not None:
|
||||
steps.append(elim_node)
|
||||
nbrs = graph[elim_node]
|
||||
|
||||
for u, v in itertools.permutations(nbrs, 2):
|
||||
if v not in graph[u]:
|
||||
graph[u].add(v)
|
||||
|
||||
for u in graph:
|
||||
if elim_node in graph[u]:
|
||||
graph[u].remove(elim_node)
|
||||
|
||||
del graph[elim_node]
|
||||
elim_node = deg_heuristic.best_node(graph)
|
||||
|
||||
# check only the first 5 elements for equality
|
||||
assert steps[:5] == [0, 1, 2, 3, 4]
|
||||
|
||||
|
||||
class TestTreewidthMinFillIn:
|
||||
"""Unit tests for the treewidth_min_fill_in function."""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Setup for different kinds of trees"""
|
||||
cls.complete = nx.Graph()
|
||||
cls.complete.add_edge(1, 2)
|
||||
cls.complete.add_edge(2, 3)
|
||||
cls.complete.add_edge(1, 3)
|
||||
|
||||
cls.small_tree = nx.Graph()
|
||||
cls.small_tree.add_edge(1, 2)
|
||||
cls.small_tree.add_edge(2, 3)
|
||||
cls.small_tree.add_edge(3, 4)
|
||||
cls.small_tree.add_edge(1, 4)
|
||||
cls.small_tree.add_edge(2, 4)
|
||||
cls.small_tree.add_edge(4, 5)
|
||||
cls.small_tree.add_edge(5, 6)
|
||||
cls.small_tree.add_edge(5, 7)
|
||||
cls.small_tree.add_edge(6, 7)
|
||||
|
||||
cls.deterministic_graph = nx.Graph()
|
||||
cls.deterministic_graph.add_edge(1, 2)
|
||||
cls.deterministic_graph.add_edge(1, 3)
|
||||
cls.deterministic_graph.add_edge(3, 4)
|
||||
cls.deterministic_graph.add_edge(2, 4)
|
||||
cls.deterministic_graph.add_edge(3, 5)
|
||||
cls.deterministic_graph.add_edge(4, 5)
|
||||
cls.deterministic_graph.add_edge(3, 6)
|
||||
cls.deterministic_graph.add_edge(5, 6)
|
||||
|
||||
def test_petersen_graph(self):
|
||||
"""Test Petersen graph tree decomposition result"""
|
||||
G = nx.petersen_graph()
|
||||
_, decomp = treewidth_min_fill_in(G)
|
||||
is_tree_decomp(G, decomp)
|
||||
|
||||
def test_small_tree_treewidth(self):
|
||||
"""Test if the computed treewidth of the known self.small_tree is 2"""
|
||||
G = self.small_tree
|
||||
# the order of removal should be [1,2,4]3[5,6,7]
|
||||
# (with [] denoting any order of the containing nodes)
|
||||
# resulting in treewidth 2 for the heuristic
|
||||
treewidth, _ = treewidth_min_fill_in(G)
|
||||
assert treewidth == 2
|
||||
|
||||
def test_heuristic_abort(self):
|
||||
"""Test if min_fill_in returns None for fully connected graph"""
|
||||
graph = {}
|
||||
for u in self.complete:
|
||||
graph[u] = set()
|
||||
for v in self.complete[u]:
|
||||
if u != v: # ignore self-loop
|
||||
graph[u].add(v)
|
||||
next_node = min_fill_in_heuristic(graph)
|
||||
if next_node is None:
|
||||
pass
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_empty_graph(self):
|
||||
"""Test empty graph"""
|
||||
G = nx.Graph()
|
||||
_, _ = treewidth_min_fill_in(G)
|
||||
|
||||
def test_two_component_graph(self):
|
||||
G = nx.Graph()
|
||||
G.add_node(1)
|
||||
G.add_node(2)
|
||||
treewidth, _ = treewidth_min_fill_in(G)
|
||||
assert treewidth == 0
|
||||
|
||||
def test_not_sortable_nodes(self):
|
||||
G = nx.Graph([(0, "a")])
|
||||
treewidth_min_fill_in(G)
|
||||
|
||||
def test_heuristic_first_steps(self):
|
||||
"""Test first steps of min_fill_in heuristic"""
|
||||
graph = {
|
||||
n: set(self.deterministic_graph[n]) - {n} for n in self.deterministic_graph
|
||||
}
|
||||
elim_node = min_fill_in_heuristic(graph)
|
||||
steps = []
|
||||
|
||||
while elim_node is not None:
|
||||
steps.append(elim_node)
|
||||
nbrs = graph[elim_node]
|
||||
|
||||
for u, v in itertools.permutations(nbrs, 2):
|
||||
if v not in graph[u]:
|
||||
graph[u].add(v)
|
||||
|
||||
for u in graph:
|
||||
if elim_node in graph[u]:
|
||||
graph[u].remove(elim_node)
|
||||
|
||||
del graph[elim_node]
|
||||
elim_node = min_fill_in_heuristic(graph)
|
||||
|
||||
# check only the first 2 elements for equality
|
||||
assert steps[:2] == [6, 5]
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import min_weighted_vertex_cover
|
||||
|
||||
|
||||
def is_cover(G, node_cover):
|
||||
return all({u, v} & node_cover for u, v in G.edges())
|
||||
|
||||
|
||||
class TestMWVC:
|
||||
"""Unit tests for the approximate minimum weighted vertex cover
|
||||
function,
|
||||
:func:`~networkx.algorithms.approximation.vertex_cover.min_weighted_vertex_cover`.
|
||||
|
||||
"""
|
||||
|
||||
def test_unweighted_directed(self):
|
||||
# Create a star graph in which half the nodes are directed in
|
||||
# and half are directed out.
|
||||
G = nx.DiGraph()
|
||||
G.add_edges_from((0, v) for v in range(1, 26))
|
||||
G.add_edges_from((v, 0) for v in range(26, 51))
|
||||
cover = min_weighted_vertex_cover(G)
|
||||
assert 1 == len(cover)
|
||||
assert is_cover(G, cover)
|
||||
|
||||
def test_unweighted_undirected(self):
|
||||
# create a simple star graph
|
||||
size = 50
|
||||
sg = nx.star_graph(size)
|
||||
cover = min_weighted_vertex_cover(sg)
|
||||
assert 1 == len(cover)
|
||||
assert is_cover(sg, cover)
|
||||
|
||||
def test_weighted(self):
|
||||
wg = nx.Graph()
|
||||
wg.add_node(0, weight=10)
|
||||
wg.add_node(1, weight=1)
|
||||
wg.add_node(2, weight=1)
|
||||
wg.add_node(3, weight=1)
|
||||
wg.add_node(4, weight=1)
|
||||
|
||||
wg.add_edge(0, 1)
|
||||
wg.add_edge(0, 2)
|
||||
wg.add_edge(0, 3)
|
||||
wg.add_edge(0, 4)
|
||||
|
||||
wg.add_edge(1, 2)
|
||||
wg.add_edge(2, 3)
|
||||
wg.add_edge(3, 4)
|
||||
wg.add_edge(4, 1)
|
||||
|
||||
cover = min_weighted_vertex_cover(wg, weight="weight")
|
||||
csum = sum(wg.nodes[node]["weight"] for node in cover)
|
||||
assert 4 == csum
|
||||
assert is_cover(wg, cover)
|
||||
|
||||
def test_unweighted_self_loop(self):
|
||||
slg = nx.Graph()
|
||||
slg.add_node(0)
|
||||
slg.add_node(1)
|
||||
slg.add_node(2)
|
||||
|
||||
slg.add_edge(0, 1)
|
||||
slg.add_edge(2, 2)
|
||||
|
||||
cover = min_weighted_vertex_cover(slg)
|
||||
assert 2 == len(cover)
|
||||
assert is_cover(slg, cover)
|
||||
+1508
File diff suppressed because it is too large
Load Diff
+255
@@ -0,0 +1,255 @@
|
||||
"""Functions for computing treewidth decomposition.
|
||||
|
||||
Treewidth of an undirected graph is a number associated with the graph.
|
||||
It can be defined as the size of the largest vertex set (bag) in a tree
|
||||
decomposition of the graph minus one.
|
||||
|
||||
`Wikipedia: Treewidth <https://en.wikipedia.org/wiki/Treewidth>`_
|
||||
|
||||
The notions of treewidth and tree decomposition have gained their
|
||||
attractiveness partly because many graph and network problems that are
|
||||
intractable (e.g., NP-hard) on arbitrary graphs become efficiently
|
||||
solvable (e.g., with a linear time algorithm) when the treewidth of the
|
||||
input graphs is bounded by a constant [1]_ [2]_.
|
||||
|
||||
There are two different functions for computing a tree decomposition:
|
||||
:func:`treewidth_min_degree` and :func:`treewidth_min_fill_in`.
|
||||
|
||||
.. [1] Hans L. Bodlaender and Arie M. C. A. Koster. 2010. "Treewidth
|
||||
computations I.Upper bounds". Inf. Comput. 208, 3 (March 2010),259-275.
|
||||
http://dx.doi.org/10.1016/j.ic.2009.03.008
|
||||
|
||||
.. [2] Hans L. Bodlaender. "Discovering Treewidth". Institute of Information
|
||||
and Computing Sciences, Utrecht University.
|
||||
Technical Report UU-CS-2005-018.
|
||||
http://www.cs.uu.nl
|
||||
|
||||
.. [3] K. Wang, Z. Lu, and J. Hicks *Treewidth*.
|
||||
https://web.archive.org/web/20210507025929/http://web.eecs.utk.edu/~cphill25/cs594_spring2015_projects/treewidth.pdf
|
||||
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import sys
|
||||
from heapq import heapify, heappop, heappush
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = ["treewidth_min_degree", "treewidth_min_fill_in"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable(returns_graph=True)
|
||||
def treewidth_min_degree(G):
|
||||
"""Returns a treewidth decomposition using the Minimum Degree heuristic.
|
||||
|
||||
The heuristic chooses the nodes according to their degree, i.e., first
|
||||
the node with the lowest degree is chosen, then the graph is updated
|
||||
and the corresponding node is removed. Next, a new node with the lowest
|
||||
degree is chosen, and so on.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
Treewidth decomposition : (int, Graph) tuple
|
||||
2-tuple with treewidth and the corresponding decomposed tree.
|
||||
"""
|
||||
deg_heuristic = MinDegreeHeuristic(G)
|
||||
return treewidth_decomp(G, lambda graph: deg_heuristic.best_node(graph))
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable(returns_graph=True)
|
||||
def treewidth_min_fill_in(G):
|
||||
"""Returns a treewidth decomposition using the Minimum Fill-in heuristic.
|
||||
|
||||
The heuristic chooses a node from the graph, where the number of edges
|
||||
added turning the neighborhood of the chosen node into clique is as
|
||||
small as possible.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
Treewidth decomposition : (int, Graph) tuple
|
||||
2-tuple with treewidth and the corresponding decomposed tree.
|
||||
"""
|
||||
return treewidth_decomp(G, min_fill_in_heuristic)
|
||||
|
||||
|
||||
class MinDegreeHeuristic:
|
||||
"""Implements the Minimum Degree heuristic.
|
||||
|
||||
The heuristic chooses the nodes according to their degree
|
||||
(number of neighbors), i.e., first the node with the lowest degree is
|
||||
chosen, then the graph is updated and the corresponding node is
|
||||
removed. Next, a new node with the lowest degree is chosen, and so on.
|
||||
"""
|
||||
|
||||
def __init__(self, graph):
|
||||
self._graph = graph
|
||||
|
||||
# nodes that have to be updated in the heap before each iteration
|
||||
self._update_nodes = []
|
||||
|
||||
self._degreeq = [] # a heapq with 3-tuples (degree,unique_id,node)
|
||||
self.count = itertools.count()
|
||||
|
||||
# build heap with initial degrees
|
||||
for n in graph:
|
||||
self._degreeq.append((len(graph[n]), next(self.count), n))
|
||||
heapify(self._degreeq)
|
||||
|
||||
def best_node(self, graph):
|
||||
# update nodes in self._update_nodes
|
||||
for n in self._update_nodes:
|
||||
# insert changed degrees into degreeq
|
||||
heappush(self._degreeq, (len(graph[n]), next(self.count), n))
|
||||
|
||||
# get the next valid (minimum degree) node
|
||||
while self._degreeq:
|
||||
(min_degree, _, elim_node) = heappop(self._degreeq)
|
||||
if elim_node not in graph or len(graph[elim_node]) != min_degree:
|
||||
# outdated entry in degreeq
|
||||
continue
|
||||
elif min_degree == len(graph) - 1:
|
||||
# fully connected: abort condition
|
||||
return None
|
||||
|
||||
# remember to update nodes in the heap before getting the next node
|
||||
self._update_nodes = graph[elim_node]
|
||||
return elim_node
|
||||
|
||||
# the heap is empty: abort
|
||||
return None
|
||||
|
||||
|
||||
def min_fill_in_heuristic(graph_dict):
|
||||
"""Implements the Minimum Degree heuristic.
|
||||
|
||||
graph_dict: dict keyed by node to sets of neighbors (no self-loops)
|
||||
|
||||
Returns the node from the graph, where the number of edges added when
|
||||
turning the neighborhood of the chosen node into clique is as small as
|
||||
possible. This algorithm chooses the nodes using the Minimum Fill-In
|
||||
heuristic. The running time of the algorithm is :math:`O(V^3)` and it uses
|
||||
additional constant memory.
|
||||
"""
|
||||
|
||||
if len(graph_dict) == 0:
|
||||
return None
|
||||
|
||||
min_fill_in_node = None
|
||||
|
||||
min_fill_in = sys.maxsize
|
||||
|
||||
# sort nodes by degree
|
||||
nodes_by_degree = sorted(graph_dict, key=lambda x: len(graph_dict[x]))
|
||||
min_degree = len(graph_dict[nodes_by_degree[0]])
|
||||
|
||||
# abort condition (handle complete graph)
|
||||
if min_degree == len(graph_dict) - 1:
|
||||
return None
|
||||
|
||||
for node in nodes_by_degree:
|
||||
num_fill_in = 0
|
||||
nbrs = graph_dict[node]
|
||||
for nbr in nbrs:
|
||||
# count how many nodes in nbrs current nbr is not connected to
|
||||
# subtract 1 for the node itself
|
||||
num_fill_in += len(nbrs - graph_dict[nbr]) - 1
|
||||
if num_fill_in >= 2 * min_fill_in:
|
||||
break
|
||||
|
||||
num_fill_in /= 2 # divide by 2 because of double counting
|
||||
|
||||
if num_fill_in < min_fill_in: # update min-fill-in node
|
||||
if num_fill_in == 0:
|
||||
return node
|
||||
min_fill_in = num_fill_in
|
||||
min_fill_in_node = node
|
||||
|
||||
return min_fill_in_node
|
||||
|
||||
|
||||
@nx._dispatchable(returns_graph=True)
|
||||
def treewidth_decomp(G, heuristic=min_fill_in_heuristic):
|
||||
"""Returns a treewidth decomposition using the passed heuristic.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
heuristic : heuristic function
|
||||
|
||||
Returns
|
||||
-------
|
||||
Treewidth decomposition : (int, Graph) tuple
|
||||
2-tuple with treewidth and the corresponding decomposed tree.
|
||||
"""
|
||||
|
||||
# make dict-of-sets structure
|
||||
graph_dict = {n: set(G[n]) - {n} for n in G}
|
||||
|
||||
# stack containing nodes and neighbors in the order from the heuristic
|
||||
node_stack = []
|
||||
|
||||
# get first node from heuristic
|
||||
elim_node = heuristic(graph_dict)
|
||||
while elim_node is not None:
|
||||
# connect all neighbors with each other
|
||||
nbrs = graph_dict[elim_node]
|
||||
for u, v in itertools.permutations(nbrs, 2):
|
||||
if v not in graph_dict[u]:
|
||||
graph_dict[u].add(v)
|
||||
|
||||
# push node and its current neighbors on stack
|
||||
node_stack.append((elim_node, nbrs))
|
||||
|
||||
# remove node from graph_dict
|
||||
for u in graph_dict[elim_node]:
|
||||
graph_dict[u].remove(elim_node)
|
||||
|
||||
del graph_dict[elim_node]
|
||||
elim_node = heuristic(graph_dict)
|
||||
|
||||
# the abort condition is met; put all remaining nodes into one bag
|
||||
decomp = nx.Graph()
|
||||
first_bag = frozenset(graph_dict.keys())
|
||||
decomp.add_node(first_bag)
|
||||
|
||||
treewidth = len(first_bag) - 1
|
||||
|
||||
while node_stack:
|
||||
# get node and its neighbors from the stack
|
||||
(curr_node, nbrs) = node_stack.pop()
|
||||
|
||||
# find a bag all neighbors are in
|
||||
old_bag = None
|
||||
for bag in decomp.nodes:
|
||||
if nbrs <= bag:
|
||||
old_bag = bag
|
||||
break
|
||||
|
||||
if old_bag is None:
|
||||
# no old_bag was found: just connect to the first_bag
|
||||
old_bag = first_bag
|
||||
|
||||
# create new node for decomposition
|
||||
nbrs.add(curr_node)
|
||||
new_bag = frozenset(nbrs)
|
||||
|
||||
# update treewidth
|
||||
treewidth = max(treewidth, len(new_bag) - 1)
|
||||
|
||||
# add edge to decomposition (implicitly also adds the new node)
|
||||
decomp.add_edge(old_bag, new_bag)
|
||||
|
||||
return treewidth, decomp
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
"""Functions for computing an approximate minimum weight vertex cover.
|
||||
|
||||
A |vertex cover|_ is a subset of nodes such that each edge in the graph
|
||||
is incident to at least one node in the subset.
|
||||
|
||||
.. _vertex cover: https://en.wikipedia.org/wiki/Vertex_cover
|
||||
.. |vertex cover| replace:: *vertex cover*
|
||||
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["min_weighted_vertex_cover"]
|
||||
|
||||
|
||||
@nx._dispatchable(node_attrs="weight")
|
||||
def min_weighted_vertex_cover(G, weight=None):
|
||||
r"""Returns an approximate minimum weighted vertex cover.
|
||||
|
||||
The set of nodes returned by this function is guaranteed to be a
|
||||
vertex cover, and the total weight of the set is guaranteed to be at
|
||||
most twice the total weight of the minimum weight vertex cover. In
|
||||
other words,
|
||||
|
||||
.. math::
|
||||
|
||||
w(S) \leq 2 * w(S^*),
|
||||
|
||||
where $S$ is the vertex cover returned by this function,
|
||||
$S^*$ is the vertex cover of minimum weight out of all vertex
|
||||
covers of the graph, and $w$ is the function that computes the
|
||||
sum of the weights of each node in that given set.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
weight : string, optional (default = None)
|
||||
If None, every node has weight 1. If a string, use this node
|
||||
attribute as the node weight. A node without this attribute is
|
||||
assumed to have weight 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
min_weighted_cover : set
|
||||
Returns a set of nodes whose weight sum is no more than twice
|
||||
the weight sum of the minimum weight vertex cover.
|
||||
|
||||
Notes
|
||||
-----
|
||||
For a directed graph, a vertex cover has the same definition: a set
|
||||
of nodes such that each edge in the graph is incident to at least
|
||||
one node in the set. Whether the node is the head or tail of the
|
||||
directed edge is ignored.
|
||||
|
||||
This is the local-ratio algorithm for computing an approximate
|
||||
vertex cover. The algorithm greedily reduces the costs over edges,
|
||||
iteratively building a cover. The worst-case runtime of this
|
||||
implementation is $O(m \log n)$, where $n$ is the number
|
||||
of nodes and $m$ the number of edges in the graph.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Bar-Yehuda, R., and Even, S. (1985). "A local-ratio theorem for
|
||||
approximating the weighted vertex cover problem."
|
||||
*Annals of Discrete Mathematics*, 25, 27–46
|
||||
<http://www.cs.technion.ac.il/~reuven/PDF/vc_lr.pdf>
|
||||
|
||||
"""
|
||||
cost = dict(G.nodes(data=weight, default=1))
|
||||
# While there are uncovered edges, choose an uncovered and update
|
||||
# the cost of the remaining edges.
|
||||
cover = set()
|
||||
for u, v in G.edges():
|
||||
if u in cover or v in cover:
|
||||
continue
|
||||
if cost[u] <= cost[v]:
|
||||
cover.add(u)
|
||||
cost[v] -= cost[u]
|
||||
else:
|
||||
cover.add(v)
|
||||
cost[u] -= cost[v]
|
||||
return cover
|
||||
Reference in New Issue
Block a user