Restore 0.1.5 version from stash

This commit is contained in:
liaibo
2025-12-08 19:56:24 +08:00
parent de189e938d
commit 8db3f4e32d
8578 changed files with 2703426 additions and 217 deletions
@@ -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), 180196. 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), 180196. 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), 180196. 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
@@ -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
@@ -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}.")
@@ -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 Goldbergs 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)
@@ -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))
@@ -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)
@@ -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})
@@ -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)
@@ -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): 14145.
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): 12528.
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
@@ -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
@@ -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
@@ -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)
@@ -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))
@@ -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
@@ -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)
@@ -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)
)
@@ -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
@@ -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)
@@ -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
@@ -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]
@@ -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]
@@ -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)
@@ -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
@@ -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, 2746
<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