I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,6 @@
from .branchings import *
from .coding import *
from .mst import *
from .recognition import *
from .operations import *
from .decomposition import *

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,413 @@
"""Functions for encoding and decoding trees.
Since a tree is a highly restricted form of graph, it can be represented
concisely in several ways. This module includes functions for encoding
and decoding trees in the form of nested tuples and Prüfer
sequences. The former requires a rooted tree, whereas the latter can be
applied to unrooted trees. Furthermore, there is a bijection from Prüfer
sequences to labeled trees.
"""
from collections import Counter
from itertools import chain
import networkx as nx
from networkx.utils import not_implemented_for
__all__ = [
"from_nested_tuple",
"from_prufer_sequence",
"NotATree",
"to_nested_tuple",
"to_prufer_sequence",
]
class NotATree(nx.NetworkXException):
"""Raised when a function expects a tree (that is, a connected
undirected graph with no cycles) but gets a non-tree graph as input
instead.
"""
@not_implemented_for("directed")
@nx._dispatchable(graphs="T")
def to_nested_tuple(T, root, canonical_form=False):
"""Returns a nested tuple representation of the given tree.
The nested tuple representation of a tree is defined
recursively. The tree with one node and no edges is represented by
the empty tuple, ``()``. A tree with ``k`` subtrees is represented
by a tuple of length ``k`` in which each element is the nested tuple
representation of a subtree.
Parameters
----------
T : NetworkX graph
An undirected graph object representing a tree.
root : node
The node in ``T`` to interpret as the root of the tree.
canonical_form : bool
If ``True``, each tuple is sorted so that the function returns
a canonical form for rooted trees. This means "lighter" subtrees
will appear as nested tuples before "heavier" subtrees. In this
way, each isomorphic rooted tree has the same nested tuple
representation.
Returns
-------
tuple
A nested tuple representation of the tree.
Notes
-----
This function is *not* the inverse of :func:`from_nested_tuple`; the
only guarantee is that the rooted trees are isomorphic.
See also
--------
from_nested_tuple
to_prufer_sequence
Examples
--------
The tree need not be a balanced binary tree::
>>> T = nx.Graph()
>>> T.add_edges_from([(0, 1), (0, 2), (0, 3)])
>>> T.add_edges_from([(1, 4), (1, 5)])
>>> T.add_edges_from([(3, 6), (3, 7)])
>>> root = 0
>>> nx.to_nested_tuple(T, root)
(((), ()), (), ((), ()))
Continuing the above example, if ``canonical_form`` is ``True``, the
nested tuples will be sorted::
>>> nx.to_nested_tuple(T, root, canonical_form=True)
((), ((), ()), ((), ()))
Even the path graph can be interpreted as a tree::
>>> T = nx.path_graph(4)
>>> root = 0
>>> nx.to_nested_tuple(T, root)
((((),),),)
"""
def _make_tuple(T, root, _parent):
"""Recursively compute the nested tuple representation of the
given rooted tree.
``_parent`` is the parent node of ``root`` in the supertree in
which ``T`` is a subtree, or ``None`` if ``root`` is the root of
the supertree. This argument is used to determine which
neighbors of ``root`` are children and which is the parent.
"""
# Get the neighbors of `root` that are not the parent node. We
# are guaranteed that `root` is always in `T` by construction.
children = set(T[root]) - {_parent}
if len(children) == 0:
return ()
nested = (_make_tuple(T, v, root) for v in children)
if canonical_form:
nested = sorted(nested)
return tuple(nested)
# Do some sanity checks on the input.
if not nx.is_tree(T):
raise nx.NotATree("provided graph is not a tree")
if root not in T:
raise nx.NodeNotFound(f"Graph {T} contains no node {root}")
return _make_tuple(T, root, None)
@nx._dispatchable(graphs=None, returns_graph=True)
def from_nested_tuple(sequence, sensible_relabeling=False):
"""Returns the rooted tree corresponding to the given nested tuple.
The nested tuple representation of a tree is defined
recursively. The tree with one node and no edges is represented by
the empty tuple, ``()``. A tree with ``k`` subtrees is represented
by a tuple of length ``k`` in which each element is the nested tuple
representation of a subtree.
Parameters
----------
sequence : tuple
A nested tuple representing a rooted tree.
sensible_relabeling : bool
Whether to relabel the nodes of the tree so that nodes are
labeled in increasing order according to their breadth-first
search order from the root node.
Returns
-------
NetworkX graph
The tree corresponding to the given nested tuple, whose root
node is node 0. If ``sensible_labeling`` is ``True``, nodes will
be labeled in breadth-first search order starting from the root
node.
Notes
-----
This function is *not* the inverse of :func:`to_nested_tuple`; the
only guarantee is that the rooted trees are isomorphic.
See also
--------
to_nested_tuple
from_prufer_sequence
Examples
--------
Sensible relabeling ensures that the nodes are labeled from the root
starting at 0::
>>> balanced = (((), ()), ((), ()))
>>> T = nx.from_nested_tuple(balanced, sensible_relabeling=True)
>>> edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
>>> all((u, v) in T.edges() or (v, u) in T.edges() for (u, v) in edges)
True
"""
def _make_tree(sequence):
"""Recursively creates a tree from the given sequence of nested
tuples.
This function employs the :func:`~networkx.tree.join` function
to recursively join subtrees into a larger tree.
"""
# The empty sequence represents the empty tree, which is the
# (unique) graph with a single node. We mark the single node
# with an attribute that indicates that it is the root of the
# graph.
if len(sequence) == 0:
return nx.empty_graph(1)
# For a nonempty sequence, get the subtrees for each child
# sequence and join all the subtrees at their roots. After
# joining the subtrees, the root is node 0.
return nx.tree.join_trees([(_make_tree(child), 0) for child in sequence])
# Make the tree and remove the `is_root` node attribute added by the
# helper function.
T = _make_tree(sequence)
if sensible_relabeling:
# Relabel the nodes according to their breadth-first search
# order, starting from the root node (that is, the node 0).
bfs_nodes = chain([0], (v for u, v in nx.bfs_edges(T, 0)))
labels = {v: i for i, v in enumerate(bfs_nodes)}
# We would like to use `copy=False`, but `relabel_nodes` doesn't
# allow a relabel mapping that can't be topologically sorted.
T = nx.relabel_nodes(T, labels)
return T
@not_implemented_for("directed")
@nx._dispatchable(graphs="T")
def to_prufer_sequence(T):
r"""Returns the Prüfer sequence of the given tree.
A *Prüfer sequence* is a list of *n* - 2 numbers between 0 and
*n* - 1, inclusive. The tree corresponding to a given Prüfer
sequence can be recovered by repeatedly joining a node in the
sequence with a node with the smallest potential degree according to
the sequence.
Parameters
----------
T : NetworkX graph
An undirected graph object representing a tree.
Returns
-------
list
The Prüfer sequence of the given tree.
Raises
------
NetworkXPointlessConcept
If the number of nodes in `T` is less than two.
NotATree
If `T` is not a tree.
KeyError
If the set of nodes in `T` is not {0, …, *n* - 1}.
Notes
-----
There is a bijection from labeled trees to Prüfer sequences. This
function is the inverse of the :func:`from_prufer_sequence`
function.
Sometimes Prüfer sequences use nodes labeled from 1 to *n* instead
of from 0 to *n* - 1. This function requires nodes to be labeled in
the latter form. You can use :func:`~networkx.relabel_nodes` to
relabel the nodes of your tree to the appropriate format.
This implementation is from [1]_ and has a running time of
$O(n)$.
See also
--------
to_nested_tuple
from_prufer_sequence
References
----------
.. [1] Wang, Xiaodong, Lei Wang, and Yingjie Wu.
"An optimal algorithm for Prufer codes."
*Journal of Software Engineering and Applications* 2.02 (2009): 111.
<https://doi.org/10.4236/jsea.2009.22016>
Examples
--------
There is a bijection between Prüfer sequences and labeled trees, so
this function is the inverse of the :func:`from_prufer_sequence`
function:
>>> edges = [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)]
>>> tree = nx.Graph(edges)
>>> sequence = nx.to_prufer_sequence(tree)
>>> sequence
[3, 3, 3, 4]
>>> tree2 = nx.from_prufer_sequence(sequence)
>>> list(tree2.edges()) == edges
True
"""
# Perform some sanity checks on the input.
n = len(T)
if n < 2:
msg = "Prüfer sequence undefined for trees with fewer than two nodes"
raise nx.NetworkXPointlessConcept(msg)
if not nx.is_tree(T):
raise nx.NotATree("provided graph is not a tree")
if set(T) != set(range(n)):
raise KeyError("tree must have node labels {0, ..., n - 1}")
degree = dict(T.degree())
def parents(u):
return next(v for v in T[u] if degree[v] > 1)
index = u = next(k for k in range(n) if degree[k] == 1)
result = []
for i in range(n - 2):
v = parents(u)
result.append(v)
degree[v] -= 1
if v < index and degree[v] == 1:
u = v
else:
index = u = next(k for k in range(index + 1, n) if degree[k] == 1)
return result
@nx._dispatchable(graphs=None, returns_graph=True)
def from_prufer_sequence(sequence):
r"""Returns the tree corresponding to the given Prüfer sequence.
A *Prüfer sequence* is a list of *n* - 2 numbers between 0 and
*n* - 1, inclusive. The tree corresponding to a given Prüfer
sequence can be recovered by repeatedly joining a node in the
sequence with a node with the smallest potential degree according to
the sequence.
Parameters
----------
sequence : list
A Prüfer sequence, which is a list of *n* - 2 integers between
zero and *n* - 1, inclusive.
Returns
-------
NetworkX graph
The tree corresponding to the given Prüfer sequence.
Raises
------
NetworkXError
If the Prüfer sequence is not valid.
Notes
-----
There is a bijection from labeled trees to Prüfer sequences. This
function is the inverse of the :func:`from_prufer_sequence` function.
Sometimes Prüfer sequences use nodes labeled from 1 to *n* instead
of from 0 to *n* - 1. This function requires nodes to be labeled in
the latter form. You can use :func:`networkx.relabel_nodes` to
relabel the nodes of your tree to the appropriate format.
This implementation is from [1]_ and has a running time of
$O(n)$.
References
----------
.. [1] Wang, Xiaodong, Lei Wang, and Yingjie Wu.
"An optimal algorithm for Prufer codes."
*Journal of Software Engineering and Applications* 2.02 (2009): 111.
<https://doi.org/10.4236/jsea.2009.22016>
See also
--------
from_nested_tuple
to_prufer_sequence
Examples
--------
There is a bijection between Prüfer sequences and labeled trees, so
this function is the inverse of the :func:`to_prufer_sequence`
function:
>>> edges = [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)]
>>> tree = nx.Graph(edges)
>>> sequence = nx.to_prufer_sequence(tree)
>>> sequence
[3, 3, 3, 4]
>>> tree2 = nx.from_prufer_sequence(sequence)
>>> list(tree2.edges()) == edges
True
"""
n = len(sequence) + 2
# `degree` stores the remaining degree (plus one) for each node. The
# degree of a node in the decoded tree is one more than the number
# of times it appears in the code.
degree = Counter(chain(sequence, range(n)))
T = nx.empty_graph(n)
# `not_orphaned` is the set of nodes that have a parent in the
# tree. After the loop, there should be exactly two nodes that are
# not in this set.
not_orphaned = set()
index = u = next(k for k in range(n) if degree[k] == 1)
for v in sequence:
# check the validity of the prufer sequence
if v < 0 or v > n - 1:
raise nx.NetworkXError(
f"Invalid Prufer sequence: Values must be between 0 and {n-1}, got {v}"
)
T.add_edge(u, v)
not_orphaned.add(u)
degree[v] -= 1
if v < index and degree[v] == 1:
u = v
else:
index = u = next(k for k in range(index + 1, n) if degree[k] == 1)
# At this point, there must be exactly two orphaned nodes; join them.
orphans = set(T) - not_orphaned
u, v = orphans
T.add_edge(u, v)
return T

View File

@ -0,0 +1,88 @@
r"""Function for computing a junction tree of a graph."""
from itertools import combinations
import networkx as nx
from networkx.algorithms import chordal_graph_cliques, complete_to_chordal_graph, moral
from networkx.utils import not_implemented_for
__all__ = ["junction_tree"]
@not_implemented_for("multigraph")
@nx._dispatchable(returns_graph=True)
def junction_tree(G):
r"""Returns a junction tree of a given graph.
A junction tree (or clique tree) is constructed from a (un)directed graph G.
The tree is constructed based on a moralized and triangulated version of G.
The tree's nodes consist of maximal cliques and sepsets of the revised graph.
The sepset of two cliques is the intersection of the nodes of these cliques,
e.g. the sepset of (A,B,C) and (A,C,E,F) is (A,C). These nodes are often called
"variables" in this literature. The tree is bipartite with each sepset
connected to its two cliques.
Junction Trees are not unique as the order of clique consideration determines
which sepsets are included.
The junction tree algorithm consists of five steps [1]_:
1. Moralize the graph
2. Triangulate the graph
3. Find maximal cliques
4. Build the tree from cliques, connecting cliques with shared
nodes, set edge-weight to number of shared variables
5. Find maximum spanning tree
Parameters
----------
G : networkx.Graph
Directed or undirected graph.
Returns
-------
junction_tree : networkx.Graph
The corresponding junction tree of `G`.
Raises
------
NetworkXNotImplemented
Raised if `G` is an instance of `MultiGraph` or `MultiDiGraph`.
References
----------
.. [1] Junction tree algorithm:
https://en.wikipedia.org/wiki/Junction_tree_algorithm
.. [2] Finn V. Jensen and Frank Jensen. 1994. Optimal
junction trees. In Proceedings of the Tenth international
conference on Uncertainty in artificial intelligence (UAI94).
Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 360366.
"""
clique_graph = nx.Graph()
if G.is_directed():
G = moral.moral_graph(G)
chordal_graph, _ = complete_to_chordal_graph(G)
cliques = [tuple(sorted(i)) for i in chordal_graph_cliques(chordal_graph)]
clique_graph.add_nodes_from(cliques, type="clique")
for edge in combinations(cliques, 2):
set_edge_0 = set(edge[0])
set_edge_1 = set(edge[1])
if not set_edge_0.isdisjoint(set_edge_1):
sepset = tuple(sorted(set_edge_0.intersection(set_edge_1)))
clique_graph.add_edge(edge[0], edge[1], weight=len(sepset), sepset=sepset)
junction_tree = nx.maximum_spanning_tree(clique_graph)
for edge in list(junction_tree.edges(data=True)):
junction_tree.add_node(edge[2]["sepset"], type="sepset")
junction_tree.add_edge(edge[0], edge[2]["sepset"])
junction_tree.add_edge(edge[1], edge[2]["sepset"])
junction_tree.remove_edge(edge[0], edge[1])
return junction_tree

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
"""Operations on trees."""
from functools import partial
from itertools import accumulate, chain
import networkx as nx
__all__ = ["join_trees"]
# Argument types don't match dispatching, but allow manual selection of backend
@nx._dispatchable(graphs=None, returns_graph=True)
def join_trees(rooted_trees, *, label_attribute=None, first_label=0):
"""Returns a new rooted tree made by joining `rooted_trees`
Constructs a new tree by joining each tree in `rooted_trees`.
A new root node is added and connected to each of the roots
of the input trees. While copying the nodes from the trees,
relabeling to integers occurs. If the `label_attribute` is provided,
the old node labels will be stored in the new tree under this attribute.
Parameters
----------
rooted_trees : list
A list of pairs in which each left element is a NetworkX graph
object representing a tree and each right element is the root
node of that tree. The nodes of these trees will be relabeled to
integers.
label_attribute : str
If provided, the old node labels will be stored in the new tree
under this node attribute. If not provided, the original labels
of the nodes in the input trees are not stored.
first_label : int, optional (default=0)
Specifies the label for the new root node. If provided, the root node of the joined tree
will have this label. If not provided, the root node will default to a label of 0.
Returns
-------
NetworkX graph
The rooted tree resulting from joining the provided `rooted_trees`. The new tree has a root node
labeled as specified by `first_label` (defaulting to 0 if not provided). Subtrees from the input
`rooted_trees` are attached to this new root node. Each non-root node, if the `label_attribute`
is provided, has an attribute that indicates the original label of the node in the input tree.
Notes
-----
Trees are stored in NetworkX as NetworkX Graphs. There is no specific
enforcement of the fact that these are trees. Testing for each tree
can be done using :func:`networkx.is_tree`.
Graph, edge, and node attributes are propagated from the given
rooted trees to the created tree. If there are any overlapping graph
attributes, those from later trees will overwrite those from earlier
trees in the tuple of positional arguments.
Examples
--------
Join two full balanced binary trees of height *h* to get a full
balanced binary tree of depth *h* + 1::
>>> h = 4
>>> left = nx.balanced_tree(2, h)
>>> right = nx.balanced_tree(2, h)
>>> joined_tree = nx.join_trees([(left, 0), (right, 0)])
>>> nx.is_isomorphic(joined_tree, nx.balanced_tree(2, h + 1))
True
"""
if not rooted_trees:
return nx.empty_graph(1)
# Unzip the zipped list of (tree, root) pairs.
trees, roots = zip(*rooted_trees)
# The join of the trees has the same type as the type of the first tree.
R = type(trees[0])()
lengths = (len(tree) for tree in trees[:-1])
first_labels = list(accumulate(lengths, initial=first_label + 1))
new_roots = []
for tree, root, first_node in zip(trees, roots, first_labels):
new_root = first_node + list(tree.nodes()).index(root)
new_roots.append(new_root)
# Relabel the nodes so that their union is the integers starting at first_label.
relabel = partial(
nx.convert_node_labels_to_integers, label_attribute=label_attribute
)
new_trees = [
relabel(tree, first_label=first_label)
for tree, first_label in zip(trees, first_labels)
]
# Add all sets of nodes and edges, attributes
for tree in new_trees:
R.update(tree)
# Finally, join the subtrees at the root. We know first_label is unused by the way we relabeled the subtrees.
R.add_node(first_label)
R.add_edges_from((first_label, root) for root in new_roots)
return R

View File

@ -0,0 +1,273 @@
"""
Recognition Tests
=================
A *forest* is an acyclic, undirected graph, and a *tree* is a connected forest.
Depending on the subfield, there are various conventions for generalizing these
definitions to directed graphs.
In one convention, directed variants of forest and tree are defined in an
identical manner, except that the direction of the edges is ignored. In effect,
each directed edge is treated as a single undirected edge. Then, additional
restrictions are imposed to define *branchings* and *arborescences*.
In another convention, directed variants of forest and tree correspond to
the previous convention's branchings and arborescences, respectively. Then two
new terms, *polyforest* and *polytree*, are defined to correspond to the other
convention's forest and tree.
Summarizing::
+-----------------------------+
| Convention A | Convention B |
+=============================+
| forest | polyforest |
| tree | polytree |
| branching | forest |
| arborescence | tree |
+-----------------------------+
Each convention has its reasons. The first convention emphasizes definitional
similarity in that directed forests and trees are only concerned with
acyclicity and do not have an in-degree constraint, just as their undirected
counterparts do not. The second convention emphasizes functional similarity
in the sense that the directed analog of a spanning tree is a spanning
arborescence. That is, take any spanning tree and choose one node as the root.
Then every edge is assigned a direction such there is a directed path from the
root to every other node. The result is a spanning arborescence.
NetworkX follows convention "A". Explicitly, these are:
undirected forest
An undirected graph with no undirected cycles.
undirected tree
A connected, undirected forest.
directed forest
A directed graph with no undirected cycles. Equivalently, the underlying
graph structure (which ignores edge orientations) is an undirected forest.
In convention B, this is known as a polyforest.
directed tree
A weakly connected, directed forest. Equivalently, the underlying graph
structure (which ignores edge orientations) is an undirected tree. In
convention B, this is known as a polytree.
branching
A directed forest with each node having, at most, one parent. So the maximum
in-degree is equal to 1. In convention B, this is known as a forest.
arborescence
A directed tree with each node having, at most, one parent. So the maximum
in-degree is equal to 1. In convention B, this is known as a tree.
For trees and arborescences, the adjective "spanning" may be added to designate
that the graph, when considered as a forest/branching, consists of a single
tree/arborescence that includes all nodes in the graph. It is true, by
definition, that every tree/arborescence is spanning with respect to the nodes
that define the tree/arborescence and so, it might seem redundant to introduce
the notion of "spanning". However, the nodes may represent a subset of
nodes from a larger graph, and it is in this context that the term "spanning"
becomes a useful notion.
"""
import networkx as nx
__all__ = ["is_arborescence", "is_branching", "is_forest", "is_tree"]
@nx.utils.not_implemented_for("undirected")
@nx._dispatchable
def is_arborescence(G):
"""
Returns True if `G` is an arborescence.
An arborescence is a directed tree with maximum in-degree equal to 1.
Parameters
----------
G : graph
The graph to test.
Returns
-------
b : bool
A boolean that is True if `G` is an arborescence.
Examples
--------
>>> G = nx.DiGraph([(0, 1), (0, 2), (2, 3), (3, 4)])
>>> nx.is_arborescence(G)
True
>>> G.remove_edge(0, 1)
>>> G.add_edge(1, 2) # maximum in-degree is 2
>>> nx.is_arborescence(G)
False
Notes
-----
In another convention, an arborescence is known as a *tree*.
See Also
--------
is_tree
"""
return is_tree(G) and max(d for n, d in G.in_degree()) <= 1
@nx.utils.not_implemented_for("undirected")
@nx._dispatchable
def is_branching(G):
"""
Returns True if `G` is a branching.
A branching is a directed forest with maximum in-degree equal to 1.
Parameters
----------
G : directed graph
The directed graph to test.
Returns
-------
b : bool
A boolean that is True if `G` is a branching.
Examples
--------
>>> G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> nx.is_branching(G)
True
>>> G.remove_edge(2, 3)
>>> G.add_edge(3, 1) # maximum in-degree is 2
>>> nx.is_branching(G)
False
Notes
-----
In another convention, a branching is also known as a *forest*.
See Also
--------
is_forest
"""
return is_forest(G) and max(d for n, d in G.in_degree()) <= 1
@nx._dispatchable
def is_forest(G):
"""
Returns True if `G` is a forest.
A forest is a graph with no undirected cycles.
For directed graphs, `G` is a forest if the underlying graph is a forest.
The underlying graph is obtained by treating each directed edge as a single
undirected edge in a multigraph.
Parameters
----------
G : graph
The graph to test.
Returns
-------
b : bool
A boolean that is True if `G` is a forest.
Raises
------
NetworkXPointlessConcept
If `G` is empty.
Examples
--------
>>> G = nx.Graph()
>>> G.add_edges_from([(1, 2), (1, 3), (2, 4), (2, 5)])
>>> nx.is_forest(G)
True
>>> G.add_edge(4, 1)
>>> nx.is_forest(G)
False
Notes
-----
In another convention, a directed forest is known as a *polyforest* and
then *forest* corresponds to a *branching*.
See Also
--------
is_branching
"""
if len(G) == 0:
raise nx.exception.NetworkXPointlessConcept("G has no nodes.")
if G.is_directed():
components = (G.subgraph(c) for c in nx.weakly_connected_components(G))
else:
components = (G.subgraph(c) for c in nx.connected_components(G))
return all(len(c) - 1 == c.number_of_edges() for c in components)
@nx._dispatchable
def is_tree(G):
"""
Returns True if `G` is a tree.
A tree is a connected graph with no undirected cycles.
For directed graphs, `G` is a tree if the underlying graph is a tree. The
underlying graph is obtained by treating each directed edge as a single
undirected edge in a multigraph.
Parameters
----------
G : graph
The graph to test.
Returns
-------
b : bool
A boolean that is True if `G` is a tree.
Raises
------
NetworkXPointlessConcept
If `G` is empty.
Examples
--------
>>> G = nx.Graph()
>>> G.add_edges_from([(1, 2), (1, 3), (2, 4), (2, 5)])
>>> nx.is_tree(G) # n-1 edges
True
>>> G.add_edge(3, 4)
>>> nx.is_tree(G) # n edges
False
Notes
-----
In another convention, a directed tree is known as a *polytree* and then
*tree* corresponds to an *arborescence*.
See Also
--------
is_arborescence
"""
if len(G) == 0:
raise nx.exception.NetworkXPointlessConcept("G has no nodes.")
if G.is_directed():
is_connected = nx.is_weakly_connected
else:
is_connected = nx.is_connected
# A connected graph with no cycles has n-1 edges.
return len(G) - 1 == G.number_of_edges() and is_connected(G)

View File

@ -0,0 +1,624 @@
import math
from operator import itemgetter
import pytest
np = pytest.importorskip("numpy")
import networkx as nx
from networkx.algorithms.tree import branchings, recognition
#
# Explicitly discussed examples from Edmonds paper.
#
# Used in Figures A-F.
#
# fmt: off
G_array = np.array([
# 0 1 2 3 4 5 6 7 8
[0, 0, 12, 0, 12, 0, 0, 0, 0], # 0
[4, 0, 0, 0, 0, 13, 0, 0, 0], # 1
[0, 17, 0, 21, 0, 12, 0, 0, 0], # 2
[5, 0, 0, 0, 17, 0, 18, 0, 0], # 3
[0, 0, 0, 0, 0, 0, 0, 12, 0], # 4
[0, 0, 0, 0, 0, 0, 14, 0, 12], # 5
[0, 0, 21, 0, 0, 0, 0, 0, 15], # 6
[0, 0, 0, 19, 0, 0, 15, 0, 0], # 7
[0, 0, 0, 0, 0, 0, 0, 18, 0], # 8
], dtype=int)
# Two copies of the graph from the original paper as disconnected components
G_big_array = np.zeros(np.array(G_array.shape) * 2, dtype=int)
G_big_array[:G_array.shape[0], :G_array.shape[1]] = G_array
G_big_array[G_array.shape[0]:, G_array.shape[1]:] = G_array
# fmt: on
def G1():
G = nx.from_numpy_array(G_array, create_using=nx.MultiDiGraph)
return G
def G2():
# Now we shift all the weights by -10.
# Should not affect optimal arborescence, but does affect optimal branching.
Garr = G_array.copy()
Garr[np.nonzero(Garr)] -= 10
G = nx.from_numpy_array(Garr, create_using=nx.MultiDiGraph)
return G
# An optimal branching for G1 that is also a spanning arborescence. So it is
# also an optimal spanning arborescence.
#
optimal_arborescence_1 = [
(0, 2, 12),
(2, 1, 17),
(2, 3, 21),
(1, 5, 13),
(3, 4, 17),
(3, 6, 18),
(6, 8, 15),
(8, 7, 18),
]
# For G2, the optimal branching of G1 (with shifted weights) is no longer
# an optimal branching, but it is still an optimal spanning arborescence
# (just with shifted weights). An optimal branching for G2 is similar to what
# appears in figure G (this is greedy_subopt_branching_1a below), but with the
# edge (3, 0, 5), which is now (3, 0, -5), removed. Thus, the optimal branching
# is not a spanning arborescence. The code finds optimal_branching_2a.
# An alternative and equivalent branching is optimal_branching_2b. We would
# need to modify the code to iterate through all equivalent optimal branchings.
#
# These are maximal branchings or arborescences.
optimal_branching_2a = [
(5, 6, 4),
(6, 2, 11),
(6, 8, 5),
(8, 7, 8),
(2, 1, 7),
(2, 3, 11),
(3, 4, 7),
]
optimal_branching_2b = [
(8, 7, 8),
(7, 3, 9),
(3, 4, 7),
(3, 6, 8),
(6, 2, 11),
(2, 1, 7),
(1, 5, 3),
]
optimal_arborescence_2 = [
(0, 2, 2),
(2, 1, 7),
(2, 3, 11),
(1, 5, 3),
(3, 4, 7),
(3, 6, 8),
(6, 8, 5),
(8, 7, 8),
]
# Two suboptimal maximal branchings on G1 obtained from a greedy algorithm.
# 1a matches what is shown in Figure G in Edmonds's paper.
greedy_subopt_branching_1a = [
(5, 6, 14),
(6, 2, 21),
(6, 8, 15),
(8, 7, 18),
(2, 1, 17),
(2, 3, 21),
(3, 0, 5),
(3, 4, 17),
]
greedy_subopt_branching_1b = [
(8, 7, 18),
(7, 6, 15),
(6, 2, 21),
(2, 1, 17),
(2, 3, 21),
(1, 5, 13),
(3, 0, 5),
(3, 4, 17),
]
def build_branching(edges, double=False):
G = nx.DiGraph()
for u, v, weight in edges:
G.add_edge(u, v, weight=weight)
if double:
G.add_edge(u + 9, v + 9, weight=weight)
return G
def sorted_edges(G, attr="weight", default=1):
edges = [(u, v, data.get(attr, default)) for (u, v, data) in G.edges(data=True)]
edges = sorted(edges, key=lambda x: (x[2], x[1], x[0]))
return edges
def assert_equal_branchings(G1, G2, attr="weight", default=1):
edges1 = list(G1.edges(data=True))
edges2 = list(G2.edges(data=True))
assert len(edges1) == len(edges2)
# Grab the weights only.
e1 = sorted_edges(G1, attr, default)
e2 = sorted_edges(G2, attr, default)
for a, b in zip(e1, e2):
assert a[:2] == b[:2]
np.testing.assert_almost_equal(a[2], b[2])
################
def test_optimal_branching1():
G = build_branching(optimal_arborescence_1)
assert recognition.is_arborescence(G), True
assert branchings.branching_weight(G) == 131
def test_optimal_branching2a():
G = build_branching(optimal_branching_2a)
assert recognition.is_arborescence(G), True
assert branchings.branching_weight(G) == 53
def test_optimal_branching2b():
G = build_branching(optimal_branching_2b)
assert recognition.is_arborescence(G), True
assert branchings.branching_weight(G) == 53
def test_optimal_arborescence2():
G = build_branching(optimal_arborescence_2)
assert recognition.is_arborescence(G), True
assert branchings.branching_weight(G) == 51
def test_greedy_suboptimal_branching1a():
G = build_branching(greedy_subopt_branching_1a)
assert recognition.is_arborescence(G), True
assert branchings.branching_weight(G) == 128
def test_greedy_suboptimal_branching1b():
G = build_branching(greedy_subopt_branching_1b)
assert recognition.is_arborescence(G), True
assert branchings.branching_weight(G) == 127
def test_greedy_max1():
# Standard test.
#
G = G1()
B = branchings.greedy_branching(G)
# There are only two possible greedy branchings. The sorting is such
# that it should equal the second suboptimal branching: 1b.
B_ = build_branching(greedy_subopt_branching_1b)
assert_equal_branchings(B, B_)
def test_greedy_branching_kwarg_kind():
G = G1()
with pytest.raises(nx.NetworkXException, match="Unknown value for `kind`."):
B = branchings.greedy_branching(G, kind="lol")
def test_greedy_branching_for_unsortable_nodes():
G = nx.DiGraph()
G.add_weighted_edges_from([((2, 3), 5, 1), (3, "a", 1), (2, 4, 5)])
edges = [(u, v, data.get("weight", 1)) for (u, v, data) in G.edges(data=True)]
with pytest.raises(TypeError):
edges.sort(key=itemgetter(2, 0, 1), reverse=True)
B = branchings.greedy_branching(G, kind="max").edges(data=True)
assert list(B) == [
((2, 3), 5, {"weight": 1}),
(3, "a", {"weight": 1}),
(2, 4, {"weight": 5}),
]
def test_greedy_max2():
# Different default weight.
#
G = G1()
del G[1][0][0]["weight"]
B = branchings.greedy_branching(G, default=6)
# Chosen so that edge (3,0,5) is not selected and (1,0,6) is instead.
edges = [
(1, 0, 6),
(1, 5, 13),
(7, 6, 15),
(2, 1, 17),
(3, 4, 17),
(8, 7, 18),
(2, 3, 21),
(6, 2, 21),
]
B_ = build_branching(edges)
assert_equal_branchings(B, B_)
def test_greedy_max3():
# All equal weights.
#
G = G1()
B = branchings.greedy_branching(G, attr=None)
# This is mostly arbitrary...the output was generated by running the algo.
edges = [
(2, 1, 1),
(3, 0, 1),
(3, 4, 1),
(5, 8, 1),
(6, 2, 1),
(7, 3, 1),
(7, 6, 1),
(8, 7, 1),
]
B_ = build_branching(edges)
assert_equal_branchings(B, B_, default=1)
def test_greedy_min():
G = G1()
B = branchings.greedy_branching(G, kind="min")
edges = [
(1, 0, 4),
(0, 2, 12),
(0, 4, 12),
(2, 5, 12),
(4, 7, 12),
(5, 8, 12),
(5, 6, 14),
(7, 3, 19),
]
B_ = build_branching(edges)
assert_equal_branchings(B, B_)
def test_edmonds1_maxbranch():
G = G1()
x = branchings.maximum_branching(G)
x_ = build_branching(optimal_arborescence_1)
assert_equal_branchings(x, x_)
def test_edmonds1_maxarbor():
G = G1()
x = branchings.maximum_spanning_arborescence(G)
x_ = build_branching(optimal_arborescence_1)
assert_equal_branchings(x, x_)
def test_edmonds1_minimal_branching():
# graph will have something like a minimum arborescence but no spanning one
G = nx.from_numpy_array(G_big_array, create_using=nx.DiGraph)
B = branchings.minimal_branching(G)
edges = [
(3, 0, 5),
(0, 2, 12),
(0, 4, 12),
(2, 5, 12),
(4, 7, 12),
(5, 8, 12),
(5, 6, 14),
(2, 1, 17),
]
B_ = build_branching(edges, double=True)
assert_equal_branchings(B, B_)
def test_edmonds2_maxbranch():
G = G2()
x = branchings.maximum_branching(G)
x_ = build_branching(optimal_branching_2a)
assert_equal_branchings(x, x_)
def test_edmonds2_maxarbor():
G = G2()
x = branchings.maximum_spanning_arborescence(G)
x_ = build_branching(optimal_arborescence_2)
assert_equal_branchings(x, x_)
def test_edmonds2_minarbor():
G = G1()
x = branchings.minimum_spanning_arborescence(G)
# This was obtained from algorithm. Need to verify it independently.
# Branch weight is: 96
edges = [
(3, 0, 5),
(0, 2, 12),
(0, 4, 12),
(2, 5, 12),
(4, 7, 12),
(5, 8, 12),
(5, 6, 14),
(2, 1, 17),
]
x_ = build_branching(edges)
assert_equal_branchings(x, x_)
def test_edmonds3_minbranch1():
G = G1()
x = branchings.minimum_branching(G)
edges = []
x_ = build_branching(edges)
assert_equal_branchings(x, x_)
def test_edmonds3_minbranch2():
G = G1()
G.add_edge(8, 9, weight=-10)
x = branchings.minimum_branching(G)
edges = [(8, 9, -10)]
x_ = build_branching(edges)
assert_equal_branchings(x, x_)
# Need more tests
def test_mst():
# Make sure we get the same results for undirected graphs.
# Example from: https://en.wikipedia.org/wiki/Kruskal's_algorithm
G = nx.Graph()
edgelist = [
(0, 3, [("weight", 5)]),
(0, 1, [("weight", 7)]),
(1, 3, [("weight", 9)]),
(1, 2, [("weight", 8)]),
(1, 4, [("weight", 7)]),
(3, 4, [("weight", 15)]),
(3, 5, [("weight", 6)]),
(2, 4, [("weight", 5)]),
(4, 5, [("weight", 8)]),
(4, 6, [("weight", 9)]),
(5, 6, [("weight", 11)]),
]
G.add_edges_from(edgelist)
G = G.to_directed()
x = branchings.minimum_spanning_arborescence(G)
edges = [
({0, 1}, 7),
({0, 3}, 5),
({3, 5}, 6),
({1, 4}, 7),
({4, 2}, 5),
({4, 6}, 9),
]
assert x.number_of_edges() == len(edges)
for u, v, d in x.edges(data=True):
assert ({u, v}, d["weight"]) in edges
def test_mixed_nodetypes():
# Smoke test to make sure no TypeError is raised for mixed node types.
G = nx.Graph()
edgelist = [(0, 3, [("weight", 5)]), (0, "1", [("weight", 5)])]
G.add_edges_from(edgelist)
G = G.to_directed()
x = branchings.minimum_spanning_arborescence(G)
def test_edmonds1_minbranch():
# Using -G_array and min should give the same as optimal_arborescence_1,
# but with all edges negative.
edges = [(u, v, -w) for (u, v, w) in optimal_arborescence_1]
G = nx.from_numpy_array(-G_array, create_using=nx.DiGraph)
# Quickly make sure max branching is empty.
x = branchings.maximum_branching(G)
x_ = build_branching([])
assert_equal_branchings(x, x_)
# Now test the min branching.
x = branchings.minimum_branching(G)
x_ = build_branching(edges)
assert_equal_branchings(x, x_)
def test_edge_attribute_preservation_normal_graph():
# Test that edge attributes are preserved when finding an optimum graph
# using the Edmonds class for normal graphs.
G = nx.Graph()
edgelist = [
(0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
(0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
(1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
]
G.add_edges_from(edgelist)
B = branchings.maximum_branching(G, preserve_attrs=True)
assert B[0][1]["otherattr"] == 1
assert B[0][1]["otherattr2"] == 3
def test_edge_attribute_preservation_multigraph():
# Test that edge attributes are preserved when finding an optimum graph
# using the Edmonds class for multigraphs.
G = nx.MultiGraph()
edgelist = [
(0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
(0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
(1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
]
G.add_edges_from(edgelist * 2) # Make sure we have duplicate edge paths
B = branchings.maximum_branching(G, preserve_attrs=True)
assert B[0][1][0]["otherattr"] == 1
assert B[0][1][0]["otherattr2"] == 3
def test_edge_attribute_discard():
# Test that edge attributes are discarded if we do not specify to keep them
G = nx.Graph()
edgelist = [
(0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
(0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
(1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
]
G.add_edges_from(edgelist)
B = branchings.maximum_branching(G, preserve_attrs=False)
edge_dict = B[0][1]
with pytest.raises(KeyError):
_ = edge_dict["otherattr"]
def test_partition_spanning_arborescence():
"""
Test that we can generate minimum spanning arborescences which respect the
given partition.
"""
G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
G[3][0]["partition"] = nx.EdgePartition.EXCLUDED
G[2][3]["partition"] = nx.EdgePartition.INCLUDED
G[7][3]["partition"] = nx.EdgePartition.EXCLUDED
G[0][2]["partition"] = nx.EdgePartition.EXCLUDED
G[6][2]["partition"] = nx.EdgePartition.INCLUDED
actual_edges = [
(0, 4, 12),
(1, 0, 4),
(1, 5, 13),
(2, 3, 21),
(4, 7, 12),
(5, 6, 14),
(5, 8, 12),
(6, 2, 21),
]
B = branchings.minimum_spanning_arborescence(G, partition="partition")
assert_equal_branchings(build_branching(actual_edges), B)
def test_arborescence_iterator_min():
"""
Tests the arborescence iterator.
A brute force method found 680 arborescences in this graph.
This test will not verify all of them individually, but will check two
things
* The iterator returns 680 arborescences
* The weight of the arborescences is non-strictly increasing
for more information please visit
https://mjschwenne.github.io/2021/06/10/implementing-the-iterators.html
"""
G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
arborescence_count = 0
arborescence_weight = -math.inf
for B in branchings.ArborescenceIterator(G):
arborescence_count += 1
new_arborescence_weight = B.size(weight="weight")
assert new_arborescence_weight >= arborescence_weight
arborescence_weight = new_arborescence_weight
assert arborescence_count == 680
def test_arborescence_iterator_max():
"""
Tests the arborescence iterator.
A brute force method found 680 arborescences in this graph.
This test will not verify all of them individually, but will check two
things
* The iterator returns 680 arborescences
* The weight of the arborescences is non-strictly decreasing
for more information please visit
https://mjschwenne.github.io/2021/06/10/implementing-the-iterators.html
"""
G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
arborescence_count = 0
arborescence_weight = math.inf
for B in branchings.ArborescenceIterator(G, minimum=False):
arborescence_count += 1
new_arborescence_weight = B.size(weight="weight")
assert new_arborescence_weight <= arborescence_weight
arborescence_weight = new_arborescence_weight
assert arborescence_count == 680
def test_arborescence_iterator_initial_partition():
"""
Tests the arborescence iterator with three included edges and three excluded
in the initial partition.
A brute force method similar to the one used in the above tests found that
there are 16 arborescences which contain the included edges and not the
excluded edges.
"""
G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
included_edges = [(1, 0), (5, 6), (8, 7)]
excluded_edges = [(0, 2), (3, 6), (1, 5)]
arborescence_count = 0
arborescence_weight = -math.inf
for B in branchings.ArborescenceIterator(
G, init_partition=(included_edges, excluded_edges)
):
arborescence_count += 1
new_arborescence_weight = B.size(weight="weight")
assert new_arborescence_weight >= arborescence_weight
arborescence_weight = new_arborescence_weight
for e in included_edges:
assert e in B.edges
for e in excluded_edges:
assert e not in B.edges
assert arborescence_count == 16
def test_branchings_with_default_weights():
"""
Tests that various branching algorithms work on graphs without weights.
For more information, see issue #7279.
"""
graph = nx.erdos_renyi_graph(10, p=0.2, directed=True, seed=123)
assert all(
"weight" not in d for (u, v, d) in graph.edges(data=True)
), "test is for graphs without a weight attribute"
# Calling these functions will modify graph inplace to add weights
# copy the graph to avoid this.
nx.minimum_spanning_arborescence(graph.copy())
nx.maximum_spanning_arborescence(graph.copy())
nx.minimum_branching(graph.copy())
nx.maximum_branching(graph.copy())
nx.algorithms.tree.minimal_branching(graph.copy())
nx.algorithms.tree.branching_weight(graph.copy())
nx.algorithms.tree.greedy_branching(graph.copy())
assert all(
"weight" not in d for (u, v, d) in graph.edges(data=True)
), "The above calls should not modify the initial graph in-place"

View File

@ -0,0 +1,114 @@
"""Unit tests for the :mod:`~networkx.algorithms.tree.coding` module."""
from itertools import product
import pytest
import networkx as nx
from networkx.utils import edges_equal, nodes_equal
class TestPruferSequence:
"""Unit tests for the Prüfer sequence encoding and decoding
functions.
"""
def test_nontree(self):
with pytest.raises(nx.NotATree):
G = nx.cycle_graph(3)
nx.to_prufer_sequence(G)
def test_null_graph(self):
with pytest.raises(nx.NetworkXPointlessConcept):
nx.to_prufer_sequence(nx.null_graph())
def test_trivial_graph(self):
with pytest.raises(nx.NetworkXPointlessConcept):
nx.to_prufer_sequence(nx.trivial_graph())
def test_bad_integer_labels(self):
with pytest.raises(KeyError):
T = nx.Graph(nx.utils.pairwise("abc"))
nx.to_prufer_sequence(T)
def test_encoding(self):
"""Tests for encoding a tree as a Prüfer sequence using the
iterative strategy.
"""
# Example from Wikipedia.
tree = nx.Graph([(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)])
sequence = nx.to_prufer_sequence(tree)
assert sequence == [3, 3, 3, 4]
def test_decoding(self):
"""Tests for decoding a tree from a Prüfer sequence."""
# Example from Wikipedia.
sequence = [3, 3, 3, 4]
tree = nx.from_prufer_sequence(sequence)
assert nodes_equal(list(tree), list(range(6)))
edges = [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)]
assert edges_equal(list(tree.edges()), edges)
def test_decoding2(self):
# Example from "An Optimal Algorithm for Prufer Codes".
sequence = [2, 4, 0, 1, 3, 3]
tree = nx.from_prufer_sequence(sequence)
assert nodes_equal(list(tree), list(range(8)))
edges = [(0, 1), (0, 4), (1, 3), (2, 4), (2, 5), (3, 6), (3, 7)]
assert edges_equal(list(tree.edges()), edges)
def test_inverse(self):
"""Tests that the encoding and decoding functions are inverses."""
for T in nx.nonisomorphic_trees(4):
T2 = nx.from_prufer_sequence(nx.to_prufer_sequence(T))
assert nodes_equal(list(T), list(T2))
assert edges_equal(list(T.edges()), list(T2.edges()))
for seq in product(range(4), repeat=2):
seq2 = nx.to_prufer_sequence(nx.from_prufer_sequence(seq))
assert list(seq) == seq2
class TestNestedTuple:
"""Unit tests for the nested tuple encoding and decoding functions."""
def test_nontree(self):
with pytest.raises(nx.NotATree):
G = nx.cycle_graph(3)
nx.to_nested_tuple(G, 0)
def test_unknown_root(self):
with pytest.raises(nx.NodeNotFound):
G = nx.path_graph(2)
nx.to_nested_tuple(G, "bogus")
def test_encoding(self):
T = nx.full_rary_tree(2, 2**3 - 1)
expected = (((), ()), ((), ()))
actual = nx.to_nested_tuple(T, 0)
assert nodes_equal(expected, actual)
def test_canonical_form(self):
T = nx.Graph()
T.add_edges_from([(0, 1), (0, 2), (0, 3)])
T.add_edges_from([(1, 4), (1, 5)])
T.add_edges_from([(3, 6), (3, 7)])
root = 0
actual = nx.to_nested_tuple(T, root, canonical_form=True)
expected = ((), ((), ()), ((), ()))
assert actual == expected
def test_decoding(self):
balanced = (((), ()), ((), ()))
expected = nx.full_rary_tree(2, 2**3 - 1)
actual = nx.from_nested_tuple(balanced)
assert nx.is_isomorphic(expected, actual)
def test_sensible_relabeling(self):
balanced = (((), ()), ((), ()))
T = nx.from_nested_tuple(balanced, sensible_relabeling=True)
edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
assert nodes_equal(list(T), list(range(2**3 - 1)))
assert edges_equal(list(T.edges()), edges)

View File

@ -0,0 +1,79 @@
import networkx as nx
from networkx.algorithms.tree.decomposition import junction_tree
def test_junction_tree_directed_confounders():
B = nx.DiGraph()
B.add_edges_from([("A", "C"), ("B", "C"), ("C", "D"), ("C", "E")])
G = junction_tree(B)
J = nx.Graph()
J.add_edges_from(
[
(("C", "E"), ("C",)),
(("C",), ("A", "B", "C")),
(("A", "B", "C"), ("C",)),
(("C",), ("C", "D")),
]
)
assert nx.is_isomorphic(G, J)
def test_junction_tree_directed_unconnected_nodes():
B = nx.DiGraph()
B.add_nodes_from([("A", "B", "C", "D")])
G = junction_tree(B)
J = nx.Graph()
J.add_nodes_from([("A", "B", "C", "D")])
assert nx.is_isomorphic(G, J)
def test_junction_tree_directed_cascade():
B = nx.DiGraph()
B.add_edges_from([("A", "B"), ("B", "C"), ("C", "D")])
G = junction_tree(B)
J = nx.Graph()
J.add_edges_from(
[
(("A", "B"), ("B",)),
(("B",), ("B", "C")),
(("B", "C"), ("C",)),
(("C",), ("C", "D")),
]
)
assert nx.is_isomorphic(G, J)
def test_junction_tree_directed_unconnected_edges():
B = nx.DiGraph()
B.add_edges_from([("A", "B"), ("C", "D"), ("E", "F")])
G = junction_tree(B)
J = nx.Graph()
J.add_nodes_from([("A", "B"), ("C", "D"), ("E", "F")])
assert nx.is_isomorphic(G, J)
def test_junction_tree_undirected():
B = nx.Graph()
B.add_edges_from([("A", "C"), ("A", "D"), ("B", "C"), ("C", "E")])
G = junction_tree(B)
J = nx.Graph()
J.add_edges_from(
[
(("A", "D"), ("A",)),
(("A",), ("A", "C")),
(("A", "C"), ("C",)),
(("C",), ("B", "C")),
(("B", "C"), ("C",)),
(("C",), ("C", "E")),
]
)
assert nx.is_isomorphic(G, J)

View File

@ -0,0 +1,918 @@
"""Unit tests for the :mod:`networkx.algorithms.tree.mst` module."""
import pytest
import networkx as nx
from networkx.utils import edges_equal, nodes_equal
def test_unknown_algorithm():
with pytest.raises(ValueError):
nx.minimum_spanning_tree(nx.Graph(), algorithm="random")
with pytest.raises(
ValueError, match="random is not a valid choice for an algorithm."
):
nx.maximum_spanning_edges(nx.Graph(), algorithm="random")
class MinimumSpanningTreeTestBase:
"""Base class for test classes for minimum spanning tree algorithms.
This class contains some common tests that will be inherited by
subclasses. Each subclass must have a class attribute
:data:`algorithm` that is a string representing the algorithm to
run, as described under the ``algorithm`` keyword argument for the
:func:`networkx.minimum_spanning_edges` function. Subclasses can
then implement any algorithm-specific tests.
"""
def setup_method(self, method):
"""Creates an example graph and stores the expected minimum and
maximum spanning tree edges.
"""
# This stores the class attribute `algorithm` in an instance attribute.
self.algo = self.algorithm
# This example graph comes from Wikipedia:
# https://en.wikipedia.org/wiki/Kruskal's_algorithm
edges = [
(0, 1, 7),
(0, 3, 5),
(1, 2, 8),
(1, 3, 9),
(1, 4, 7),
(2, 4, 5),
(3, 4, 15),
(3, 5, 6),
(4, 5, 8),
(4, 6, 9),
(5, 6, 11),
]
self.G = nx.Graph()
self.G.add_weighted_edges_from(edges)
self.minimum_spanning_edgelist = [
(0, 1, {"weight": 7}),
(0, 3, {"weight": 5}),
(1, 4, {"weight": 7}),
(2, 4, {"weight": 5}),
(3, 5, {"weight": 6}),
(4, 6, {"weight": 9}),
]
self.maximum_spanning_edgelist = [
(0, 1, {"weight": 7}),
(1, 2, {"weight": 8}),
(1, 3, {"weight": 9}),
(3, 4, {"weight": 15}),
(4, 6, {"weight": 9}),
(5, 6, {"weight": 11}),
]
def test_minimum_edges(self):
edges = nx.minimum_spanning_edges(self.G, algorithm=self.algo)
# Edges from the spanning edges functions don't come in sorted
# orientation, so we need to sort each edge individually.
actual = sorted((min(u, v), max(u, v), d) for u, v, d in edges)
assert edges_equal(actual, self.minimum_spanning_edgelist)
def test_maximum_edges(self):
edges = nx.maximum_spanning_edges(self.G, algorithm=self.algo)
# Edges from the spanning edges functions don't come in sorted
# orientation, so we need to sort each edge individually.
actual = sorted((min(u, v), max(u, v), d) for u, v, d in edges)
assert edges_equal(actual, self.maximum_spanning_edgelist)
def test_without_data(self):
edges = nx.minimum_spanning_edges(self.G, algorithm=self.algo, data=False)
# Edges from the spanning edges functions don't come in sorted
# orientation, so we need to sort each edge individually.
actual = sorted((min(u, v), max(u, v)) for u, v in edges)
expected = [(u, v) for u, v, d in self.minimum_spanning_edgelist]
assert edges_equal(actual, expected)
def test_nan_weights(self):
# Edge weights NaN never appear in the spanning tree. see #2164
G = self.G
G.add_edge(0, 12, weight=float("nan"))
edges = nx.minimum_spanning_edges(
G, algorithm=self.algo, data=False, ignore_nan=True
)
actual = sorted((min(u, v), max(u, v)) for u, v in edges)
expected = [(u, v) for u, v, d in self.minimum_spanning_edgelist]
assert edges_equal(actual, expected)
# Now test for raising exception
edges = nx.minimum_spanning_edges(
G, algorithm=self.algo, data=False, ignore_nan=False
)
with pytest.raises(ValueError):
list(edges)
# test default for ignore_nan as False
edges = nx.minimum_spanning_edges(G, algorithm=self.algo, data=False)
with pytest.raises(ValueError):
list(edges)
def test_nan_weights_MultiGraph(self):
G = nx.MultiGraph()
G.add_edge(0, 12, weight=float("nan"))
edges = nx.minimum_spanning_edges(
G, algorithm="prim", data=False, ignore_nan=False
)
with pytest.raises(ValueError):
list(edges)
# test default for ignore_nan as False
edges = nx.minimum_spanning_edges(G, algorithm="prim", data=False)
with pytest.raises(ValueError):
list(edges)
def test_nan_weights_order(self):
# now try again with a nan edge at the beginning of G.nodes
edges = [
(0, 1, 7),
(0, 3, 5),
(1, 2, 8),
(1, 3, 9),
(1, 4, 7),
(2, 4, 5),
(3, 4, 15),
(3, 5, 6),
(4, 5, 8),
(4, 6, 9),
(5, 6, 11),
]
G = nx.Graph()
G.add_weighted_edges_from([(u + 1, v + 1, wt) for u, v, wt in edges])
G.add_edge(0, 7, weight=float("nan"))
edges = nx.minimum_spanning_edges(
G, algorithm=self.algo, data=False, ignore_nan=True
)
actual = sorted((min(u, v), max(u, v)) for u, v in edges)
shift = [(u + 1, v + 1) for u, v, d in self.minimum_spanning_edgelist]
assert edges_equal(actual, shift)
def test_isolated_node(self):
# now try again with an isolated node
edges = [
(0, 1, 7),
(0, 3, 5),
(1, 2, 8),
(1, 3, 9),
(1, 4, 7),
(2, 4, 5),
(3, 4, 15),
(3, 5, 6),
(4, 5, 8),
(4, 6, 9),
(5, 6, 11),
]
G = nx.Graph()
G.add_weighted_edges_from([(u + 1, v + 1, wt) for u, v, wt in edges])
G.add_node(0)
edges = nx.minimum_spanning_edges(
G, algorithm=self.algo, data=False, ignore_nan=True
)
actual = sorted((min(u, v), max(u, v)) for u, v in edges)
shift = [(u + 1, v + 1) for u, v, d in self.minimum_spanning_edgelist]
assert edges_equal(actual, shift)
def test_minimum_tree(self):
T = nx.minimum_spanning_tree(self.G, algorithm=self.algo)
actual = sorted(T.edges(data=True))
assert edges_equal(actual, self.minimum_spanning_edgelist)
def test_maximum_tree(self):
T = nx.maximum_spanning_tree(self.G, algorithm=self.algo)
actual = sorted(T.edges(data=True))
assert edges_equal(actual, self.maximum_spanning_edgelist)
def test_disconnected(self):
G = nx.Graph([(0, 1, {"weight": 1}), (2, 3, {"weight": 2})])
T = nx.minimum_spanning_tree(G, algorithm=self.algo)
assert nodes_equal(list(T), list(range(4)))
assert edges_equal(list(T.edges()), [(0, 1), (2, 3)])
def test_empty_graph(self):
G = nx.empty_graph(3)
T = nx.minimum_spanning_tree(G, algorithm=self.algo)
assert nodes_equal(sorted(T), list(range(3)))
assert T.number_of_edges() == 0
def test_attributes(self):
G = nx.Graph()
G.add_edge(1, 2, weight=1, color="red", distance=7)
G.add_edge(2, 3, weight=1, color="green", distance=2)
G.add_edge(1, 3, weight=10, color="blue", distance=1)
G.graph["foo"] = "bar"
T = nx.minimum_spanning_tree(G, algorithm=self.algo)
assert T.graph == G.graph
assert nodes_equal(T, G)
for u, v in T.edges():
assert T.adj[u][v] == G.adj[u][v]
def test_weight_attribute(self):
G = nx.Graph()
G.add_edge(0, 1, weight=1, distance=7)
G.add_edge(0, 2, weight=30, distance=1)
G.add_edge(1, 2, weight=1, distance=1)
G.add_node(3)
T = nx.minimum_spanning_tree(G, algorithm=self.algo, weight="distance")
assert nodes_equal(sorted(T), list(range(4)))
assert edges_equal(sorted(T.edges()), [(0, 2), (1, 2)])
T = nx.maximum_spanning_tree(G, algorithm=self.algo, weight="distance")
assert nodes_equal(sorted(T), list(range(4)))
assert edges_equal(sorted(T.edges()), [(0, 1), (0, 2)])
class TestBoruvka(MinimumSpanningTreeTestBase):
"""Unit tests for computing a minimum (or maximum) spanning tree
using Borůvka's algorithm.
"""
algorithm = "boruvka"
def test_unicode_name(self):
"""Tests that using a Unicode string can correctly indicate
Borůvka's algorithm.
"""
edges = nx.minimum_spanning_edges(self.G, algorithm="borůvka")
# Edges from the spanning edges functions don't come in sorted
# orientation, so we need to sort each edge individually.
actual = sorted((min(u, v), max(u, v), d) for u, v, d in edges)
assert edges_equal(actual, self.minimum_spanning_edgelist)
class MultigraphMSTTestBase(MinimumSpanningTreeTestBase):
# Abstract class
def test_multigraph_keys_min(self):
"""Tests that the minimum spanning edges of a multigraph
preserves edge keys.
"""
G = nx.MultiGraph()
G.add_edge(0, 1, key="a", weight=2)
G.add_edge(0, 1, key="b", weight=1)
min_edges = nx.minimum_spanning_edges
mst_edges = min_edges(G, algorithm=self.algo, data=False)
assert edges_equal([(0, 1, "b")], list(mst_edges))
def test_multigraph_keys_max(self):
"""Tests that the maximum spanning edges of a multigraph
preserves edge keys.
"""
G = nx.MultiGraph()
G.add_edge(0, 1, key="a", weight=2)
G.add_edge(0, 1, key="b", weight=1)
max_edges = nx.maximum_spanning_edges
mst_edges = max_edges(G, algorithm=self.algo, data=False)
assert edges_equal([(0, 1, "a")], list(mst_edges))
class TestKruskal(MultigraphMSTTestBase):
"""Unit tests for computing a minimum (or maximum) spanning tree
using Kruskal's algorithm.
"""
algorithm = "kruskal"
def test_key_data_bool(self):
"""Tests that the keys and data values are included in
MST edges based on whether keys and data parameters are
true or false"""
G = nx.MultiGraph()
G.add_edge(1, 2, key=1, weight=2)
G.add_edge(1, 2, key=2, weight=3)
G.add_edge(3, 2, key=1, weight=2)
G.add_edge(3, 1, key=1, weight=4)
# keys are included and data is not included
mst_edges = nx.minimum_spanning_edges(
G, algorithm=self.algo, keys=True, data=False
)
assert edges_equal([(1, 2, 1), (2, 3, 1)], list(mst_edges))
# keys are not included and data is included
mst_edges = nx.minimum_spanning_edges(
G, algorithm=self.algo, keys=False, data=True
)
assert edges_equal(
[(1, 2, {"weight": 2}), (2, 3, {"weight": 2})], list(mst_edges)
)
# both keys and data are not included
mst_edges = nx.minimum_spanning_edges(
G, algorithm=self.algo, keys=False, data=False
)
assert edges_equal([(1, 2), (2, 3)], list(mst_edges))
# both keys and data are included
mst_edges = nx.minimum_spanning_edges(
G, algorithm=self.algo, keys=True, data=True
)
assert edges_equal(
[(1, 2, 1, {"weight": 2}), (2, 3, 1, {"weight": 2})], list(mst_edges)
)
class TestPrim(MultigraphMSTTestBase):
"""Unit tests for computing a minimum (or maximum) spanning tree
using Prim's algorithm.
"""
algorithm = "prim"
def test_prim_mst_edges_simple_graph(self):
H = nx.Graph()
H.add_edge(1, 2, key=2, weight=3)
H.add_edge(3, 2, key=1, weight=2)
H.add_edge(3, 1, key=1, weight=4)
mst_edges = nx.minimum_spanning_edges(H, algorithm=self.algo, ignore_nan=True)
assert edges_equal(
[(1, 2, {"key": 2, "weight": 3}), (2, 3, {"key": 1, "weight": 2})],
list(mst_edges),
)
def test_ignore_nan(self):
"""Tests that the edges with NaN weights are ignored or
raise an Error based on ignore_nan is true or false"""
H = nx.MultiGraph()
H.add_edge(1, 2, key=1, weight=float("nan"))
H.add_edge(1, 2, key=2, weight=3)
H.add_edge(3, 2, key=1, weight=2)
H.add_edge(3, 1, key=1, weight=4)
# NaN weight edges are ignored when ignore_nan=True
mst_edges = nx.minimum_spanning_edges(H, algorithm=self.algo, ignore_nan=True)
assert edges_equal(
[(1, 2, 2, {"weight": 3}), (2, 3, 1, {"weight": 2})], list(mst_edges)
)
# NaN weight edges raise Error when ignore_nan=False
with pytest.raises(ValueError):
list(nx.minimum_spanning_edges(H, algorithm=self.algo, ignore_nan=False))
def test_multigraph_keys_tree(self):
G = nx.MultiGraph()
G.add_edge(0, 1, key="a", weight=2)
G.add_edge(0, 1, key="b", weight=1)
T = nx.minimum_spanning_tree(G, algorithm=self.algo)
assert edges_equal([(0, 1, 1)], list(T.edges(data="weight")))
def test_multigraph_keys_tree_max(self):
G = nx.MultiGraph()
G.add_edge(0, 1, key="a", weight=2)
G.add_edge(0, 1, key="b", weight=1)
T = nx.maximum_spanning_tree(G, algorithm=self.algo)
assert edges_equal([(0, 1, 2)], list(T.edges(data="weight")))
class TestSpanningTreeIterator:
"""
Tests the spanning tree iterator on the example graph in the 2005 Sörensen
and Janssens paper An Algorithm to Generate all Spanning Trees of a Graph in
Order of Increasing Cost
"""
def setup_method(self):
# Original Graph
edges = [(0, 1, 5), (1, 2, 4), (1, 4, 6), (2, 3, 5), (2, 4, 7), (3, 4, 3)]
self.G = nx.Graph()
self.G.add_weighted_edges_from(edges)
# List of lists of spanning trees in increasing order
self.spanning_trees = [
# 1, MST, cost = 17
[
(0, 1, {"weight": 5}),
(1, 2, {"weight": 4}),
(2, 3, {"weight": 5}),
(3, 4, {"weight": 3}),
],
# 2, cost = 18
[
(0, 1, {"weight": 5}),
(1, 2, {"weight": 4}),
(1, 4, {"weight": 6}),
(3, 4, {"weight": 3}),
],
# 3, cost = 19
[
(0, 1, {"weight": 5}),
(1, 4, {"weight": 6}),
(2, 3, {"weight": 5}),
(3, 4, {"weight": 3}),
],
# 4, cost = 19
[
(0, 1, {"weight": 5}),
(1, 2, {"weight": 4}),
(2, 4, {"weight": 7}),
(3, 4, {"weight": 3}),
],
# 5, cost = 20
[
(0, 1, {"weight": 5}),
(1, 2, {"weight": 4}),
(1, 4, {"weight": 6}),
(2, 3, {"weight": 5}),
],
# 6, cost = 21
[
(0, 1, {"weight": 5}),
(1, 4, {"weight": 6}),
(2, 4, {"weight": 7}),
(3, 4, {"weight": 3}),
],
# 7, cost = 21
[
(0, 1, {"weight": 5}),
(1, 2, {"weight": 4}),
(2, 3, {"weight": 5}),
(2, 4, {"weight": 7}),
],
# 8, cost = 23
[
(0, 1, {"weight": 5}),
(1, 4, {"weight": 6}),
(2, 3, {"weight": 5}),
(2, 4, {"weight": 7}),
],
]
def test_minimum_spanning_tree_iterator(self):
"""
Tests that the spanning trees are correctly returned in increasing order
"""
tree_index = 0
for tree in nx.SpanningTreeIterator(self.G):
actual = sorted(tree.edges(data=True))
assert edges_equal(actual, self.spanning_trees[tree_index])
tree_index += 1
def test_maximum_spanning_tree_iterator(self):
"""
Tests that the spanning trees are correctly returned in decreasing order
"""
tree_index = 7
for tree in nx.SpanningTreeIterator(self.G, minimum=False):
actual = sorted(tree.edges(data=True))
assert edges_equal(actual, self.spanning_trees[tree_index])
tree_index -= 1
class TestSpanningTreeMultiGraphIterator:
"""
Uses the same graph as the above class but with an added edge of twice the weight.
"""
def setup_method(self):
# New graph
edges = [
(0, 1, 5),
(0, 1, 10),
(1, 2, 4),
(1, 2, 8),
(1, 4, 6),
(1, 4, 12),
(2, 3, 5),
(2, 3, 10),
(2, 4, 7),
(2, 4, 14),
(3, 4, 3),
(3, 4, 6),
]
self.G = nx.MultiGraph()
self.G.add_weighted_edges_from(edges)
# There are 128 trees. I'd rather not list all 128 here, and computing them
# on such a small graph actually doesn't take that long.
from itertools import combinations
self.spanning_trees = []
for e in combinations(self.G.edges, 4):
tree = self.G.edge_subgraph(e)
if nx.is_tree(tree):
self.spanning_trees.append(sorted(tree.edges(keys=True, data=True)))
def test_minimum_spanning_tree_iterator_multigraph(self):
"""
Tests that the spanning trees are correctly returned in increasing order
"""
tree_index = 0
last_weight = 0
for tree in nx.SpanningTreeIterator(self.G):
actual = sorted(tree.edges(keys=True, data=True))
weight = sum([e[3]["weight"] for e in actual])
assert actual in self.spanning_trees
assert weight >= last_weight
tree_index += 1
def test_maximum_spanning_tree_iterator_multigraph(self):
"""
Tests that the spanning trees are correctly returned in decreasing order
"""
tree_index = 127
# Maximum weight tree is 46
last_weight = 50
for tree in nx.SpanningTreeIterator(self.G, minimum=False):
actual = sorted(tree.edges(keys=True, data=True))
weight = sum([e[3]["weight"] for e in actual])
assert actual in self.spanning_trees
assert weight <= last_weight
tree_index -= 1
def test_random_spanning_tree_multiplicative_small():
"""
Using a fixed seed, sample one tree for repeatability.
"""
from math import exp
pytest.importorskip("scipy")
gamma = {
(0, 1): -0.6383,
(0, 2): -0.6827,
(0, 5): 0,
(1, 2): -1.0781,
(1, 4): 0,
(2, 3): 0,
(5, 3): -0.2820,
(5, 4): -0.3327,
(4, 3): -0.9927,
}
# The undirected support of gamma
G = nx.Graph()
for u, v in gamma:
G.add_edge(u, v, lambda_key=exp(gamma[(u, v)]))
solution_edges = [(2, 3), (3, 4), (0, 5), (5, 4), (4, 1)]
solution = nx.Graph()
solution.add_edges_from(solution_edges)
sampled_tree = nx.random_spanning_tree(G, "lambda_key", seed=42)
assert nx.utils.edges_equal(solution.edges, sampled_tree.edges)
@pytest.mark.slow
def test_random_spanning_tree_multiplicative_large():
"""
Sample many trees from the distribution created in the last test
"""
from math import exp
from random import Random
pytest.importorskip("numpy")
stats = pytest.importorskip("scipy.stats")
gamma = {
(0, 1): -0.6383,
(0, 2): -0.6827,
(0, 5): 0,
(1, 2): -1.0781,
(1, 4): 0,
(2, 3): 0,
(5, 3): -0.2820,
(5, 4): -0.3327,
(4, 3): -0.9927,
}
# The undirected support of gamma
G = nx.Graph()
for u, v in gamma:
G.add_edge(u, v, lambda_key=exp(gamma[(u, v)]))
# Find the multiplicative weight for each tree.
total_weight = 0
tree_expected = {}
for t in nx.SpanningTreeIterator(G):
# Find the multiplicative weight of the spanning tree
weight = 1
for u, v, d in t.edges(data="lambda_key"):
weight *= d
tree_expected[t] = weight
total_weight += weight
# Assert that every tree has an entry in the expected distribution
assert len(tree_expected) == 75
# Set the sample size and then calculate the expected number of times we
# expect to see each tree. This test uses a near minimum sample size where
# the most unlikely tree has an expected frequency of 5.15.
# (Minimum required is 5)
#
# Here we also initialize the tree_actual dict so that we know the keys
# match between the two. We will later take advantage of the fact that since
# python 3.7 dict order is guaranteed so the expected and actual data will
# have the same order.
sample_size = 1200
tree_actual = {}
for t in tree_expected:
tree_expected[t] = (tree_expected[t] / total_weight) * sample_size
tree_actual[t] = 0
# Sample the spanning trees
#
# Assert that they are actually trees and record which of the 75 trees we
# have sampled.
#
# For repeatability, we want to take advantage of the decorators in NetworkX
# to randomly sample the same sample each time. However, if we pass in a
# constant seed to sample_spanning_tree we will get the same tree each time.
# Instead, we can create our own random number generator with a fixed seed
# and pass those into sample_spanning_tree.
rng = Random(37)
for _ in range(sample_size):
sampled_tree = nx.random_spanning_tree(G, "lambda_key", seed=rng)
assert nx.is_tree(sampled_tree)
for t in tree_expected:
if nx.utils.edges_equal(t.edges, sampled_tree.edges):
tree_actual[t] += 1
break
# Conduct a Chi squared test to see if the actual distribution matches the
# expected one at an alpha = 0.05 significance level.
#
# H_0: The distribution of trees in tree_actual matches the normalized product
# of the edge weights in the tree.
#
# H_a: The distribution of trees in tree_actual follows some other
# distribution of spanning trees.
_, p = stats.chisquare(list(tree_actual.values()), list(tree_expected.values()))
# Assert that p is greater than the significance level so that we do not
# reject the null hypothesis
assert not p < 0.05
def test_random_spanning_tree_additive_small():
"""
Sample a single spanning tree from the additive method.
"""
pytest.importorskip("scipy")
edges = {
(0, 1): 1,
(0, 2): 1,
(0, 5): 3,
(1, 2): 2,
(1, 4): 3,
(2, 3): 3,
(5, 3): 4,
(5, 4): 5,
(4, 3): 4,
}
# Build the graph
G = nx.Graph()
for u, v in edges:
G.add_edge(u, v, weight=edges[(u, v)])
solution_edges = [(0, 2), (1, 2), (2, 3), (3, 4), (3, 5)]
solution = nx.Graph()
solution.add_edges_from(solution_edges)
sampled_tree = nx.random_spanning_tree(
G, weight="weight", multiplicative=False, seed=37
)
assert nx.utils.edges_equal(solution.edges, sampled_tree.edges)
@pytest.mark.slow
def test_random_spanning_tree_additive_large():
"""
Sample many spanning trees from the additive method.
"""
from random import Random
pytest.importorskip("numpy")
stats = pytest.importorskip("scipy.stats")
edges = {
(0, 1): 1,
(0, 2): 1,
(0, 5): 3,
(1, 2): 2,
(1, 4): 3,
(2, 3): 3,
(5, 3): 4,
(5, 4): 5,
(4, 3): 4,
}
# Build the graph
G = nx.Graph()
for u, v in edges:
G.add_edge(u, v, weight=edges[(u, v)])
# Find the additive weight for each tree.
total_weight = 0
tree_expected = {}
for t in nx.SpanningTreeIterator(G):
# Find the multiplicative weight of the spanning tree
weight = 0
for u, v, d in t.edges(data="weight"):
weight += d
tree_expected[t] = weight
total_weight += weight
# Assert that every tree has an entry in the expected distribution
assert len(tree_expected) == 75
# Set the sample size and then calculate the expected number of times we
# expect to see each tree. This test uses a near minimum sample size where
# the most unlikely tree has an expected frequency of 5.07.
# (Minimum required is 5)
#
# Here we also initialize the tree_actual dict so that we know the keys
# match between the two. We will later take advantage of the fact that since
# python 3.7 dict order is guaranteed so the expected and actual data will
# have the same order.
sample_size = 500
tree_actual = {}
for t in tree_expected:
tree_expected[t] = (tree_expected[t] / total_weight) * sample_size
tree_actual[t] = 0
# Sample the spanning trees
#
# Assert that they are actually trees and record which of the 75 trees we
# have sampled.
#
# For repeatability, we want to take advantage of the decorators in NetworkX
# to randomly sample the same sample each time. However, if we pass in a
# constant seed to sample_spanning_tree we will get the same tree each time.
# Instead, we can create our own random number generator with a fixed seed
# and pass those into sample_spanning_tree.
rng = Random(37)
for _ in range(sample_size):
sampled_tree = nx.random_spanning_tree(
G, "weight", multiplicative=False, seed=rng
)
assert nx.is_tree(sampled_tree)
for t in tree_expected:
if nx.utils.edges_equal(t.edges, sampled_tree.edges):
tree_actual[t] += 1
break
# Conduct a Chi squared test to see if the actual distribution matches the
# expected one at an alpha = 0.05 significance level.
#
# H_0: The distribution of trees in tree_actual matches the normalized product
# of the edge weights in the tree.
#
# H_a: The distribution of trees in tree_actual follows some other
# distribution of spanning trees.
_, p = stats.chisquare(list(tree_actual.values()), list(tree_expected.values()))
# Assert that p is greater than the significance level so that we do not
# reject the null hypothesis
assert not p < 0.05
def test_random_spanning_tree_empty_graph():
G = nx.Graph()
rst = nx.tree.random_spanning_tree(G)
assert len(rst.nodes) == 0
assert len(rst.edges) == 0
def test_random_spanning_tree_single_node_graph():
G = nx.Graph()
G.add_node(0)
rst = nx.tree.random_spanning_tree(G)
assert len(rst.nodes) == 1
assert len(rst.edges) == 0
def test_random_spanning_tree_single_node_loop():
G = nx.Graph()
G.add_node(0)
G.add_edge(0, 0)
rst = nx.tree.random_spanning_tree(G)
assert len(rst.nodes) == 1
assert len(rst.edges) == 0
class TestNumberSpanningTrees:
@classmethod
def setup_class(cls):
global np
np = pytest.importorskip("numpy")
sp = pytest.importorskip("scipy")
def test_nst_disconnected(self):
G = nx.empty_graph(2)
assert np.isclose(nx.number_of_spanning_trees(G), 0)
def test_nst_no_nodes(self):
G = nx.Graph()
with pytest.raises(nx.NetworkXPointlessConcept):
nx.number_of_spanning_trees(G)
def test_nst_weight(self):
G = nx.Graph()
G.add_edge(1, 2, weight=1)
G.add_edge(1, 3, weight=1)
G.add_edge(2, 3, weight=2)
# weights are ignored
assert np.isclose(nx.number_of_spanning_trees(G), 3)
# including weight
assert np.isclose(nx.number_of_spanning_trees(G, weight="weight"), 5)
def test_nst_negative_weight(self):
G = nx.Graph()
G.add_edge(1, 2, weight=1)
G.add_edge(1, 3, weight=-1)
G.add_edge(2, 3, weight=-2)
# weights are ignored
assert np.isclose(nx.number_of_spanning_trees(G), 3)
# including weight
assert np.isclose(nx.number_of_spanning_trees(G, weight="weight"), -1)
def test_nst_selfloop(self):
# self-loops are ignored
G = nx.complete_graph(3)
G.add_edge(1, 1)
assert np.isclose(nx.number_of_spanning_trees(G), 3)
def test_nst_multigraph(self):
G = nx.MultiGraph()
G.add_edge(1, 2)
G.add_edge(1, 2)
G.add_edge(1, 3)
G.add_edge(2, 3)
assert np.isclose(nx.number_of_spanning_trees(G), 5)
def test_nst_complete_graph(self):
# this is known as Cayley's formula
N = 5
G = nx.complete_graph(N)
assert np.isclose(nx.number_of_spanning_trees(G), N ** (N - 2))
def test_nst_path_graph(self):
G = nx.path_graph(5)
assert np.isclose(nx.number_of_spanning_trees(G), 1)
def test_nst_cycle_graph(self):
G = nx.cycle_graph(5)
assert np.isclose(nx.number_of_spanning_trees(G), 5)
def test_nst_directed_noroot(self):
G = nx.empty_graph(3, create_using=nx.MultiDiGraph)
with pytest.raises(nx.NetworkXError):
nx.number_of_spanning_trees(G)
def test_nst_directed_root_not_exist(self):
G = nx.empty_graph(3, create_using=nx.MultiDiGraph)
with pytest.raises(nx.NetworkXError):
nx.number_of_spanning_trees(G, root=42)
def test_nst_directed_not_weak_connected(self):
G = nx.DiGraph()
G.add_edge(1, 2)
G.add_edge(3, 4)
assert np.isclose(nx.number_of_spanning_trees(G, root=1), 0)
def test_nst_directed_cycle_graph(self):
G = nx.DiGraph()
G = nx.cycle_graph(7, G)
assert np.isclose(nx.number_of_spanning_trees(G, root=0), 1)
def test_nst_directed_complete_graph(self):
G = nx.DiGraph()
G = nx.complete_graph(7, G)
assert np.isclose(nx.number_of_spanning_trees(G, root=0), 7**5)
def test_nst_directed_multi(self):
G = nx.MultiDiGraph()
G = nx.cycle_graph(3, G)
G.add_edge(1, 2)
assert np.isclose(nx.number_of_spanning_trees(G, root=0), 2)
def test_nst_directed_selfloop(self):
G = nx.MultiDiGraph()
G = nx.cycle_graph(3, G)
G.add_edge(1, 1)
assert np.isclose(nx.number_of_spanning_trees(G, root=0), 1)
def test_nst_directed_weak_connected(self):
G = nx.MultiDiGraph()
G = nx.cycle_graph(3, G)
G.remove_edge(1, 2)
assert np.isclose(nx.number_of_spanning_trees(G, root=0), 0)
def test_nst_directed_weighted(self):
# from root=1:
# arborescence 1: 1->2, 1->3, weight=2*1
# arborescence 2: 1->2, 2->3, weight=2*3
G = nx.DiGraph()
G.add_edge(1, 2, weight=2)
G.add_edge(1, 3, weight=1)
G.add_edge(2, 3, weight=3)
Nst = nx.number_of_spanning_trees(G, root=1, weight="weight")
assert np.isclose(Nst, 8)
Nst = nx.number_of_spanning_trees(G, root=2, weight="weight")
assert np.isclose(Nst, 0)
Nst = nx.number_of_spanning_trees(G, root=3, weight="weight")
assert np.isclose(Nst, 0)

View File

@ -0,0 +1,53 @@
from itertools import chain
import networkx as nx
from networkx.utils import edges_equal, nodes_equal
def _check_custom_label_attribute(input_trees, res_tree, label_attribute):
res_attr_dict = nx.get_node_attributes(res_tree, label_attribute)
res_attr_set = set(res_attr_dict.values())
input_label = (tree for tree, root in input_trees)
input_label_set = set(chain.from_iterable(input_label))
return res_attr_set == input_label_set
def test_empty_sequence():
"""Joining the empty sequence results in the tree with one node."""
T = nx.join_trees([])
assert len(T) == 1
assert T.number_of_edges() == 0
def test_single():
"""Joining just one tree yields a tree with one more node."""
T = nx.empty_graph(1)
trees = [(T, 0)]
actual_with_label = nx.join_trees(trees, label_attribute="custom_label")
expected = nx.path_graph(2)
assert nodes_equal(list(expected), list(actual_with_label))
assert edges_equal(list(expected.edges()), list(actual_with_label.edges()))
def test_basic():
"""Joining multiple subtrees at a root node."""
trees = [(nx.full_rary_tree(2, 2**2 - 1), 0) for i in range(2)]
expected = nx.full_rary_tree(2, 2**3 - 1)
actual = nx.join_trees(trees, label_attribute="old_labels")
assert nx.is_isomorphic(actual, expected)
assert _check_custom_label_attribute(trees, actual, "old_labels")
actual_without_label = nx.join_trees(trees)
assert nx.is_isomorphic(actual_without_label, expected)
# check that no labels were stored
assert all(not data for _, data in actual_without_label.nodes(data=True))
def test_first_label():
"""Test the functionality of the first_label argument."""
T1 = nx.path_graph(3)
T2 = nx.path_graph(2)
actual = nx.join_trees([(T1, 0), (T2, 0)], first_label=10)
expected_nodes = set(range(10, 16))
assert set(actual.nodes()) == expected_nodes
assert set(actual.neighbors(10)) == {11, 14}

View File

@ -0,0 +1,174 @@
import pytest
import networkx as nx
class TestTreeRecognition:
graph = nx.Graph
multigraph = nx.MultiGraph
@classmethod
def setup_class(cls):
cls.T1 = cls.graph()
cls.T2 = cls.graph()
cls.T2.add_node(1)
cls.T3 = cls.graph()
cls.T3.add_nodes_from(range(5))
edges = [(i, i + 1) for i in range(4)]
cls.T3.add_edges_from(edges)
cls.T5 = cls.multigraph()
cls.T5.add_nodes_from(range(5))
edges = [(i, i + 1) for i in range(4)]
cls.T5.add_edges_from(edges)
cls.T6 = cls.graph()
cls.T6.add_nodes_from([6, 7])
cls.T6.add_edge(6, 7)
cls.F1 = nx.compose(cls.T6, cls.T3)
cls.N4 = cls.graph()
cls.N4.add_node(1)
cls.N4.add_edge(1, 1)
cls.N5 = cls.graph()
cls.N5.add_nodes_from(range(5))
cls.N6 = cls.graph()
cls.N6.add_nodes_from(range(3))
cls.N6.add_edges_from([(0, 1), (1, 2), (2, 0)])
cls.NF1 = nx.compose(cls.T6, cls.N6)
def test_null_tree(self):
with pytest.raises(nx.NetworkXPointlessConcept):
nx.is_tree(self.graph())
def test_null_tree2(self):
with pytest.raises(nx.NetworkXPointlessConcept):
nx.is_tree(self.multigraph())
def test_null_forest(self):
with pytest.raises(nx.NetworkXPointlessConcept):
nx.is_forest(self.graph())
def test_null_forest2(self):
with pytest.raises(nx.NetworkXPointlessConcept):
nx.is_forest(self.multigraph())
def test_is_tree(self):
assert nx.is_tree(self.T2)
assert nx.is_tree(self.T3)
assert nx.is_tree(self.T5)
def test_is_not_tree(self):
assert not nx.is_tree(self.N4)
assert not nx.is_tree(self.N5)
assert not nx.is_tree(self.N6)
def test_is_forest(self):
assert nx.is_forest(self.T2)
assert nx.is_forest(self.T3)
assert nx.is_forest(self.T5)
assert nx.is_forest(self.F1)
assert nx.is_forest(self.N5)
def test_is_not_forest(self):
assert not nx.is_forest(self.N4)
assert not nx.is_forest(self.N6)
assert not nx.is_forest(self.NF1)
class TestDirectedTreeRecognition(TestTreeRecognition):
graph = nx.DiGraph
multigraph = nx.MultiDiGraph
def test_disconnected_graph():
# https://github.com/networkx/networkx/issues/1144
G = nx.Graph()
G.add_edges_from([(0, 1), (1, 2), (2, 0), (3, 4)])
assert not nx.is_tree(G)
G = nx.DiGraph()
G.add_edges_from([(0, 1), (1, 2), (2, 0), (3, 4)])
assert not nx.is_tree(G)
def test_dag_nontree():
G = nx.DiGraph()
G.add_edges_from([(0, 1), (0, 2), (1, 2)])
assert not nx.is_tree(G)
assert nx.is_directed_acyclic_graph(G)
def test_multicycle():
G = nx.MultiDiGraph()
G.add_edges_from([(0, 1), (0, 1)])
assert not nx.is_tree(G)
assert nx.is_directed_acyclic_graph(G)
def test_emptybranch():
G = nx.DiGraph()
G.add_nodes_from(range(10))
assert nx.is_branching(G)
assert not nx.is_arborescence(G)
def test_is_branching_empty_graph_raises():
G = nx.DiGraph()
with pytest.raises(nx.NetworkXPointlessConcept, match="G has no nodes."):
nx.is_branching(G)
def test_path():
G = nx.DiGraph()
nx.add_path(G, range(5))
assert nx.is_branching(G)
assert nx.is_arborescence(G)
def test_notbranching1():
# Acyclic violation.
G = nx.MultiDiGraph()
G.add_nodes_from(range(10))
G.add_edges_from([(0, 1), (1, 0)])
assert not nx.is_branching(G)
assert not nx.is_arborescence(G)
def test_notbranching2():
# In-degree violation.
G = nx.MultiDiGraph()
G.add_nodes_from(range(10))
G.add_edges_from([(0, 1), (0, 2), (3, 2)])
assert not nx.is_branching(G)
assert not nx.is_arborescence(G)
def test_notarborescence1():
# Not an arborescence due to not spanning.
G = nx.MultiDiGraph()
G.add_nodes_from(range(10))
G.add_edges_from([(0, 1), (0, 2), (1, 3), (5, 6)])
assert nx.is_branching(G)
assert not nx.is_arborescence(G)
def test_notarborescence2():
# Not an arborescence due to in-degree violation.
G = nx.MultiDiGraph()
nx.add_path(G, range(5))
G.add_edge(6, 4)
assert not nx.is_branching(G)
assert not nx.is_arborescence(G)
def test_is_arborescense_empty_graph_raises():
G = nx.DiGraph()
with pytest.raises(nx.NetworkXPointlessConcept, match="G has no nodes."):
nx.is_arborescence(G)