about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_branchings.py624
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_coding.py114
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_decomposition.py79
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_mst.py918
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_operations.py53
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_recognition.py174
7 files changed, 1962 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_branchings.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_branchings.py
new file mode 100644
index 00000000..e19ddee3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_branchings.py
@@ -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"
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_coding.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_coding.py
new file mode 100644
index 00000000..26bd4083
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_coding.py
@@ -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)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_decomposition.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_decomposition.py
new file mode 100644
index 00000000..8c376053
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_decomposition.py
@@ -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)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_mst.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_mst.py
new file mode 100644
index 00000000..f8945a71
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_mst.py
@@ -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)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_operations.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_operations.py
new file mode 100644
index 00000000..284d94e2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_operations.py
@@ -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}
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_recognition.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_recognition.py
new file mode 100644
index 00000000..105f5a89
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tree/tests/test_recognition.py
@@ -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)