add padding function to imgScalePadding()
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
from .branchings import *
|
||||
from .coding import *
|
||||
from .mst import *
|
||||
from .recognition import *
|
||||
from .operations import *
|
||||
from .decomposition import *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,398 +0,0 @@
|
||||
"""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")
|
||||
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)
|
||||
|
||||
|
||||
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([(_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")
|
||||
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
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
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
|
||||
@@ -1,87 +0,0 @@
|
||||
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")
|
||||
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 bipartitie 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 (UAI’94).
|
||||
Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 360–366.
|
||||
"""
|
||||
|
||||
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
@@ -1,106 +0,0 @@
|
||||
"""Operations on trees."""
|
||||
from functools import partial
|
||||
from itertools import accumulate, chain
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["join"]
|
||||
|
||||
|
||||
def join(rooted_trees, label_attribute=None):
|
||||
"""Returns a new rooted tree with a root node joined with the roots
|
||||
of each of the given rooted trees.
|
||||
|
||||
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 node attribute
|
||||
``'_old'`` will store the original label of the node in the
|
||||
rooted trees given in the input.
|
||||
|
||||
Returns
|
||||
-------
|
||||
NetworkX graph
|
||||
The rooted tree whose subtrees are the given rooted trees. The
|
||||
new root node is labeled 0. Each non-root node has an attribute,
|
||||
as described under the keyword argument ``label_attribute``,
|
||||
that indicates the label of the original node in the input tree.
|
||||
|
||||
Notes
|
||||
-----
|
||||
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([(left, 0), (right, 0)])
|
||||
>>> nx.is_isomorphic(joined_tree, nx.balanced_tree(2, h + 1))
|
||||
True
|
||||
|
||||
"""
|
||||
if len(rooted_trees) == 0:
|
||||
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])()
|
||||
|
||||
# Relabel the nodes so that their union is the integers starting at 1.
|
||||
if label_attribute is None:
|
||||
label_attribute = "_old"
|
||||
relabel = partial(
|
||||
nx.convert_node_labels_to_integers, label_attribute=label_attribute
|
||||
)
|
||||
lengths = (len(tree) for tree in trees[:-1])
|
||||
first_labels = chain([0], accumulate(lengths))
|
||||
trees = [
|
||||
relabel(tree, first_label=first_label + 1)
|
||||
for tree, first_label in zip(trees, first_labels)
|
||||
]
|
||||
|
||||
# Get the relabeled roots.
|
||||
roots = [
|
||||
next(v for v, d in tree.nodes(data=True) if d.get("_old") == root)
|
||||
for tree, root in zip(trees, roots)
|
||||
]
|
||||
|
||||
# Remove the old node labels.
|
||||
for tree in trees:
|
||||
for v in tree:
|
||||
tree.nodes[v].pop("_old")
|
||||
|
||||
# Add all sets of nodes and edges, with data.
|
||||
nodes = (tree.nodes(data=True) for tree in trees)
|
||||
edges = (tree.edges(data=True) for tree in trees)
|
||||
R.add_nodes_from(chain.from_iterable(nodes))
|
||||
R.add_edges_from(chain.from_iterable(edges))
|
||||
|
||||
# Add graph attributes; later attributes take precedent over earlier
|
||||
# attributes.
|
||||
for tree in trees:
|
||||
R.graph.update(tree.graph)
|
||||
|
||||
# Finally, join the subtrees at the root. We know 0 is unused by the
|
||||
# way we relabeled the subtrees.
|
||||
R.add_node(0)
|
||||
R.add_edges_from((0, root) for root in roots)
|
||||
|
||||
return R
|
||||
@@ -1,269 +0,0 @@
|
||||
"""
|
||||
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")
|
||||
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")
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,609 +0,0 @@
|
||||
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)
|
||||
|
||||
|
||||
# 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):
|
||||
G = nx.DiGraph()
|
||||
for u, v, weight in edges:
|
||||
G.add_edge(u, v, 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_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)
|
||||
|
||||
ed = branchings.Edmonds(G)
|
||||
B = ed.find_optimum("weight", preserve_attrs=True, seed=1)
|
||||
|
||||
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
|
||||
|
||||
ed = branchings.Edmonds(G)
|
||||
B = ed.find_optimum("weight", preserve_attrs=True)
|
||||
|
||||
assert B[0][1][0]["otherattr"] == 1
|
||||
assert B[0][1][0]["otherattr2"] == 3
|
||||
|
||||
|
||||
def test_Edmond_kind():
|
||||
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
|
||||
ed = branchings.Edmonds(G)
|
||||
with pytest.raises(nx.NetworkXException, match="Unknown value for `kind`."):
|
||||
ed.find_optimum(kind="lol", preserve_attrs=True)
|
||||
|
||||
|
||||
def test_MultiDiGraph_EdgeKey():
|
||||
# test if more than one edges has the same key
|
||||
G = branchings.MultiDiGraph_EdgeKey()
|
||||
G.add_edge(1, 2, "A")
|
||||
with pytest.raises(Exception, match="Key 'A' is already in use."):
|
||||
G.add_edge(3, 4, "A")
|
||||
# test if invalid edge key was specified
|
||||
with pytest.raises(KeyError, match="Invalid edge key 'B'"):
|
||||
G.remove_edge_with_key("B")
|
||||
# test remove_edge_with_key works
|
||||
if G.remove_edge_with_key("A"):
|
||||
assert list(G.edges(data=True)) == []
|
||||
# test that remove_edges_from doesn't work
|
||||
G.add_edge(1, 3, "A")
|
||||
with pytest.raises(NotImplementedError):
|
||||
G.remove_edges_from([(1, 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)
|
||||
|
||||
ed = branchings.Edmonds(G)
|
||||
B = ed.find_optimum("weight", 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 arboresecences in this graph.
|
||||
This test will not verify all of them individually, but will check two
|
||||
things
|
||||
|
||||
* The iterator returns 680 arboresecences
|
||||
* 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 arboresecences in this graph.
|
||||
This test will not verify all of them individually, but will check two
|
||||
things
|
||||
|
||||
* The iterator returns 680 arboresecences
|
||||
* 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
|
||||
@@ -1,113 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,79 +0,0 @@
|
||||
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)
|
||||
@@ -1,671 +0,0 @@
|
||||
"""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")
|
||||
|
||||
|
||||
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_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))
|
||||
|
||||
|
||||
class TestPrim(MultigraphMSTTestBase):
|
||||
"""Unit tests for computing a minimum (or maximum) spanning tree
|
||||
using Prim's algorithm.
|
||||
"""
|
||||
|
||||
algorithm = "prim"
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
@@ -1,37 +0,0 @@
|
||||
"""Unit tests for the :mod:`networkx.algorithms.tree.operations` module.
|
||||
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import edges_equal, nodes_equal
|
||||
|
||||
|
||||
class TestJoin:
|
||||
"""Unit tests for the :func:`networkx.tree.join` function."""
|
||||
|
||||
def test_empty_sequence(self):
|
||||
"""Tests that joining the empty sequence results in the tree
|
||||
with one node.
|
||||
|
||||
"""
|
||||
T = nx.join([])
|
||||
assert len(T) == 1
|
||||
assert T.number_of_edges() == 0
|
||||
|
||||
def test_single(self):
|
||||
"""Tests that joining just one tree yields a tree with one more
|
||||
node.
|
||||
|
||||
"""
|
||||
T = nx.empty_graph(1)
|
||||
actual = nx.join([(T, 0)])
|
||||
expected = nx.path_graph(2)
|
||||
assert nodes_equal(list(expected), list(actual))
|
||||
assert edges_equal(list(expected.edges()), list(actual.edges()))
|
||||
|
||||
def test_basic(self):
|
||||
"""Tests for joining multiple subtrees at a root node."""
|
||||
trees = [(nx.full_rary_tree(2, 2**2 - 1), 0) for i in range(2)]
|
||||
actual = nx.join(trees)
|
||||
expected = nx.full_rary_tree(2, 2**3 - 1)
|
||||
assert nx.is_isomorphic(actual, expected)
|
||||
@@ -1,162 +0,0 @@
|
||||
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_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)
|
||||
Reference in New Issue
Block a user